fix(router): reconcile request-path overload marks (#12540) #41
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
| # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | |
| # SPDX-License-Identifier: Apache-2.0 | |
| # | |
| # Licensed under the Apache License, Version 2.0 (the "License"); | |
| # you may not use this file except in compliance with the License. | |
| # You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. | |
| # Trusted Fern Documentation Workflow | |
| # | |
| # This workflow handles trusted Fern documentation automation: | |
| # | |
| # Safe pre-merge Fern lint runs in pre-merge.yml on normal pull_request events. | |
| # | |
| # 1. SYNC & PUBLISH/PREVIEW: Syncs docs/ from source branch to fern/ on docs-website | |
| # - Triggers on trusted pushes to main or pull-request/N when docs-site files change | |
| # - On main: commits and pushes to docs-website, then publishes via `fern generate --docs` | |
| # - On gated PR branches: generates a preview URL via `fern generate --docs --preview` and comments on PR | |
| # - Preserves release-managed versions from docs-website's docs.yml | |
| # | |
| # 2. VERSION RELEASE (tags): Creates versioned documentation snapshot | |
| # - Triggers on new version tags (vX.Y.Z format) | |
| # - Creates fern/pages-vX.Y.Z/ directory on docs-website branch | |
| # - Updates fern/docs.yml with new version entry | |
| # - Publishes docs to Fern after releasing | |
| # | |
| # Note: The publish step is included inline because pushes made with GITHUB_TOKEN | |
| # do not trigger other workflows (GitHub's anti-recursion guard), so we cannot | |
| # rely on a separate publish-fern-docs.yml workflow for bot-initiated pushes. | |
| # | |
| # Validation: the sync/release composition (rsync scopes, nav path transforms, | |
| # the shared-Reference machinery) only executes on main pushes and tag cuts. | |
| # Before changing it, replay both jobs locally against the docs-website branch: | |
| # docs/fern/scripts/simulate_docs_website.sh | |
| name: Fern Docs | |
| on: | |
| push: | |
| branches: | |
| - main | |
| - "pull-request/[0-9]+" | |
| tags: | |
| # Match only clean semver tags: vX.Y.Z | |
| - 'v[0-9]+.[0-9]+.[0-9]+' | |
| schedule: | |
| # Refresh the Home page from the public Google Calendar. | |
| - cron: '0 */6 * * *' | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: 'Version tag to release (e.g., v0.9.0). Leave empty to sync dev docs.' | |
| required: false | |
| type: string | |
| force_rebuild: | |
| description: 'Overwrite an existing version snapshot (manual dispatch only). This replaces pages-$TAG and versions/$TAG.yml.' | |
| required: false | |
| default: false | |
| type: boolean | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| jobs: | |
| # Detect changed files for conditional job execution | |
| changed-files: | |
| runs-on: ubuntu-latest | |
| # Skip for tag pushes - version release doesn't need changed-files check | |
| if: github.ref_type != 'tag' && github.event_name != 'schedule' | |
| outputs: | |
| docs: ${{ steps.changes.outputs.docs }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| # Do not use fetch-depth: 0 — changed-files now works with shallow clone | |
| - name: Check for changes | |
| id: changes | |
| uses: ./.github/actions/changed-files | |
| with: | |
| gh_token: ${{ github.token }} | |
| ############################################################################# | |
| # TRUSTED SYNC & PUBLISH/PREVIEW - Syncs docs content to docs-website structure | |
| # On main: commits, pushes, and publishes to Fern | |
| # On gated PR branches: generates a preview URL and comments on the PR | |
| ############################################################################# | |
| preview-or-publish-docs: | |
| name: Preview or publish docs | |
| needs: changed-files | |
| if: | | |
| always() && | |
| github.ref_type != 'tag' && | |
| (github.event_name == 'schedule' || needs.changed-files.outputs.docs == 'true' || github.event_name == 'workflow_dispatch') && | |
| (github.event.inputs.tag == '' || github.event.inputs.tag == null) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Determine context | |
| id: ctx | |
| run: | | |
| if [ "$GITHUB_REF" = "refs/heads/main" ]; then | |
| echo "is_main=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "is_main=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Checkout source branch | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| path: source-checkout | |
| fetch-depth: 1 | |
| - name: Checkout docs-website branch | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: docs-website | |
| path: docs-checkout | |
| fetch-depth: 1 | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 | |
| with: | |
| node-version: '22' | |
| - name: Fetch community events from Google Calendar | |
| run: | | |
| # Install the pinned generator deps from the committed lockfile rather than | |
| # a floating `npm install` — reproducible and offline-cache-friendly. | |
| npm ci --prefix source-checkout/.github/scripts/events | |
| # generate-events.js lives at .github/scripts/ and `require('node-ical')`s, | |
| # so point Node at the events node_modules (npm --prefix installs there). | |
| NODE_PATH=source-checkout/.github/scripts/events/node_modules \ | |
| node source-checkout/.github/scripts/generate-events.js | |
| - name: Sync dev content from main | |
| run: | | |
| # Authored content lives under docs/fern/pages/. Mirror that directory | |
| # directly into the docs-website branch's pages-dev/ snapshot. The Home | |
| # page is copied separately to fern/index.mdx below. | |
| echo "Syncing content pages to docs-website branch..." | |
| rm -rf docs-checkout/fern/pages-dev | |
| mkdir -p docs-checkout/fern/pages-dev | |
| rsync -a \ | |
| --exclude='/home/index.mdx' \ | |
| source-checkout/docs/fern/pages/ docs-checkout/fern/pages-dev/ | |
| # pages/ is a source-only container. Snapshot pages sit one directory | |
| # shallower, so adjust build-time Code includes that escape to examples/. | |
| python3 source-checkout/docs/fern/scripts/rewrite_snapshot_paths.py \ | |
| docs-checkout/fern/pages-dev | |
| # Backend deploy manifests are embedded into templates/{vllm,sglang, | |
| # trtllm}.mdx and DGDR manifests into recipes/kubernetes-templates/dgdr.mdx via build-time | |
| # <Code src="../../../examples/..."> file reads. Those resolve to the | |
| # repo-root examples/ tree in the source layout, which does not exist | |
| # on docs-website — sync just the referenced subtrees to the branch | |
| # root so both ../../../examples (from pages-dev) and the versioned | |
| # snapshots resolve. | |
| echo "Syncing examples/backends/*/deploy/ and examples/deployments/dgdr/ to branch root..." | |
| rsync -a \ | |
| --include='*/' \ | |
| --include='backends/*/deploy/**' \ | |
| --include='deployments/dgdr/**' \ | |
| --exclude='*' \ | |
| --prune-empty-dirs \ | |
| source-checkout/examples/ docs-checkout/examples/ | |
| # Sync index.yml as versions/dev.yml and transform paths for docs-website layout | |
| echo "Syncing index.yml to docs-website branch as versions/dev.yml..." | |
| cp source-checkout/docs/fern/index.yml docs-checkout/fern/versions/dev.yml | |
| # Sync fern.config.json | |
| echo "Syncing fern.config.json to docs-website branch..." | |
| cp source-checkout/docs/fern/fern.config.json docs-checkout/fern/fern.config.json | |
| # Sync README.md (developer guide, referenced from dev.yml) | |
| if [ -f source-checkout/docs/fern/pages/developer-guide/contributing/documentation/building-and-publishing.md ]; then | |
| cp source-checkout/docs/fern/pages/developer-guide/contributing/documentation/building-and-publishing.md docs-checkout/fern/README.md | |
| fi | |
| # Sync .gitignore if it exists | |
| if [ -f source-checkout/docs/fern/.gitignore ]; then | |
| cp source-checkout/docs/fern/.gitignore docs-checkout/fern/.gitignore | |
| fi | |
| # Sync the callout converter under fern/scripts/ and remove its old | |
| # root-level location from docs-website after the source-tree move. | |
| rm -f docs-checkout/fern/convert_callouts.py | |
| if [ -f source-checkout/docs/fern/scripts/convert_callouts.py ]; then | |
| mkdir -p docs-checkout/fern/scripts | |
| cp source-checkout/docs/fern/scripts/convert_callouts.py docs-checkout/fern/scripts/convert_callouts.py | |
| fi | |
| # Sync components/ directory (e.g., CustomFooter.tsx) | |
| if [ -d source-checkout/docs/fern/components ]; then | |
| echo "Syncing components/ to docs-website branch..." | |
| rm -rf docs-checkout/fern/components | |
| cp -r source-checkout/docs/fern/components docs-checkout/fern/components | |
| fi | |
| # Remove the legacy product configs and sync the standalone Home page | |
| # used by docs.yml and the Home tab. | |
| rm -rf docs-checkout/fern/products | |
| cp source-checkout/docs/fern/pages/home/index.mdx docs-checkout/fern/index.mdx | |
| sed -i 's|\.\./\.\./assets/|./assets/|g' docs-checkout/fern/index.mdx | |
| # Sync root-level assets/ (docs.yml logos/fonts and index.mdx reference | |
| # ./assets/ relative to the Fern root, not pages-dev). Merge-copy without | |
| # deleting so assets still referenced by older versioned pages survive. | |
| if [ -d source-checkout/docs/fern/assets ]; then | |
| echo "Syncing assets/ to docs-website branch..." | |
| cp -r source-checkout/docs/fern/assets/. docs-checkout/fern/assets/ | |
| fi | |
| # Sync legacy Digest asset mirror | |
| if [ -d source-checkout/docs/fern/pages/blog/_assets ]; then | |
| echo "Syncing Blog assets to the legacy digest/ compatibility path..." | |
| # Keep legacy root Digest pages for older versions that still reference ../digest/. | |
| mkdir -p docs-checkout/fern/digest | |
| cp -r source-checkout/docs/fern/pages/blog/_assets/. docs-checkout/fern/digest/ | |
| # The digest posts were renamed .md -> .mdx at the source; retarget the | |
| # release-managed versions/v*.yml snapshots that still point at the old | |
| # extension. Harmless no-op once every snapshot references .mdx. | |
| sed -i 's|\(path: \.\./digest/.*\)\.md$|\1.mdx|' docs-checkout/fern/versions/v*.yml | |
| fi | |
| # Sync site-wide CSS and the Reference-page interaction script. | |
| if [ -f source-checkout/docs/fern/main.css ]; then | |
| echo "Syncing main.css to docs-website branch..." | |
| cp source-checkout/docs/fern/main.css docs-checkout/fern/main.css | |
| fi | |
| if [ -f source-checkout/docs/fern/custom.js ]; then | |
| echo "Syncing custom.js to docs-website branch..." | |
| cp source-checkout/docs/fern/custom.js docs-checkout/fern/custom.js | |
| fi | |
| # Sync Fern's native-localization mirror. Guarded on the source tree so | |
| # this no-ops cleanly until docs/fern/translations/ is present. | |
| # fern/translations/<lang>/pages-dev/<path> pairs with the base page at | |
| # fern/pages-dev/<path>, per locale. Source mirrors live under pages/. | |
| # Replace only the pages-dev mirrors -- | |
| # versioned snapshots (pages-v*) are committed by the release job and must | |
| # survive dev syncs (see #11195). | |
| if [ -d source-checkout/docs/fern/translations ]; then | |
| echo "Syncing translations/ to docs-website branch..." | |
| for d in docs-checkout/fern/translations/*/pages-dev; do | |
| [ -d "$d" ] || continue | |
| lang=$(basename "$(dirname "$d")") | |
| if [ ! -d "source-checkout/docs/fern/translations/$lang/pages" ]; then | |
| echo "Removing dev mirror for retired locale $lang..." | |
| rm -rf "$d" | |
| fi | |
| done | |
| for lang_dir in source-checkout/docs/fern/translations/*/; do | |
| lang=$(basename "$lang_dir") | |
| if [ -d "$lang_dir/pages" ]; then | |
| rm -rf "docs-checkout/fern/translations/$lang/pages-dev" | |
| mkdir -p "docs-checkout/fern/translations/$lang" | |
| cp -r "$lang_dir/pages" "docs-checkout/fern/translations/$lang/pages-dev" | |
| fi | |
| done | |
| fi | |
| - name: Transform paths in dev.yml for docs-website layout | |
| run: | | |
| # In the source repo, index.yml uses paths under pages/ (for example, pages/cli/getting-started/quickstart.mdx). | |
| # On docs-website, fern/versions/dev.yml needs ../pages-dev/ prefix for content | |
| # and ../ prefix for the legacy Digest compatibility tree. | |
| # | |
| # Transform the standalone Home page first, then map the pages/ source prefix | |
| # directly onto pages-dev/. | |
| yq -i '(.. | select(has("path")).path) |= sub("^digest/", "../digest/")' docs-checkout/fern/versions/dev.yml | |
| yq -i '(.. | select(has("path")).path) |= sub("^pages/home/index\.mdx$", "../index.mdx")' docs-checkout/fern/versions/dev.yml | |
| yq -i '(.. | select(has("path")).path) |= sub("^pages/", "../pages-dev/")' docs-checkout/fern/versions/dev.yml | |
| - name: Propagate shared Reference nav to released versions | |
| run: | | |
| # The Reference tab's General variant (Compatibility, Release | |
| # Artifacts, Releases, Known Issues, Deprecations, Model EA Builds, | |
| # Glossary) is SHARED across versions: release snapshots keep its nav | |
| # paths on ../pages-dev/ so every version dropdown renders the | |
| # always-current reference. When a page is added to the shared | |
| # reference on main (e.g. a new release-notes page), copy the | |
| # variant's nav block from dev.yml into each released version's yml | |
| # so the new page appears in every dropdown, not just dev. Version | |
| # snapshots cut before the reference rework have no such variant and | |
| # are left untouched (the yq selection matches nothing). | |
| yq '[.navigation[] | select(.tab == "reference") | .variants[] | select(.title == "General")][0]' \ | |
| docs-checkout/fern/versions/dev.yml > /tmp/reference_general_variant.yml | |
| if [ "$(yq 'length' /tmp/reference_general_variant.yml)" = "0" ] || \ | |
| [ "$(head -c4 /tmp/reference_general_variant.yml)" = "null" ]; then | |
| echo "No shared Reference General variant in dev.yml; skipping propagation" | |
| else | |
| for vfile in docs-checkout/fern/versions/v*.yml; do | |
| [ -e "$vfile" ] || continue | |
| # Only rewrite files that actually carry the shared variant — | |
| # yq -i normalizes whitespace, so touching pre-rework snapshots | |
| # (which have no reference General variant) is pure churn. | |
| if [ "$(yq '[.navigation[] | select(.tab == "reference") | .variants[] | select(.title == "General")] | length' "$vfile")" != "0" ]; then | |
| yq -i '(.navigation[] | select(.tab == "reference") | .variants[] | select(.title == "General")) = load("/tmp/reference_general_variant.yml")' "$vfile" | |
| echo "Synced shared Reference nav into $vfile" | |
| else | |
| echo "Skipped (no shared Reference variant): $vfile" | |
| fi | |
| done | |
| fi | |
| - name: Convert GitHub callouts to Fern format | |
| run: | | |
| echo "Converting GitHub-style callouts to Fern format in pages/..." | |
| python3 docs-checkout/fern/scripts/convert_callouts.py --dir docs-checkout/fern/pages-dev | |
| # Convert the localized mirrors too (no-op when translations/ is absent). | |
| if [ -d docs-checkout/fern/translations ]; then | |
| python3 docs-checkout/fern/scripts/convert_callouts.py --dir docs-checkout/fern/translations | |
| fi | |
| echo "Callout conversion complete." | |
| - name: Resolve relative links in translated pages | |
| run: | | |
| # Rewrites relative page links in translations to site URLs computed | |
| # from the current nav (Fern's early-access localization does not yet | |
| # resolve them; see docs/fern/scripts/resolve_translation_links.py). | |
| # Guarded on both the mirror and resolver so it no-ops until A4 lands them. | |
| if [ -d docs-checkout/fern/translations ] && [ -f source-checkout/docs/fern/scripts/resolve_translation_links.py ]; then | |
| pip install --quiet pyyaml | |
| python3 source-checkout/docs/fern/scripts/resolve_translation_links.py \ | |
| --nav source-checkout/docs/fern/index.yml \ | |
| --translations-root docs-checkout/fern/translations \ | |
| --site-root /dynamo --version-slug dev --pages-dir pages-dev | |
| fi | |
| - name: Update docs.yml preserving versions | |
| run: | | |
| cd docs-checkout/fern | |
| # Preserve the release-managed version list. Production may still use | |
| # the older Docs/Dynamo product wrapper during the first sync after this | |
| # branch's navigation refactor, so accept either shape. | |
| yq '. as $doc | ([$doc.products[]? | select(.display-name == "Docs" or .display-name == "Dynamo")][0].versions // $doc.versions)' \ | |
| docs.yml > /tmp/preserved_versions.yml | |
| if [ "$(head -c4 /tmp/preserved_versions.yml)" = "null" ]; then | |
| echo "::error::No release-managed versions found in docs.yml" | |
| exit 1 | |
| fi | |
| echo "Preserved release state:" | |
| cat /tmp/preserved_versions.yml | |
| # Copy docs.yml from source to get config updates (redirects, layout, etc.) | |
| cp ../../source-checkout/docs/fern/docs.yml docs.yml | |
| # Fix paths for the composed docs-website layout. Assets stay at the | |
| # Fern root, while the source Home page is copied to fern/index.mdx. | |
| sed -i 's|\.\./docs/assets/|./assets/|g' docs.yml | |
| yq -i '."landing-page".path = "./index.mdx"' docs.yml | |
| # The refactored source uses standalone top-level versioning rather than | |
| # a product switcher. Restore the release history in that native shape. | |
| yq -i '.versions = load("/tmp/preserved_versions.yml")' docs.yml | |
| # Inject the private NVIDIA global theme for production builds only. | |
| # It is intentionally absent from the source repo's docs.yml so that | |
| # external contributors can run `fern docs dev` without an nvidia-org | |
| # FERN_TOKEN (otherwise the CLI 403s fetching the private theme and the | |
| # local preview renders blank). See https://github.com/ai-dynamo/dynamo/issues/10073 | |
| # | |
| # PR previews skip the theme: applying it masks the project `css:` | |
| # stylesheet in hosted builds, so previews of any fern/main.css change | |
| # render unstyled. The repo main.css already carries the NVIDIA | |
| # branding rules, so unthemed previews stay visually faithful. | |
| if [ "${{ steps.ctx.outputs.is_main }}" = "true" ]; then | |
| yq -i '.global-theme = "nvidia"' docs.yml | |
| else | |
| yq -i 'del(.global-theme)' docs.yml | |
| fi | |
| echo "Updated docs.yml:" | |
| cat docs.yml | |
| - name: Check for changes | |
| id: changes | |
| run: | | |
| cd docs-checkout | |
| if [ -z "$(git status --porcelain)" ]; then | |
| echo "has_changes=false" >> $GITHUB_OUTPUT | |
| echo "No changes detected" | |
| else | |
| echo "has_changes=true" >> $GITHUB_OUTPUT | |
| echo "Changes detected:" | |
| git status --short | |
| fi | |
| - name: Install Fern CLI | |
| if: steps.changes.outputs.has_changes == 'true' | |
| run: npm install -g fern-api | |
| ########################################################################## | |
| # PREVIEW - Generate a preview URL for docs changes | |
| ########################################################################## | |
| - name: Generate docs preview | |
| if: steps.ctx.outputs.is_main != 'true' && steps.changes.outputs.has_changes == 'true' | |
| id: preview | |
| working-directory: docs-checkout/fern | |
| env: | |
| FERN_TOKEN: ${{ secrets.FERN_TOKEN }} | |
| run: | | |
| if OUTPUT=$(fern generate --docs --preview 2>&1); then | |
| FERN_EXIT=0 | |
| else | |
| FERN_EXIT=$? | |
| fi | |
| echo "$OUTPUT" | |
| if [ $FERN_EXIT -ne 0 ]; then | |
| echo "::error::Fern docs preview generation failed (exit $FERN_EXIT)" | |
| exit 1 | |
| fi | |
| URL=$(echo "$OUTPUT" | grep -oP 'Published docs to \K\S+') || true | |
| if [ -n "$URL" ]; then | |
| echo "url=$URL" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Comment preview URL on PR | |
| if: steps.ctx.outputs.is_main != 'true' && steps.preview.outputs.url != '' && startsWith(github.ref, 'refs/heads/pull-request/') | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| PR_NUM="${GITHUB_REF##*/}" | |
| gh pr comment "https://github.com/${{ github.repository }}/pull/${PR_NUM}" \ | |
| --edit-last --create-if-none \ | |
| --body "🌿 **Fern Docs Preview:** ${{ steps.preview.outputs.url }}/dev" | |
| ########################################################################## | |
| # PUSH AND PUBLISH - push changes to docs-website branch and publish docs | |
| ########################################################################## | |
| - name: Setup Git | |
| if: steps.ctx.outputs.is_main == 'true' && steps.changes.outputs.has_changes == 'true' | |
| run: | | |
| cd docs-checkout | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| - name: Commit and push changes | |
| if: steps.ctx.outputs.is_main == 'true' && steps.changes.outputs.has_changes == 'true' | |
| run: | | |
| cd docs-checkout | |
| git add -A | |
| git commit -m "docs(fern): sync dev from main | |
| Automated sync of docs/ directory from main branch. | |
| Preserves versioned documentation snapshots. | |
| Source commit: ${{ github.sha }}" | |
| git push origin docs-website | |
| echo "Successfully synced dev docs to docs-website branch" | |
| - name: Publish Docs | |
| if: steps.ctx.outputs.is_main == 'true' && steps.changes.outputs.has_changes == 'true' | |
| env: | |
| FERN_TOKEN: ${{ secrets.FERN_TOKEN }} | |
| working-directory: docs-checkout/fern | |
| run: fern generate --docs | |
| ############################################################################# | |
| # VERSION RELEASE - Run on new version tags (vX.Y.Z) | |
| ############################################################################# | |
| release-version: | |
| name: Release Version to docs-website | |
| # Run on tag push OR manual dispatch with a tag specified | |
| if: | | |
| github.ref_type == 'tag' || | |
| (github.event_name == 'workflow_dispatch' && github.event.inputs.tag != '' && github.event.inputs.tag != null) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Determine version tag | |
| id: version | |
| run: | | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| TAG="${{ github.event.inputs.tag }}" | |
| else | |
| TAG="${GITHUB_REF#refs/tags/}" | |
| fi | |
| # Validate tag format (must be vX.Y.Z exactly) | |
| if ! echo "$TAG" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+$'; then | |
| echo "::error::Invalid tag format: $TAG. Must be vX.Y.Z (e.g., v0.9.0)" | |
| exit 1 | |
| fi | |
| # Extract version without 'v' prefix | |
| VERSION="${TAG#v}" | |
| echo "tag=$TAG" >> $GITHUB_OUTPUT | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| echo "Processing version: $VERSION (tag: $TAG)" | |
| - name: Checkout source at tag | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| # workflow_dispatch runs from a branch, so resolve the requested tag | |
| # explicitly. This checkout provides the tagged docs, navigation, and | |
| # callout converter used to build the release snapshot, so a tag cut | |
| # from a release branch snapshots its own docs — never main's (#11140). | |
| ref: ${{ steps.version.outputs.tag }} | |
| path: source-checkout | |
| fetch-depth: 1 | |
| - name: Checkout docs-website branch | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: docs-website | |
| path: docs-checkout | |
| fetch-depth: 0 | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Check if version already exists | |
| env: | |
| # Empty on tag-push events; "true" or "false" on workflow_dispatch. | |
| FORCE_REBUILD: ${{ github.event.inputs.force_rebuild }} | |
| run: | | |
| TAG="${{ steps.version.outputs.tag }}" | |
| if [ -d "docs-checkout/fern/pages-$TAG" ] || [ -f "docs-checkout/fern/versions/$TAG.yml" ]; then | |
| if [ "$FORCE_REBUILD" = "true" ]; then | |
| echo "::warning::Version $TAG already exists; force_rebuild=true, overwriting its snapshot." | |
| else | |
| echo "::error::Version $TAG already exists. If a prior run committed but failed to publish, re-run via workflow_dispatch with force_rebuild=true to overwrite it intentionally." | |
| exit 1 | |
| fi | |
| else | |
| echo "Version $TAG does not exist yet, proceeding with release" | |
| fi | |
| - name: Setup Git | |
| working-directory: docs-checkout | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| - name: Build versioned pages from tagged commit | |
| run: | | |
| TAG="${{ steps.version.outputs.tag }}" | |
| echo "Building docs-checkout/fern/pages-$TAG/ from source @ $TAG docs/fern/pages/..." | |
| # pages-dev tracks main and can differ from a tag cut from a release | |
| # branch. Copy the tag's authored pages directly; Fern-root config, | |
| # components, assets, scripts, and translations remain outside snapshots. | |
| rm -rf "docs-checkout/fern/pages-$TAG" | |
| mkdir -p "docs-checkout/fern/pages-$TAG" | |
| rsync -a \ | |
| --exclude='/home/index.mdx' \ | |
| source-checkout/docs/fern/pages/ "docs-checkout/fern/pages-$TAG/" | |
| python3 source-checkout/docs/fern/scripts/rewrite_snapshot_paths.py \ | |
| "docs-checkout/fern/pages-$TAG" | |
| echo "Created docs-checkout/fern/pages-$TAG/" | |
| ls -la "docs-checkout/fern/pages-$TAG/" | head -20 | |
| - name: Verify snapshot inventory matches the tag | |
| run: | | |
| TAG="${{ steps.version.outputs.tag }}" | |
| # Faithfulness guard (part of release validation): every file in the | |
| # snapshot must trace back to a file in the tag's docs/fern/ tree. | |
| # Runs before the shared-reference drop and callout conversion mutate | |
| # the snapshot, so it catches foreign/injected files. Missing pages | |
| # that the nav references are caught later by the nav-target existence | |
| # check in "Validate release snapshot". | |
| missing=0 | |
| while IFS= read -r f; do | |
| rel="${f#docs-checkout/fern/pages-$TAG/}" | |
| if [ ! -e "source-checkout/docs/fern/pages/$rel" ]; then | |
| echo "::error::Snapshot file has no counterpart in the tagged source: $rel" | |
| missing=1 | |
| fi | |
| done < <(find "docs-checkout/fern/pages-$TAG" -type f) | |
| if [ "$missing" != "0" ]; then | |
| echo "::error::Release snapshot for $TAG contains files absent from the tag; aborting." | |
| exit 1 | |
| fi | |
| echo "Snapshot inventory for $TAG matches the tagged source." | |
| - name: Drop shared Reference pages from the snapshot | |
| run: | | |
| TAG="${{ steps.version.outputs.tag }}" | |
| # The Reference tab's General variant is shared (always-current) | |
| # across versions: its nav paths stay on ../pages-dev/ (see the | |
| # version-config step), so drop exactly the files that variant | |
| # references from the snapshot rather than freezing stale copies. | |
| # Everything else under reference/ (observability pages, config | |
| # references) belongs to versioned tabs and stays in the snapshot. | |
| # If the selectors stop matching (tab or variant renamed), this and | |
| # the version-config revert silently no-op and the reference quietly | |
| # freezes per version again — warn loudly so the rename gets fixed. | |
| if [ "$(yq '[.navigation[] | select(.tab == "reference") | .variants[] | select(.title == "General")] | length' docs-checkout/fern/versions/dev.yml)" = "0" ]; then | |
| echo "::warning::No Reference General variant found in dev.yml — the shared-reference exclusion is a no-op and $TAG will freeze its own reference copy. If the tab or variant was renamed, update the yq selectors in this workflow." | |
| fi | |
| yq '.navigation[] | select(.tab == "reference") | .variants[] | select(.title == "General") | .. | select(has("path")) | .path' \ | |
| docs-checkout/fern/versions/dev.yml | sed 's|^\.\./pages-dev/||' | while read -r relpath; do | |
| [ -n "$relpath" ] && rm -f "docs-checkout/fern/pages-$TAG/$relpath" | |
| done | |
| find "docs-checkout/fern/pages-$TAG/reference" -type d -empty -delete 2>/dev/null || true | |
| - name: Snapshot translation mirrors | |
| run: | | |
| TAG="${{ steps.version.outputs.tag }}" | |
| # Snapshot translations from the tagged source, not the docs-website | |
| # pages-dev mirror. The dev mirror has already had relative links | |
| # rewritten to /dynamo/.../dev URLs; copying it would pin released | |
| # translations to dev. Starting from the tag's authored relative links | |
| # lets the resolver below produce /dynamo/.../$TAG URLs. | |
| # fern/translations/<lang>/pages-$TAG/<path> pairs with | |
| # fern/pages-$TAG/<path>. No-op when the tag has no translations. | |
| if [ -d source-checkout/docs/fern/translations ]; then | |
| for lang_dir in source-checkout/docs/fern/translations/*/; do | |
| lang=$(basename "$lang_dir") | |
| if [ -d "$lang_dir/pages" ]; then | |
| echo "Snapshotting tagged translations for $lang -> pages-$TAG..." | |
| rm -rf "docs-checkout/fern/translations/$lang/pages-$TAG" | |
| mkdir -p "docs-checkout/fern/translations/$lang/pages-$TAG" | |
| rsync -a "$lang_dir/pages/" "docs-checkout/fern/translations/$lang/pages-$TAG/" | |
| fi | |
| done | |
| fi | |
| - name: Freeze the install selector in the versioned Quickstart | |
| # Dev keeps the interactive selector; a release snapshot is static and | |
| # pinned, so this substitutes version-pinned commands and strips the | |
| # <InstallSelector /> import from the tagged page. The script no-ops if | |
| # the marker is absent (older snapshots). Runs before callout conversion. | |
| run: | | |
| python3 source-checkout/.github/scripts/freeze_install_selector.py \ | |
| "docs-checkout/fern/pages-${{ steps.version.outputs.tag }}/cli/getting-started/quickstart.mdx" \ | |
| "${{ steps.version.outputs.version }}" | |
| - name: Update GitHub links to version tag | |
| run: | | |
| TAG="${{ steps.version.outputs.tag }}" | |
| echo "Pinning GitHub links from main to $TAG in docs-checkout/fern/pages-$TAG/..." | |
| # Base pages plus any translation snapshots for this tag. | |
| SNAP_DIRS="docs-checkout/fern/pages-$TAG" | |
| for d in docs-checkout/fern/translations/*/"pages-$TAG"; do | |
| [ -d "$d" ] && SNAP_DIRS="$SNAP_DIRS $d" | |
| done | |
| # Pin tree/main and blob/main links before callout conversion. | |
| find $SNAP_DIRS -type f \( -name "*.md" -o -name "*.mdx" \) | while read -r file; do | |
| if grep -q "github.com/ai-dynamo/dynamo/tree/main" "$file"; then | |
| echo "Updating tree links: $file" | |
| sed -i "s|github.com/ai-dynamo/dynamo/tree/main|github.com/ai-dynamo/dynamo/tree/$TAG|g" "$file" | |
| fi | |
| done | |
| find $SNAP_DIRS -type f \( -name "*.md" -o -name "*.mdx" \) | while read -r file; do | |
| if grep -q "github.com/ai-dynamo/dynamo/blob/main" "$file"; then | |
| echo "Updating blob links: $file" | |
| sed -i "s|github.com/ai-dynamo/dynamo/blob/main|github.com/ai-dynamo/dynamo/blob/$TAG|g" "$file" | |
| fi | |
| done | |
| echo "GitHub link update complete." | |
| - name: Convert GitHub callouts to Fern format | |
| run: | | |
| TAG="${{ steps.version.outputs.tag }}" | |
| # Use the tag's own converter so conversion matches the release. | |
| echo "Converting callouts in pages-$TAG/ with the tag's convert_callouts.py..." | |
| python3 source-checkout/docs/fern/scripts/convert_callouts.py --dir "docs-checkout/fern/pages-$TAG" | |
| for d in docs-checkout/fern/translations/*/"pages-$TAG"; do | |
| [ -d "$d" ] && python3 source-checkout/docs/fern/scripts/convert_callouts.py --dir "$d" | |
| done | |
| echo "Callout conversion complete." | |
| - name: Resolve relative links in translated snapshot | |
| run: | | |
| TAG="${{ steps.version.outputs.tag }}" | |
| # Rewrites relative page links in the translation snapshot to site URLs | |
| # under this tag's version slug, from the tag's nav (see | |
| # docs/fern/scripts/resolve_translation_links.py). Guarded on the | |
| # snapshot and resolver; capability-gated on --pages-dir so workflow_dispatch | |
| # rebuilds of tags whose resolver predates that flag skip cleanly. | |
| if compgen -G "docs-checkout/fern/translations/*/pages-$TAG" > /dev/null \ | |
| && [ -f source-checkout/docs/fern/scripts/resolve_translation_links.py ]; then | |
| if grep -q -- '--pages-dir' source-checkout/docs/fern/scripts/resolve_translation_links.py; then | |
| pip install --quiet pyyaml | |
| python3 source-checkout/docs/fern/scripts/resolve_translation_links.py \ | |
| --nav source-checkout/docs/fern/index.yml \ | |
| --translations-root docs-checkout/fern/translations \ | |
| --site-root /dynamo --version-slug "$TAG" \ | |
| --pages-dir "pages-$TAG" --github-ref "$TAG" | |
| else | |
| echo "::warning::$TAG's resolver predates --pages-dir; skipping translated-snapshot link resolution" | |
| fi | |
| fi | |
| - name: Create version config file | |
| run: | | |
| TAG="${{ steps.version.outputs.tag }}" | |
| VERSION_FILE="docs-checkout/fern/versions/$TAG.yml" | |
| echo "Creating version config from the tag's index.yml: $VERSION_FILE" | |
| # Navigation must come from the tag so it references exactly the pages | |
| # in the release snapshot (#11140). Mirror the dev-nav transform, but | |
| # point content pages at this tag. Digest and the Home index are shared | |
| # siblings on docs-website; rewrite them first so the content-page rule | |
| # below does not re-match. | |
| cp source-checkout/docs/fern/index.yml "$VERSION_FILE" | |
| yq -i '(.. | select(has("path")).path) |= sub("^digest/", "../digest/")' "$VERSION_FILE" | |
| yq -i '(.. | select(has("path")).path) |= sub("^pages/home/index\.mdx$", "../index.mdx")' "$VERSION_FILE" | |
| yq -i '(.. | select(has("path")).path) |= sub("^pages/", "../pages-'"$TAG"'/")' "$VERSION_FILE" | |
| # Revert the Reference tab's General variant to the shared source: | |
| # it is always-current across versions (release metadata is | |
| # cumulative — each page carries per-release sections), so its pages | |
| # render from pages-dev in every version (paired with the snapshot | |
| # drop above). The Kubernetes API and Components variants stay on the | |
| # frozen snapshot (CRD fields and config flags are genuinely per-version). | |
| yq -i "(.navigation[] | select(.tab == \"reference\") | .variants[] | select(.title == \"General\") | .. | select(has(\"path\")).path) |= sub(\"\.\./pages-$TAG/\", \"../pages-dev/\")" "$VERSION_FILE" | |
| # Keep cross-navigation links within the selected documentation version. | |
| sed -i "s|href: /dynamo/dev/|href: /dynamo/$TAG/|g" "$VERSION_FILE" | |
| echo "Created $VERSION_FILE" | |
| echo "First 30 lines:" | |
| head -30 "$VERSION_FILE" | |
| - name: Update docs.yml with new version | |
| run: | | |
| TAG="${{ steps.version.outputs.tag }}" | |
| DOCS_FILE="docs-checkout/fern/docs.yml" | |
| echo "Updating $DOCS_FILE to include $TAG..." | |
| if yq ".versions[] | select(.display-name == \"$TAG\")" "$DOCS_FILE" | grep -q .; then | |
| echo "Version $TAG already in docs.yml, skipping update" | |
| exit 0 | |
| fi | |
| # Find the dev entry and insert the release immediately after it, | |
| # preserving Latest at index 0. | |
| DEV_IDX=$(yq '.versions | to_entries | map(select(.value.display-name == "dev")) | .[0].key' "$DOCS_FILE") | |
| if [ "$DEV_IDX" = "null" ]; then | |
| echo "::error::dev version not found in $DOCS_FILE" | |
| exit 1 | |
| fi | |
| INSERT_IDX=$((DEV_IDX + 1)) | |
| TAG="$TAG" INSERT_IDX="$INSERT_IDX" yq -i ' | |
| .versions |= ( | |
| .[:env(INSERT_IDX)] + | |
| [{"display-name": env(TAG), "path": ("./versions/" + env(TAG) + ".yml"), "slug": env(TAG), "availability": "stable"}] + | |
| .[env(INSERT_IDX):] | |
| ) | |
| ' "$DOCS_FILE" | |
| # Point the Latest entry at the new release. | |
| yq -i ".versions[0].path = \"./versions/$TAG.yml\"" "$DOCS_FILE" | |
| yq -i ".versions[0].display-name = \"Latest ($TAG)\"" "$DOCS_FILE" | |
| echo "Updated versions:" | |
| yq '.versions' "$DOCS_FILE" | |
| - name: Setup Node.js | |
| uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 | |
| with: | |
| node-version: '22' | |
| - name: Install Fern CLI | |
| # Pin to the tag's fern.config.json version so a release publishes with the | |
| # exact CLI the tag was authored against, not whatever floats on npm. | |
| run: npm install -g fern-api@$(jq -r '.version' source-checkout/docs/fern/fern.config.json) | |
| - name: Validate release snapshot | |
| env: | |
| FERN_TOKEN: ${{ secrets.FERN_TOKEN }} | |
| run: | | |
| TAG="${{ steps.version.outputs.tag }}" | |
| VERSION_FILE="docs-checkout/fern/versions/$TAG.yml" | |
| # Every navigation target must resolve. Versioned pages are part of this | |
| # release and must exist; the shared Digest is synced independently from | |
| # main, so a tag's frozen Digest nav can drift — report without blocking. | |
| while IFS= read -r path; do | |
| case "$path" in | |
| ../pages-$TAG/*) | |
| if [ ! -e "docs-checkout/fern/versions/$path" ]; then | |
| echo "::error::Version navigation target does not exist: $path" | |
| exit 1 | |
| fi | |
| ;; | |
| ../digest/*) | |
| if [ ! -e "docs-checkout/fern/versions/$path" ]; then | |
| echo "::warning::Shared Digest navigation target does not exist in docs-website: $path" | |
| fi | |
| ;; | |
| esac | |
| done < <(yq -r '(.. | select(has("path")).path)' "$VERSION_FILE") | |
| # Run Fern's structural validation. The docs-website branch has | |
| # historical broken-link debt, so link checking is scoped above to the | |
| # release navigation rather than failing on unrelated versions. | |
| cd docs-checkout | |
| fern check | |
| - name: Commit and push changes | |
| working-directory: docs-checkout | |
| run: | | |
| TAG="${{ steps.version.outputs.tag }}" | |
| git add "fern/pages-$TAG/" | |
| git add "fern/versions/$TAG.yml" | |
| git add fern/docs.yml | |
| # Versioned translation snapshots (see #11195); the glob is empty for | |
| # tags cut from branches without fern/translations. | |
| for d in fern/translations/*/"pages-$TAG"; do | |
| [ -d "$d" ] && git add "$d" | |
| done | |
| if git diff --cached --quiet; then | |
| echo "No release artifact changes for $TAG; skipping commit and push." | |
| exit 0 | |
| fi | |
| git commit -m "docs(fern): release version $TAG | |
| - Created fern/pages-$TAG/ with documentation snapshot | |
| - Created fern/versions/$TAG.yml version navigation config | |
| - Updated fern/docs.yml to include $TAG in version list | |
| - Snapshotted translation mirrors (fern/translations/*/pages-$TAG), when present | |
| Automated by fern-docs workflow | |
| Source tag: $TAG" | |
| git push origin docs-website | |
| echo "Successfully released documentation for $TAG on docs-website branch" | |
| - name: Publish Docs | |
| env: | |
| FERN_TOKEN: ${{ secrets.FERN_TOKEN }} | |
| working-directory: docs-checkout/fern | |
| run: fern generate --docs |