Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 50 additions & 8 deletions .github/workflows/fern-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -137,18 +137,41 @@ jobs:
python-version: '3.13'

- name: Generate API references
# The Python/Rust reference pages are publish-time artifacts:
# gitignored in the source tree and generated here against the exact
# ref being published, so source PRs never carry (or review) the
# regenerated MDX. Generation must precede the pages-dev sync below,
# which rsyncs whatever sits under docs/fern/pages/. The Kubernetes
# reference regenerates from a docs-owned source file and stays
# committed, so it keeps the freshness check.
id: api_refs
# Python/Rust references are publish-time artifacts: generated here
# against the ref being published, never committed. Generation must
# precede the pages-dev sync below, which rsyncs docs/fern/pages/.
#
# The Kubernetes reference stays committed, but this path REGENERATES
# it rather than gating on it. As `--check` the step failed, which
# skipped the sync and publish steps entirely, so a main left stale by
# merge skew silently froze the live site. Drift is still fatal, just
# after the site is current: the probe records it and the gate at the
# end of this job fails the run.
run: |
set -euo pipefail
python3 -m pip install 'griffe==2.1.0'
python3 source-checkout/docs/fern/scripts/gen_python_api.py
python3 source-checkout/docs/fern/scripts/gen_rust_api.py
python3 source-checkout/docs/fern/scripts/gen_kubernetes_api.py --check
if python3 source-checkout/docs/fern/scripts/gen_kubernetes_api.py --check >/dev/null 2>&1; then
echo "kubernetes_drift=false" >> "$GITHUB_OUTPUT"
else
echo "kubernetes_drift=true" >> "$GITHUB_OUTPUT"
fi
k8s_page=source-checkout/docs/fern/pages/reference/kubernetes-api/full-api-reference.mdx
committed_bytes=$(wc -c < "$k8s_page")
python3 source-checkout/docs/fern/scripts/gen_kubernetes_api.py
Comment thread
dagil-nvidia marked this conversation as resolved.
Comment thread
dagil-nvidia marked this conversation as resolved.
regenerated_bytes=$(wc -c < "$k8s_page")
# Publishing through drift is the point of this step, but the
# regenerated page is what the sync below rsyncs, so a generator that
# exits 0 while emitting a gutted reference would reach the live site
# and only be reported afterwards. Ordinary skew moves this page by a
# few hundred bytes; losing a fifth of it means generation itself is
# what is wrong, so stop before anything is synced or published.
if [ "$regenerated_bytes" -lt "$(( committed_bytes * 4 / 5 ))" ]; then
echo "::error file=docs/fern/pages/reference/kubernetes-api/full-api-reference.mdx::Regenerated Kubernetes API reference is $regenerated_bytes bytes against $committed_bytes committed, a loss of more than a fifth. Refusing to sync or publish. Check gen_kubernetes_api.py and its source, docs/fern/pages/reference/kubernetes-api/api-reference-k8s.md."
exit 1
fi

- name: Checkout docs-website branch
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
Expand Down Expand Up @@ -664,6 +687,25 @@ jobs:
if: steps.ctx.outputs.is_main == 'true'
run: python3 source-checkout/docs/fern/scripts/check_published_styles.py

- name: Fail if the committed Kubernetes API reference was stale
# Deliberately last, and deliberately not gated on is_main: the preview
# path should report drift too. Whether the site actually published is
# a separate question - Publish Docs needs both is_main and has_changes
# - so the message reports which of the two happened rather than
# asserting a publish. `!cancelled()` keeps the drift annotation on runs
# where an earlier step already failed; a bare `if:` carries an implicit
# success() and would drop the report on exactly those runs.
if: ${{ !cancelled() && steps.api_refs.outputs.kubernetes_drift == 'true' }}
run: |
set -euo pipefail
if [ "${{ steps.ctx.outputs.is_main }}" = "true" ] && [ "${{ steps.changes.outputs.has_changes }}" = "true" ]; then
outcome="The site published the regenerated output, so docs are current, but the committed page differs from the reviewed bytes."
else
outcome="Nothing was published on this run, so the live site is unchanged."
fi
echo "::error file=docs/fern/pages/reference/kubernetes-api/full-api-reference.mdx::Committed Kubernetes API reference does not match its source. $outcome Fix with: python3 docs/fern/scripts/gen_kubernetes_api.py"
exit 1

#############################################################################
# VERSION RELEASE - Run on new version tags (vX.Y.Z)
#############################################################################
Expand Down
69 changes: 67 additions & 2 deletions docs/fern/scripts/tests/test_api_reference_regressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,28 @@ def test_pre_merge_gates_every_api_generator_input() -> None:
assert "steps.filter.outputs.api_docs_any_modified" in action


K8S_DRIFT_GATE_STEP = "Fail if the committed Kubernetes API reference was stale"


def _publish_step_run(step_name: str) -> str:
"""Return a fern-docs.yml step's `run` body, parsed rather than grepped.

Substring-matching the whole workflow cannot tell a real command from the
same text inside a comment, so an assertion can pass on prose while the
command it guards has changed underneath it.
"""
document = yaml.safe_load(
(REPO_ROOT / ".github" / "workflows" / "fern-docs.yml").read_text(
encoding="utf-8"
)
)
for job in document["jobs"].values():
for step in job.get("steps", []):
if step.get("name") == step_name:
return step.get("run", "")
raise AssertionError(f"fern-docs.yml has no step named {step_name!r}")


def test_pre_merge_runs_all_api_generators_hermetically() -> None:
workflow = (REPO_ROOT / ".github" / "workflows" / "pre-merge.yml").read_text(
encoding="utf-8"
Expand Down Expand Up @@ -598,9 +620,52 @@ def test_pre_merge_runs_all_api_generators_hermetically() -> None:
assert "gen_kubernetes_api.py --check" in workflow
# The publish and preview paths must GENERATE the pages before syncing
# them to the docs-website branch (dev sync and version snapshots both).
# Asserted against the parsed `run` body, not the file text: the step's
# comment names these same commands, so a whole-file substring check would
# pass on prose after the command itself changed.
generate = _publish_step_run("Generate API references")
for generator in ("python", "rust"):
assert f"gen_{generator}_api.py --check" not in publish
assert "gen_kubernetes_api.py --check" in publish
assert f"gen_{generator}_api.py --check" not in generate
# Kubernetes output stays committed, but publish REGENERATES it rather than
# gating on it. As `--check` this step failed, which skipped the sync and
# publish steps below, so a main left stale by merge skew silently froze the
# live site. The probe records drift and the gate below fails the run after
# the site is current.
assert re.search(r"gen_kubernetes_api\.py$", generate, re.MULTILINE)
assert "gen_kubernetes_api.py --check" in generate
assert "kubernetes_drift=true" in generate
gate = _publish_step_run(K8S_DRIFT_GATE_STEP)
assert "exit 1" in gate
Comment thread
dagil-nvidia marked this conversation as resolved.
# Wiring and polarity, not only shape. `steps.<id>` for an id that does
# not exist renders empty, so dropping or renaming `id: api_refs`, or
# inverting either the probe or the gate, leaves the gate permanently
# inert while every assertion above still passes. Nothing else in the repo
# catches a dangling step reference: no hook or workflow runs actionlint.
fern_docs = yaml.safe_load(
(REPO_ROOT / ".github" / "workflows" / "fern-docs.yml").read_text(
encoding="utf-8"
)
)
by_name = {
step.get("name"): step
for step in fern_docs["jobs"]["preview-or-publish-docs"]["steps"]
}
assert by_name[K8S_DRIFT_GATE_STEP]["if"] == (
"${{ !cancelled() && steps."
f"{by_name['Generate API references']['id']}"
".outputs.kubernetes_drift == 'true' }}"
)
assert re.search(
r"--check[^\n]*then\n\s*echo \"kubernetes_drift=false\"[^\n]*\n"
r"\s*else\n\s*echo \"kubernetes_drift=true\"",
generate,
)
# The regenerated page is rsynced before the gate runs, so write mode needs
# its own floor: a generator that exits 0 on a gutted page must not publish.
assert "regenerated_bytes" in generate
assert "committed_bytes * 4 / 5" in generate
# The gate is only honest if it runs after the site has been published.
assert publish.index("Publish Docs") < publish.index(K8S_DRIFT_GATE_STEP)
assert "Generate API references" in publish
assert "Generate API references at the tag" in publish
# fern check validates nav paths, so the fern-check job must materialize
Expand Down
Loading