Merge branch 'main' into pdmack/fern-doc-support #10
Workflow file for this run
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 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | |
| # SPDX-License-Identifier: Apache-2.0 | |
| # Consolidated Fern Documentation Workflow | |
| # | |
| # This workflow handles all Fern documentation automation: | |
| # | |
| # 1. LINT (PRs): Validates Fern configuration and checks for broken links | |
| # - Triggers on PRs when docs/** or fern/** files change | |
| # - Runs `fern check` and `fern docs broken-links` | |
| # | |
| # 2. SYNC & PUBLISH/PREVIEW: Syncs docs/ from source branch to fern/ on docs-website | |
| # - Triggers on push to main or PRs when docs/** files change | |
| # - On main: commits and pushes to docs-website, then publishes via `fern generate --docs` | |
| # - On PRs: generates a preview URL via `fern generate --docs --preview` and comments on PR | |
| # - Preserves versioned documentation (products[0]) from docs-website's docs.yml | |
| # | |
| # 3. 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 | |
| # | |
| # Prerequisites: | |
| # - A `docs-website` branch must exist (create with: git checkout --orphan docs-website) | |
| # - A FERN_TOKEN secret must be set in repo settings (obtain from buildwithfern.com) | |
| # - The Fern org/project must be registered at buildwithfern.com | |
| # | |
| # Note: The publish step is included inline because pushes made with GITHUB_TOKEN | |
| # do not trigger other workflows (GitHub's anti-recursion guard). | |
| 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]+' | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: 'Version tag to release (e.g., v1.0.0). Leave empty to sync dev docs.' | |
| required: false | |
| type: string | |
| jobs: | |
| # Detect changed files for conditional job execution | |
| changed-files: | |
| runs-on: linux-amd64-cpu32 | |
| permissions: | |
| contents: read | |
| # Skip for tag pushes - version release doesn't need changed-files check | |
| if: github.ref_type != 'tag' | |
| outputs: | |
| docs: ${{ steps.changes.outputs.docs }} | |
| docs_website_exists: ${{ steps.branch_check.outputs.exists }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Check for docs changes | |
| id: changes | |
| run: | | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| echo "docs=true" >> $GITHUB_OUTPUT | |
| elif echo "$GITHUB_REF" | grep -qE '^refs/heads/pull-request/[0-9]+$'; then | |
| echo "docs=true" >> $GITHUB_OUTPUT | |
| elif [ "${{ github.event.before }}" = "0000000000000000000000000000000000000000" ]; then | |
| # New branch push - treat as having docs changes | |
| echo "docs=true" >> $GITHUB_OUTPUT | |
| elif git diff --name-only "${{ github.event.before }}" HEAD 2>/dev/null | grep -qE '^(docs/|fern/)'; then | |
| echo "docs=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "docs=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Check if docs-website branch exists | |
| id: branch_check | |
| run: | | |
| if git ls-remote --exit-code --heads origin docs-website > /dev/null 2>&1; then | |
| echo "exists=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "exists=false" >> $GITHUB_OUTPUT | |
| echo "::warning::docs-website branch does not exist. Skipping sync/preview. See fern/README.md for one-time setup instructions." | |
| fi | |
| ############################################################################# | |
| # LINT JOBS - Run on PRs when docs/** or fern/** files change | |
| ############################################################################# | |
| fern-check: | |
| name: Fern Configuration Check | |
| needs: changed-files | |
| if: | | |
| github.ref_type != 'tag' && | |
| needs.changed-files.outputs.docs == 'true' && | |
| startsWith(github.ref, 'refs/heads/pull-request/') | |
| runs-on: linux-amd64-cpu32 | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| - name: Install Fern CLI | |
| run: npm install -g fern-api@4.23.0 | |
| - name: Validate Fern configuration | |
| run: fern check | |
| fern-broken-links: | |
| name: Fern Broken Links Check | |
| needs: changed-files | |
| if: | | |
| github.ref_type != 'tag' && | |
| needs.changed-files.outputs.docs == 'true' && | |
| startsWith(github.ref, 'refs/heads/pull-request/') | |
| runs-on: linux-amd64-cpu32 | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| - name: Install Fern CLI | |
| run: npm install -g fern-api@4.23.0 | |
| - name: Check for broken links | |
| run: fern docs broken-links | |
| ############################################################################# | |
| # SYNC & PUBLISH/PREVIEW | |
| # On main: commits, pushes, and publishes to Fern | |
| # On PRs: generates a preview URL and comments on the PR | |
| ############################################################################# | |
| preview-or-publish-docs: | |
| name: Preview or publish docs | |
| needs: changed-files | |
| if: | | |
| github.ref_type != 'tag' && | |
| needs.changed-files.outputs.docs == 'true' && | |
| needs.changed-files.outputs.docs_website_exists == 'true' && | |
| (github.event.inputs.tag == '' || github.event.inputs.tag == null) | |
| runs-on: linux-amd64-cpu32 | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| 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@v4 | |
| with: | |
| path: source-checkout | |
| fetch-depth: 1 | |
| - name: Checkout docs-website branch | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: docs-website | |
| path: docs-checkout | |
| fetch-depth: 1 | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Sync dev content from main | |
| run: | | |
| echo "Syncing content pages to docs-website branch..." | |
| rm -rf docs-checkout/fern/pages-dev | |
| mkdir -p docs-checkout/fern/pages-dev | |
| rsync -a source-checkout/docs/ docs-checkout/fern/pages-dev/ | |
| echo "Syncing index.yml to docs-website branch as versions/dev.yml..." | |
| mkdir -p docs-checkout/fern/versions | |
| cp source-checkout/docs/index.yml docs-checkout/fern/versions/dev.yml | |
| echo "Syncing fern.config.json to docs-website branch..." | |
| cp source-checkout/fern/fern.config.json docs-checkout/fern/fern.config.json | |
| if [ -f source-checkout/fern/.gitignore ]; then | |
| cp source-checkout/fern/.gitignore docs-checkout/fern/.gitignore | |
| fi | |
| if [ -f source-checkout/fern/convert_callouts.py ]; then | |
| cp source-checkout/fern/convert_callouts.py docs-checkout/fern/convert_callouts.py | |
| fi | |
| if [ -d source-checkout/fern/components ]; then | |
| echo "Syncing components/ to docs-website branch..." | |
| rm -rf docs-checkout/fern/components | |
| cp -r source-checkout/fern/components docs-checkout/fern/components | |
| fi | |
| if [ -f source-checkout/fern/main.css ]; then | |
| echo "Syncing main.css to docs-website branch..." | |
| cp source-checkout/fern/main.css docs-checkout/fern/main.css | |
| fi | |
| if [ -d source-checkout/fern/assets ]; then | |
| echo "Syncing assets/ to docs-website branch..." | |
| rm -rf docs-checkout/fern/assets | |
| cp -r source-checkout/fern/assets docs-checkout/fern/assets | |
| fi | |
| - name: Install yq | |
| uses: mikefarah/yq@v4 | |
| - name: Transform paths in dev.yml for docs-website layout | |
| run: | | |
| # In the source repo, index.yml uses paths relative to docs/ (e.g. OVERVIEW.md). | |
| # On docs-website, fern/versions/dev.yml needs ../pages-dev/ prefix for content. | |
| yq -i '(.. | select(has("path")).path) |= sub("^([a-zA-Z])", "../pages-dev/${1}")' docs-checkout/fern/versions/dev.yml | |
| - name: Convert GitHub callouts to Fern format | |
| run: | | |
| echo "Converting GitHub-style callouts to Fern format in pages-dev/..." | |
| python3 docs-checkout/fern/convert_callouts.py --dir docs-checkout/fern/pages-dev | |
| echo "Callout conversion complete." | |
| - name: Update docs.yml preserving products | |
| run: | | |
| cd docs-checkout/fern | |
| # Save the full products[0] block from docs-website (versions, path, etc.) | |
| yq '.products[0]' docs.yml > /tmp/preserved_product.yml | |
| echo "Preserved products[0] block:" | |
| cat /tmp/preserved_product.yml | |
| # Copy docs.yml from source to get config updates (redirects, layout, etc.) | |
| cp ../../source-checkout/fern/docs.yml docs.yml | |
| # Fix asset paths: source uses ./assets/ (relative to fern/), same on docs-website | |
| # Fix instance/logo paths that reference ../docs/assets/ if any | |
| sed -i 's|\.\./docs/assets/|./assets/|g' docs.yml | |
| # Restore the preserved products[0] block | |
| yq -i '.products[0] = load("/tmp/preserved_product.yml")' docs.yml | |
| 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: Setup Node.js | |
| if: steps.changes.outputs.has_changes == 'true' | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| - name: Install Fern CLI | |
| if: steps.changes.outputs.has_changes == 'true' | |
| run: npm install -g fern-api@4.23.0 | |
| ########################################################################## | |
| # 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_NUMBER="${GITHUB_REF#refs/heads/pull-request/}" | |
| gh pr comment "$PR_NUMBER" \ | |
| --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: linux-amd64-cpu32 | |
| permissions: | |
| contents: write | |
| 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., v1.0.0)" | |
| exit 1 | |
| fi | |
| VERSION="${TAG#v}" | |
| echo "tag=$TAG" >> $GITHUB_OUTPUT | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| echo "Processing version: $VERSION (tag: $TAG)" | |
| - name: Check if docs-website branch exists | |
| run: | | |
| if ! git ls-remote --exit-code --heads origin docs-website > /dev/null 2>&1; then | |
| echo "::error::docs-website branch does not exist. See fern/README.md for one-time setup instructions." | |
| exit 1 | |
| fi | |
| - name: Checkout docs-website branch | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: docs-website | |
| fetch-depth: 0 | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Check if version already exists | |
| 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 | |
| fi | |
| echo "Version $TAG does not exist yet, proceeding with release" | |
| - name: Setup Git | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| - name: Create versioned pages directory | |
| run: | | |
| TAG="${{ steps.version.outputs.tag }}" | |
| echo "Creating fern/pages-$TAG/ from fern/pages-dev/..." | |
| cp -r fern/pages-dev "fern/pages-$TAG" | |
| echo "Created fern/pages-$TAG/" | |
| - name: Update GitHub links to version tag | |
| run: | | |
| TAG="${{ steps.version.outputs.tag }}" | |
| find "fern/pages-$TAG" -name "*.md" -o -name "*.mdx" | while read file; do | |
| if grep -q "github.com/NVIDIA/NVSentinel/tree/main" "$file"; then | |
| sed -i "s|github.com/NVIDIA/NVSentinel/tree/main|github.com/NVIDIA/NVSentinel/tree/$TAG|g" "$file" | |
| fi | |
| if grep -q "github.com/NVIDIA/NVSentinel/blob/main" "$file"; then | |
| sed -i "s|github.com/NVIDIA/NVSentinel/blob/main|github.com/NVIDIA/NVSentinel/blob/$TAG|g" "$file" | |
| fi | |
| done | |
| - name: Convert GitHub callouts to Fern format | |
| run: | | |
| TAG="${{ steps.version.outputs.tag }}" | |
| python3 fern/convert_callouts.py --dir "fern/pages-$TAG" | |
| - name: Create version config file | |
| run: | | |
| TAG="${{ steps.version.outputs.tag }}" | |
| VERSION_FILE="fern/versions/$TAG.yml" | |
| echo "Creating version config: $VERSION_FILE" | |
| cp fern/versions/dev.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" | |
| echo "Created $VERSION_FILE" | |
| - name: Install yq | |
| uses: mikefarah/yq@v4 | |
| - name: Update docs.yml with new version | |
| run: | | |
| TAG="${{ steps.version.outputs.tag }}" | |
| DOCS_FILE="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 | |
| echo "Version $TAG already in docs.yml, skipping update" | |
| exit 0 | |
| 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") | |
| if [ -z "$DEV_IDX" ] || [ "$DEV_IDX" = "null" ]; then | |
| echo "::error::Could not find 'dev' version entry in docs.yml products[0].versions" | |
| exit 1 | |
| fi | |
| INSERT_IDX=$((DEV_IDX + 1)) | |
| yq -i " | |
| .products[0].versions |= ( | |
| .[:$INSERT_IDX] + | |
| [{\"display-name\": \"$TAG\", \"path\": \"./versions/$TAG.yml\", \"slug\": \"$TAG\", \"availability\": \"stable\"}] + | |
| .[$INSERT_IDX:] | |
| ) | |
| " "$DOCS_FILE" | |
| # Update the top-level entry to point to the new version | |
| yq -i ".products[0].path = \"./versions/$TAG.yml\"" "$DOCS_FILE" | |
| # 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" | |
| echo "Updated docs.yml products/versions section:" | |
| yq '.products[0].versions' "$DOCS_FILE" | |
| - name: Commit and push changes | |
| run: | | |
| TAG="${{ steps.version.outputs.tag }}" | |
| git add "fern/pages-$TAG/" | |
| git add "fern/versions/$TAG.yml" | |
| git add fern/docs.yml | |
| 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 | |
| Automated by fern-docs workflow | |
| Source tag: $TAG" | |
| git push origin docs-website | |
| 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@4.23.0 | |
| - name: Publish Docs | |
| env: | |
| FERN_TOKEN: ${{ secrets.FERN_TOKEN }} | |
| working-directory: ./fern | |
| run: fern generate --docs |