docsy(v2): bump infra (CLAUDE.md reconcile + build-info docs) (#1568) #859
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: Build and deploy docs | |
| # Part of the gha-docs-build migration: builds the docs via `make dist` | |
| # and pushes the result to a Cloudflare Pages project via Direct Upload. | |
| # Replaces CF Pages' native build runner — CF Pages stays only as the | |
| # static host. | |
| # | |
| # Production-only path. PR previews are handled by the two-stage pair | |
| # `build-pr.yml` (untrusted build, no secrets) + `deploy-pr-preview.yml` | |
| # (trusted workflow_run deploy, has secrets), because GitHub does NOT expose | |
| # repo secrets to pull_request workflows triggered from forks (so the deploy | |
| # here fails on fork PRs). See DOC-1228. | |
| # | |
| # Triggers: | |
| # - push to main — production deploy (project=docs, --branch=main). | |
| # This is the prod path; CF auto-deploys on the `docs` project are | |
| # disabled, so GHA owns prod end-to-end. | |
| # - workflow_dispatch — manual run; redeploys whatever commit the branch | |
| # is at to the docs project (useful for re-runs after a flake). | |
| on: | |
| push: | |
| branches: | |
| - main | |
| workflow_dispatch: | |
| # contents: write — the one-merge cut (DOC-1245) materializes + pushes the stable | |
| # tag named by versions.toml as a pre-build step (see "Materialize stable tag"). | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| # Opt in early to the Node.js 24 runtime for JavaScript actions. GitHub forces | |
| # this default on 2026-06-02 and removes Node.js 20 on 2026-09-16. Setting the | |
| # variable now silences the deprecation warning, tests compatibility with our | |
| # pinned action versions (checkout@v4, setup-python@v5, upload-artifact@v4, | |
| # setup-uv@v5, wrangler-action@v3, sticky-pull-request-comment@v2), and gives | |
| # us a controlled window to bump if any break. | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" | |
| # Concurrency policy: | |
| # - production deploys use one `prod` group PER LINE, queued in order (never | |
| # cancel), so two merges on the same line can't race on the CF Pages | |
| # project — and, crucially, can't race on cutting the same stable tag (the | |
| # cut runs in-job, so serializing the job serializes the cut). | |
| # - workflow_dispatch: own `manual` group, so manual re-runs never block or | |
| # get blocked by an in-progress production deploy. The ref suffix applies to | |
| # this arm too, so manual runs are per-line as well -- a manual v1 deploy | |
| # cannot evict a manual main one, which is the same guarantee `prod` gets and | |
| # the reason the suffix is unconditional rather than applied to `prod` only. | |
| # | |
| # `github.ref_name` is what makes the group per-line, and it is not cosmetic. | |
| # This policy was written when main was the only branch that deployed, so one | |
| # global `prod` group was the same thing as one group per line. Go-live | |
| # (DOC-1245) made `v1` a deploying line too and nothing revisited the group, | |
| # which left main and v1 sharing one queue. | |
| # | |
| # The failure that produces is silent, and it follows documented behaviour | |
| # rather than a quirk: `cancel-in-progress: false` protects a RUNNING job, but | |
| # per GitHub's concurrency docs, "any existing pending job or workflow in the | |
| # same concurrency group will be canceled and the new queued job or workflow | |
| # will take its place" — newest wins, cancellation setting notwithstanding. | |
| # https://docs.github.com/en/actions/using-jobs/using-concurrency | |
| # | |
| # So a v1 deploy queued behind a main deploy is discarded the moment any second | |
| # main deploy arrives, and it reports `cancelled`, not `failure`: nothing turns | |
| # red and nobody is told. Observed 2026-08-18, where | |
| # v1's deploy queued at 10:20:02 and was evicted at 10:20:17 by a main deploy | |
| # created one second earlier; the v1 line silently did not ship. | |
| # | |
| # Same-line eviction stays, deliberately. Two pushes to the same branch are | |
| # ordered, so the newer commit already contains the older one and superseding | |
| # is correct. Only CROSS-line eviction is wrong, and that is exactly what | |
| # keying on the ref removes. | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && 'manual' || 'prod' }}-${{ github.ref_name }} | |
| cancel-in-progress: false | |
| jobs: | |
| build-and-deploy: | |
| name: "Build and deploy docs" | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: recursive | |
| # 0 (was 1): the multi-version assembly checks out cut tags + main into | |
| # isolated worktrees, so it needs full history + all tags. | |
| fetch-depth: 0 | |
| - name: Set up Hugo | |
| uses: peaceiris/actions-hugo@v3 | |
| with: | |
| hugo-version: '0.161.1' | |
| extended: true | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.12' | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v5 | |
| - name: Print environment | |
| run: | | |
| echo "::group::versions" | |
| hugo version | |
| python --version | |
| uv --version | |
| echo "::endgroup::" | |
| # One-merge cut (DOC-1245): versions.toml names the stable tag /docs/v2 serves. | |
| # If that tag doesn't exist yet, THIS merge is the promotion — materialize it | |
| # (guarded: PyPI-published SDK + versions.toml agrees with the resolver) BEFORE | |
| # the build, so /docs/v2 assembles from it in the same job (no cut↔deploy race). | |
| # Idempotent: an already-cut stable is a no-op. Inert until versioning go-live | |
| # (no versions.toml → skipped), matching the Build-dist feature gate below. | |
| - name: Materialize stable tag if needed (cut) | |
| if: github.event_name == 'push' | |
| env: | |
| # cut-docs-version.sh reads flyteorg/flyte releases for the backend row. | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| REPO_ROOT: ${{ github.workspace }} | |
| run: | | |
| set -euo pipefail | |
| if [ ! -f versions.toml ]; then | |
| echo "no versions.toml → versioning off; skip cut"; exit 0 | |
| fi | |
| STABLE=$(sed -n 's/^stable *= *"\(.*\)"/\1/p' versions.toml | head -1) | |
| if [ -z "$STABLE" ]; then echo "versions.toml has no stable → skip"; exit 0; fi | |
| if git rev-parse -q --verify "refs/tags/$STABLE" >/dev/null; then | |
| echo "stable tag $STABLE already cut → no-op"; exit 0 | |
| fi | |
| echo "stable $STABLE not yet cut → materializing at this merge" | |
| bash unionai-docs-infra/scripts/cut-docs-version.sh --push | |
| - name: Build dist | |
| run: | | |
| start=$(date +%s) | |
| # One shared CI build step (DOC-1333): `auto` = the versions.toml gate, | |
| # identical to the previous inline logic (multi-version assembly when | |
| # present — /docs/latest, /docs/v2, pins — else single-version). | |
| # build-pr.yml calls the SAME script with an explicit mode, so the two | |
| # build paths cannot silently drift again (that drift is how previews | |
| # spent months building a different artifact class than production). | |
| LATEST_REF=origin/main bash unionai-docs-infra/scripts/ci-build-dist.sh auto | |
| echo "BUILD_SECONDS=$(( $(date +%s) - start ))" >> "$GITHUB_ENV" | |
| - name: Check generated links | |
| run: | | |
| # The links the GENERATOR writes into dist/. `Check Internal Links` | |
| # reads content/ and cannot see these: 202 of them pointed at nothing | |
| # while that job stayed green (DOC-1499, DOC-1511, infra#291). | |
| # Runs here rather than in its own workflow because it needs the dist | |
| # this job already built, and a second full Hugo build to check links | |
| # is not worth the minutes. | |
| make check-generated-links | |
| - name: Check rendered images | |
| run: | | |
| # The images the BUILT site serves, resolved the way a browser does -- | |
| # against the page's URL, not the source file's directory. `Check | |
| # Images` reads content/ and cannot see this: Hugo's render hooks | |
| # rebase an image src, so a path can be correct in the markdown and | |
| # wrong in the HTML. unionai-docs#1529 shipped an image that 404s on | |
| # its own preview while all 15 checks passed (DOC-1515, infra#295). | |
| # Same reasoning as the link check above: it needs the dist this job | |
| # already built. | |
| make check-rendered-images | |
| - name: Emit build provenance | |
| if: success() | |
| run: | | |
| # Write build-info.json at dist/docs/ so it's served at | |
| # docs.union.ai/docs/build-info.json (CloudFront routes /docs/* | |
| # to the CF Pages origin). Lets us verify GHA is what's | |
| # actually serving prod after Phase 4 cutover, and gives | |
| # incident response a quick "what version is live?" probe. | |
| mkdir -p dist/docs | |
| cat > dist/docs/build-info.json <<EOF | |
| { | |
| "builder": "github-actions", | |
| "repository": "${{ github.repository }}", | |
| "workflow_file": ".github/workflows/build-and-deploy.yml", | |
| "run_id": "${{ github.run_id }}", | |
| "run_url": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}", | |
| "commit": "${{ github.sha }}", | |
| "ref": "${{ github.ref_name }}", | |
| "event": "${{ github.event_name }}", | |
| "built_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)" | |
| } | |
| EOF | |
| cat dist/docs/build-info.json | |
| - name: Build summary | |
| if: always() | |
| run: | | |
| echo "::group::dist tree (top 2 levels)" | |
| find dist -maxdepth 2 -mindepth 1 -print 2>/dev/null | sort || true | |
| echo "::endgroup::" | |
| echo "::group::dist size" | |
| du -sh dist 2>/dev/null || true | |
| du -sh dist/docs/v2 dist/docs/v1 2>/dev/null || true | |
| echo "::endgroup::" | |
| if [ -n "${BUILD_SECONDS:-}" ]; then | |
| echo "Build wall time: ${BUILD_SECONDS}s" | |
| fi | |
| - name: Upload dist artifact | |
| if: success() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: dist | |
| path: dist/ | |
| retention-days: 7 | |
| if-no-files-found: error | |
| - name: Sanitize branch name for CF Pages | |
| if: success() | |
| id: branch | |
| run: | | |
| # CF Pages branch names: lowercase alphanumeric + hyphens, max 28 chars. | |
| raw="${{ github.ref_name }}" | |
| sanitized=$(echo "$raw" | tr '[:upper:]' '[:lower:]' | sed -E 's#[^a-z0-9]+#-#g' | sed -E 's#^-+|-+$##g' | cut -c1-28) | |
| echo "Source branch: $raw → CF Pages branch: $sanitized" | |
| echo "name=$sanitized" >> "$GITHUB_OUTPUT" | |
| - name: Deploy to Cloudflare Pages | |
| if: success() | |
| id: deploy | |
| # Step-level cap: a normal CF Pages deploy is ~1-3 min. Without this a | |
| # hung wrangler upload eats the whole job's timeout-minutes budget | |
| # (incl. the build) before failing — observed once at ~26 min on a large | |
| # API-docs regen. Fail fast so the deploy can be re-run. See DOC-1229. | |
| timeout-minutes: 10 | |
| uses: cloudflare/wrangler-action@v3 | |
| with: | |
| apiToken: ${{ secrets.CLOUDFLARE_PAGES_API_TOKEN }} | |
| accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | |
| # --commit-dirty: make dist regenerates tracked files (notebooks, | |
| # API docs), so the working tree is always dirty by the time we hit | |
| # wrangler. The warning is noise, not a real condition. | |
| # We don't pass --commit-hash because we use fetch-depth: 1 above | |
| # (shallow checkout), which means the local repo can't resolve the | |
| # SHA wrangler tries to look up and would emit "fatal: bad object". | |
| # wrangler-action records the commit metadata on its own via the | |
| # GITHUB_SHA env var. | |
| command: pages deploy ./dist --project-name=docs --branch=${{ steps.branch.outputs.name }} --commit-dirty=true | |
| - name: Update search index | |
| # After the deploy, not before: the index should describe what is now | |
| # served. Scoped to the slices THIS branch built (main: v2 + latest; | |
| # v1 branch: v1), so the two lines cannot clobber each other's records. | |
| # | |
| # Skips itself when the secret is absent, so forks and any repo without | |
| # Algolia credentials still deploy normally. | |
| if: success() && github.event_name == 'push' | |
| timeout-minutes: 15 | |
| env: | |
| ALGOLIA_DOCS_2_APPLICATION_ID: ${{ secrets.ALGOLIA_DOCS_2_APPLICATION_ID }} | |
| ALGOLIA_DOCS_2_WRITE_API_KEY: ${{ secrets.ALGOLIA_DOCS_2_WRITE_API_KEY }} | |
| run: make index-search | |
| - name: Deploy summary | |
| if: success() | |
| run: | | |
| echo "## Deployment" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "- Project: \`docs\`" >> "$GITHUB_STEP_SUMMARY" | |
| echo "- Branch (CF Pages): \`${{ steps.branch.outputs.name }}\`" >> "$GITHUB_STEP_SUMMARY" | |
| echo "- Commit: \`${{ github.sha }}\`" >> "$GITHUB_STEP_SUMMARY" | |
| if [ -n "${{ steps.deploy.outputs.deployment-url }}" ]; then | |
| echo "- Deployment URL: ${{ steps.deploy.outputs.deployment-url }}" >> "$GITHUB_STEP_SUMMARY" | |
| fi |