Skip to content

Commit 303cb7f

Browse files
Add global-theme, navbar-links, two-stage PR preview, and fern broken-links
- Add `global-theme: nvidia` to docs.yml (mandatory for NVIDIA Fern branding) - Add GitHub navbar link to docs.yml header - Fix accentPrimary light color to match NVIDIA green (#76B900) - CI: split PR preview into two stages so FERN_TOKEN is never exposed to fork PRs; stage 2 runs via workflow_run in base-branch context - CI: add `fern docs broken-links` step for internal link checking - CI: stage 1 saves PR metadata artifact so stage 2 can post preview comment - README: document fern login requirement, how to get FERN_TOKEN, custom domain requirement, and two-stage PR preview security model Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 7664f55 commit 303cb7f

5 files changed

Lines changed: 131 additions & 32 deletions

File tree

.github/workflows/fern-docs.yml

Lines changed: 96 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,17 @@ on:
2121
- "python/cuopt_server/**"
2222
- "fern/**"
2323
workflow_dispatch:
24-
25-
# Shared setup extracted as a reusable step sequence via defaults.
26-
# Both jobs repeat the setup because GitHub Actions has no native
27-
# "setup once, share artifacts" without a separate job + upload/download cycle.
24+
# Stage 2: triggered after a PR build of this same workflow completes.
25+
# Runs in base-branch context so FERN_TOKEN secret is accessible.
26+
workflow_run:
27+
workflows: ["Fern Docs"]
28+
types: [completed]
2829

2930
jobs:
31+
# ──────────────────────────────────────────────────────────────────────────
32+
# Stage 1 (runs without secrets): convert RST→MDX, validate, upload artifact.
33+
# Safe for fork PRs — no secrets are accessed here.
34+
# ──────────────────────────────────────────────────────────────────────────
3035
convert-and-check:
3136
name: Convert RST → MDX and fern check
3237
runs-on: ubuntu-latest
@@ -59,8 +64,19 @@ jobs:
5964
env:
6065
FERN_TOKEN: ${{ secrets.FERN_TOKEN }}
6166

62-
# Upload the generated MDX + docs.yml as an artifact so reviewers can
63-
# inspect exactly what will be published without running the build locally.
67+
- name: Check internal links
68+
run: fern docs broken-links
69+
continue-on-error: true
70+
env:
71+
FERN_TOKEN: ${{ secrets.FERN_TOKEN }}
72+
73+
# Upload fern/ directory + PR metadata as artifact.
74+
# Stage 2 (preview-comment) downloads this artifact and uses FERN_TOKEN
75+
# from secrets to publish the preview — secrets are never exposed to fork PRs.
76+
- name: Save PR metadata
77+
if: github.event_name == 'pull_request'
78+
run: echo "${{ github.event.pull_request.number }}" > fern/pr-number.txt
79+
6480
- name: Upload generated docs as artifact
6581
uses: actions/upload-artifact@v4
6682
with:
@@ -70,23 +86,17 @@ jobs:
7086
fern/docs-v*.yml
7187
fern/docs/pages/
7288
fern/openapi/
89+
fern/fern.config.json
90+
fern/pr-number.txt
7391
retention-days: 14
7492

75-
# On pull requests, publish a temporary preview URL (does not affect the
76-
# live site) so reviewers can browse the rendered docs in a browser.
77-
# Requires FERN_TOKEN secret; skipped silently if the secret is absent.
78-
- name: Publish Fern preview (PR only)
79-
if: github.event_name == 'pull_request' && secrets.FERN_TOKEN != ''
80-
run: fern generate --docs --preview
81-
env:
82-
FERN_TOKEN: ${{ secrets.FERN_TOKEN }}
83-
93+
# ──────────────────────────────────────────────────────────────────────────
94+
# Check external URLs with lychee (non-blocking).
95+
# ──────────────────────────────────────────────────────────────────────────
8496
link-check:
85-
name: Check URLs in generated MDX
97+
name: Check external URLs
8698
runs-on: ubuntu-latest
8799
needs: convert-and-check
88-
# Non-blocking: broken external links shouldn't prevent publishing.
89-
# Check the job summary for any failures.
90100
continue-on-error: true
91101
steps:
92102
- uses: actions/checkout@v4
@@ -106,18 +116,12 @@ jobs:
106116
pip install -r fern/requirements-docs.txt
107117
pip install -e python/cuopt_server --no-deps
108118
109-
- name: Install Fern CLI
110-
run: npm install -g fern-api@$(jq -r .version fern/fern.config.json)
111-
112119
- name: Generate MDX pages
113120
run: python fern/convert_docs.py
114121

115122
- name: Check URLs with lychee
116123
uses: lycheeverse/lychee-action@v2
117124
with:
118-
# --exclude-path: skip version redirect pages (they contain no real links)
119-
# --exclude-file: patterns to ignore (rate-limited hosts, auth-required, etc.)
120-
# --accept 429: treat rate-limit responses as passing (not a dead link)
121125
args: >-
122126
--exclude-path fern/docs/pages/versions
123127
--exclude-file fern/.lycheeignore
@@ -136,6 +140,74 @@ jobs:
136140
path: lychee-report.md
137141
retention-days: 14
138142

143+
# ──────────────────────────────────────────────────────────────────────────
144+
# Stage 2 (PR preview): runs with secrets in base-branch context.
145+
# Triggered by workflow_run so fork PRs cannot access the FERN_TOKEN secret.
146+
# ──────────────────────────────────────────────────────────────────────────
147+
preview-comment:
148+
name: Publish PR preview
149+
runs-on: ubuntu-latest
150+
# Only runs when triggered by a PR build of convert-and-check.
151+
# workflow_run always executes in the context of the base branch (trusted).
152+
if: >
153+
github.event_name == 'workflow_run' &&
154+
github.event.workflow_run.event == 'pull_request' &&
155+
github.event.workflow_run.conclusion == 'success' &&
156+
secrets.FERN_TOKEN != ''
157+
permissions:
158+
pull-requests: write
159+
steps:
160+
- name: Download docs artifact
161+
uses: actions/download-artifact@v4
162+
with:
163+
name: fern-docs-${{ github.event.workflow_run.id }}
164+
path: fern/
165+
166+
- name: Install Fern CLI
167+
run: npm install -g fern-api@$(jq -r .version fern/fern.config.json)
168+
169+
- name: Read PR number
170+
id: pr
171+
run: echo "number=$(cat fern/pr-number.txt)" >> "$GITHUB_OUTPUT"
172+
173+
- name: Publish preview to Fern cloud
174+
id: preview
175+
run: |
176+
OUTPUT=$(fern generate --docs --preview 2>&1)
177+
echo "$OUTPUT"
178+
URL=$(echo "$OUTPUT" | grep -oP 'https://[^ ]+\.docs\.buildwithfern\.com[^ ]*' | head -1)
179+
echo "url=${URL}" >> "$GITHUB_OUTPUT"
180+
env:
181+
FERN_TOKEN: ${{ secrets.FERN_TOKEN }}
182+
183+
- name: Post preview URL as PR comment
184+
if: steps.preview.outputs.url != ''
185+
uses: actions/github-script@v7
186+
with:
187+
script: |
188+
const pr = ${{ steps.pr.outputs.number }};
189+
const url = "${{ steps.preview.outputs.url }}";
190+
const body = `## 📚 Docs preview\n\n${url}\n\n*Preview published from run [#${{ github.run_id }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})*`;
191+
const { data: comments } = await github.rest.issues.listComments({
192+
owner: context.repo.owner, repo: context.repo.repo, issue_number: pr
193+
});
194+
const existing = comments.find(c => c.body.startsWith('## 📚 Docs preview'));
195+
if (existing) {
196+
await github.rest.issues.updateComment({
197+
owner: context.repo.owner, repo: context.repo.repo,
198+
comment_id: existing.id, body
199+
});
200+
} else {
201+
await github.rest.issues.createComment({
202+
owner: context.repo.owner, repo: context.repo.repo,
203+
issue_number: pr, body
204+
});
205+
}
206+
207+
# ──────────────────────────────────────────────────────────────────────────
208+
# Publish to the live site on push to docs-fern-migration.
209+
# Requires FERN_TOKEN secret.
210+
# ──────────────────────────────────────────────────────────────────────────
139211
publish:
140212
name: Publish to Fern
141213
runs-on: ubuntu-latest

README.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@ Documentation sources live in `docs/cuopt/source/` (RST) and `fern/` (Fern platf
4242

4343
The Fern CLI (`fern-api`) is installed automatically by `build.sh` at the version pinned in `fern/fern.config.json`.
4444

45+
### Log in for local preview (first time only)
46+
47+
The docs use `global-theme: nvidia`, which requires a Fern account authorized for the NVIDIA organization. Run this once:
48+
49+
```bash
50+
fern login --email <your-nvidia-email>
51+
```
52+
4553
### Build and preview locally
4654

4755
```bash
@@ -59,16 +67,21 @@ To publish manually (requires `FERN_TOKEN`):
5967
FERN_TOKEN=<your-token> ./build.sh docs --publish-docs
6068
```
6169

62-
The live site is at **https://nvidia-cuopt.docs.buildwithfern.com**.
70+
**Getting `FERN_TOKEN`**: Ping `@aschilling`, `@dkoperda`, `@llane`, `@mmckiernan`, or `@nmckimpson` in the NVIDIA internal `#cdd-fern` Slack channel. Add the token as a `FERN_TOKEN` GitHub Actions secret in the repository settings.
71+
72+
The live site is at **https://nvidia-cuopt.docs.buildwithfern.com**. Before going to production, a custom domain (`docs.nvidia.com/cuopt`) must be configured by the Fern team with Akamai fronting — contact `#cdd-fern` when ready.
6373

6474
### How CI works
6575

6676
| Trigger | What happens |
6777
|---|---|
68-
| Any push / PR touching `docs/`, `fern/`, or `python/cuopt/` | RST → MDX conversion + `fern check` + URL link check |
69-
| Pull request | Fern preview URL published (temporary, doesn't affect live site) |
78+
| Any push / PR touching `docs/`, `fern/`, or `python/cuopt/` | RST → MDX conversion + `fern check` + internal link check + external URL check |
79+
| Pull request (stage 1, no secrets) | Converts RST → MDX, uploads `fern/` as artifact |
80+
| Pull request (stage 2, secrets safe) | Publishes Fern preview, posts URL as PR comment |
7081
| Push to `docs-fern-migration` | Publishes to the live site via `fern generate --docs` |
7182

83+
PR preview uses a two-stage workflow so that `FERN_TOKEN` is never exposed to fork PRs. Stage 2 runs in the base-branch context via `workflow_run`, where repository secrets are accessible.
84+
7285
CI workflow: [`.github/workflows/fern-docs.yml`](.github/workflows/fern-docs.yml)
7386

7487
## Supported APIs

fern/convert_docs.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,13 @@ def _collect_rst_files(src: Path) -> list[Path]:
592592
- url: nvidia-cuopt.docs.buildwithfern.com
593593
594594
title: NVIDIA cuOpt
595+
596+
global-theme: nvidia
597+
598+
navbar-links:
599+
- type: github
600+
value: https://github.com/NVIDIA/cuopt
601+
595602
logo:
596603
dark: docs/images/nvidia-cuopt-logo-dark.svg
597604
light: docs/images/nvidia-cuopt-logo-light.svg
@@ -600,7 +607,7 @@ def _collect_rst_files(src: Path) -> list[Path]:
600607
colors:
601608
accentPrimary:
602609
dark: "#76B900"
603-
light: "#4a7600"
610+
light: "#76B900"
604611
605612
libraries:
606613
cuopt-c-api:

fern/docs.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@ instances:
22
- url: nvidia-cuopt.docs.buildwithfern.com
33

44
title: NVIDIA cuOpt
5+
6+
global-theme: nvidia
7+
8+
navbar-links:
9+
- type: github
10+
value: https://github.com/NVIDIA/cuopt
11+
512
logo:
613
dark: docs/images/nvidia-cuopt-logo-dark.svg
714
light: docs/images/nvidia-cuopt-logo-light.svg
@@ -10,7 +17,7 @@ logo:
1017
colors:
1118
accentPrimary:
1219
dark: "#76B900"
13-
light: "#4a7600"
20+
light: "#76B900"
1421

1522
libraries:
1623
cuopt-c-api:

fern/openapi/cuopt_spec.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1019,7 +1019,7 @@ paths:
10191019
type: array
10201020
reqId:
10211021
description: Id of request
1022-
example: 3325c06f-0627-44bd-9feb-01705d482e18
1022+
example: 8e08db34-f717-43d2-8527-ad9275fb3447
10231023
title: Reqid
10241024
type: string
10251025
required:
@@ -3855,7 +3855,7 @@ components:
38553855
type: string
38563856
title: Reqid
38573857
description: Id to poll for result
3858-
example: 51a011fb-e9c8-4384-9a9f-f3b4f1680e92
3858+
example: 99674305-d8f7-41f0-a883-5374e48ea8da
38593859
type: object
38603860
required:
38613861
- reqId
@@ -4201,7 +4201,7 @@ components:
42014201
type: string
42024202
title: Reqid
42034203
description: Id of request
4204-
example: 3325c06f-0627-44bd-9feb-01705d482e18
4204+
example: 8e08db34-f717-43d2-8527-ad9275fb3447
42054205
additionalProperties: false
42064206
type: object
42074207
required:

0 commit comments

Comments
 (0)