Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions .github/workflows/prune-see-also.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: Prune stale See also links

# Companion to regenerate-see-also.yml's scheduled full regeneration: that
# job only fires once enough docs have changed (see the threshold there) and
# always does a full re-embed. This workflow instead runs on every PR that
# deletes or renames a docs page and does the cheap part immediately --
# dropping (or, for a rename, remapping) the affected doc id in
# src/data/see_also.json -- so a reader never sees a "See also" section drop
# a link (or worse, keep showing one) for longer than it takes this job to
# run, without waiting on the next scheduled full regeneration.
#
# scripts/prune_see_also.py has no heavy dependencies (no numpy/torch/
# sentence-transformers) -- it's pure git-diff + JSON bookkeeping -- so this
# is cheap enough to run on every relevant PR, unlike the full embed.
on:
pull_request:
types: [opened, synchronize]
paths:
- 'docs/**/*.md'
- 'docs/**/*.mdx'

# A fast-follow push to the same PR shouldn't race a still-running prune from
# the previous push -- same reasoning (and same pattern) as
# translate-ja-docs.yml's concurrency group.
concurrency:
group: prune-see-also-${{ github.head_ref }}
cancel-in-progress: true

permissions:
contents: write

jobs:
prune:
# Fork PRs get a read-only GITHUB_TOKEN against this repo (a deliberate
# GitHub security boundary), so the push step below can never succeed
# for one -- skip cleanly instead of failing on a permissions error that
# has nothing to do with the contributor's actual change. A fork PR that
# deletes/renames a page just relies on the scheduled full regeneration
# (regenerate-see-also.yml) to catch up -- safe, since a stale id is
# dropped at render time (src/components/SeeAlso), never rendered as a
# dead link.
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.head_ref }}

- uses: actions/setup-python@v5
with: { python-version: '3.12' }

- name: Prune deleted/renamed doc ids from see_also.json
run: |
BASE="${{ github.event.pull_request.base.sha }}"
git diff --name-status -M "$BASE" HEAD -- docs \
| python3 scripts/prune_see_also.py

- name: Commit if anything changed
run: |
git config user.name "bitrise-docs-bot"
git config user.email "docs-bot@bitrise.io"
git add src/data/see_also.json
if ! git diff --cached --quiet; then
git commit -m "docs(see-also): prune stale ids after page delete/rename [skip ci]"
git push
else
echo "No see_also.json changes to commit."
fi
168 changes: 168 additions & 0 deletions .github/workflows/regenerate-see-also.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
name: Regenerate See also links

# Full re-embed of the "See also" relatedness graph (scripts/generate_see_also.py).
# Unlike prune-see-also.yml (cheap, runs on every relevant PR), this installs
# sentence-transformers/torch and re-embeds the whole docs corpus -- real
# cost, so it's gated to run only when enough has actually changed.
#
# The gate job is dependency-free and cheap: it counts docs files
# changed since the last regeneration (scripts/see_also_sync_state.json's
# last_embedded_sha), excluding generated API reference pages, and only lets
# the expensive `regenerate` job proceed once that count reaches the
# threshold below. Measured: ~40 changed files corresponds to roughly once
# a week at this repo's edit pace, with no more than ~2 weeks between
# regenerations even in a slow stretch -- deliberately erring towards fewer,
# cheaper runs. Staleness in between is safe, not just tolerated: a stale or
# removed id is silently dropped at render time (src/components/SeeAlso),
# and prune-see-also.yml already keeps deletions/renames from ever showing a
# dead link. This job exists purely to keep relatedness itself fresh, not to
# prevent breakage.
on:
schedule:
- cron: '0 7 * * *' # 07:00 UTC daily -- the gate below decides if anything actually runs
workflow_dispatch:

concurrency:
group: regenerate-see-also
cancel-in-progress: false

env:
CHANGED_FILES_THRESHOLD: 40

jobs:
gate:
runs-on: ubuntu-latest
outputs:
should_run: ${{ steps.check.outputs.should_run }}
changed_count: ${{ steps.check.outputs.changed_count }}
since_sha: ${{ steps.check.outputs.since_sha }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- id: check
run: |
STATE_FILE="scripts/see_also_sync_state.json"
LAST_SHA=""
if [ -f "$STATE_FILE" ]; then
LAST_SHA=$(python3 -c "import json; print(json.load(open('$STATE_FILE')).get('last_embedded_sha', ''))")
fi

if [ -z "$LAST_SHA" ] || ! git cat-file -e "$LAST_SHA" 2>/dev/null; then
echo "No valid prior state -- running a first regeneration to establish one."
echo "should_run=true" >> "$GITHUB_OUTPUT"
echo "changed_count=n/a (no prior state)" >> "$GITHUB_OUTPUT"
echo "since_sha=n/a (first run)" >> "$GITHUB_OUTPUT"
exit 0
fi

CHANGED=$(git diff --name-only "$LAST_SHA" HEAD -- docs \
| grep -v -E '^docs/(bitrise-api|bitrise-rde-api)/api-reference/' \
| wc -l | tr -d ' ')

echo "Docs files changed since last embed ($LAST_SHA): $CHANGED (threshold: $CHANGED_FILES_THRESHOLD)"
echo "changed_count=$CHANGED" >> "$GITHUB_OUTPUT"
echo "since_sha=${LAST_SHA:0:10}" >> "$GITHUB_OUTPUT"
if [ "$CHANGED" -ge "$CHANGED_FILES_THRESHOLD" ]; then
echo "should_run=true" >> "$GITHUB_OUTPUT"
else
echo "should_run=false" >> "$GITHUB_OUTPUT"
fi

regenerate:
needs: gate
if: needs.gate.outputs.should_run == 'true'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: Cache node_modules
uses: actions/cache@v4
with:
path: |
node_modules
!node_modules/.cache
key: node-modules-${{ runner.os }}-node20-${{ hashFiles('package-lock.json') }}

- uses: actions/setup-python@v5
with: { python-version: '3.12' }

- name: Cache pip packages
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: pip-see-also-${{ runner.os }}-${{ hashFiles('scripts/requirements-see-also.txt') }}

- name: Cache Hugging Face model weights
uses: actions/cache@v4
with:
path: ~/.cache/huggingface
key: hf-model-all-mpnet-base-v2

- name: Install Python dependencies
run: |
# sentence-transformers depends on torch; installing the CPU-only
# wheel first (this runner has no GPU) means the second command
# below finds a satisfying torch already present instead of
# pulling the much larger CUDA-enabled build. requirements-see-also.txt
# doesn't pin a torch version, so a future bump to the
# sentence-transformers pin could require a newer torch than
# what's already installed here -- passing the CPU wheel index as
# an *extra* index (not the sole index) on the second command too
# means that re-resolution can still find a CPU build instead of
# silently falling back to the default (CUDA) one.
pip install --index-url https://download.pytorch.org/whl/cpu torch
pip install -r scripts/requirements-see-also.txt --extra-index-url https://download.pytorch.org/whl/cpu

- run: npm install

- name: Build the English locale
run: npx docusaurus build --locale en

- name: Regenerate see_also.json
run: python3 scripts/generate_see_also.py

- name: Update sync state
run: |
python3 -c "
import json
from pathlib import Path
Path('scripts/see_also_sync_state.json').write_text(
json.dumps({'last_embedded_sha': '${{ github.sha }}'}, indent=2) + '\n'
)
"

- name: Open PR
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.DOCS_SYNC_TOKEN || github.token }}
base: main
branch: auto/see-also-regen
delete-branch: true
add-paths: |
src/data/see_also.json
scripts/see_also_sync_state.json
commit-message: 'docs: regenerate See also links'
title: 'docs: regenerate See also links'
labels: |
automated
documentation
body: |
Automated full re-embedding of the "See also" relatedness graph.

- Docs files changed (excluding generated API reference pages): ${{ needs.gate.outputs.changed_count }}
- Threshold: ${{ env.CHANGED_FILES_THRESHOLD }}
- Since commit: ${{ needs.gate.outputs.since_sha }}

See `scripts/generate_see_also.py`.

Triggered by `.github/workflows/regenerate-see-also.yml`.
36 changes: 36 additions & 0 deletions .github/workflows/test-see-also.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Test See also pipeline

# Runs scripts/test_prune_see_also.py whenever the "See also" pipeline's own
# code changes -- not on ordinary docs content changes, which never touch
# this code at all (that's what prune-see-also.yml and regenerate-see-also.yml
# exercise, against real data, not the logic itself).
#
# Plain stdlib unittest, no dependencies to install, so this has nothing to
# set up and finishes in well under a second. No special permissions needed
# either (read-only: checkout + run), so unlike prune-see-also.yml this runs
# the same way for fork PRs too.
on:
pull_request:
paths:
- 'scripts/generate_see_also.py'
- 'scripts/prune_see_also.py'
- 'scripts/see_also_common.py'
- 'scripts/test_prune_see_also.py'
push:
branches: [main]
paths:
- 'scripts/generate_see_also.py'
- 'scripts/prune_see_also.py'
- 'scripts/see_also_common.py'
- 'scripts/test_prune_see_also.py'

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with: { python-version: '3.12' }

- run: python3 scripts/test_prune_see_also.py -v
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@
# Python bytecode (scripts/)
__pycache__/
*.pyc

# Python virtualenv
.venv/
Loading
Loading