Skip to content

Commit cdcc6ca

Browse files
committed
ci: require latest Fern nav on release
1 parent 7d2785a commit cdcc6ca

4 files changed

Lines changed: 45 additions & 6 deletions

File tree

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,14 @@ jobs:
5151
5252
- name: Check Fern release version
5353
env:
54+
EVENT_NAME: ${{ github.event_name }}
5455
RELEASE_TAG: ${{ steps.release-tag.outputs.release_tag }}
55-
run: python3 fern/scripts/fern-release-version.py check --version "$RELEASE_TAG"
56+
run: |
57+
args=(check --version "$RELEASE_TAG")
58+
if [ "$EVENT_NAME" = "release" ]; then
59+
args+=(--require-latest-matches-release)
60+
fi
61+
python3 fern/scripts/fern-release-version.py "${args[@]}"
5662
5763
build-notebooks:
5864
needs: check-release-version

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -515,7 +515,7 @@ check-fern-release-version:
515515
ifndef VERSION
516516
$(error VERSION is required, e.g. make check-fern-release-version VERSION=0.5.10)
517517
endif
518-
$(DOCS_PYTHON) fern/scripts/fern-release-version.py check --version $(VERSION)
518+
$(DOCS_PYTHON) fern/scripts/fern-release-version.py check --version $(VERSION) $(if $(REQUIRE_LATEST),--require-latest-matches-release,)
519519

520520
prepare-fern-docs: generate-fern-api-reference generate-fern-notebooks
521521
@echo "✅ Fern local artifacts ready"

fern/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ Fern publishing runs alongside MkDocs during migration:
6767

6868
These workflows require the org-level `DOCS_FERN_TOKEN` secret. The workflows expose it to the Fern CLI as `FERN_TOKEN`.
6969

70-
Release publishing also runs `fern/scripts/fern-release-version.py check` before building notebooks. A release fails early if the release tag is not represented in `docs.yml` and `versions/vX.Y.Z.yml`. Manual dispatch can validate a specific tag through the workflow's `release_tag` input; otherwise it uses the latest published release.
70+
Release publishing also runs `fern/scripts/fern-release-version.py check` before building notebooks. A release fails early if the release tag is not represented in `docs.yml` and `versions/vX.Y.Z.yml`, or if `latest.yml` does not match the release nav on the release event. Manual dispatch can validate a specific tag through the workflow's `release_tag` input; otherwise it uses the latest published release.
7171

7272
## Versioning
7373

@@ -151,7 +151,7 @@ Primary local commands:
151151
| `make serve-fern-docs-locally` | Generate local Fern artifacts and serve local docs |
152152
| `make generate-fern-notebooks-with-outputs` | Full notebook pipeline: execute (needs `NVIDIA_API_KEY`) → colabify → convert |
153153
| `make prepare-fern-release VERSION=X.Y.Z` | Add Fern version files before cutting a release |
154-
| `make check-fern-release-version VERSION=X.Y.Z` | Verify Fern release metadata exists before publishing |
154+
| `make check-fern-release-version VERSION=X.Y.Z REQUIRE_LATEST=1` | Verify Fern release metadata exists before publishing |
155155

156156
Support and CI targets:
157157

fern/scripts/fern-release-version.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,19 @@ def check_as_of_versions(root: Path) -> list[str]:
148148
return errors
149149

150150

151+
def check_latest_matches_release(root: Path, slug: str) -> list[str]:
152+
latest_nav = root / "versions" / "latest.yml"
153+
release_nav = root / "versions" / f"{slug}.yml"
154+
if not latest_nav.exists() or not release_nav.exists():
155+
return []
156+
157+
latest_content = strip_leading_comment_block(latest_nav.read_text())
158+
release_content = strip_leading_comment_block(release_nav.read_text())
159+
if latest_content != release_content:
160+
return [f"{latest_nav} must match {release_nav} when publishing {slug}"]
161+
return []
162+
163+
151164
def update_docs_yml(root: Path, slug: str) -> None:
152165
docs_yml = root / "docs.yml"
153166
lines = read_docs_lines(root)
@@ -213,7 +226,17 @@ def write_release_nav(root: Path, slug: str, force: bool) -> bool:
213226
return copied_pages
214227

215228

216-
def check_release(root: Path, slug: str) -> list[str]:
229+
def update_latest_nav(root: Path, slug: str) -> bool:
230+
latest_nav = root / "versions" / "latest.yml"
231+
content = latest_nav.read_text()
232+
updated = content.replace("./latest/pages/", f"./{slug}/pages/")
233+
if updated == content:
234+
return False
235+
latest_nav.write_text(updated)
236+
return True
237+
238+
239+
def check_release(root: Path, slug: str, require_latest_matches_release: bool = False) -> list[str]:
217240
errors: list[str] = []
218241
block = versions_block_text(root)
219242
nav = root / "versions" / f"{slug}.yml"
@@ -235,27 +258,32 @@ def check_release(root: Path, slug: str) -> list[str]:
235258

236259
errors.extend(check_latest_display_name(root))
237260
errors.extend(check_as_of_versions(root))
261+
if require_latest_matches_release:
262+
errors.extend(check_latest_matches_release(root, slug))
238263
return errors
239264

240265

241266
def prepare(args: argparse.Namespace) -> int:
242267
root = Path(args.root)
243268
slug = version_slug(args.version)
244269
copied_pages = write_release_nav(root, slug, args.force)
270+
updated_latest = update_latest_nav(root, slug)
245271
update_docs_yml(root, slug)
246272
print(f"Prepared Fern release {slug}")
247273
if copied_pages:
248274
print(f"Copied latest-only pages into {root / 'versions' / slug / 'pages'}")
249275
else:
250276
print("No latest-only pages needed copying")
277+
if updated_latest:
278+
print(f"Updated latest.yml to point at {slug} page copies")
251279
print("Review reused page paths before publishing the release.")
252280
return 0
253281

254282

255283
def check(args: argparse.Namespace) -> int:
256284
root = Path(args.root)
257285
slug = version_slug(args.version)
258-
errors = check_release(root, slug)
286+
errors = check_release(root, slug, args.require_latest_matches_release)
259287
if errors:
260288
for error in errors:
261289
print(f"ERROR: {error}", file=sys.stderr)
@@ -276,6 +304,11 @@ def build_parser() -> argparse.ArgumentParser:
276304

277305
check_parser = subparsers.add_parser("check", help="Check Fern files include a release")
278306
check_parser.add_argument("--version", required=True, help="Release version or tag, e.g. v0.5.10")
307+
check_parser.add_argument(
308+
"--require-latest-matches-release",
309+
action="store_true",
310+
help="Fail unless latest.yml matches the requested release nav, ignoring leading comments",
311+
)
279312
check_parser.set_defaults(func=check)
280313
return parser
281314

0 commit comments

Comments
 (0)