docs: redirect unversioned deep links to the default version #345
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Docs | |
| on: | |
| push: | |
| branches: | |
| - main | |
| tags: | |
| - 'v*' | |
| workflow_dispatch: | |
| pull_request_target: | |
| types: [opened, synchronize, reopened] | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| env: | |
| DOCS_REPO: light-curve/light-curve.snad.space | |
| DOCS_BRANCH: main | |
| jobs: | |
| docs-deploy: | |
| if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' | |
| # Queue deploys; never cancel — concurrent pushes would corrupt the docs branch | |
| concurrency: | |
| group: docs-deploy | |
| cancel-in-progress: false | |
| runs-on: ubuntu-latest | |
| defaults: | |
| run: | |
| shell: bash | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| submodules: true | |
| persist-credentials: false | |
| - uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.14" | |
| - name: Set up Rust toolchain | |
| uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: "light-curve" | |
| - name: Install system dependencies | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y libgsl-dev | |
| - name: Configure git and docs remote | |
| env: | |
| DOCS_DEPLOY_TOKEN: ${{ secrets.DOCS_DEPLOY_TOKEN }} | |
| run: | | |
| git config --global user.name "github-actions[bot]" | |
| git config --global user.email "github-actions[bot]@users.noreply.github.com" | |
| git remote add docs-site \ | |
| "https://x-access-token:${DOCS_DEPLOY_TOKEN}@github.com/${DOCS_REPO}.git" | |
| - name: Install from source with docs dependencies | |
| run: | | |
| python -m venv .venv | |
| source .venv/bin/activate | |
| pip install maturin | |
| cd light-curve && maturin develop --extras=full --group=docs -q | |
| - name: Deploy dev (main branch) | |
| if: github.ref_type == 'branch' | |
| env: | |
| HF_TOKEN: ${{ secrets.HF_TOKEN }} | |
| run: | | |
| source .venv/bin/activate | |
| # Detach HEAD and drop the local branch so mike fetches docs-site/${DOCS_BRANCH} | |
| # fresh instead of reusing the source-code branch of the same name. | |
| git fetch docs-site "${DOCS_BRANCH}" | |
| git checkout --detach HEAD | |
| git branch -D "${DOCS_BRANCH}" | |
| mike deploy --push --remote docs-site --branch "${DOCS_BRANCH}" \ | |
| --update-aliases dev | |
| mike list --remote docs-site --branch "${DOCS_BRANCH}" | grep -q "latest" \ | |
| || mike set-default --push --remote docs-site --branch "${DOCS_BRANCH}" dev | |
| - name: Deploy release (tag) | |
| if: github.ref_type == 'tag' | |
| env: | |
| HF_TOKEN: ${{ secrets.HF_TOKEN }} | |
| run: | | |
| source .venv/bin/activate | |
| git fetch docs-site "${DOCS_BRANCH}" | |
| git checkout --detach HEAD | |
| git branch -D "${DOCS_BRANCH}" 2>/dev/null || true | |
| mike deploy --push --remote docs-site --branch "${DOCS_BRANCH}" \ | |
| --update-aliases latest | |
| mike set-default --push --remote docs-site --branch "${DOCS_BRANCH}" latest | |
| - name: Deploy root 404 redirect to default version | |
| # mike only redirects the bare root `/` to the default version; this root | |
| # 404.html redirects unversioned deep links (/<path> -> /<default>/<path>). | |
| # It lives at the docs-site root (outside any version dir) and survives | |
| # mike deploys, so we just keep it in sync after each deploy. | |
| env: | |
| DOCS_DEPLOY_TOKEN: ${{ secrets.DOCS_DEPLOY_TOKEN }} | |
| run: | | |
| git clone --depth=1 --branch="${DOCS_BRANCH}" \ | |
| "https://x-access-token:${DOCS_DEPLOY_TOKEN}@github.com/${DOCS_REPO}.git" \ | |
| /tmp/docs-404 | |
| cd /tmp/docs-404 | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| cp "${GITHUB_WORKSPACE}/.github/docs-site/404.html" 404.html | |
| git add 404.html | |
| if git diff --cached --quiet; then | |
| echo "Root 404.html already up to date." | |
| else | |
| git commit -m "docs: update root 404 redirect to default version" | |
| for i in 1 2 3; do | |
| git push && break | |
| git pull --rebase origin "${DOCS_BRANCH}" | |
| done | |
| fi | |
| - name: Remove stale PR previews | |
| # pull_request.closed doesn't fire for bot-merged PRs (copilot[bot], | |
| # pre-commit-ci[bot]). Run this after every deploy to catch leftovers. | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| DOCS_DEPLOY_TOKEN: ${{ secrets.DOCS_DEPLOY_TOKEN }} | |
| run: | | |
| git clone --depth=1 --branch="${DOCS_BRANCH}" \ | |
| "https://x-access-token:${DOCS_DEPLOY_TOKEN}@github.com/${DOCS_REPO}.git" \ | |
| /tmp/docs-stale | |
| cd /tmp/docs-stale | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| gh pr list --repo "${{ github.repository }}" --state open \ | |
| --json number --limit 200 > /tmp/open_prs.json | |
| python3 - <<'PYEOF' | |
| import json, os, re, subprocess, sys | |
| with open('/tmp/open_prs.json') as f: | |
| open_set = {f"pr{p['number']}" for p in json.load(f)} | |
| with open('versions.json') as f: | |
| versions = json.load(f) | |
| stale = [v['version'] for v in versions | |
| if re.fullmatch(r'pr\d+', v['version']) and v['version'] not in open_set] | |
| if not stale: | |
| print("No stale PR previews.") | |
| sys.exit(0) | |
| print("Removing stale PR previews:", stale) | |
| for ver in stale: | |
| if os.path.isdir(ver): | |
| subprocess.run(['git', 'rm', '-rf', ver], check=True) | |
| cleaned = [v for v in versions if v['version'] not in stale] | |
| with open('versions.json', 'w') as f: | |
| json.dump(cleaned, f, indent=2) | |
| f.write('\n') | |
| subprocess.run(['git', 'add', 'versions.json'], check=True) | |
| PYEOF | |
| if git diff --cached --quiet; then | |
| echo "Nothing to commit." | |
| else | |
| git commit -m "docs: remove stale PR previews" | |
| for i in 1 2 3; do | |
| git push && break | |
| git pull --rebase origin "${DOCS_BRANCH}" | |
| done | |
| fi | |
| docs-preview: | |
| # pull_request_target gives write access even for fork PRs. | |
| # We explicitly check out the PR head so the docs reflect the PR content. | |
| if: github.event_name == 'pull_request_target' | |
| # Cancel older builds for the same PR; only the latest commit's preview matters | |
| concurrency: | |
| group: docs-preview-pr${{ github.event.pull_request.number }} | |
| cancel-in-progress: true | |
| runs-on: ubuntu-latest | |
| defaults: | |
| run: | |
| shell: bash | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha }} | |
| fetch-depth: 0 | |
| submodules: true | |
| persist-credentials: false | |
| - uses: actions/checkout@v6 | |
| with: | |
| repository: ${{ env.DOCS_REPO }} | |
| token: ${{ secrets.DOCS_DEPLOY_TOKEN }} | |
| ref: ${{ env.DOCS_BRANCH }} | |
| path: docs-deploy | |
| fetch-depth: 1 | |
| - uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.14" | |
| - name: Set up Rust toolchain | |
| uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: "light-curve" | |
| - name: Install system dependencies | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y libgsl-dev | |
| - name: Configure git | |
| run: | | |
| git config --global user.name "github-actions[bot]" | |
| git config --global user.email "github-actions[bot]@users.noreply.github.com" | |
| - name: Install from source with docs dependencies | |
| run: | | |
| python -m venv .venv | |
| source .venv/bin/activate | |
| pip install maturin | |
| cd light-curve && maturin develop --extras=full --group=docs -q | |
| - name: Build docs | |
| env: | |
| HF_TOKEN: ${{ secrets.HF_TOKEN }} | |
| run: | | |
| source .venv/bin/activate | |
| mkdocs build --site-dir /tmp/pr-site | |
| - name: Push preview to docs repo | |
| env: | |
| PR: ${{ github.event.pull_request.number }} | |
| run: | | |
| rm -rf docs-deploy/pr${PR} | |
| cp -r /tmp/pr-site docs-deploy/pr${PR} | |
| rm -f docs-deploy/pr${PR}/CNAME | |
| python3 -c " | |
| import json, os, re | |
| pr = int(os.environ['PR']) | |
| entry = {'version': f'pr{pr}', 'title': f'pr{pr}', 'aliases': []} | |
| with open('docs-deploy/versions.json') as f: | |
| versions = json.load(f) | |
| versions = [v for v in versions if v['version'] != entry['version']] | |
| non_pr = [v for v in versions if not re.fullmatch(r'pr\d+', v['version'])] | |
| prs = [v for v in versions if re.fullmatch(r'pr\d+', v['version'])] | |
| prs.append(entry) | |
| prs.sort(key=lambda v: int(v['version'][2:])) | |
| with open('docs-deploy/versions.json', 'w') as f: | |
| json.dump(non_pr + prs, f, indent=2) | |
| f.write('\n') | |
| " | |
| cd docs-deploy | |
| git add pr${PR} versions.json | |
| git diff --cached --quiet && echo "No changes" && exit 0 | |
| git commit -m "docs: update PR #${PR} preview" | |
| # Retry push with rebase in case of concurrent preview builds | |
| for i in 1 2 3; do | |
| git push && break | |
| git pull --rebase origin "${DOCS_BRANCH}" | |
| done | |
| - name: Comment preview URL on PR | |
| uses: actions/github-script@v9 | |
| with: | |
| script: | | |
| const marker = '<!-- docs-preview -->'; | |
| const url = `https://light-curve.snad.space/pr${{ github.event.pull_request.number }}/`; | |
| const body = `${marker}\n📖 **Docs preview:** ${url}`; | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| }); | |
| const existing = comments.find(c => c.body.includes(marker)); | |
| if (existing) { | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: existing.id, | |
| body, | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body, | |
| }); | |
| } |