feat(ci): gate documentation drift, and make docs a pre-tag release step
Five doc claims had rotted by v1.9.0, all the same shape: a value written once by hand, in a file nothing verifies, about a number that lives elsewhere and moved. README pin table wrong on all three rows; a "Planned" section describing something already shipped; DOCKER_HUB.md claiming Node v22 against Node 24. DOCKER_HUB.md is why this is a gate and not a resolution to be careful: it is PUBLISHED (update-description POSTs it as Docker Hub full_description on every tag), it had gone eight releases untouched, nothing generates it, and it is read from the TAG -- so the stale page shipped with v1.9.0 regardless. scripts/check-doc-drift.sh: seven checks, all repo-local (no network, token, image, or sibling clone). Exit 0/1/2 matching lint-shell.sh; a renamed ARG is a red 2, not a green tick. Wired as a fourth lint.yml job so "the docs lie" is its own red name. Verified with 15 controls, including two false-positive controls: the first placeholder check flagged README.md:900, a Go template in a legitimate `docker inspect --format` example. The gate was wrong, not the doc, so the pattern is now anchored to the UPPER_SNAKE convention CI substitutes. Not gated, deliberately: counts/sizes needing a running image (they belong in smoke-test.sh -- a guessing gate is worse than none), and Dockerfile.base BASE_REBUILD_DATE, because base_tag hashes that file content-wise and demanding it be current would force a ~60 min rebuild on releases that touch no base files. Free during a rebuild, expensive otherwise. AGENTS.md step 3 rewritten around the mechanism: checkout@v4 with no ref: means every job reads github.ref, the tag. Docs must be right BEFORE tagging.
This commit is contained in:
@@ -11,6 +11,89 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`).
|
||||
|
||||
---
|
||||
|
||||
## Unreleased
|
||||
|
||||
**A gate for documentation drift, because five claims rotted in one release and
|
||||
one of them was published.** Preparing v1.9.0 turned up a cluster of stale
|
||||
facts, all the same shape — a value written once by hand, in a file nothing
|
||||
verifies, about a number that lives somewhere else and moved:
|
||||
|
||||
- `README.md`'s "Version pins" table was wrong on **all three rows**: pi
|
||||
`0.84.4` vs `ARG PI_VERSION=0.85.1`, pi-atelier `v0.10.0` vs `v0.10.1`,
|
||||
mempalace `3.8.0` vs `3.9.0`. That table is the worst possible place for this,
|
||||
because it exists *specifically* to be the reviewable record of what the repo
|
||||
freezes deliberately — so a wrong row destroys the only thing it is for.
|
||||
- `README.md` listed already-shipped typst PDF export under "Planned for an
|
||||
upcoming minor release", carrying the self-contradicting marker "(shipped in
|
||||
Unreleased/base)". The **fourth** instance of the stale-`Unreleased`-pointer
|
||||
class this changelog already documented three of.
|
||||
- `DOCKER_HUB.md` claimed "Node.js v22" while v1.9.0 ships Node 24.
|
||||
|
||||
The last one is why this became a gate rather than a resolution to be careful.
|
||||
`DOCKER_HUB.md` is **published**: `update-description` POSTs it to Docker Hub as
|
||||
`full_description` on every tag. It had gone **eight releases** (v1.8.6 →
|
||||
v1.9.0) without a touch. Nothing generates it — CI only substitutes
|
||||
`{{PI_VERSION}}` — and nothing checked it, so the sole mechanism keeping it true
|
||||
was whoever remembered. Worse, it is read from the **tag**, so the stale page
|
||||
published with v1.9.0 anyway and the fix could only ride the next release.
|
||||
|
||||
**New: `scripts/check-doc-drift.sh` + a `doc-drift` job in `lint.yml`.** Seven
|
||||
checks, all comparing a doc string to a value that exists in this repo, so it
|
||||
needs no network, no token, no built image, and no sibling clone:
|
||||
|
||||
- README's three pin-table rows vs the ARGs they name *by name*
|
||||
- `DOCKER_HUB.md`'s Node claim vs `ARG NODE_VERSION`
|
||||
- placeholders CI will not substitute — the publish step greps for leftovers of
|
||||
`{{PI_VERSION}}` only, so any *second* token sails through and publishes
|
||||
literally
|
||||
- `DOCKER_HUB.md` under Docker Hub's 25 000-char `full_description` limit
|
||||
(previously discoverable only as a non-200 *after* the full build)
|
||||
- `Unreleased` appearing in a user-facing doc, which is always a pointer that
|
||||
outlived what it pointed at
|
||||
|
||||
Exit codes match `lint-shell.sh` and `check-skill-floor.sh`: `0` in sync, `1`
|
||||
drift, `2` cannot run — a renamed ARG makes the gate blind, which is a red `2`,
|
||||
never a green tick. Verified with **15 controls**: every check fails when its
|
||||
claim is broken, the real v1.9.0 Node bug is caught, and two false-positive
|
||||
controls pass — the first version of the placeholder check wrongly flagged
|
||||
`README.md:900`'s `docker inspect --format '{{json .Config.Labels}}'`, a Go
|
||||
template in a legitimate example, so the pattern is now anchored to the
|
||||
UPPER_SNAKE convention CI actually substitutes. **The gate was wrong, not the
|
||||
doc** — which is the whole reason a gate gets negative controls.
|
||||
|
||||
Deliberately **not** gated, and the reasons matter more than the list:
|
||||
|
||||
- Counts and sizes (`~1.1 GB`, "N `mempalace_*` tools", "7 extensions") need a
|
||||
running image. A gate that cannot evaluate a claim honestly would have to
|
||||
guess, and a guessing gate is worse than none — assert these in
|
||||
`scripts/smoke-test.sh`, where a real image exists.
|
||||
- `Dockerfile.base`'s `# BASE_REBUILD_DATE:` marker, itself stale (2026-07-13,
|
||||
three base rebuilds ago). `base_tag` hashes Dockerfile.base's *content*,
|
||||
comments included, so demanding it be current would force a ~60 min base
|
||||
rebuild on a release that touched no base files at all. It is free to fix
|
||||
while the base is *already* rebuilding, and expensive at any other moment.
|
||||
That cost asymmetry is now written into the release checklist rather than
|
||||
enforced.
|
||||
|
||||
**Release checklist step 3 rewritten** (`AGENTS.md`) around the mechanism that
|
||||
made this expensive: `docker-publish.yml` runs `actions/checkout@v4` with no
|
||||
`ref:`, so every job reads `github.ref` — the tag. Docs must be correct *before*
|
||||
tagging; afterwards the only routes are re-pointing the tag (its own hazard —
|
||||
v1.8.14 went `601fc98` → `361babd` and broke deploy verification until
|
||||
`git fetch --tags --force`) or waiting for the next release. The step now also
|
||||
names what the gate cannot see, so "gate is green" is not mistaken for "docs are
|
||||
true". The same reflex went into the `ci-release-watcher` skill, as the first
|
||||
correctness rule — it is the only one that expires once the tag exists.
|
||||
|
||||
Also fixed in passing: README's `pi-devbox-version` sample was v1.5.0-era and
|
||||
structurally outdated (it predated the `palace:` line the surrounding prose
|
||||
advertises, the `pi-atelier` component, and the whole `skills:` block). Replaced
|
||||
with real observed output rather than hand-written text. `DOCKER_HUB.md`'s "7
|
||||
user-facing extensions" was **verified correct**; its "29 `mempalace_*` tools"
|
||||
is stale (a live client shows 45) but left alone rather than corrected on a
|
||||
guess, since that count cannot be attributed to the baked 3.9.0 server without
|
||||
measuring it.
|
||||
|
||||
## v1.9.0 — 2026-09-10
|
||||
|
||||
**`shellcheck` is now in the image, because the release gate it depends on could
|
||||
|
||||
Reference in New Issue
Block a user