diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c37bc7b..e877e16 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,6 +72,12 @@ jobs: # would switch the guards off precisely when prose changes. The job is # cheap enough that running it redundantly alongside the matrix on code # PRs costs less than maintaining a list of which packages are guards. + # + # The capabilities gate rides along for the same reason. The semantic + # assertion — the checked-in capabilities page is the rendering of the + # YAML — is already a unit test; the make target adds the generator's + # own `go run` entry point, which release.yml runs against the tagged + # tree, so a broken target fails a PR here rather than a release. unit: name: unit tests (no Docker; docs guards) runs-on: ubuntu-latest @@ -82,6 +88,9 @@ jobs: - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 with: go-version-file: go.mod + # The gate is one `go run` and a diff; it goes first so a broken + # generator is reported even when an unrelated unit test fails. + - run: make check-capabilities - run: make test-unit lint: @@ -213,7 +222,7 @@ jobs: # Single required status for branch protection ("all-green" is the # context to require). Succeeds when nothing failed — including # docs-only PRs, where the code-gated jobs were skipped but the unit - # job (and its docs guards) still had to pass. + # job (its docs guards and the capabilities gate) still had to pass. all-green: if: always() needs: [changes, unit, lint, build, test, demo] diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e5d79ba..c9fce43 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,6 +41,10 @@ jobs: # previous workflow could have poisoned. cache: false + # A tag cannot ship a capabilities matrix that disagrees with its YAML. + - name: Check the capabilities matrix + run: make check-capabilities + # Belt and braces on top of the ancestry gate: re-run the suite against # the exact tree being released. - name: Test the tagged tree diff --git a/Makefile b/Makefile index 70928ed..1605a48 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ PG_DSN_LOCAL = postgres://$(PG_USER):$(PG_PASSWORD)@localhost:$(PG_PORT)/$(PG_DA # Which corpus replay project to run (replay//project.conf). REPLAY_PROJECT ?= buzz -.PHONY: build gen-capabilities test test-unit test-db test-supported-postgres test-aws-boundary lint setup db-up db-down demos clean demo demo-seed demo-check replay replay-refresh replay-down +.PHONY: build gen-capabilities check-capabilities test test-unit test-db test-supported-postgres test-aws-boundary lint setup db-up db-down demos clean demo demo-seed demo-check replay replay-refresh replay-down # The first target is make's default goal: keep build here so a bare # `make` builds the binary rather than rewriting a checked-in document. @@ -54,9 +54,28 @@ lint: golangci-lint run # Regenerate the marked regions of docs/capabilities.md from the embedded -# matrix (pkg/capabilities/capabilities.yaml); CI fails if they drift. +# matrix (pkg/capabilities/capabilities.yaml); CI fails if they drift. The +# gate below runs the same command, so the two cannot drift apart. +GEN_CAPABILITIES = $(GO) run ./internal/cmd/gen-capabilities + gen-capabilities: - $(GO) run ./internal/cmd/gen-capabilities + $(GEN_CAPABILITIES) + +# Regenerate the capabilities page and fail if regeneration changed it. Only the +# generator's own edits count, so an uncommitted edit to the hand-written prose +# outside the marker regions does not trip the gate. The target is a pure +# check: on failure — a generator error or a diff — it puts the page back as it +# was before the run, so a rerun fails the same way and `make gen-capabilities` +# is the only command that writes the page. +check-capabilities: + @before=$$(mktemp); cp docs/capabilities.md "$$before"; \ + $(GEN_CAPABILITIES) || { cp "$$before" docs/capabilities.md; rm -f "$$before"; exit 1; }; \ + if ! diff -u --label docs/capabilities.md --label regenerated "$$before" docs/capabilities.md; then \ + cp "$$before" docs/capabilities.md; rm -f "$$before"; \ + echo "docs/capabilities.md disagrees with pkg/capabilities/capabilities.yaml; run make gen-capabilities and commit the result" >&2; \ + exit 1; \ + fi; \ + rm -f "$$before" # Configure git hooks (relative path so worktrees work too). setup: diff --git a/docs/capabilities-contract.md b/docs/capabilities-contract.md index 4592d88..b377898 100644 --- a/docs/capabilities-contract.md +++ b/docs/capabilities-contract.md @@ -74,7 +74,7 @@ path on T3 rows. │ go:embed │ regenerate │ committed ▼ ▼ ▼ ┌───────────────────────┐ ┌─────────────────────────────────────────────────────┐ -│ pkg/capabilities │ │ CI gate: regenerate, then require an empty git diff │ +│ pkg/capabilities │ │ CI gate: regenerate, then require a no-op rewrite │ └───────────┬───────────┘ └─────────────────────────────────────────────────────┘ │ ▼ @@ -154,10 +154,16 @@ markers — the introduction, tier explanation, legend, peer comparison, refusal and operator recipes — remains hand-written. Generated output is deterministic: source order is display order, formatting has no timestamps, and a second generation is a no-op. -The generator lands with the YAML file, not later. A Make target runs its `go run` -entry point. CI runs that target and then fails unless `git diff --exit-code` is empty. -The test validates semantics; regenerate-and-diff proves the checked-in human page is -the rendering of the validated data. +The generator lands with the YAML file, not later. `make check-capabilities` runs its +`go run` entry point and fails unless regeneration leaves the page byte-identical; only +the generator's own edits count, so an uncommitted edit to the hand-written prose does +not trip it, and a failing run — a generator error or a diff — restores the page as it was +before the run, so the check has no side effect and `make gen-capabilities` is the one +command that writes it. The unconditional +unit job in `.github/workflows/ci.yml` runs that target beside the unit tests on code and +docs-only changes alike, and `.github/workflows/release.yml` repeats it for the tagged +tree before the test sweep. The test validates semantics; regenerate-and-diff proves the +checked-in human page is the rendering of the validated data. The capability-statement rule still applies beyond the generated matrix. A behavior change updates the YAML, [limitations.md](limitations.md), and the README's short @@ -261,7 +267,7 @@ prerequisite for the others; the rest land independently: and markers together, making the repository single-source on day one; *(done)* 2. add `pg-sprite capabilities`, including `--json` and the embedded binary version; *(done)* -3. add the regenerate-and-diff CI gate to the normal pipeline; and *(pending)* +3. add the regenerate-and-diff CI gate to the normal pipeline; and *(done)* 4. add documentation and `jq` recipes for consumers. *(done)* The generator is part of the first step rather than a cleanup step: there is never an