Repository navigation
fix(cycles): name the order by its number on the live line #1412
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: Documentation | |
| on: | |
| push: | |
| tags: | |
| - 'v*' | |
| - '!v*-*' # exclude pre-release tags (e.g. v0.2.0-beta.0) | |
| branches: | |
| - main | |
| - 'docs/v*' | |
| - '!docs/v*-*' # exclude pre-release doc branches | |
| paths: | |
| - 'docs/**' | |
| - 'docs-site/**' | |
| - '.github/workflows/docs.yml' | |
| # No paths filter here on purpose. docs-build:required is a required status | |
| # check, so it must report a conclusion on EVERY pull request. A workflow-level | |
| # paths filter would skip the whole workflow on docs-less PRs, leaving the | |
| # required check permanently "expected" and blocking the merge (e.g. release | |
| # PRs that only touch Cargo.toml/CHANGELOG.md). Instead the `changes` job below | |
| # gates the build, so unrelated PRs report a passing (skipped) check. This | |
| # mirrors checks.yml / validate-examples.yml. | |
| pull_request: | |
| workflow_dispatch: | |
| permissions: | |
| contents: write | |
| env: | |
| PUBLIC_SITE: https://cli.internetcomputer.org | |
| # Concurrency is split by event so PR validation never shares a lane with deployments: | |
| # - Pull requests get an isolated per-ref lane and cancel superseded builds, so a | |
| # burst of unrelated PRs (or pushes) can no longer cancel a PR's own build before | |
| # it runs — which previously let a broken docs build slip through review. | |
| # - Pushes, tags, and manual runs share the single "pages" lane with | |
| # cancel-in-progress: false: only one deployment runs at a time, queued runs | |
| # between in-progress and latest are skipped, and in-progress production | |
| # deployments are always allowed to complete. | |
| concurrency: | |
| group: ${{ github.event_name == 'pull_request' && format('docs-pr-{0}', github.ref) || 'pages' }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| jobs: | |
| # Detect whether documentation sources changed. Runs only on pull requests; | |
| # pushes/tags/manual runs skip it and rely on always() in `build` below. | |
| # This lets docs-build:required report a passing (skipped) check on PRs that | |
| # don't touch docs, instead of the workflow being filtered out entirely. | |
| changes: | |
| if: github.event_name == 'pull_request' | |
| runs-on: ubuntu-latest | |
| outputs: | |
| docs: ${{ steps.filter.outputs.docs }} | |
| permissions: | |
| pull-requests: read | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 | |
| id: filter | |
| with: | |
| filters: | | |
| docs: | |
| - 'docs/**' | |
| - 'docs-site/**' | |
| - '.github/workflows/docs.yml' | |
| # Build job - validates that docs build successfully | |
| build: | |
| # Explicit :required name follows the repo convention for gating checks and | |
| # gives branch-protection an unambiguous context to require (vs. bare "build"). | |
| name: docs-build:required | |
| needs: changes | |
| # Always build for pushes, tags, and manual runs (the workflow-level push | |
| # paths filter already gates those). On pull requests, build only when docs | |
| # changed; otherwise the job is skipped and docs-build:required reports as a | |
| # passing (skipped) required check. | |
| # | |
| # Fail closed: if `changes` did not succeed (e.g. checkout or the paths-filter | |
| # step failed) its `docs` output is empty, which would skip the build and let | |
| # the required check pass vacuously — bypassing the gate. Building whenever | |
| # `changes.result != 'success'` ensures a broken detection step can't do that. | |
| # | |
| # always() is required because `changes` is skipped on non-PR events and would | |
| # otherwise cascade-skip this job. | |
| if: always() && (github.event_name != 'pull_request' || needs.changes.result != 'success' || needs.changes.outputs.docs == 'true') | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Node | |
| uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 | |
| with: | |
| node-version: '22' | |
| cache: 'npm' | |
| cache-dependency-path: docs-site/package-lock.json | |
| - name: Install dependencies | |
| working-directory: ./docs-site | |
| run: npm ci | |
| - name: Build documentation site | |
| working-directory: ./docs-site | |
| run: npm run build | |
| env: | |
| NODE_ENV: production | |
| PUBLIC_SITE: ${{ env.PUBLIC_SITE }} | |
| # Publish root index, versions list, and IC config files - only runs on main branch | |
| publish-root-files: | |
| needs: build | |
| # `!cancelled()` is a STATUS-CHECK FUNCTION, so GitHub does NOT prepend the | |
| # implicit success() gate to this `if`. We then gate explicitly on build having | |
| # succeeded via `needs.build.result`. This is required because `build` depends on | |
| # the `changes` job, which is SKIPPED on push/workflow_dispatch events. Without a | |
| # status-check function here, the implicit success() would evaluate the whole | |
| # needs chain (including that skipped `changes` job) and skip this job even though | |
| # build itself succeeded — the exact bug that stopped the v1.2 docs from | |
| # publishing (see #671). | |
| if: >- | |
| !cancelled() && needs.build.result == 'success' && | |
| github.ref == 'refs/heads/main' && | |
| (github.event_name == 'push' || github.event_name == 'workflow_dispatch') | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Setup Node | |
| uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 | |
| with: | |
| node-version: '22' | |
| - name: Prepare root files | |
| run: | | |
| mkdir -p root/.well-known | |
| # Copy versions.json from repo | |
| cp docs-site/versions.json root/versions.json | |
| # Copy IC canister config files. `_headers` configures response headers | |
| # for the certified-assets canister (replaces the old .ic-assets.json5); | |
| # it must sit at the root of the deployed directory (dir: .). | |
| cp docs-site/icp.yaml root/icp.yaml | |
| cp docs-site/_headers root/_headers | |
| mkdir -p root/.icp/data/mappings | |
| cp docs-site/.icp/data/mappings/ic.ids.json root/.icp/data/mappings/ic.ids.json | |
| # Copy custom domain file | |
| cp docs-site/public/.well-known/ic-domains root/.well-known/ic-domains | |
| # Copy Matomo analytics script (site ID defined once here, not per-version) | |
| cp docs-site/matomo.js root/matomo.js | |
| # Extract the latest version from versions.json | |
| LATEST_VERSION=$(jq -r ".versions[] | select(.latest == true) | .version" docs-site/versions.json) | |
| # If no releases yet, redirect to main branch docs | |
| if [[ -z "$LATEST_VERSION" ]]; then | |
| echo "⚠️ No releases yet, redirecting to main branch docs" | |
| LATEST_VERSION="main" | |
| else | |
| echo "✅ Redirecting to version: ${LATEST_VERSION}" | |
| fi | |
| # Generate index.html that redirects to latest version | |
| cat > root/index.html << EOF | |
| <!doctype html> | |
| <html> | |
| <head> | |
| <meta http-equiv="refresh" content="0; url=./${LATEST_VERSION}/" /> | |
| <meta name="robots" content="noindex" /> | |
| </head> | |
| </html> | |
| EOF | |
| echo "LATEST_VERSION=${LATEST_VERSION}" >> $GITHUB_ENV | |
| # Generate robots.txt — allow only the latest version, block old versions. | |
| # /main/ is disallowed unless it IS the latest version (no releases yet fallback). | |
| { | |
| echo "User-agent: *" | |
| echo "Allow: /${LATEST_VERSION}/" | |
| for version in $(jq -r '.versions[].version' docs-site/versions.json); do | |
| if [[ "$version" != "$LATEST_VERSION" ]]; then | |
| echo "Disallow: /${version}/" | |
| fi | |
| done | |
| if [[ "$LATEST_VERSION" != "main" ]]; then echo "Disallow: /main/"; fi | |
| echo "" | |
| echo "Sitemap: ${PUBLIC_SITE}/sitemap.xml" | |
| echo "" | |
| echo "# LLM and AI agent discovery (llmstxt.org)" | |
| echo "# ${PUBLIC_SITE}/llms.txt" | |
| echo "# ${PUBLIC_SITE}/llms-full.txt" | |
| } > root/robots.txt | |
| echo "✅ Generated robots.txt (latest: ${LATEST_VERSION})" | |
| # Generate root sitemap.xml pointing directly to the latest version's sitemap. | |
| # Points to sitemap-0.xml (not sitemap-index.xml) to stay spec-compliant: | |
| # a sitemapindex must reference sitemaps, not other sitemapindex files. | |
| { | |
| echo '<?xml version="1.0" encoding="UTF-8"?>' | |
| echo '<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' | |
| echo ' <sitemap>' | |
| echo " <loc>${PUBLIC_SITE}/${LATEST_VERSION}/sitemap-0.xml</loc>" | |
| echo ' </sitemap>' | |
| echo '</sitemapindex>' | |
| } > root/sitemap.xml | |
| echo "✅ Generated sitemap.xml → /${LATEST_VERSION}/sitemap-0.xml" | |
| - name: Copy files from latest version on docs-deployment branch | |
| run: | | |
| git fetch origin docs-deployment --depth=1 | |
| # llms.txt — agent discovery index (versioned copy is always in sync with .md endpoints) | |
| if git show "origin/docs-deployment:${LATEST_VERSION}/llms.txt" > root/llms.txt 2>/dev/null; then | |
| echo "✅ Copied llms.txt from /${LATEST_VERSION}/llms.txt to root" | |
| else | |
| echo "⚠️ No llms.txt found at /${LATEST_VERSION}/llms.txt on docs-deployment — skipping" | |
| rm -f root/llms.txt | |
| fi | |
| # llms-full.txt — full content dump for bulk ingestion / RAG pipelines | |
| if git show "origin/docs-deployment:${LATEST_VERSION}/llms-full.txt" > root/llms-full.txt 2>/dev/null; then | |
| echo "✅ Copied llms-full.txt from /${LATEST_VERSION}/llms-full.txt to root" | |
| else | |
| echo "⚠️ No llms-full.txt found at /${LATEST_VERSION}/llms-full.txt on docs-deployment — skipping" | |
| rm -f root/llms-full.txt | |
| fi | |
| # og-image.png — social sharing preview image | |
| if git show "origin/docs-deployment:${LATEST_VERSION}/og-image.png" > root/og-image.png 2>/dev/null; then | |
| echo "✅ Copied og-image.png from /${LATEST_VERSION}/og-image.png to root" | |
| else | |
| echo "⚠️ No og-image.png found at /${LATEST_VERSION}/og-image.png on docs-deployment — skipping" | |
| rm -f root/og-image.png | |
| fi | |
| # 404.html — use Starlight-generated page from latest version so it matches the docs design; | |
| # asset paths (/_astro/…) are absolute to that version folder which is kept permanently. | |
| if git show "origin/docs-deployment:${LATEST_VERSION}/404.html" > root/404.html 2>/dev/null; then | |
| echo "✅ Copied 404.html from /${LATEST_VERSION}/404.html to root" | |
| else | |
| echo "⚠️ No 404.html found at /${LATEST_VERSION}/404.html on docs-deployment — skipping" | |
| rm -f root/404.html | |
| fi | |
| # feed.xml — RSS feed | |
| if git show "origin/docs-deployment:${LATEST_VERSION}/feed.xml" > root/feed.xml 2>/dev/null; then | |
| echo "✅ Copied feed.xml from /${LATEST_VERSION}/feed.xml to root" | |
| else | |
| echo "⚠️ No feed.xml found at /${LATEST_VERSION}/feed.xml on docs-deployment — skipping" | |
| rm -f root/feed.xml | |
| fi | |
| - name: Prepend version navigation to root llms.txt | |
| if: hashFiles('root/llms.txt') != '' | |
| run: | | |
| # Build a version navigation header from versions.json. | |
| # Only the root llms.txt gets this — versioned copies stay static. | |
| SITE="${PUBLIC_SITE}" | |
| HEADER="This documentation is for v${LATEST_VERSION}. Other versions are available:" | |
| VERSION_LINES="" | |
| for version in $(jq -r '.versions[].version' docs-site/versions.json); do | |
| if [[ "$version" != "$LATEST_VERSION" ]]; then | |
| VERSION_LINES="${VERSION_LINES}\n- [v${version}](${SITE}/${version}/llms.txt)" | |
| fi | |
| done | |
| VERSION_LINES="${VERSION_LINES}\n- [Development (main)](${SITE}/main/llms.txt)" | |
| # Insert after the description blockquote (line starting with ">") | |
| sed -i "s|^> .*|&\n\n${HEADER}${VERSION_LINES}|" root/llms.txt | |
| echo "✅ Added version navigation to root llms.txt" | |
| - name: Deploy root files to docs-deployment branch | |
| uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4.0.0 | |
| with: | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| publish_dir: ./root | |
| publish_branch: docs-deployment | |
| keep_files: true | |
| # Publish main branch docs for preview (always available at /main/) | |
| publish-main-docs: | |
| needs: build | |
| # See publish-root-files: `!cancelled()` suppresses the implicit success() so the | |
| # skipped `changes` job in the needs chain can't cascade-skip this job; we gate | |
| # explicitly on build succeeding instead. | |
| if: >- | |
| !cancelled() && needs.build.result == 'success' && | |
| github.ref == 'refs/heads/main' && | |
| (github.event_name == 'push' || github.event_name == 'workflow_dispatch') | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Node | |
| uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 | |
| with: | |
| node-version: '22' | |
| cache: 'npm' | |
| cache-dependency-path: docs-site/package-lock.json | |
| - name: Install dependencies | |
| working-directory: ./docs-site | |
| run: npm ci | |
| - name: Build main branch docs | |
| working-directory: ./docs-site | |
| run: | | |
| echo "Building with base: /main/" | |
| npm run build | |
| env: | |
| NODE_ENV: production | |
| PUBLIC_BASE_PATH: /main/ | |
| PUBLIC_SITE: ${{ env.PUBLIC_SITE }} | |
| - name: Deploy main docs to docs-deployment branch | |
| uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4.0.0 | |
| with: | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| publish_dir: ./docs-site/dist | |
| publish_branch: docs-deployment | |
| destination_dir: main | |
| keep_files: true | |
| # Publish versioned docs - runs on tags (v*) or docs branches (docs/v*) | |
| publish-versioned-docs: | |
| needs: build | |
| # See publish-root-files: `!cancelled()` suppresses the implicit success() so the | |
| # skipped `changes` job in the needs chain can't cascade-skip this job; we gate | |
| # explicitly on build succeeding instead. This is the job that publishes /X.Y/; | |
| # the cascade-skip here is why the v1.2.0 tag push produced no /1.2/ folder. | |
| if: >- | |
| !cancelled() && needs.build.result == 'success' && | |
| github.event_name == 'push' && | |
| (startsWith(github.ref, 'refs/tags/v') || startsWith(github.ref, 'refs/heads/docs/v')) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Node | |
| uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 | |
| with: | |
| node-version: '22' | |
| cache: 'npm' | |
| cache-dependency-path: docs-site/package-lock.json | |
| - name: Install dependencies | |
| working-directory: ./docs-site | |
| run: npm ci | |
| - name: Extract version from tag or branch | |
| run: | | |
| if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then | |
| # Tag: v0.1.0 -> extract major.minor -> 0.1 | |
| VERSION=${GITHUB_REF#refs/tags/v} | |
| # Strip patch version (0.1.0 -> 0.1) | |
| VERSION=${VERSION%.*} | |
| echo "DOCS_VERSION=${VERSION}" >> $GITHUB_ENV | |
| echo "DOCS_BASE_PATH=/${VERSION}/" >> $GITHUB_ENV | |
| elif [[ "${GITHUB_REF}" == refs/heads/docs/v* ]]; then | |
| # Branch: docs/v0.1 -> extract version -> 0.1 | |
| VERSION=${GITHUB_REF#refs/heads/docs/v} | |
| echo "DOCS_VERSION=${VERSION}" >> $GITHUB_ENV | |
| echo "DOCS_BASE_PATH=/${VERSION}/" >> $GITHUB_ENV | |
| else | |
| echo "❌ Docs should only be published for version tags (v*) or docs branches (docs/v*)" | |
| echo "Current ref: ${GITHUB_REF}" | |
| exit 1 | |
| fi | |
| echo "Building docs for version ${VERSION} with base: /${VERSION}/" | |
| - name: Build documentation site | |
| working-directory: ./docs-site | |
| run: npm run build | |
| env: | |
| NODE_ENV: production | |
| PUBLIC_BASE_PATH: ${{ env.DOCS_BASE_PATH }} | |
| PUBLIC_SITE: ${{ env.PUBLIC_SITE }} | |
| - name: Deploy versioned docs to docs-deployment branch | |
| uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4.0.0 | |
| with: | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| publish_dir: ./docs-site/dist | |
| publish_branch: docs-deployment | |
| destination_dir: ${{ env.DOCS_VERSION }} | |
| # Deploy docs-deployment branch to IC asset canister. | |
| # always() prevents auto-skip when some publish jobs are skipped (e.g. | |
| # publish-versioned-docs is legitimately skipped on main-branch pushes). | |
| # | |
| # `contains(needs.*.result, 'success')` requires that AT LEAST ONE publish job | |
| # actually succeeded. Without it, a run where ALL three publish jobs were skipped | |
| # would still deploy — silently re-pushing the stale docs-deployment branch to IC | |
| # mainnet and reporting green. That is exactly what masked the v1.2 failure: every | |
| # publish job skipped, yet this job ran and redeployed unchanged content. Now, if | |
| # nothing was published there is nothing to deploy, and the run makes that visible. | |
| deploy-to-ic: | |
| needs: [publish-root-files, publish-main-docs, publish-versioned-docs] | |
| if: >- | |
| always() && github.event_name != 'pull_request' && | |
| !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') && | |
| contains(needs.*.result, 'success') | |
| uses: ./.github/workflows/docs-deploy.yml | |
| secrets: inherit |