Publish Fern Docs #29
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. | |
| # 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/</\</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 |