Status: Accepted
Date: 2026-07-06
Context
.context/findings/gh-pages-docs-drift-2026-07-05.md investigated a report that the deployed GH Pages docs site wasn’t reflecting recent repo changes. The deploy pipeline itself (docs.yml + actions/deploy-pages) turned out to be working correctly — the actual gap was content drift: scripts/check-docs-drift.sh already maps each docs/** page to related source globs and flags a doc as possibly stale when source commits post-date the doc’s last commit, but it runs only as a pre-push hook and always exit 0s. That means it prints a warning to the pusher’s own scrolling terminal, never fails the push, never runs in CI, and never surfaces on the PR itself — so a reviewer on GitHub’s web UI has no way to see it, and staleness accumulates silently. The finding left open, as a maintainer call rather than something to auto-apply, whether to promote this check into a blocking CI gate.
At investigation time the cumulative check already had 8 flagged stale docs (README.md, docs/index.md, and six others). Any gate reusing that same all-history logic verbatim would fail every PR immediately on pre-existing debt unrelated to that PR’s own changes, which would make the gate impossible to land without first clearing the backlog.
Decision
scripts/check-docs-drift.shgains a second mode, selected by passing a base ref as$1. With no argument, it keeps its existing behavior unchanged: the pre-push hook’s cumulative, all-history, always-exit 0advisory check. With a base ref, it switches to gate mode.- Gate mode only fails on drift introduced by the current diff. For each mapped doc, it checks whether the doc itself was touched between the base ref and
HEAD; if so, it’s considered current regardless of source changes. If not, it checks whether any of that doc’s mapped source globs were touched in the same range — if so, the gate fails (exit 1) and names the doc and the offending commits. This deliberately does not reuse the pre-push hook’s “any commit since the doc’s last touch, however long ago” comparison, so pre-existing drift never blocks a PR that doesn’t touch the affected source. - Wired into
.github/workflows/ci.ymlas a newdocs-driftjob, scoped topull_requestonly (notpush: [main]— by the time a change lands onmainit’s already merged, too late to block). The job checks out withfetch-depth: 0(required for thebase...HEADcommit-range diff) and runsscripts/check-docs-drift.sh "origin/${{ github.base_ref }}". - The 8 pre-existing stale docs are not addressed by this change and are not force-fixed as a prerequisite. They remain visible via the unchanged pre-push advisory check; closing that backlog is separate follow-up work, not a blocker for landing the gate.
Consequences
- Easier: new source changes that should come with a doc update now fail CI visibly on the PR itself, instead of relying on a warning that only the pushing developer’s terminal ever showed.
- Easier: the gate can land immediately without a separate cleanup pass, because it only judges the PR’s own diff, not the repo’s accumulated history.
- Harder: a PR that legitimately doesn’t need a doc update (e.g. a source change that doesn’t change any user-facing behavior described in the mapped doc) will still fail the gate and needs a docs touch (even a trivial one) to pass, or an explanation in the PR description — there’s no override flag by design, matching this repo’s other advisory-vs-blocking precedents of favoring an explicit human override over a machine-parsed skip mechanism.
- Binding for future work: any new
docs/**page or new source area that should be kept in sync needs an entry added tocheck-docs-drift.sh’sMAPPINGSarray to be covered by either mode; unmapped source has no drift detection at all. - Deferred: whether/when to clear the 8 pre-existing stale docs remains open, tracked informally via the unchanged cumulative pre-push check rather than a dedicated plan.