Skip to content

fix(cycles): name the order by its number on the live line #1412

fix(cycles): name the order by its number on the live line

fix(cycles): name the order by its number on the live line #1412

Workflow file for this run

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