Skip to content

Publish Fern Docs

Publish Fern Docs #29

# 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.
# Publishes the Fern documentation site on release tag push or manual dispatch.
#
# All versions serve frozen content extracted from their git tag — there is no
# "live docs" entry. The newest version is stamped "Latest · vX.Y.Z" at publish
# time so readers know which is current; the stamp is transient and not persisted.
#
# Manual dispatch accepts an optional `tag` input:
# - empty: resolves to the latest GitHub release tag
# - explicit (e.g. v0.5.0): publishes that tag
#
# On a vX.Y.Z release tag (push or dispatch), the workflow also:
# 1. Generates fern/versions/vX.Y.Z.yml from the tag's docs/index.yml
# 2. Adds a version entry to fern/docs.yml, sorted by semver descending
# 3. Prunes older versions to keep only the MAX_VERSIONS most recent
# 4. Opens a PR to persist registry changes back to main (after publish)
#
# Pre-release tags (e.g. v0.5.0-rc1) trigger publish but skip version registration.
#
# Required configuration:
# - Organization secret: DOCS_FERN_TOKEN (from `fern token` for the nvidia Fern org)
name: Publish Fern Docs
on:
push:
tags:
- "v[0-9]*.[0-9]*.[0-9]*"
workflow_dispatch:
inputs:
tag:
description: "Tag to publish (e.g. v0.5.0). Leave empty to use the latest GitHub release tag."
required: false
type: string
permissions:
contents: write
pull-requests: write
concurrency:
group: fern-publish
cancel-in-progress: true
env:
YQ_VERSION: v4.53.2
MAX_VERSIONS: 3
jobs:
publish:
runs-on: linux-amd64-cpu8
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: main
fetch-tags: true
- name: Install yq
run: |
if command -v yq &>/dev/null; then
echo "yq already installed: $(yq --version)"
else
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m)
case "$ARCH" in
x86_64) ARCH="amd64" ;;
aarch64) ARCH="arm64" ;;
esac
mkdir -p "$HOME/.local/bin"
curl -sSfL "https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/yq_${OS}_${ARCH}" -o "$HOME/.local/bin/yq"
chmod +x "$HOME/.local/bin/yq"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
fi
- name: Resolve target tag
id: resolve
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
INPUT_TAG: ${{ inputs.tag }}
run: |
set -eo pipefail
if [ "${GITHUB_EVENT_NAME}" = "push" ]; then
TAG="${GITHUB_REF#refs/tags/}"
elif [ -n "${INPUT_TAG}" ]; then
TAG="${INPUT_TAG}"
else
TAG=$(gh release view --repo "${GITHUB_REPOSITORY}" --json tagName -q .tagName)
if [ -z "$TAG" ] || [ "$TAG" = "null" ]; then
echo "::error::Could not determine latest release tag"
exit 1
fi
fi
if ! [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.]+)?$ ]]; then
echo "::error::Tag '${TAG}' is not a valid semver tag (expected vMAJOR.MINOR.PATCH[-PRERELEASE])"
exit 1
fi
if ! git show-ref --verify --quiet "refs/tags/${TAG}"; then
echo "::error::Tag '${TAG}' does not exist in this repository"
exit 1
fi
if [[ "$TAG" == *-* ]]; then
IS_RELEASE=false
else
IS_RELEASE=true
fi
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "is_release=$IS_RELEASE" >> "$GITHUB_OUTPUT"
echo "Resolved target tag: $TAG (is_release=$IS_RELEASE)"
- name: Register new version from tag
if: steps.resolve.outputs.is_release == 'true'
env:
TAG_VERSION: ${{ steps.resolve.outputs.tag }}
run: |
set -eo pipefail
# Skip if version file already exists
if [ -f "fern/versions/${TAG_VERSION}.yml" ]; then
echo "Version ${TAG_VERSION} already registered — skipping"
exit 0
fi
# Generate version nav from the tag's docs/index.yml
echo "Generating fern/versions/${TAG_VERSION}.yml from tag $TAG_VERSION"
git archive "refs/tags/${TAG_VERSION}" -- docs/index.yml | tar -xO docs/index.yml | \
sed "s|path: |path: ${TAG_VERSION}-content/|g" > "fern/versions/${TAG_VERSION}.yml"
# Add version entry to docs.yml
export TAG_VERSION
EXPR='.products[0].versions += [{"display-name": env(TAG_VERSION),'
EXPR+=' "path": "versions/" + env(TAG_VERSION) + ".yml",'
EXPR+=' "slug": env(TAG_VERSION), "availability": "stable"}]'
yq -i "$EXPR" fern/docs.yml
# Sort all version entries by semver descending
SORTED_SLUGS=$(yq '.products[0].versions[].slug' fern/docs.yml | sort -rV)
yq -i '.products[0].versions = []' fern/docs.yml
for slug in $SORTED_SLUGS; do
export slug
yq -i '.products[0].versions += [{"display-name": env(slug), "path": "versions/" + env(slug) + ".yml", "slug": env(slug), "availability": "stable"}]' fern/docs.yml
done
echo "--- docs.yml versions after insert + sort ---"
yq '.products[0].versions[].display-name' fern/docs.yml
- name: Prune old versions
if: steps.resolve.outputs.is_release == 'true'
run: |
set -eo pipefail
TOTAL=$(yq '.products[0].versions | length' fern/docs.yml)
if [ "$TOTAL" -le "$MAX_VERSIONS" ]; then
echo "Only $TOTAL versions — nothing to prune (keeping $MAX_VERSIONS)"
exit 0
fi
# Remove excess entries from the end (oldest) and their version files
PRUNED=$(yq ".products[0].versions[$MAX_VERSIONS:][].slug" fern/docs.yml)
yq -i ".products[0].versions |= .[:$MAX_VERSIONS]" fern/docs.yml
for slug in $PRUNED; do
rm -f "fern/versions/${slug}.yml"
echo "Pruned $slug"
done
echo "--- docs.yml versions after prune ---"
yq '.products[0].versions[].display-name' fern/docs.yml
- name: Checkout frozen version content
run: |
set -eo pipefail
for version_file in fern/versions/v*.yml; do
[ -f "$version_file" ] || continue
version=$(basename "$version_file" .yml)
if git show-ref --verify --quiet "refs/tags/${version}"; then
mkdir -p "fern/versions/${version}-content"
git archive "refs/tags/${version}" -- docs/ | tar -x --strip-components=1 -C "fern/versions/${version}-content"
find "fern/versions/${version}-content" -name '*.md' -print0 | xargs -0 -r sed -i \
-e 's/{/\\{/g' \
-e 's/}/\\}/g' \
-e 's/</\&lt;/g'
# Fix broken relative image paths in older tags
for subdir in "fern/versions/${version}-content"/*/; do
if [ ! -d "${subdir}assets" ] && [ -d "fern/versions/${version}-content/assets" ]; then
ln -sf ../assets "${subdir}assets"
fi
done
echo "Extracted docs from $version"
else
echo "::error::Tag $version not found — cannot pin frozen docs content"
exit 1
fi
done
- name: Stamp newest version as Latest
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
cp fern/docs.yml /tmp/docs.yml.preStamp
NEWEST=$(yq '.products[0].versions[0].display-name' fern/docs.yml)
if [ -n "$NEWEST" ] && [ "$NEWEST" != "null" ]; then
export NEWEST
yq -i '.products[0].versions[0].display-name = "Latest · " + env(NEWEST)' fern/docs.yml
fi
echo "--- docs.yml versions after stamp ---"
yq '.products[0].versions[].display-name' fern/docs.yml
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '20'
- name: Install Fern CLI
run: npm install -g fern-api@$(jq -r .version fern/fern.config.json)
- name: Publish docs
env:
FERN_TOKEN: ${{ secrets.DOCS_FERN_TOKEN }}
working-directory: ./fern
run: |
set -o pipefail
fern generate --docs 2>&1 | tee /tmp/fern-output.log
URL=$(grep -oP 'Published docs to \K.*(?= \()' /tmp/fern-output.log || true)
if [ -n "$URL" ]; then
echo "### Published: [$URL]($URL)" >> "$GITHUB_STEP_SUMMARY"
fi
- name: Restore unstamped docs.yml for persistence
if: steps.resolve.outputs.is_release == 'true'
run: cp /tmp/docs.yml.preStamp fern/docs.yml
- name: Open PR for version registry changes
if: steps.resolve.outputs.is_release == 'true'
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
base: main
branch: chore/fern-register-${{ steps.resolve.outputs.tag }}
delete-branch: true
sign-commits: true
commit-message: "chore(fern): register ${{ steps.resolve.outputs.tag }}, keep latest ${{ env.MAX_VERSIONS }} versions [automated]"
title: "chore(fern): register ${{ steps.resolve.outputs.tag }}"
body: |
Automated registration of `${{ steps.resolve.outputs.tag }}` in the Fern docs versions registry.
Generated by the `Publish Fern Docs` workflow.
add-paths: |
fern/versions/v*.yml
fern/docs.yml