Skip to content

Preview Fern Docs: Comment #1774

Preview Fern Docs: Comment

Preview Fern Docs: Comment #1774

# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
#
# 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.
# Workflow 2 of 2 for Fern doc previews.
#
# Triggered by workflow_run after "Preview Fern Docs: Build" completes.
# Downloads the fern/ artifact, builds a preview with DOCS_FERN_TOKEN, and
# posts a stable :herb: comment on the PR. This workflow never checks out the
# PR branch directly, keeping secrets isolated from untrusted code.
#
# Required configuration:
# - Organization secret: DOCS_FERN_TOKEN (from `fern token` for the nvidia Fern org)
name: "Preview Fern Docs: Comment"
on:
workflow_run:
workflows: ["Preview Fern Docs: Build"]
types: [completed]
permissions:
pull-requests: write
actions: read
concurrency:
# Include head repository identity so fork branches with the same name
# don't share a group key and cancel unrelated runs.
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_repository.full_name || github.repository }}-${{ github.event.workflow_run.head_branch || github.event.workflow_run.id }}
cancel-in-progress: true
jobs:
preview:
runs-on: ubuntu-latest
timeout-minutes: 15
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Download fern sources and metadata
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: fern-preview
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Read PR metadata
id: metadata
run: |
echo "pr_number=$(cat preview-metadata/pr_number)" >> "$GITHUB_OUTPUT"
echo "head_ref=$(cat preview-metadata/head_ref)" >> "$GITHUB_OUTPUT"
# Check out the BASE repo (trusted default branch) so the fern-api CLI
# version is pinned from committed config, never from the fork-controlled
# artifact. Treat the downloaded fern/ strictly as data fed to the CLI.
- name: Checkout base repo for trusted config
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
path: base
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '20'
- name: Install Fern CLI
# Version is read from the BASE repo's committed fern.config.json (not
# the untrusted artifact), so a crafted fork .version can neither select
# an arbitrary fern-api nor inject shell. Still semver-validated as
# defense in depth.
run: |
set -euo pipefail
VERSION=$(jq -r .version base/fern/fern.config.json)
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.]+)?$ ]]; then
echo "::error::base fern.config.json .version '$VERSION' is not a valid semver"
exit 1
fi
npm install -g "fern-api@${VERSION}"
- name: Generate preview URL
id: generate-docs
env:
FERN_TOKEN: ${{ secrets.DOCS_FERN_TOKEN }}
HEAD_REF: ${{ steps.metadata.outputs.head_ref }}
working-directory: ./fern
run: |
set -o pipefail
fern generate --docs --preview --id "$HEAD_REF" 2>&1 | tee /tmp/fern-output.log
URL=$(grep -oP 'Published docs to \K.*(?= \()' /tmp/fern-output.log || true)
if [ -z "$URL" ]; then
echo "::error::Failed to generate preview URL. See fern output above."
exit 1
fi
echo "preview_url=$URL" >> "$GITHUB_OUTPUT"
- name: Build page links for changed files
id: page-links
env:
FERN_TOKEN: ${{ secrets.DOCS_FERN_TOKEN }}
PREVIEW_URL: ${{ steps.generate-docs.outputs.preview_url }}
run: |
CHANGED_FILES=""
if [ -f preview-metadata/changed_md_files ]; then
CHANGED_FILES=$(cat preview-metadata/changed_md_files)
fi
if [ -z "$CHANGED_FILES" ] || [ -z "$PREVIEW_URL" ]; then
echo "page_links=" >> "$GITHUB_OUTPUT"; exit 0
fi
BASE_URL=$(echo "$PREVIEW_URL" | grep -oP 'https?://[^/]+')
FILES_PARAM=$(echo "$CHANGED_FILES" | tr '\n' ',' | sed 's/,$//' \
| python3 -c "import sys, urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip(), safe=',/'))")
RESPONSE=$(curl -sf -H "FERN_TOKEN: $FERN_TOKEN" "${PREVIEW_URL}/api/fern-docs/get-slug-for-file?files=${FILES_PARAM}" 2>/dev/null) || {
echo "page_links=" >> "$GITHUB_OUTPUT"; exit 0
}
PAGE_LINKS=$(echo "$RESPONSE" | jq -r --arg url "$BASE_URL" \
'.mappings[] | select(.slug != null) | "- [\(.slug)](\($url)/\(.slug))"')
if [ -n "$PAGE_LINKS" ]; then
{ echo "page_links<<EOF"; echo "$PAGE_LINKS"; echo "EOF"; } >> "$GITHUB_OUTPUT"
else
echo "page_links=" >> "$GITHUB_OUTPUT"
fi
- name: Post or update PR comment
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ steps.metadata.outputs.pr_number }}
PREVIEW_URL: ${{ steps.generate-docs.outputs.preview_url }}
PAGE_LINKS: ${{ steps.page-links.outputs.page_links }}
run: |
BODY=":herb: **Preview your docs:** <${PREVIEW_URL}>"
if [ -n "${PAGE_LINKS}" ]; then
BODY="${BODY}
Here are the markdown pages you've updated:
${PAGE_LINKS}"
fi
MARKER="<!-- preview-docs -->"
BODY="${BODY}
${MARKER}"
COMMENT_ID=$(gh api "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \
--jq ".[] | select(.body | contains(\"${MARKER}\")) | .id" | tr -d '\r' | head -1)
if [ -n "$COMMENT_ID" ]; then
gh api "repos/${{ github.repository }}/issues/comments/${COMMENT_ID}" \
-X PATCH -f body="$BODY"
else
gh api "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \
-f body="$BODY"
fi