Skip to content

Commit 66ab398

Browse files
committed
ci: record Fern publish provenance
1 parent 2b3248c commit 66ab398

5 files changed

Lines changed: 98 additions & 4 deletions

File tree

.github/workflows/build-fern-docs.yml

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,19 @@ jobs:
114114
- name: Prepare Fern release snapshot
115115
env:
116116
RELEASE_TAG: ${{ needs.resolve-release.outputs.release_tag }}
117+
SOURCE_REF: ${{ needs.resolve-release.outputs.source_ref }}
118+
SOURCE_REPOSITORY: ${{ github.repository }}
119+
PUBLISHED_BRANCH: ${{ env.FERN_PUBLISHED_BRANCH }}
117120
run: |
121+
source_sha=$(git -C source rev-parse HEAD)
118122
python3 workflow/fern/scripts/fern-published-branch.py sync-source \
119123
--source-root source \
120-
--published-root website
124+
--published-root website \
125+
--metadata-source-repository "$SOURCE_REPOSITORY" \
126+
--metadata-source-ref "$SOURCE_REF" \
127+
--metadata-source-sha "$source_sha" \
128+
--metadata-release-tag "$RELEASE_TAG" \
129+
--metadata-published-branch "$PUBLISHED_BRANCH"
121130
python3 workflow/fern/scripts/fern-release-version.py --root website/fern prepare --version "$RELEASE_TAG" --force
122131
python3 workflow/fern/scripts/fern-release-version.py --root website/fern check --version "$RELEASE_TAG" --require-latest-matches-release
123132
@@ -146,17 +155,24 @@ jobs:
146155
- name: Commit published branch
147156
env:
148157
RELEASE_TAG: ${{ needs.resolve-release.outputs.release_tag }}
158+
SOURCE_REF: ${{ needs.resolve-release.outputs.source_ref }}
159+
SOURCE_REPOSITORY: ${{ github.repository }}
149160
working-directory: website
150161
run: |
151162
if [ -z "$(git status --porcelain)" ]; then
152163
echo "::notice::Fern published branch already has $RELEASE_TAG."
153164
exit 0
154165
fi
155166
167+
source_sha=$(git -C ../source rev-parse HEAD)
168+
previous_published_sha=$(git rev-parse HEAD)
156169
git config user.name "github-actions[bot]"
157170
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
158171
git add -A
159-
git commit -m "docs: publish Fern docs for $RELEASE_TAG"
172+
git commit \
173+
-m "docs: publish Fern docs for $RELEASE_TAG" \
174+
-m "Source: $SOURCE_REPOSITORY@$SOURCE_REF ($source_sha)" \
175+
-m "Previous docs-website head: $previous_published_sha"
160176
git push origin HEAD:"$FERN_PUBLISHED_BRANCH"
161177
162178
build-notebooks:

.github/workflows/publish-fern-devnotes.yml

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,19 @@ jobs:
6565
git checkout -B "$FERN_PUBLISHED_BRANCH" FETCH_HEAD
6666
6767
- name: Patch Dev Notes into latest Fern docs
68+
env:
69+
SOURCE_REF: ${{ github.ref }}
70+
SOURCE_REPOSITORY: ${{ github.repository }}
71+
PUBLISHED_BRANCH: ${{ env.FERN_PUBLISHED_BRANCH }}
6872
run: |
73+
source_sha=$(git -C source rev-parse HEAD)
6974
python3 workflow/fern/scripts/fern-published-branch.py patch-devnotes \
7075
--source-root source \
71-
--published-root website
76+
--published-root website \
77+
--metadata-source-repository "$SOURCE_REPOSITORY" \
78+
--metadata-source-ref "$SOURCE_REF" \
79+
--metadata-source-sha "$source_sha" \
80+
--metadata-published-branch "$PUBLISHED_BRANCH"
7281
7382
- name: Install uv
7483
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
@@ -115,17 +124,25 @@ jobs:
115124
run: make check-fern-docs
116125

117126
- name: Commit published branch
127+
env:
128+
SOURCE_REF: ${{ github.ref }}
129+
SOURCE_REPOSITORY: ${{ github.repository }}
118130
working-directory: website
119131
run: |
120132
if [ -z "$(git status --porcelain)" ]; then
121133
echo "::notice::Fern Dev Notes are already up to date."
122134
exit 0
123135
fi
124136
137+
source_sha=$(git -C ../source rev-parse HEAD)
138+
previous_published_sha=$(git rev-parse HEAD)
125139
git config user.name "github-actions[bot]"
126140
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
127141
git add -A
128-
git commit -m "docs: patch Fern devnotes into latest"
142+
git commit \
143+
-m "docs: patch Fern devnotes into latest" \
144+
-m "Source: $SOURCE_REPOSITORY@$SOURCE_REF ($source_sha)" \
145+
-m "Previous docs-website head: $previous_published_sha"
129146
git push origin HEAD:"$FERN_PUBLISHED_BRANCH"
130147
131148
- name: Publish Fern docs

fern/AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ This folder contains the Fern docs site for NeMo Data Designer. Use `fern/README
2424

2525
Published release snapshots live on the CI-managed `docs-website` branch. Do not manually edit `docs-website` unless the user explicitly asks for release archive repair.
2626

27+
`docs-website` is an orphan-style publish branch. Published commits should include `fern/publish-metadata.json` with source repository, ref, SHA, release tag when applicable, and published branch.
28+
2729
The `docs-website` branch must already contain the historical Fern archive (`v0.6.0`, `v0.5.9`, `v0.5.8`, and `older`). The release workflow fails if those redirect targets are missing.
2830

2931
Frozen `vX.Y.Z.yml` navs on `docs-website` must point only at their own `vX.Y.Z/pages/...` files. The release sync materializes shared historical pages into each version folder before publishing.

fern/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ These workflows require the org-level `DOCS_FERN_TOKEN` secret. The workflows ex
7171

7272
Fern release snapshots live on `docs-website`, not on `main`. This mirrors the MkDocs `gh-pages` model without mixing Fern source state into the MkDocs output branch. The branch stores a source snapshot, not only `fern/`, because `make check-fern-docs` needs the Python packages and workspace metadata. Pushes to `docs-website` use `GITHUB_TOKEN`, so publishing happens inline in the same workflow instead of relying on a second workflow trigger.
7373

74+
The `docs-website` branch is an orphan-style publish branch. Published commits include `fern/publish-metadata.json` with the source repository, ref, SHA, release tag when applicable, and published branch.
75+
7476
The `docs-website` branch must already contain the historical Fern archive (`v0.6.0`, `v0.5.9`, `v0.5.8`, and `older`) before release publishing runs. The workflow fails if those redirect targets are missing.
7577

7678
Manual dispatch with `release_tag` creates or refreshes that release snapshot. For the already-published `v0.6.0` release, run **Build Fern docs** with `release_tag=v0.6.0` and `source_ref=main` after this fix merges. Future release events default `source_ref` to the release tag.

fern/scripts/fern-published-branch.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from __future__ import annotations
88

99
import argparse
10+
import json
1011
import re
1112
import shutil
1213
import sys
@@ -31,6 +32,7 @@
3132
"dist",
3233
"site",
3334
}
35+
PUBLISH_METADATA_PATH = Path("fern/publish-metadata.json")
3436
FERN_DEVNOTE_SUPPORT_PATHS = [
3537
"fern/assets",
3638
"fern/components/Authors.tsx",
@@ -166,6 +168,49 @@ def validate_redirect_targets(published_root: Path) -> None:
166168
)
167169

168170

171+
def write_publish_metadata(published_root: Path, args: argparse.Namespace, action: str) -> None:
172+
provided = [
173+
args.metadata_source_repository,
174+
args.metadata_source_ref,
175+
args.metadata_source_sha,
176+
args.metadata_release_tag,
177+
args.metadata_published_branch,
178+
]
179+
if not any(provided):
180+
return
181+
182+
missing = [
183+
name
184+
for name, value in (
185+
("metadata source repository", args.metadata_source_repository),
186+
("metadata source ref", args.metadata_source_ref),
187+
("metadata source sha", args.metadata_source_sha),
188+
)
189+
if not value
190+
]
191+
if missing:
192+
raise PublishedBranchError(f"Incomplete publish metadata; missing {', '.join(missing)}")
193+
194+
metadata: dict[str, object] = {
195+
"schema_version": 1,
196+
"kind": "fern-docs-website",
197+
"action": action,
198+
"source": {
199+
"repository": args.metadata_source_repository,
200+
"ref": args.metadata_source_ref,
201+
"sha": args.metadata_source_sha,
202+
},
203+
}
204+
if args.metadata_release_tag:
205+
metadata["release_tag"] = args.metadata_release_tag
206+
if args.metadata_published_branch:
207+
metadata["published_branch"] = args.metadata_published_branch
208+
209+
target = published_root / PUBLISH_METADATA_PATH
210+
target.parent.mkdir(parents=True, exist_ok=True)
211+
target.write_text(json.dumps(metadata, indent=2) + "\n")
212+
213+
169214
def ignore_source(_dir: str, names: list[str]) -> set[str]:
170215
return {name for name in names if name in SKIP_NAMES or name == "__pycache__"}
171216

@@ -347,6 +392,7 @@ def sync_source(args: argparse.Namespace) -> int:
347392
materialize_version_nav_pages(published_root)
348393
restore_versions_block(published_root / "fern" / "docs.yml", preserved_versions_block)
349394
validate_redirect_targets(published_root)
395+
write_publish_metadata(published_root, args, "release-snapshot")
350396
return 0
351397

352398

@@ -396,21 +442,32 @@ def patch_devnotes(args: argparse.Namespace) -> int:
396442

397443
source_block = extract_devnotes_block(source_nav)
398444
replace_devnotes_block(target_nav, rewrite_devnotes_block(source_root, published_root, source_block))
445+
write_publish_metadata(published_root, args, "devnotes-patch")
399446
return 0
400447

401448

449+
def add_metadata_args(parser: argparse.ArgumentParser) -> None:
450+
parser.add_argument("--metadata-source-repository", help="Repository used to produce this published snapshot")
451+
parser.add_argument("--metadata-source-ref", help="Git ref used to produce this published snapshot")
452+
parser.add_argument("--metadata-source-sha", help="Git commit used to produce this published snapshot")
453+
parser.add_argument("--metadata-release-tag", help="Release tag represented by this published snapshot")
454+
parser.add_argument("--metadata-published-branch", help="Published branch updated by this snapshot")
455+
456+
402457
def build_parser() -> argparse.ArgumentParser:
403458
parser = argparse.ArgumentParser(description=__doc__)
404459
subparsers = parser.add_subparsers(required=True)
405460

406461
sync_parser = subparsers.add_parser("sync-source")
407462
sync_parser.add_argument("--source-root", required=True, help="Repository checkout with authoring content")
408463
sync_parser.add_argument("--published-root", required=True, help="docs-website checkout to update")
464+
add_metadata_args(sync_parser)
409465
sync_parser.set_defaults(func=sync_source)
410466

411467
devnotes_parser = subparsers.add_parser("patch-devnotes")
412468
devnotes_parser.add_argument("--source-root", required=True, help="Repository checkout with latest Dev Notes")
413469
devnotes_parser.add_argument("--published-root", required=True, help="docs-website checkout to patch")
470+
add_metadata_args(devnotes_parser)
414471
devnotes_parser.set_defaults(func=patch_devnotes)
415472
return parser
416473

0 commit comments

Comments
 (0)