[AAASM-3523] 📝 (docs): Standardize reference links to latest stable channel #410
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 | |
| permissions: | |
| contents: read | |
| pages: write | |
| id-token: write | |
| on: | |
| pull_request: | |
| branches: [master] | |
| paths: | |
| - 'docs/**' | |
| - 'README.md' | |
| - 'CONTRIBUTING.md' | |
| - '.github/workflows/docs.yml' | |
| push: | |
| branches: [master] | |
| paths: | |
| - 'docs/**' | |
| - 'README.md' | |
| - 'CONTRIBUTING.md' | |
| - '.github/workflows/docs.yml' | |
| # Release-driven frozen-version cut (AAASM-2752). Instead of reacting to a | |
| # raw tag push, the frozen snapshot is cut only after the Release workflow | |
| # has *succeeded* for that tag. This guarantees we never publish docs for a | |
| # release whose binaries/crates failed to build. The triggering run's | |
| # head_branch is the tag name (Release fires on `push: tags`). | |
| workflow_run: | |
| workflows: ["Release"] | |
| types: [completed] | |
| jobs: | |
| build: | |
| name: Build mdBook | |
| runs-on: ubuntu-latest | |
| # PR + master-push always build. A workflow_run only proceeds when the | |
| # upstream Release succeeded AND it was a release tag (vX.Y.Z[-pre]). | |
| if: >- | |
| github.event_name != 'workflow_run' || | |
| (github.event.workflow_run.conclusion == 'success' && | |
| startsWith(github.event.workflow_run.head_branch, 'v')) | |
| steps: | |
| - uses: actions/checkout@v7.0.0 | |
| with: | |
| # Full history so the last-changed preprocessor can read per-page | |
| # git dates; a shallow clone would collapse every page to one commit. | |
| fetch-depth: 0 | |
| # For a release cut, build the docs from the tagged commit, not master. | |
| ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.ref }} | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Cache mdBook binaries | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| ~/.cargo/bin/mdbook | |
| ~/.cargo/bin/mdbook-mermaid | |
| key: ${{ runner.os }}-mdbook-0.5.2-mermaid-0.17.0 | |
| - name: Install mdBook | |
| run: | | |
| if ! command -v mdbook >/dev/null; then | |
| cargo install --locked --version 0.5.2 mdbook | |
| fi | |
| - name: Install mdbook-mermaid | |
| run: | | |
| if ! command -v mdbook-mermaid >/dev/null; then | |
| cargo install --locked --version 0.17.0 mdbook-mermaid | |
| fi | |
| # Guard the pre-release semver gate (docs/ci/channels.py): a logic | |
| # regression here would silently mis-classify release channels, so run | |
| # the channel-gate unit tests on every PR/push before building. | |
| - name: Run channel-gate unit tests | |
| run: python3 docs/ci/test_channels.py | |
| - name: Build book | |
| run: mdbook build docs | |
| # --- Doc versioning (AAASM-2752) --- | |
| # The Pages site is laid out as per-version subpaths under `_site/`: | |
| # _site/index.html -> root redirect (stable -> pre-release -> latest) | |
| # _site/versions.json -> manifest read by the selector + warning banner | |
| # _site/latest/ -> live docs tracking master | |
| # _site/<vX.Y.Z[-pre]>/ -> frozen, immutable release snapshots | |
| # | |
| # versions.json describes moving CHANNELS that point at a concrete | |
| # subpath, plus immutable ARCHIVED concrete versions: | |
| # { "channels": [ {id,title,target}, ... ], | |
| # "archived": [ {id,title}, ... ] } | |
| # Channels: latest (master), pre-release (newest vX.Y.Z-pre tag), | |
| # stable (newest vX.Y.Z tag). Channel id classification: | |
| # ^v\d+\.\d+\.\d+$ -> stable | |
| # ^v\d+\.\d+\.\d+-.+ -> pre-release | |
| # | |
| # On master push we publish into latest/. On a *successful* Release | |
| # (workflow_run) we publish into <tag>/, archive it, and repoint the | |
| # stable or pre-release channel to it. | |
| - name: Resolve target version subpath | |
| if: github.event_name != 'pull_request' | |
| id: docver | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| RELEASE_TAG: ${{ github.event.workflow_run.head_branch }} | |
| run: | | |
| set -euo pipefail | |
| if [ "$EVENT_NAME" = "workflow_run" ]; then | |
| tag="$RELEASE_TAG" | |
| # Classify the channel from the tag shape. | |
| if printf '%s' "$tag" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then | |
| channel="stable" | |
| elif printf '%s' "$tag" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+-.+'; then | |
| channel="pre-release" | |
| else | |
| echo "::error::Tag '$tag' is not a release version; skipping docs cut." | |
| exit 1 | |
| fi | |
| echo "version=$tag" >> "$GITHUB_OUTPUT" | |
| echo "channel=$channel" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "version=latest" >> "$GITHUB_OUTPUT" | |
| echo "channel=latest" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Fetch prior live versions.json | |
| # AAASM-2752 + AAASM-2827: the live manifest is the only place that | |
| # carries the moving channel targets (pre-release -> vX.Y.Z-pre). | |
| # archived[] is rebuilt from git tags in the next step so a missing | |
| # or truncated prior manifest is no longer a permanent loss. | |
| if: github.event_name != 'pull_request' | |
| id: restore | |
| continue-on-error: true | |
| run: | | |
| base_url="https://${GITHUB_REPOSITORY_OWNER}.github.io/${GITHUB_REPOSITORY##*/}" | |
| echo "Fetching existing versions.json from ${base_url}/versions.json" | |
| if curl -fsSL "${base_url}/versions.json" -o prior-versions.json; then | |
| echo "prior_versions=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "No existing versions.json (first deploy)." | |
| echo "prior_versions=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Rebuild every release tag's docs subpath from git | |
| # AAASM-2827: the previous design mirrored prior subpaths from the | |
| # live Pages site with `wget -r`, which silently lost versions when | |
| # the mirror failed and only worked if the prior deploy was itself | |
| # whole. Rebuild every archived release directly from its git tag | |
| # instead — git is the source of truth, and this loop is idempotent | |
| # and self-healing: if a prior release never published docs (e.g. | |
| # because its Release workflow failed), it is recovered on the next | |
| # successful deploy. | |
| if: github.event_name != 'pull_request' | |
| env: | |
| VERSION: ${{ steps.docver.outputs.version }} | |
| run: | | |
| set -euo pipefail | |
| mkdir -p _site | |
| # Enumerate every release tag (vX.Y.Z[-pre]) reachable in the | |
| # repository. The workflow's checkout uses fetch-depth: 0 so all | |
| # tags are local. | |
| mapfile -t TAGS < <(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' | sort) | |
| : > extra-archived.txt | |
| # Workspace for tag worktrees. We use git worktree (cheaper than | |
| # a fresh clone per tag) and tear each one down after use. | |
| BUILD_ROOT="$(mktemp -d)" | |
| trap 'rm -rf "$BUILD_ROOT" || true' EXIT | |
| for tag in "${TAGS[@]}"; do | |
| # Skip the tag being cut this run; the freshly built docs/book | |
| # is placed in _site/$VERSION below by the assemble step. | |
| if [ "$tag" = "$VERSION" ]; then | |
| echo "$tag" >> extra-archived.txt | |
| continue | |
| fi | |
| # Already restored from the artifact upload of a prior step? | |
| # (Defensive — no current step pre-populates _site, but this | |
| # keeps the loop idempotent if one is added later.) | |
| if [ -f "_site/$tag/index.html" ]; then | |
| echo "Skipping $tag (already present in _site)" | |
| echo "$tag" >> extra-archived.txt | |
| continue | |
| fi | |
| workdir="$BUILD_ROOT/$tag" | |
| echo "::group::Rebuild $tag" | |
| # `git worktree add --detach` checks out the tag's tree without | |
| # touching the current branch state. A tag whose docs/ subtree | |
| # is missing or whose mdBook config has drifted past this | |
| # workflow's mdbook version is logged and skipped — its subpath | |
| # will fall back to the prior live mirror (if any) or 404. | |
| if ! git worktree add --detach "$workdir" "$tag" >/dev/null 2>&1; then | |
| echo "::warning::Could not check out $tag; skipping." | |
| echo "::endgroup::" | |
| continue | |
| fi | |
| if [ ! -f "$workdir/docs/book.toml" ]; then | |
| echo "::warning::$tag has no docs/book.toml; skipping." | |
| git worktree remove --force "$workdir" >/dev/null 2>&1 || true | |
| echo "::endgroup::" | |
| continue | |
| fi | |
| if ! mdbook build "$workdir/docs"; then | |
| echo "::warning::mdbook build failed for $tag; skipping." | |
| git worktree remove --force "$workdir" >/dev/null 2>&1 || true | |
| echo "::endgroup::" | |
| continue | |
| fi | |
| mkdir -p "_site/$tag" | |
| cp -R "$workdir/docs/book/." "_site/$tag/" | |
| git worktree remove --force "$workdir" >/dev/null 2>&1 || true | |
| echo "$tag" >> extra-archived.txt | |
| echo "::endgroup::" | |
| done | |
| echo "Rebuilt archive subpaths:" | |
| find _site -maxdepth 1 -mindepth 1 -type d | sort | |
| echo "extra-archived.txt:" | |
| cat extra-archived.txt | |
| - name: Assemble versioned Pages site | |
| if: github.event_name != 'pull_request' | |
| env: | |
| VERSION: ${{ steps.docver.outputs.version }} | |
| CHANNEL: ${{ steps.docver.outputs.channel }} | |
| run: | | |
| set -euo pipefail | |
| mkdir -p _site | |
| # 1. Place the freshly built book at the target version subpath. | |
| # Archive subpaths for all *other* release tags were already | |
| # materialised by the rebuild step above. | |
| rm -rf "_site/$VERSION" | |
| mkdir -p "_site/$VERSION" | |
| cp -R docs/book/. "_site/$VERSION/" | |
| # 2. Recompute versions.json via the testable channel module | |
| # (docs/ci/channels.py), which applies the pre-release semver | |
| # gate and unions extra-archived.txt (written by the rebuild | |
| # step) into archived[]. AAASM-2827: this makes archived[] | |
| # self-healing against a missing/truncated prior manifest. | |
| python3 docs/ci/build_versions.py "$VERSION" "$CHANNEL" _site/versions.json | |
| # 3. Root redirect (stable -> pre-release -> latest, resolved client-side). | |
| cp docs/site-root-index.html _site/index.html | |
| echo "Assembled _site:"; find _site -maxdepth 2 -type d | sort | |
| - name: Upload Pages artifact | |
| if: github.event_name != 'pull_request' | |
| uses: actions/upload-pages-artifact@v5 | |
| with: | |
| path: _site | |
| deploy: | |
| name: Deploy to GitHub Pages | |
| if: github.event_name != 'pull_request' | |
| needs: build | |
| runs-on: ubuntu-latest | |
| environment: | |
| name: github-pages | |
| url: ${{ steps.deployment.outputs.page_url }} | |
| steps: | |
| - name: Deploy to GitHub Pages | |
| id: deployment | |
| uses: actions/deploy-pages@v5 | |
| verify-commands: | |
| name: Verify documented commands (Linux) | |
| # Only on PR / master push; a release cut re-verifies nothing new. | |
| if: github.event_name != 'workflow_run' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7.0.0 | |
| - name: Install system dependencies (protoc, pkg-config, libssl-dev) | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y protobuf-compiler pkg-config libssl-dev | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Cache cargo build | |
| uses: Swatinem/rust-cache@v2 | |
| - name: Verify README "Getting Started" build command | |
| run: cargo build --workspace --exclude aa-ebpf | |
| - name: Verify api-reference.md rustdoc command | |
| run: cargo doc --workspace --no-deps --exclude aa-ebpf | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} |