From 8ac49981db625524db175bcd319a18bf3678f752 Mon Sep 17 00:00:00 2001 From: "Tom D. Snyder" Date: Thu, 6 Aug 2026 23:49:58 -0400 Subject: [PATCH] CI: build/test gate, artifact-split enforcement, plan and licence invariants 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. --- .gitattributes | 41 +++++++++ .github/workflows/ci.yml | 180 +++++++++++++++++++++++++++++++++++++++ go.mod | 3 +- go.sum | 30 +++++++ internal/buildpin/pin.go | 24 ++++++ 5 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 .gitattributes create mode 100644 .github/workflows/ci.yml create mode 100644 internal/buildpin/pin.go diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..7391815 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,41 @@ +# Anvil ships Linux binaries and Linux-targeted deploy assets, and its CI runs +# on ubuntu-latest. Line endings are therefore LF everywhere, enforced here +# rather than left to each contributor's core.autocrlf setting. +# +# Without this, a Windows checkout gets CRLF working-tree files, `gofmt -l` +# reports every Go file as unformatted locally while CI (which sees LF) passes, +# and local verification stops being trustworthy. Every packet in +# plan/IMPLEMENTATION-PLAN.md owes build/test evidence, so local verification +# has to mean something. + +* text=auto eol=lf + +*.go text eol=lf diff=golang +*.sql text eol=lf +*.py text eol=lf +*.sh text eol=lf +*.yml text eol=lf +*.yaml text eol=lf +*.json text eol=lf +*.md text eol=lf +*.toml text eol=lf +go.mod text eol=lf +go.sum text eol=lf +Makefile text eol=lf +Dockerfile text eol=lf + +# systemd units are parsed on Linux only and must never carry CR. +*.service text eol=lf +*.timer text eol=lf +*.slice text eol=lf + +# Model weights, archives, and fixture corpora. +*.gguf binary +*.safetensors binary +*.onnx binary +*.db binary +*.sqlite binary +*.tar binary +*.gz binary +*.zip binary +*.png binary diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c85b20b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,180 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + go: + name: Go build, vet, test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + # Single source of truth: the toolchain line in go.mod. + go-version-file: go.mod + cache: true + + - name: go mod tidy is a no-op + run: | + cp go.mod go.mod.bak + cp go.sum go.sum.bak + go mod tidy + diff -u go.mod.bak go.mod + diff -u go.sum.bak go.sum + rm go.mod.bak go.sum.bak + + - name: gofmt + run: | + unformatted=$(gofmt -l .) + if [ -n "$unformatted" ]; then + echo "::error::gofmt needed on:" + echo "$unformatted" + gofmt -d . + exit 1 + fi + + - name: go vet + run: go vet ./... + + - name: go build + run: go build ./... + + - name: go test + run: go test -race -count=1 ./... + + # plan/00-SPINE.md S9-AMENDED splits Anvil into two distribution artifacts: + # anvil (core, no network-probing capability compiled in) and anvil-dast. + # plan/IMPLEMENTATION-PLAN.md 2.2 rules that a config flag inside a single + # binary does not address the concern the split exists for. This job is what + # makes the split real rather than conventional. + artifact-split: + name: Two-artifact split (S9-AMENDED) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Core binary has no DAST capability in its import graph + run: go test ./cmd/anvil -run TestSplit -v + + - name: Negative control -- the guard must be able to fail + run: | + set -e + mkdir -p internal/dast/ciprobe + cat > internal/dast/ciprobe/probe.go <<'EOF' + package ciprobe + + const Sentinel = "ci-negative-control" + EOF + cat > cmd/anvil/ci_negctl.go <<'EOF' + package main + + import _ "github.com/Susquehanna-Syntax/Anvil/internal/dast/ciprobe" + EOF + if go test ./cmd/anvil -run TestSplit; then + echo "::error::TestSplit PASSED with an internal/dast import present. The guard is a no-op." + exit 1 + fi + echo "guard correctly failed on the injected violation" + rm -f cmd/anvil/ci_negctl.go + rm -rf internal/dast/ciprobe + go test ./cmd/anvil -run TestSplit + + - name: Static, cgo-free builds for both artifacts + env: + CGO_ENABLED: "0" + run: | + set -e + for goarch in amd64 arm64; do + for target in anvil anvil-dast; do + GOOS=linux GOARCH=$goarch go build -o "/tmp/${target}-linux-${goarch}" "./cmd/${target}" + file "/tmp/${target}-linux-${goarch}" + done + done + # S12 mandates modernc.org/sqlite precisely so no cgo/C toolchain is + # needed. mattn/go-sqlite3 would reintroduce both. + if go list -deps ./... | grep -q 'github.com/mattn/go-sqlite3'; then + echo "::error::mattn/go-sqlite3 is in the dependency graph. S12 mandates modernc.org/sqlite." + exit 1 + fi + + plan: + name: Plan cross-reference invariants + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + + # plan/IMPLEMENTATION-PLAN.md 2.6: "Re-run it after any edit to an area + # file." Making that a CI job is the only way it actually happens. + - name: tools/plan_xref.py + run: python tools/plan_xref.py + + - name: tools/plan_fields.py parses + run: python tools/plan_fields.py > /dev/null + + licence: + name: Licence hygiene + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # plan/00-SPINE.md S8: read LICENSE file bodies, never API metadata. + # Seven audited artifacts return NOASSERTION over a real licence and one + # hides a restrictive licence behind a permissive tag. + - name: LICENSE is canonical Apache-2.0 + run: | + expected=cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30 + actual=$(sha256sum LICENSE | cut -d' ' -f1) + if [ "$actual" != "$expected" ]; then + echo "::error::LICENSE sha256 is $actual, expected the canonical Apache-2.0 $expected" + exit 1 + fi + + - name: Required compliance files exist + run: | + set -e + for f in LICENSE NOTICE THIRD-PARTY-LICENSES.md; do + test -s "$f" || { echo "::error::$f missing or empty (plan step C.1)"; exit 1; } + done + + # plan/00-SPINE.md S5 is enforced, not advisory. This is a coarse + # placeholder for cmd/license-gate (plan steps C.9/C.10), which reads + # licence bodies properly; it exists so the exclusion list is not purely + # documentary between now and Phase 6. + - name: Hard exclusions (S5) are absent from the dependency graph + run: | + set -e + fail=0 + # Deliberately narrow: module paths only, so prose in plan/ and + # NOTICE -- which must be free to *name* these to explain the + # exclusions -- does not trip the gate. + for banned in \ + "github.com/github/codeql" \ + "github.com/returntocorp/semgrep" \ + "github.com/opengrep/opengrep-rules" \ + "github.com/AFLplusplus/AFLplusplus" \ + "github.com/mattn/go-sqlite3" ; do + if grep -rn --include=go.mod --include=go.sum "$banned" . ; then + echo "::error::$banned is on the S5 hard exclusion list" + fail=1 + fi + done + exit $fail diff --git a/go.mod b/go.mod index a6ca375..daba57d 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,8 @@ module github.com/Susquehanna-Syntax/Anvil go 1.26.5 +require modernc.org/sqlite v1.56.0 + require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/google/uuid v1.6.0 // indirect @@ -12,5 +14,4 @@ require ( modernc.org/libc v1.74.4 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect - modernc.org/sqlite v1.56.0 // indirect ) diff --git a/go.sum b/go.sum index 9869c9c..1932692 100644 --- a/go.sum +++ b/go.sum @@ -1,20 +1,50 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 h1:LMLX+LgTNWpfvCBdFebv6EsYotImrt/Ppc5cXIriCSo= +github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= +modernc.org/cc/v4 v4.29.1 h1:MKgdCV3WykTSPqpVrnxdEDS0HEd2FHpKZDzxzU5LyeI= +modernc.org/cc/v4 v4.29.1/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= +modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU= +modernc.org/ccgo/v4 v4.34.6/go.mod h1:SZ8YcN9NG7XVsQYdm6jYBvi8PQP1qi+kqB6OhjqI3Fk= +modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= +modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.4 h1:2g65LGVSmFQrXeITAw97x7hCRvZFcyE1uDP+7Vng7JI= +modernc.org/gc/v3 v3.1.4/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= modernc.org/libc v1.74.4 h1:fX1Omw4o2/1C2iRkkIsrQTasJQldLhRmuPreXLoWs9k= modernc.org/libc v1.74.4/go.mod h1:eeQAS9W3sZeKYMFubydxJpII9ybHWshk+7or7bLG9co= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= +modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sqlite v1.56.0 h1:/D8e2RfFqoy/Zc6PuC76U28zFwmI/sYx1Kjm4yEn9e0= modernc.org/sqlite v1.56.0/go.mod h1:yCJ2cmAaIkHQ25oXWrF8H4O1lIfPYPR26yCEDj2P3pQ= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/internal/buildpin/pin.go b/internal/buildpin/pin.go new file mode 100644 index 0000000..b54d3bc --- /dev/null +++ b/internal/buildpin/pin.go @@ -0,0 +1,24 @@ +// Package buildpin holds Phase 0 dependency pins alive in the module graph +// until the packages that actually use them exist. +// +// plan/00-SPINE.md S12 mandates modernc.org/sqlite, and +// plan/spine-c-language.md C5 pins it at v1.56.0. plan/IMPLEMENTATION-PLAN.md +// Phase 0 requires that dependency be added at bootstrap. But nothing imports +// it yet -- the store is plan step R.4 and its FTS5 startup guard is R.5 -- +// and `go mod tidy` removes a module that no package in the module imports. +// Without this file, CI's tidy check would silently delete the pin the +// bootstrap was supposed to establish. +// +// DELETE THIS PACKAGE when internal/store lands (R.4). It has no runtime +// purpose and must never be imported by anything other than itself. +package buildpin + +import ( + // modernc.org/sqlite: the cgo-free SQLite driver S12 mandates. Do NOT + // substitute mattn/go-sqlite3 -- it needs cgo and a C toolchain, which + // breaks the single static binary and the cross-compilation matrix. + // Verified at bootstrap against this exact version: SQLite 3.53.3, FTS5 + // virtual tables and MATCH queries both work. Licence read as a file + // body per S8: BSD-3-Clause. + _ "modernc.org/sqlite" +)