forked from ai-dynamo/dynamo
-
Notifications
You must be signed in to change notification settings - Fork 0
865 lines (755 loc) · 40.9 KB
/
Copy pathfern-docs.yml
File metadata and controls
865 lines (755 loc) · 40.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
# 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.
# Trusted Fern Documentation Workflow
#
# This workflow handles trusted Fern documentation automation:
#
# Safe pre-merge Fern lint runs in pre-merge.yml on normal pull_request events.
#
# 1. SYNC & PUBLISH/PREVIEW: Syncs docs/ from source branch to fern/ on docs-website
# - Triggers on trusted pushes to main or pull-request/N when docs-site files change
# - On main: commits and pushes to docs-website, then publishes via `fern generate --docs`
# - On gated PR branches: generates a preview URL via `fern generate --docs --preview` and comments on PR
# - Preserves release-managed versions from docs-website's docs.yml
#
# 2. 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
#
# Note: The publish step is included inline because pushes made with GITHUB_TOKEN
# do not trigger other workflows (GitHub's anti-recursion guard), so we cannot
# rely on a separate publish-fern-docs.yml workflow for bot-initiated pushes.
#
# Validation: the sync/release composition (rsync scopes, nav path transforms,
# the shared-Reference machinery) only executes on main pushes and tag cuts.
# Before changing it, replay both jobs locally against the docs-website branch:
# docs/fern/scripts/simulate_docs_website.sh
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]+'
schedule:
# Refresh the Home page from the public Google Calendar.
- cron: '0 */6 * * *'
workflow_dispatch:
inputs:
tag:
description: 'Version tag to release (e.g., v0.9.0). Leave empty to sync dev docs.'
required: false
type: string
force_rebuild:
description: 'Overwrite an existing version snapshot (manual dispatch only). This replaces pages-$TAG and versions/$TAG.yml.'
required: false
default: false
type: boolean
permissions:
contents: write
pull-requests: write
jobs:
# Detect changed files for conditional job execution
changed-files:
runs-on: ubuntu-latest
# Skip for tag pushes - version release doesn't need changed-files check
if: github.ref_type != 'tag' && github.event_name != 'schedule'
outputs:
docs: ${{ steps.changes.outputs.docs }}
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# Do not use fetch-depth: 0 — changed-files now works with shallow clone
- name: Check for changes
id: changes
uses: ./.github/actions/changed-files
with:
gh_token: ${{ github.token }}
#############################################################################
# TRUSTED SYNC & PUBLISH/PREVIEW - Syncs docs content to docs-website structure
# On main: commits, pushes, and publishes to Fern
# On gated PR branches: generates a preview URL and comments on the PR
#############################################################################
preview-or-publish-docs:
name: Preview or publish docs
needs: changed-files
if: |
always() &&
github.ref_type != 'tag' &&
(github.event_name == 'schedule' || needs.changed-files.outputs.docs == 'true' || github.event_name == 'workflow_dispatch') &&
(github.event.inputs.tag == '' || github.event.inputs.tag == null)
runs-on: ubuntu-latest
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
path: source-checkout
fetch-depth: 1
- name: Checkout docs-website branch
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: docs-website
path: docs-checkout
fetch-depth: 1
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '22'
- name: Fetch community events from Google Calendar
run: |
# Install the pinned generator deps from the committed lockfile rather than
# a floating `npm install` — reproducible and offline-cache-friendly.
npm ci --prefix source-checkout/.github/scripts/events
# generate-events.js lives at .github/scripts/ and `require('node-ical')`s,
# so point Node at the events node_modules (npm --prefix installs there).
NODE_PATH=source-checkout/.github/scripts/events/node_modules \
node source-checkout/.github/scripts/generate-events.js
- name: Sync dev content from main
run: |
# Authored content lives under docs/fern/pages/. Mirror that directory
# directly into the docs-website branch's pages-dev/ snapshot. The Home
# page is copied separately to fern/index.mdx below.
echo "Syncing content pages to docs-website branch..."
rm -rf docs-checkout/fern/pages-dev
mkdir -p docs-checkout/fern/pages-dev
rsync -a \
--exclude='/home/index.mdx' \
source-checkout/docs/fern/pages/ docs-checkout/fern/pages-dev/
# pages/ is a source-only container. Snapshot pages sit one directory
# shallower, so adjust build-time Code includes that escape to examples/.
python3 source-checkout/docs/fern/scripts/rewrite_snapshot_paths.py \
docs-checkout/fern/pages-dev
# Backend deploy manifests are embedded into templates/{vllm,sglang,
# trtllm}.mdx and DGDR manifests into recipes/kubernetes-templates/dgdr.mdx via build-time
# <Code src="../../../examples/..."> file reads. Those resolve to the
# repo-root examples/ tree in the source layout, which does not exist
# on docs-website — sync just the referenced subtrees to the branch
# root so both ../../../examples (from pages-dev) and the versioned
# snapshots resolve.
echo "Syncing examples/backends/*/deploy/ and examples/deployments/dgdr/ to branch root..."
rsync -a \
--include='*/' \
--include='backends/*/deploy/**' \
--include='deployments/dgdr/**' \
--exclude='*' \
--prune-empty-dirs \
source-checkout/examples/ docs-checkout/examples/
# Sync index.yml as versions/dev.yml and transform paths for docs-website layout
echo "Syncing index.yml to docs-website branch as versions/dev.yml..."
cp source-checkout/docs/fern/index.yml docs-checkout/fern/versions/dev.yml
# Sync fern.config.json
echo "Syncing fern.config.json to docs-website branch..."
cp source-checkout/docs/fern/fern.config.json docs-checkout/fern/fern.config.json
# Sync README.md (developer guide, referenced from dev.yml)
if [ -f source-checkout/docs/fern/pages/developer-guide/contributing/documentation/building-and-publishing.md ]; then
cp source-checkout/docs/fern/pages/developer-guide/contributing/documentation/building-and-publishing.md docs-checkout/fern/README.md
fi
# Sync .gitignore if it exists
if [ -f source-checkout/docs/fern/.gitignore ]; then
cp source-checkout/docs/fern/.gitignore docs-checkout/fern/.gitignore
fi
# Sync the callout converter under fern/scripts/ and remove its old
# root-level location from docs-website after the source-tree move.
rm -f docs-checkout/fern/convert_callouts.py
if [ -f source-checkout/docs/fern/scripts/convert_callouts.py ]; then
mkdir -p docs-checkout/fern/scripts
cp source-checkout/docs/fern/scripts/convert_callouts.py docs-checkout/fern/scripts/convert_callouts.py
fi
# Sync components/ directory (e.g., CustomFooter.tsx)
if [ -d source-checkout/docs/fern/components ]; then
echo "Syncing components/ to docs-website branch..."
rm -rf docs-checkout/fern/components
cp -r source-checkout/docs/fern/components docs-checkout/fern/components
fi
# Remove the legacy product configs and sync the standalone Home page
# used by docs.yml and the Home tab.
rm -rf docs-checkout/fern/products
cp source-checkout/docs/fern/pages/home/index.mdx docs-checkout/fern/index.mdx
sed -i 's|\.\./\.\./assets/|./assets/|g' docs-checkout/fern/index.mdx
# Sync root-level assets/ (docs.yml logos/fonts and index.mdx reference
# ./assets/ relative to the Fern root, not pages-dev). Merge-copy without
# deleting so assets still referenced by older versioned pages survive.
if [ -d source-checkout/docs/fern/assets ]; then
echo "Syncing assets/ to docs-website branch..."
cp -r source-checkout/docs/fern/assets/. docs-checkout/fern/assets/
fi
# Sync legacy Digest asset mirror
if [ -d source-checkout/docs/fern/pages/blog/_assets ]; then
echo "Syncing Blog assets to the legacy digest/ compatibility path..."
# Keep legacy root Digest pages for older versions that still reference ../digest/.
mkdir -p docs-checkout/fern/digest
cp -r source-checkout/docs/fern/pages/blog/_assets/. docs-checkout/fern/digest/
# The digest posts were renamed .md -> .mdx at the source; retarget the
# release-managed versions/v*.yml snapshots that still point at the old
# extension. Harmless no-op once every snapshot references .mdx.
sed -i 's|\(path: \.\./digest/.*\)\.md$|\1.mdx|' docs-checkout/fern/versions/v*.yml
fi
# Sync site-wide CSS and the Reference-page interaction script.
if [ -f source-checkout/docs/fern/main.css ]; then
echo "Syncing main.css to docs-website branch..."
cp source-checkout/docs/fern/main.css docs-checkout/fern/main.css
fi
if [ -f source-checkout/docs/fern/custom.js ]; then
echo "Syncing custom.js to docs-website branch..."
cp source-checkout/docs/fern/custom.js docs-checkout/fern/custom.js
fi
# Sync Fern's native-localization mirror. Guarded on the source tree so
# this no-ops cleanly until docs/fern/translations/ is present.
# fern/translations/<lang>/pages-dev/<path> pairs with the base page at
# fern/pages-dev/<path>, per locale. Source mirrors live under pages/.
# Replace only the pages-dev mirrors --
# versioned snapshots (pages-v*) are committed by the release job and must
# survive dev syncs (see #11195).
if [ -d source-checkout/docs/fern/translations ]; then
echo "Syncing translations/ to docs-website branch..."
for d in docs-checkout/fern/translations/*/pages-dev; do
[ -d "$d" ] || continue
lang=$(basename "$(dirname "$d")")
if [ ! -d "source-checkout/docs/fern/translations/$lang/pages" ]; then
echo "Removing dev mirror for retired locale $lang..."
rm -rf "$d"
fi
done
for lang_dir in source-checkout/docs/fern/translations/*/; do
lang=$(basename "$lang_dir")
if [ -d "$lang_dir/pages" ]; then
rm -rf "docs-checkout/fern/translations/$lang/pages-dev"
mkdir -p "docs-checkout/fern/translations/$lang"
cp -r "$lang_dir/pages" "docs-checkout/fern/translations/$lang/pages-dev"
fi
done
fi
- name: Transform paths in dev.yml for docs-website layout
run: |
# In the source repo, index.yml uses paths under pages/ (for example, pages/cli/getting-started/quickstart.mdx).
# On docs-website, fern/versions/dev.yml needs ../pages-dev/ prefix for content
# and ../ prefix for the legacy Digest compatibility tree.
#
# Transform the standalone Home page first, then map the pages/ source prefix
# directly onto pages-dev/.
yq -i '(.. | select(has("path")).path) |= sub("^digest/", "../digest/")' docs-checkout/fern/versions/dev.yml
yq -i '(.. | select(has("path")).path) |= sub("^pages/home/index\.mdx$", "../index.mdx")' docs-checkout/fern/versions/dev.yml
yq -i '(.. | select(has("path")).path) |= sub("^pages/", "../pages-dev/")' docs-checkout/fern/versions/dev.yml
- name: Propagate shared Reference nav to released versions
run: |
# The Reference tab's General variant (Compatibility, Release
# Artifacts, Releases, Known Issues, Deprecations, Model EA Builds,
# Glossary) is SHARED across versions: release snapshots keep its nav
# paths on ../pages-dev/ so every version dropdown renders the
# always-current reference. When a page is added to the shared
# reference on main (e.g. a new release-notes page), copy the
# variant's nav block from dev.yml into each released version's yml
# so the new page appears in every dropdown, not just dev. Version
# snapshots cut before the reference rework have no such variant and
# are left untouched (the yq selection matches nothing).
yq '[.navigation[] | select(.tab == "reference") | .variants[] | select(.title == "General")][0]' \
docs-checkout/fern/versions/dev.yml > /tmp/reference_general_variant.yml
if [ "$(yq 'length' /tmp/reference_general_variant.yml)" = "0" ] || \
[ "$(head -c4 /tmp/reference_general_variant.yml)" = "null" ]; then
echo "No shared Reference General variant in dev.yml; skipping propagation"
else
for vfile in docs-checkout/fern/versions/v*.yml; do
[ -e "$vfile" ] || continue
# Only rewrite files that actually carry the shared variant —
# yq -i normalizes whitespace, so touching pre-rework snapshots
# (which have no reference General variant) is pure churn.
if [ "$(yq '[.navigation[] | select(.tab == "reference") | .variants[] | select(.title == "General")] | length' "$vfile")" != "0" ]; then
yq -i '(.navigation[] | select(.tab == "reference") | .variants[] | select(.title == "General")) = load("/tmp/reference_general_variant.yml")' "$vfile"
echo "Synced shared Reference nav into $vfile"
else
echo "Skipped (no shared Reference variant): $vfile"
fi
done
fi
- name: Convert GitHub callouts to Fern format
run: |
echo "Converting GitHub-style callouts to Fern format in pages/..."
python3 docs-checkout/fern/scripts/convert_callouts.py --dir docs-checkout/fern/pages-dev
# Convert the localized mirrors too (no-op when translations/ is absent).
if [ -d docs-checkout/fern/translations ]; then
python3 docs-checkout/fern/scripts/convert_callouts.py --dir docs-checkout/fern/translations
fi
echo "Callout conversion complete."
- name: Resolve relative links in translated pages
run: |
# Rewrites relative page links in translations to site URLs computed
# from the current nav (Fern's early-access localization does not yet
# resolve them; see docs/fern/scripts/resolve_translation_links.py).
# Guarded on both the mirror and resolver so it no-ops until A4 lands them.
if [ -d docs-checkout/fern/translations ] && [ -f source-checkout/docs/fern/scripts/resolve_translation_links.py ]; then
pip install --quiet pyyaml
python3 source-checkout/docs/fern/scripts/resolve_translation_links.py \
--nav source-checkout/docs/fern/index.yml \
--translations-root docs-checkout/fern/translations \
--site-root /dynamo --version-slug dev --pages-dir pages-dev
fi
- name: Update docs.yml preserving versions
run: |
cd docs-checkout/fern
# Preserve the release-managed version list. Production may still use
# the older Docs/Dynamo product wrapper during the first sync after this
# branch's navigation refactor, so accept either shape.
yq '. as $doc | ([$doc.products[]? | select(.display-name == "Docs" or .display-name == "Dynamo")][0].versions // $doc.versions)' \
docs.yml > /tmp/preserved_versions.yml
if [ "$(head -c4 /tmp/preserved_versions.yml)" = "null" ]; then
echo "::error::No release-managed versions found in docs.yml"
exit 1
fi
echo "Preserved release state:"
cat /tmp/preserved_versions.yml
# Copy docs.yml from source to get config updates (redirects, layout, etc.)
cp ../../source-checkout/docs/fern/docs.yml docs.yml
# Fix paths for the composed docs-website layout. Assets stay at the
# Fern root, while the source Home page is copied to fern/index.mdx.
sed -i 's|\.\./docs/assets/|./assets/|g' docs.yml
yq -i '."landing-page".path = "./index.mdx"' docs.yml
# The refactored source uses standalone top-level versioning rather than
# a product switcher. Restore the release history in that native shape.
yq -i '.versions = load("/tmp/preserved_versions.yml")' docs.yml
# Inject the private NVIDIA global theme for production builds only.
# It is intentionally absent from the source repo's docs.yml so that
# external contributors can run `fern docs dev` without an nvidia-org
# FERN_TOKEN (otherwise the CLI 403s fetching the private theme and the
# local preview renders blank). See https://github.com/ai-dynamo/dynamo/issues/10073
#
# PR previews skip the theme: applying it masks the project `css:`
# stylesheet in hosted builds, so previews of any fern/main.css change
# render unstyled. The repo main.css already carries the NVIDIA
# branding rules, so unthemed previews stay visually faithful.
if [ "${{ steps.ctx.outputs.is_main }}" = "true" ]; then
yq -i '.global-theme = "nvidia"' docs.yml
else
yq -i 'del(.global-theme)' docs.yml
fi
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: Install Fern CLI
if: steps.changes.outputs.has_changes == 'true'
run: npm install -g fern-api
##########################################################################
# 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_NUM="${GITHUB_REF##*/}"
gh pr comment "https://github.com/${{ github.repository }}/pull/${PR_NUM}" \
--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: ubuntu-latest
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., v0.9.0)"
exit 1
fi
# Extract version without 'v' prefix
VERSION="${TAG#v}"
echo "tag=$TAG" >> $GITHUB_OUTPUT
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Processing version: $VERSION (tag: $TAG)"
- name: Checkout source at tag
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# workflow_dispatch runs from a branch, so resolve the requested tag
# explicitly. This checkout provides the tagged docs, navigation, and
# callout converter used to build the release snapshot, so a tag cut
# from a release branch snapshots its own docs — never main's (#11140).
ref: ${{ steps.version.outputs.tag }}
path: source-checkout
fetch-depth: 1
- name: Checkout docs-website branch
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: docs-website
path: docs-checkout
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Check if version already exists
env:
# Empty on tag-push events; "true" or "false" on workflow_dispatch.
FORCE_REBUILD: ${{ github.event.inputs.force_rebuild }}
run: |
TAG="${{ steps.version.outputs.tag }}"
if [ -d "docs-checkout/fern/pages-$TAG" ] || [ -f "docs-checkout/fern/versions/$TAG.yml" ]; then
if [ "$FORCE_REBUILD" = "true" ]; then
echo "::warning::Version $TAG already exists; force_rebuild=true, overwriting its snapshot."
else
echo "::error::Version $TAG already exists. If a prior run committed but failed to publish, re-run via workflow_dispatch with force_rebuild=true to overwrite it intentionally."
exit 1
fi
else
echo "Version $TAG does not exist yet, proceeding with release"
fi
- name: Setup Git
working-directory: docs-checkout
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Build versioned pages from tagged commit
run: |
TAG="${{ steps.version.outputs.tag }}"
echo "Building docs-checkout/fern/pages-$TAG/ from source @ $TAG docs/fern/pages/..."
# pages-dev tracks main and can differ from a tag cut from a release
# branch. Copy the tag's authored pages directly; Fern-root config,
# components, assets, scripts, and translations remain outside snapshots.
rm -rf "docs-checkout/fern/pages-$TAG"
mkdir -p "docs-checkout/fern/pages-$TAG"
rsync -a \
--exclude='/home/index.mdx' \
source-checkout/docs/fern/pages/ "docs-checkout/fern/pages-$TAG/"
python3 source-checkout/docs/fern/scripts/rewrite_snapshot_paths.py \
"docs-checkout/fern/pages-$TAG"
echo "Created docs-checkout/fern/pages-$TAG/"
ls -la "docs-checkout/fern/pages-$TAG/" | head -20
- name: Verify snapshot inventory matches the tag
run: |
TAG="${{ steps.version.outputs.tag }}"
# Faithfulness guard (part of release validation): every file in the
# snapshot must trace back to a file in the tag's docs/fern/ tree.
# Runs before the shared-reference drop and callout conversion mutate
# the snapshot, so it catches foreign/injected files. Missing pages
# that the nav references are caught later by the nav-target existence
# check in "Validate release snapshot".
missing=0
while IFS= read -r f; do
rel="${f#docs-checkout/fern/pages-$TAG/}"
if [ ! -e "source-checkout/docs/fern/pages/$rel" ]; then
echo "::error::Snapshot file has no counterpart in the tagged source: $rel"
missing=1
fi
done < <(find "docs-checkout/fern/pages-$TAG" -type f)
if [ "$missing" != "0" ]; then
echo "::error::Release snapshot for $TAG contains files absent from the tag; aborting."
exit 1
fi
echo "Snapshot inventory for $TAG matches the tagged source."
- name: Drop shared Reference pages from the snapshot
run: |
TAG="${{ steps.version.outputs.tag }}"
# The Reference tab's General variant is shared (always-current)
# across versions: its nav paths stay on ../pages-dev/ (see the
# version-config step), so drop exactly the files that variant
# references from the snapshot rather than freezing stale copies.
# Everything else under reference/ (observability pages, config
# references) belongs to versioned tabs and stays in the snapshot.
# If the selectors stop matching (tab or variant renamed), this and
# the version-config revert silently no-op and the reference quietly
# freezes per version again — warn loudly so the rename gets fixed.
if [ "$(yq '[.navigation[] | select(.tab == "reference") | .variants[] | select(.title == "General")] | length' docs-checkout/fern/versions/dev.yml)" = "0" ]; then
echo "::warning::No Reference General variant found in dev.yml — the shared-reference exclusion is a no-op and $TAG will freeze its own reference copy. If the tab or variant was renamed, update the yq selectors in this workflow."
fi
yq '.navigation[] | select(.tab == "reference") | .variants[] | select(.title == "General") | .. | select(has("path")) | .path' \
docs-checkout/fern/versions/dev.yml | sed 's|^\.\./pages-dev/||' | while read -r relpath; do
[ -n "$relpath" ] && rm -f "docs-checkout/fern/pages-$TAG/$relpath"
done
find "docs-checkout/fern/pages-$TAG/reference" -type d -empty -delete 2>/dev/null || true
- name: Snapshot translation mirrors
run: |
TAG="${{ steps.version.outputs.tag }}"
# Snapshot translations from the tagged source, not the docs-website
# pages-dev mirror. The dev mirror has already had relative links
# rewritten to /dynamo/.../dev URLs; copying it would pin released
# translations to dev. Starting from the tag's authored relative links
# lets the resolver below produce /dynamo/.../$TAG URLs.
# fern/translations/<lang>/pages-$TAG/<path> pairs with
# fern/pages-$TAG/<path>. No-op when the tag has no translations.
if [ -d source-checkout/docs/fern/translations ]; then
for lang_dir in source-checkout/docs/fern/translations/*/; do
lang=$(basename "$lang_dir")
if [ -d "$lang_dir/pages" ]; then
echo "Snapshotting tagged translations for $lang -> pages-$TAG..."
rm -rf "docs-checkout/fern/translations/$lang/pages-$TAG"
mkdir -p "docs-checkout/fern/translations/$lang/pages-$TAG"
rsync -a "$lang_dir/pages/" "docs-checkout/fern/translations/$lang/pages-$TAG/"
fi
done
fi
- name: Freeze the install selector in the versioned Quickstart
# Dev keeps the interactive selector; a release snapshot is static and
# pinned, so this substitutes version-pinned commands and strips the
# <InstallSelector /> import from the tagged page. The script no-ops if
# the marker is absent (older snapshots). Runs before callout conversion.
run: |
python3 source-checkout/.github/scripts/freeze_install_selector.py \
"docs-checkout/fern/pages-${{ steps.version.outputs.tag }}/cli/getting-started/quickstart.mdx" \
"${{ steps.version.outputs.version }}"
- name: Update GitHub links to version tag
run: |
TAG="${{ steps.version.outputs.tag }}"
echo "Pinning GitHub links from main to $TAG in docs-checkout/fern/pages-$TAG/..."
# Base pages plus any translation snapshots for this tag.
SNAP_DIRS="docs-checkout/fern/pages-$TAG"
for d in docs-checkout/fern/translations/*/"pages-$TAG"; do
[ -d "$d" ] && SNAP_DIRS="$SNAP_DIRS $d"
done
# Pin tree/main and blob/main links before callout conversion.
find $SNAP_DIRS -type f \( -name "*.md" -o -name "*.mdx" \) | while read -r file; do
if grep -q "github.com/ai-dynamo/dynamo/tree/main" "$file"; then
echo "Updating tree links: $file"
sed -i "s|github.com/ai-dynamo/dynamo/tree/main|github.com/ai-dynamo/dynamo/tree/$TAG|g" "$file"
fi
done
find $SNAP_DIRS -type f \( -name "*.md" -o -name "*.mdx" \) | while read -r file; do
if grep -q "github.com/ai-dynamo/dynamo/blob/main" "$file"; then
echo "Updating blob links: $file"
sed -i "s|github.com/ai-dynamo/dynamo/blob/main|github.com/ai-dynamo/dynamo/blob/$TAG|g" "$file"
fi
done
echo "GitHub link update complete."
- name: Convert GitHub callouts to Fern format
run: |
TAG="${{ steps.version.outputs.tag }}"
# Use the tag's own converter so conversion matches the release.
echo "Converting callouts in pages-$TAG/ with the tag's convert_callouts.py..."
python3 source-checkout/docs/fern/scripts/convert_callouts.py --dir "docs-checkout/fern/pages-$TAG"
for d in docs-checkout/fern/translations/*/"pages-$TAG"; do
[ -d "$d" ] && python3 source-checkout/docs/fern/scripts/convert_callouts.py --dir "$d"
done
echo "Callout conversion complete."
- name: Resolve relative links in translated snapshot
run: |
TAG="${{ steps.version.outputs.tag }}"
# Rewrites relative page links in the translation snapshot to site URLs
# under this tag's version slug, from the tag's nav (see
# docs/fern/scripts/resolve_translation_links.py). Guarded on the
# snapshot and resolver; capability-gated on --pages-dir so workflow_dispatch
# rebuilds of tags whose resolver predates that flag skip cleanly.
if compgen -G "docs-checkout/fern/translations/*/pages-$TAG" > /dev/null \
&& [ -f source-checkout/docs/fern/scripts/resolve_translation_links.py ]; then
if grep -q -- '--pages-dir' source-checkout/docs/fern/scripts/resolve_translation_links.py; then
pip install --quiet pyyaml
python3 source-checkout/docs/fern/scripts/resolve_translation_links.py \
--nav source-checkout/docs/fern/index.yml \
--translations-root docs-checkout/fern/translations \
--site-root /dynamo --version-slug "$TAG" \
--pages-dir "pages-$TAG" --github-ref "$TAG"
else
echo "::warning::$TAG's resolver predates --pages-dir; skipping translated-snapshot link resolution"
fi
fi
- name: Create version config file
run: |
TAG="${{ steps.version.outputs.tag }}"
VERSION_FILE="docs-checkout/fern/versions/$TAG.yml"
echo "Creating version config from the tag's index.yml: $VERSION_FILE"
# Navigation must come from the tag so it references exactly the pages
# in the release snapshot (#11140). Mirror the dev-nav transform, but
# point content pages at this tag. Digest and the Home index are shared
# siblings on docs-website; rewrite them first so the content-page rule
# below does not re-match.
cp source-checkout/docs/fern/index.yml "$VERSION_FILE"
yq -i '(.. | select(has("path")).path) |= sub("^digest/", "../digest/")' "$VERSION_FILE"
yq -i '(.. | select(has("path")).path) |= sub("^pages/home/index\.mdx$", "../index.mdx")' "$VERSION_FILE"
yq -i '(.. | select(has("path")).path) |= sub("^pages/", "../pages-'"$TAG"'/")' "$VERSION_FILE"
# Revert the Reference tab's General variant to the shared source:
# it is always-current across versions (release metadata is
# cumulative — each page carries per-release sections), so its pages
# render from pages-dev in every version (paired with the snapshot
# drop above). The Kubernetes API and Components variants stay on the
# frozen snapshot (CRD fields and config flags are genuinely per-version).
yq -i "(.navigation[] | select(.tab == \"reference\") | .variants[] | select(.title == \"General\") | .. | select(has(\"path\")).path) |= sub(\"\.\./pages-$TAG/\", \"../pages-dev/\")" "$VERSION_FILE"
# Keep cross-navigation links within the selected documentation version.
sed -i "s|href: /dynamo/dev/|href: /dynamo/$TAG/|g" "$VERSION_FILE"
echo "Created $VERSION_FILE"
echo "First 30 lines:"
head -30 "$VERSION_FILE"
- name: Update docs.yml with new version
run: |
TAG="${{ steps.version.outputs.tag }}"
DOCS_FILE="docs-checkout/fern/docs.yml"
echo "Updating $DOCS_FILE to include $TAG..."
if yq ".versions[] | select(.display-name == \"$TAG\")" "$DOCS_FILE" | grep -q .; then
echo "Version $TAG already in docs.yml, skipping update"
exit 0
fi
# Find the dev entry and insert the release immediately after it,
# preserving Latest at index 0.
DEV_IDX=$(yq '.versions | to_entries | map(select(.value.display-name == "dev")) | .[0].key' "$DOCS_FILE")
if [ "$DEV_IDX" = "null" ]; then
echo "::error::dev version not found in $DOCS_FILE"
exit 1
fi
INSERT_IDX=$((DEV_IDX + 1))
TAG="$TAG" INSERT_IDX="$INSERT_IDX" yq -i '
.versions |= (
.[:env(INSERT_IDX)] +
[{"display-name": env(TAG), "path": ("./versions/" + env(TAG) + ".yml"), "slug": env(TAG), "availability": "stable"}] +
.[env(INSERT_IDX):]
)
' "$DOCS_FILE"
# Point the Latest entry at the new release.
yq -i ".versions[0].path = \"./versions/$TAG.yml\"" "$DOCS_FILE"
yq -i ".versions[0].display-name = \"Latest ($TAG)\"" "$DOCS_FILE"
echo "Updated versions:"
yq '.versions' "$DOCS_FILE"
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '22'
- name: Install Fern CLI
# Pin to the tag's fern.config.json version so a release publishes with the
# exact CLI the tag was authored against, not whatever floats on npm.
run: npm install -g fern-api@$(jq -r '.version' source-checkout/docs/fern/fern.config.json)
- name: Validate release snapshot
env:
FERN_TOKEN: ${{ secrets.FERN_TOKEN }}
run: |
TAG="${{ steps.version.outputs.tag }}"
VERSION_FILE="docs-checkout/fern/versions/$TAG.yml"
# Every navigation target must resolve. Versioned pages are part of this
# release and must exist; the shared Digest is synced independently from
# main, so a tag's frozen Digest nav can drift — report without blocking.
while IFS= read -r path; do
case "$path" in
../pages-$TAG/*)
if [ ! -e "docs-checkout/fern/versions/$path" ]; then
echo "::error::Version navigation target does not exist: $path"
exit 1
fi
;;
../digest/*)
if [ ! -e "docs-checkout/fern/versions/$path" ]; then
echo "::warning::Shared Digest navigation target does not exist in docs-website: $path"
fi
;;
esac
done < <(yq -r '(.. | select(has("path")).path)' "$VERSION_FILE")
# Run Fern's structural validation. The docs-website branch has
# historical broken-link debt, so link checking is scoped above to the
# release navigation rather than failing on unrelated versions.
cd docs-checkout
fern check
- name: Commit and push changes
working-directory: docs-checkout
run: |
TAG="${{ steps.version.outputs.tag }}"
git add "fern/pages-$TAG/"
git add "fern/versions/$TAG.yml"
git add fern/docs.yml
# Versioned translation snapshots (see #11195); the glob is empty for
# tags cut from branches without fern/translations.
for d in fern/translations/*/"pages-$TAG"; do
[ -d "$d" ] && git add "$d"
done
if git diff --cached --quiet; then
echo "No release artifact changes for $TAG; skipping commit and push."
exit 0
fi
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
- Snapshotted translation mirrors (fern/translations/*/pages-$TAG), when present
Automated by fern-docs workflow
Source tag: $TAG"
git push origin docs-website
echo "Successfully released documentation for $TAG on docs-website branch"
- name: Publish Docs
env:
FERN_TOKEN: ${{ secrets.FERN_TOKEN }}
working-directory: docs-checkout/fern
run: fern generate --docs