Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 113 additions & 48 deletions .github/workflows/fern-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ on:
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
Expand Down Expand Up @@ -350,61 +355,83 @@ jobs:
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.
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 "fern/pages-$TAG" ]; then
echo "::error::Version $TAG already exists (fern/pages-$TAG directory found)"
exit 1
fi

if [ -f "fern/versions/$TAG.yml" ]; then
echo "::error::Version $TAG already exists (fern/versions/$TAG.yml found)"
exit 1
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. Re-run via workflow_dispatch with force_rebuild=true to overwrite it intentionally."
Comment thread
nealvaidya marked this conversation as resolved.
Resolved
exit 1
fi
else
echo "Version $TAG does not exist yet, proceeding with release"
fi

echo "Version $TAG does not exist yet, proceeding with release"

- 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: Create versioned pages directory
- name: Build versioned pages from tagged commit
run: |
TAG="${{ steps.version.outputs.tag }}"

echo "Creating fern/pages-$TAG/ from fern/pages-dev/..."
echo "Building docs-checkout/fern/pages-$TAG/ from source @ $TAG docs/..."

# Copy current pages-dev/ to pages-vX.Y.Z/
cp -r fern/pages-dev "fern/pages-$TAG"
# pages-dev tracks main and can differ from a tag cut from a release
# branch. Build from the tag's raw docs so the snapshot is faithful
# to the release and callout conversion runs exactly once.
rm -rf "docs-checkout/fern/pages-$TAG"
mkdir -p "docs-checkout/fern/pages-$TAG"
rsync -a \
--exclude='digest' \
--exclude='index.yml' \
source-checkout/docs/ "docs-checkout/fern/pages-$TAG/"

echo "Created fern/pages-$TAG/"
ls -la "fern/pages-$TAG/" | head -20
echo "Created docs-checkout/fern/pages-$TAG/"
ls -la "docs-checkout/fern/pages-$TAG/" | head -20

- name: Update GitHub links to 'main' to version tag
- name: Update GitHub links to version tag
run: |
TAG="${{ steps.version.outputs.tag }}"

echo "Updating GitHub links from 'tree/main' to 'tree/$TAG' in fern/pages-$TAG/..."
echo "Pinning GitHub links from main to $TAG in docs-checkout/fern/pages-$TAG/..."

# Find all markdown files and replace tree/main with tree/vX.Y.Z
find "fern/pages-$TAG" -name "*.md" -o -name "*.mdx" | while read file; do
# Pin tree/main and blob/main links before callout conversion.
find "docs-checkout/fern/pages-$TAG" -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: $file"
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

# Also update blob/main references (for direct file links)
find "fern/pages-$TAG" -name "*.md" -o -name "*.mdx" | while read file; do
find "docs-checkout/fern/pages-$TAG" -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"
Expand All @@ -417,27 +444,25 @@ jobs:
run: |
TAG="${{ steps.version.outputs.tag }}"

echo "Converting GitHub-style callouts to Fern format in pages-$TAG/..."
python3 fern/convert_callouts.py --dir "fern/pages-$TAG"
echo "Converting callouts in pages-$TAG/ with the tag's convert_callouts.py..."
python3 source-checkout/fern/convert_callouts.py --dir "docs-checkout/fern/pages-$TAG"
echo "Callout conversion complete."

- name: Create version config file
run: |
TAG="${{ steps.version.outputs.tag }}"
VERSION="${{ steps.version.outputs.version }}"
VERSION_FILE="fern/versions/$TAG.yml"

echo "Creating version config: $VERSION_FILE"
VERSION_FILE="docs-checkout/fern/versions/$TAG.yml"

# Copy dev.yml as template
cp fern/versions/dev.yml "$VERSION_FILE"
echo "Creating version config from the tag's index.yml: $VERSION_FILE"

# Update the comment at the top
sed -i "s/# Navigation structure for Latest version/# Navigation structure for $TAG version/" "$VERSION_FILE"
sed -i "s|# Matching https://docs.nvidia.com/dynamo/latest/|# Snapshot from tag $TAG|" "$VERSION_FILE"
# Navigation must come from the tag so it references exactly the pages
# in the release snapshot.
cp source-checkout/docs/index.yml "$VERSION_FILE"

# Update all page paths from ../pages-dev/ to ../pages-vX.Y.Z/
sed -i "s|path: \.\./pages-dev/|path: ../pages-$TAG/|g" "$VERSION_FILE"
# Mirror the dev-nav transform, but point content pages at this tag.
# Digest remains shared on docs-website and must be rewritten first.
yq -i '(.. | select(has("path")).path) |= sub("^digest/", "../digest/")' "$VERSION_FILE"
yq -i '(.. | select(has("path")).path) |= sub("^([a-zA-Z])", "../pages-'"$TAG"'/${1}")' "$VERSION_FILE"

echo "Created $VERSION_FILE"
echo "First 30 lines:"
Expand All @@ -446,18 +471,22 @@ jobs:
- name: Update docs.yml with new version
run: |
TAG="${{ steps.version.outputs.tag }}"
DOCS_FILE="fern/docs.yml"
DOCS_FILE="docs-checkout/fern/docs.yml"

echo "Updating $DOCS_FILE to include $TAG..."

# Check if version already in docs.yml
if yq ".products[0].versions[] | select(.display-name == \"$TAG\")" "$DOCS_FILE" | grep -q .; then
if yq ".products[0].versions[] | select(.\"display-name\" == \"$TAG\")" "$DOCS_FILE" | grep -q .; then
echo "Version $TAG already in docs.yml, skipping update"
exit 0
Comment thread
nealvaidya marked this conversation as resolved.
Resolved
fi

# Find the index of the "dev" entry and insert new version right after it
DEV_IDX=$(yq '.products[0].versions | to_entries | map(select(.value.display-name == "dev")) | .[0].key' "$DOCS_FILE")
DEV_IDX=$(yq '.products[0].versions | to_entries | map(select(.value."display-name" == "dev")) | .[0].key' "$DOCS_FILE")
if [ -z "$DEV_IDX" ] || [ "$DEV_IDX" = "null" ]; then
echo "::error::Could not find dev version entry in $DOCS_FILE"
exit 1
fi
INSERT_IDX=$((DEV_IDX + 1))

yq -i "
Expand All @@ -473,19 +502,63 @@ jobs:

# Update the "Latest" entry to point to the new version
yq -i ".products[0].versions[0].path = \"./versions/$TAG.yml\"" "$DOCS_FILE"
yq -i ".products[0].versions[0].display-name = \"Latest ($TAG)\"" "$DOCS_FILE"
yq -i ".products[0].versions[0].\"display-name\" = \"Latest ($TAG)\"" "$DOCS_FILE"

echo "Updated docs.yml products/versions section:"
yq '.products[0].versions' "$DOCS_FILE"

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'

- name: Install Fern CLI
run: npm install -g fern-api@$(jq -r '.version' docs-checkout/fern/fern.config.json)

- name: Validate release snapshot
Comment thread
nealvaidya marked this conversation as resolved.
Resolved
run: |
TAG="${{ steps.version.outputs.tag }}"
VERSION_FILE="docs-checkout/fern/versions/$TAG.yml"

# Conversion may change file contents but not the inventory. Verify
# the snapshot contains exactly the tag's non-Digest documentation.
diff -u \
<(cd source-checkout/docs && find . -type f ! -path './digest/*' ! -name 'index.yml' -print | sort) \
<(cd "docs-checkout/fern/pages-$TAG" && find . -type f -print | sort)

# Verify every local path in the generated nav resolves in the
# docs-website checkout before committing it.
while IFS= read -r path; do
case "$path" in
../pages-$TAG/*|../digest/*)
Comment thread
nealvaidya marked this conversation as resolved.
Resolved
if [ ! -e "docs-checkout/fern/versions/$path" ]; then
echo "::error::Version navigation target does not exist: $path"
exit 1
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

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
Expand All @@ -499,16 +572,8 @@ jobs:

echo "Successfully released documentation for $TAG on docs-website branch"

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'

- name: Install Fern CLI
run: npm install -g fern-api

- name: Publish Docs
env:
FERN_TOKEN: ${{ secrets.FERN_TOKEN }}
working-directory: ./fern
working-directory: docs-checkout/fern
run: fern generate --docs
Comment thread
nealvaidya marked this conversation as resolved.
Resolved
55 changes: 37 additions & 18 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,10 @@ fern/
└── assets/ # Images, fonts, SVGs
```

Each `pages-vX.Y.Z/` directory is an immutable copy of `pages/` taken at
release time. The corresponding `versions/vX.Y.Z.yml` file is a copy of
`dev.yml` with all `../pages/` paths rewritten to `../pages-vX.Y.Z/`.
Each `pages-vX.Y.Z/` directory is an immutable snapshot built from `docs/` at
the corresponding Git tag. The matching `versions/vX.Y.Z.yml` is built from
that tag's `docs/index.yml`, with content paths rewritten to
`../pages-vX.Y.Z/`.

The sync workflow copies content from `main`'s `docs/` into `fern/pages/` and
transforms navigation paths in `index.yml` → `versions/dev.yml` accordingly.
Expand Down Expand Up @@ -240,20 +241,24 @@ manual `workflow_dispatch` with a tag specified.

**Steps:**
1. Validates tag format (must be exactly `vX.Y.Z`, no suffixes like `-rc1`)
2. Checks that the version doesn't already exist (no duplicate snapshots)
3. Creates `fern/pages-vX.Y.Z/` by copying `fern/pages/`
4. Rewrites GitHub links in the snapshot:
2. Checks out the tagged source and the `docs-website` branch side by side
3. Checks that the version doesn't already exist, unless a manual dispatch sets
`force_rebuild=true`
4. Creates `fern/pages-vX.Y.Z/` from the tag's raw `docs/`, excluding the
shared `digest/` tree and `index.yml`
5. Rewrites GitHub links in the snapshot:
- `github.com/ai-dynamo/dynamo/tree/main` → `tree/vX.Y.Z`
- `github.com/ai-dynamo/dynamo/blob/main` → `blob/vX.Y.Z`
5. Runs `convert_callouts.py` on the snapshot
6. Creates `fern/versions/vX.Y.Z.yml` from `dev.yml` with paths updated to
`../pages-vX.Y.Z/`
7. Updates `fern/docs.yml`:
6. Runs the tag's `convert_callouts.py` once on the raw snapshot
7. Creates `fern/versions/vX.Y.Z.yml` from the tag's `docs/index.yml`
8. Updates `fern/docs.yml`:
- Inserts new version right after the "dev" entry
- Sets the product's default `path` to the new version
- Updates the "Latest" display-name to `"Latest (vX.Y.Z)"`
8. Commits and pushes to `docs-website`
9. Publishes to Fern via `fern generate --docs`
9. Verifies the tagged file inventory, generated navigation targets, and Fern
configuration
10. Commits and pushes to `docs-website`
11. Publishes to Fern via `fern generate --docs`

**Anti-recursion note:** Pushes made with `GITHUB_TOKEN` do not trigger other
workflows (GitHub's built-in guard). This is why the publish step is inline in
Expand Down Expand Up @@ -382,6 +387,19 @@ fern docs broken-links

This is the same check that runs in CI on every pull request.

### Dry-run a version release

Build a tagged snapshot in temporary worktrees and run the release validation
without committing, pushing, or publishing:

```bash
fern/release_dryrun.sh v1.2.1
```

The script requires `git`, `fern`, `yq`, `jq`, `rsync`, and Python 3.10 or
newer. Set `PYTHON=.venv/bin/python` to select a non-default interpreter, and
set `KEEP=1` to retain the temporary worktrees for inspection.

### Start a local preview server

Run `fern docs dev` to build the site and serve it locally with hot-reload:
Expand Down Expand Up @@ -499,14 +517,15 @@ only the `dev` redirect is needed.
│ ▼ │
│ release-version job: │
│ 1. Validate tag format (vX.Y.Z only) │
│ 2. Check version doesn't already exist │
│ 3. Snapshot fern/pages/ → fern/pages-vX.Y.Z/ │
│ 2. Check out the tag and docs-website │
│ 3. Build pages-vX.Y.Z/ from the tagged docs/ │
│ 4. Rewrite GitHub links (tree/main → tree/vX.Y.Z) │
│ 5. Convert callouts in snapshot │
│ 6. Create fern/versions/vX.Y.Z.yml (paths → pages-vX.Y.Z/) │
│ 5. Convert callouts once with the tagged converter │
│ 6. Build the version nav from the tagged docs/index.yml │
│ 7. Update fern/docs.yml (insert version, set as default) │
│ 8. Commit + push to docs-website │
│ 9. fern generate --docs (publishes to Fern) │
│ 8. Validate tagged inventory, nav targets, and Fern config │
│ 9. Commit + push to docs-website │
│ 10. fern generate --docs (publishes to Fern) │
│ │ │
│ ▼ │
│ New version visible in dropdown at docs.nvidia.com/dynamo/ │
Expand Down
4 changes: 2 additions & 2 deletions docs/documentation-style-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,8 @@ Author against `docs/` on `main`; the publish step applies transforms, so you do
Fern-specific output:

- **Callouts:** `fern/convert_callouts.py` converts GitHub admonitions to Fern components.
- **Versioned paths:** content is copied `pages-dev/ → pages-vX.Y.Z/`, and nav paths are rewritten
when a version is cut.
- **Versioned paths:** the release workflow builds `pages-vX.Y.Z/` from the tagged `docs/` tree and
rewrites the tagged `docs/index.yml` paths for the versioned snapshot.

Write portable GitHub-flavored Markdown; don't pre-bake Fern components or version-specific paths.

Expand Down
Loading
Loading