Skip to content

Fern Docs

Fern Docs #39

Workflow file for this run

# 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