Skip to content

Preview Fern Docs: Comment #1225

Preview Fern Docs: Comment

Preview Fern Docs: Comment #1225

# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
# Workflow 2 of 2 for Fern doc previews.
#
# Triggered by workflow_run after "Preview Fern Docs: Build" completes.
# Downloads the docs/ 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]
workflow_dispatch:
inputs:
source_run_id:
description: "Run ID of a successful Preview Fern Docs: Build run"
required: true
type: string
pr_number:
description: "PR number to validate and comment on"
required: true
type: string
head_ref:
description: "Original PR branch name"
required: true
type: string
permissions:
pull-requests: write
actions: read
jobs:
preview:
runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Download fern sources and metadata
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: fern-preview
run-id: ${{ github.event_name == 'workflow_dispatch' && inputs.source_run_id || github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Read PR metadata
id: metadata
env:
# head_ref comes from the trusted workflow_run event, not the artifact.
WORKFLOW_HEAD_BRANCH: ${{ github.event_name == 'workflow_dispatch' && inputs.head_ref || github.event.workflow_run.head_branch }}
TRUSTED_PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.workflow_run.pull_requests[0].number }}
run: |
# Use the PR association from workflow_run. The artifact comes from the
# untrusted PR build, so it is only a consistency check.
PR_NUMBER="$TRUSTED_PR_NUMBER"
if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]]; then
echo "::error::Missing or invalid trusted PR number from workflow_run: '$PR_NUMBER'"
exit 1
fi
if [ -f preview-metadata/pr_number ]; then
ARTIFACT_PR_NUMBER=$(cat preview-metadata/pr_number)
if ! [[ "$ARTIFACT_PR_NUMBER" =~ ^[0-9]+$ ]]; then
echo "::error::Invalid PR number in artifact metadata: '$ARTIFACT_PR_NUMBER'"
exit 1
fi
if [ "$ARTIFACT_PR_NUMBER" != "$PR_NUMBER" ]; then
echo "::error::Artifact PR number '$ARTIFACT_PR_NUMBER' does not match workflow_run PR '$PR_NUMBER'"
exit 1
fi
fi
SAFE_HEAD_REF=$(printf '%s' "$WORKFLOW_HEAD_BRANCH" \
| tr '[:upper:]' '[:lower:]' \
| sed -E 's/[^a-z0-9-]+/-/g; s/^-+//; s/-+$//; s/-+/-/g' \
| cut -c1-48 \
| sed -E 's/-+$//')
if [ -n "$SAFE_HEAD_REF" ]; then
PREVIEW_ID="pr-${PR_NUMBER}-${SAFE_HEAD_REF}"
else
PREVIEW_ID="pr-${PR_NUMBER}"
fi
{
echo "head_ref=$WORKFLOW_HEAD_BRANCH"
echo "pr_number=$PR_NUMBER"
echo "preview_id=$PREVIEW_ID"
} >> "$GITHUB_OUTPUT"
echo "Using Fern preview id: $PREVIEW_ID"
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '20'
- name: Generate preview URL
id: generate-docs
env:
FERN_TOKEN: ${{ secrets.DOCS_FERN_TOKEN }}
PREVIEW_ID: ${{ steps.metadata.outputs.preview_id }}
working-directory: ./docs/fern
run: |
set +e
OUTPUT=$(npx -y fern-api@latest generate --docs --preview --id "$PREVIEW_ID" 2>&1)
STATUS=$?
set -e
echo "$OUTPUT"
if [ "$STATUS" -ne 0 ]; then
echo "::error::Fern preview generation failed for preview id '$PREVIEW_ID'."
exit "$STATUS"
fi
URL=$(echo "$OUTPUT" | grep -oP 'Published docs to \K.*(?= \()' | tail -1 || 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 MDX 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_mdx_files ]; then
CHANGED_FILES=$(cat preview-metadata/changed_mdx_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: |
# Build comment body
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
# Hidden marker for upsert
MARKER="<!-- preview-docs -->"
BODY="${BODY}
${MARKER}"
# Find existing comment with 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