Skip to content

CI: build/test gate, artifact-split enforcement, plan and licence invariants - #2

Merged
tom-snyder merged 1 commit into
mainfrom
ci/build-test-gate
Aug 7, 2026
Merged

CI: build/test gate, artifact-split enforcement, plan and licence invariants#2
tom-snyder merged 1 commit into
mainfrom
ci/build-test-gate

Conversation

@tom-snyder

Copy link
Copy Markdown
Member

Summary

PR #1 merged with statusCheckRollup: []zero check protection. I flagged that at merge time; this closes it before Phases 1–7 start landing code.

Why this shape

Anvil's plan makes three claims that are only true if something enforces them continuously:

  1. The two-artifact split is real, not conventional. 00-SPINE.md §S9-AMENDED splits anvil from anvil-dast, and §2.2 rules that a boolean inside a shipped binary still supplies the capability to everyone. An unenforced convention is the same as a boolean — it holds right up until someone adds an import.
  2. plan_xref.py gets re-run after every area-file edit. IMPLEMENTATION-PLAN.md §2.6 says so. Documentation does not cause that to happen; a required check does.
  3. Licences are read as file bodies, never metadata (§S8), and the §S5 exclusions are enforced, not advisory.

Jobs

Job What it does
go gofmt, vet, build, go test -race, plus go mod tidy must be a no-op so a dropped or drifted pin fails loudly
artifact-split Runs TestSplit, then injects an internal/dast import and fails the build if the guard passes. Builds all four linux/{amd64,arm64} × {anvil,anvil-dast} static binaries. Fails if mattn/go-sqlite3 ever enters the graph.
plan tools/plan_xref.py — 158 packets, 7 invariants
licence LICENSE sha256 == canonical Apache-2.0; C.1's three files non-empty; no §S5 exclusion in go.mod/go.sum

The negative control in artifact-split is the part I'd most want reviewed. A guard that has never failed has not been tested, and this one now has to prove it can fail on every run.

Two fixes the gate found by being run locally first

internal/buildpingo mod tidy removes modules nothing imports. Nothing imports modernc.org/sqlite yet (the store is R.4, its FTS5 guard is R.5), so the tidy check would have silently deleted the pin Phase 0 exists to establish. This package holds it and names its own deletion condition in a comment.

.gitattributes — the Windows working tree had CRLF, so gofmt -l flagged every Go file locally while CI (seeing LF) would have passed. Local verification that disagrees with CI is worse than none, and every packet owes build evidence. Now * text=auto eol=lf. Git had stored LICENSE correctly as LF the whole time — verified by hashing the blob directly (git cat-file -p HEAD:LICENSE | sha256sumcfc7749b…), so only the checkout was affected and nothing on main was wrong.

Risk

Low. No production code paths. The realistic failure mode is a CI job being wrong about its own environment, which the first run on this PR will surface.

Testing

Every job was run locally first, except one:

gofmt -l .                     clean
go vet ./...                   clean
go build ./...                 clean
go mod tidy                    no-op (verified by diff)
CGO_ENABLED=0 GOOS=linux       4 binaries, amd64 + arm64
TestSplit negative control     FAILS on injected import, PASSES after revert
sha256sum LICENSE              cfc7749b…23d30
python tools/plan_xref.py      clean

Known gap, stated rather than hidden: go test -race cannot run on this Windows host — the race detector needs a C toolchain and cgo.exe exits 2. That one line of evidence comes from CI only.

Follow-up

Once this is green, main should get branch protection requiring these four checks. That needs admin rights on the repo and is a settings change, so I have not done it — say the word and I will, or you can flip it in Settings → Branches.

…ariants

PR #1 merged with statusCheckRollup empty -- zero check protection. This adds
it before Phases 1-7 start landing code.

Four jobs:

go            gofmt, vet, build, `go test -race`, and a check that `go mod
              tidy` is a no-op so a dropped or drifted pin fails loudly.

artifact-split S9-AMENDED splits Anvil into anvil (no network-probing
              capability compiled in) and anvil-dast. IMPLEMENTATION-PLAN 2.2
              ruled that a config boolean does not address the concern the
              split exists for -- and neither does an unenforced convention.
              This job runs TestSplit, then injects an internal/dast import
              and FAILS THE BUILD IF THE GUARD PASSES, so the guard cannot
              silently rot into a no-op. It also builds all four
              linux/{amd64,arm64} x {anvil,anvil-dast} static binaries and
              fails if mattn/go-sqlite3 ever enters the graph, since S12
              mandates modernc.org/sqlite precisely to avoid cgo.

plan          Runs tools/plan_xref.py. IMPLEMENTATION-PLAN 2.6 says "re-run it
              after any edit to an area file"; making it a required check is
              the only way that actually happens.

licence       Asserts LICENSE is canonical Apache-2.0 by sha256, that C.1's
              three files exist and are non-empty, and that no S5 hard
              exclusion is in the dependency graph. The exclusion check
              matches module paths in go.mod/go.sum only, so plan/ and NOTICE
              stay free to NAME these artifacts in order to explain why they
              are excluded.

Two supporting fixes, both found by running the gate locally first:

internal/buildpin  `go mod tidy` removes a module no package imports. Nothing
              imports modernc.org/sqlite yet -- the store is R.4 and its FTS5
              guard is R.5 -- so tidy would have silently deleted the pin
              Phase 0 exists to establish. This package holds it until R.4
              lands, and says so in a comment that names its own deletion
              condition.

.gitattributes  The Windows working tree had CRLF, so `gofmt -l` reported
              every Go file as unformatted locally while CI would have passed.
              Local verification that disagrees with CI is worse than no local
              verification, and every packet in the plan owes build evidence.
              Now `* text=auto eol=lf`, with the tree renormalised. Git had
              stored LICENSE correctly as LF throughout -- only the checkout
              was affected, confirmed by hashing the blob directly.

Known gap, stated rather than hidden: `go test -race` cannot run on this
Windows host -- the race detector needs a C toolchain and cgo.exe exits 2.
That evidence comes from CI only. Everything else in this commit was verified
locally first.
@tom-snyder
tom-snyder merged commit b0f0ce5 into main Aug 7, 2026
4 checks passed
@tom-snyder
tom-snyder deleted the ci/build-test-gate branch August 7, 2026 03:53
tom-snyder added a commit that referenced this pull request Aug 7, 2026
…ariants (#2)

PR #1 merged with statusCheckRollup empty -- zero check protection. This adds
it before Phases 1-7 start landing code.

Four jobs:

go            gofmt, vet, build, `go test -race`, and a check that `go mod
              tidy` is a no-op so a dropped or drifted pin fails loudly.

artifact-split S9-AMENDED splits Anvil into anvil (no network-probing
              capability compiled in) and anvil-dast. IMPLEMENTATION-PLAN 2.2
              ruled that a config boolean does not address the concern the
              split exists for -- and neither does an unenforced convention.
              This job runs TestSplit, then injects an internal/dast import
              and FAILS THE BUILD IF THE GUARD PASSES, so the guard cannot
              silently rot into a no-op. It also builds all four
              linux/{amd64,arm64} x {anvil,anvil-dast} static binaries and
              fails if mattn/go-sqlite3 ever enters the graph, since S12
              mandates modernc.org/sqlite precisely to avoid cgo.

plan          Runs tools/plan_xref.py. IMPLEMENTATION-PLAN 2.6 says "re-run it
              after any edit to an area file"; making it a required check is
              the only way that actually happens.

licence       Asserts LICENSE is canonical Apache-2.0 by sha256, that C.1's
              three files exist and are non-empty, and that no S5 hard
              exclusion is in the dependency graph. The exclusion check
              matches module paths in go.mod/go.sum only, so plan/ and NOTICE
              stay free to NAME these artifacts in order to explain why they
              are excluded.

Two supporting fixes, both found by running the gate locally first:

internal/buildpin  `go mod tidy` removes a module no package imports. Nothing
              imports modernc.org/sqlite yet -- the store is R.4 and its FTS5
              guard is R.5 -- so tidy would have silently deleted the pin
              Phase 0 exists to establish. This package holds it until R.4
              lands, and says so in a comment that names its own deletion
              condition.

.gitattributes  The Windows working tree had CRLF, so `gofmt -l` reported
              every Go file as unformatted locally while CI would have passed.
              Local verification that disagrees with CI is worse than no local
              verification, and every packet in the plan owes build evidence.
              Now `* text=auto eol=lf`, with the tree renormalised. Git had
              stored LICENSE correctly as LF throughout -- only the checkout
              was affected, confirmed by hashing the blob directly.

Known gap, stated rather than hidden: `go test -race` cannot run on this
Windows host -- the race detector needs a C toolchain and cgo.exe exits 2.
That evidence comes from CI only. Everything else in this commit was verified
locally first.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant