ci: enforce the CLI's documented pre-commit gate - #307
Merged
Conversation
CLAUDE.md calls `cd cli && just check` the pre-commit gate. Nothing ran it, and it was red on main: 19 lint issues, plus a govulncheck failure. ## Lint All 19 were unactionable-error noise rather than defects, so this is a policy file, not a bug hunt. The exclusions are narrow and each says why: writes to stderr, cleanup of a temp file already renamed away, and Close on a handle being abandoned. Two call sites are fixed in code instead, because the repo already had a convention for them -- `_, _ = w.Write(...)` is what every other httptest handler in pkg/client does. The staticcheck list is spelled out rather than `all` minus one: bare `all` is wider than golangci-lint's default and turns on ST1003, which would rename Id -> ID across the client's wire structs. Worth recording for the next person, since it looks like a bug and isn't: `defer enc.Close()` in RenderYAML discards an error that cannot occur. yaml.v3's Encode flushes each document in full -- measured at 0 extra bytes and 0 extra writes on Close for a 32KB document and for a multi-document stream -- so Close has nothing buffered left to fail on. ## Go toolchain govulncheck failed on 4 stdlib CVEs (net/url, crypto/tls, encoding/asn1, net/http), all fixed in go1.26.6. go.mod pinned 1.26.2. This is not just a local-tooling detail: release-cli.yml builds released binaries with `go-version-file: cli/go.mod`, so published CLI binaries carried all four. Both module files move together -- test/contract replaces ../.., so a split pin fails to build. ## CI Two jobs rather than one, and separate from build-test: a lint nit and a stdlib CVE are different signals, and neither should mask a test failure. golangci-lint is pinned so an unrelated PR never goes red on a linter release; govulncheck is deliberately unpinned, since a red run there is the signal we are buying. Verified with the same golangci-lint binary CI installs (go install, v2.13.1, 0 issues) and `just check` green end to end. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review follow-ups to the previous commit.
Triggers were wrong. cli.yml also runs on backend/** so its contract tests
can catch backend drift, but nothing in backend/ can change the result of
linting or scanning the cli module. Over the last 300 commits, 42% touched
backend/ against 6% touching cli/, so the jobs would have paid full cost on
roughly seven times more pushes than can affect them. Own workflow, cli/**
only -- which also matches how cli-contract.yml is already split out.
golangci-lint now comes from golangci-lint-action in its default binary
install mode instead of `go install`. Compiling it from source pulled 320
modules and ~950MB of sources plus build objects on every run, measured at
26-32s on an 8-core box and slower on a 2-4 vCPU runner. That also stops the
tool's build artifacts from being written into the module/build cache that
setup-go saves under a cli/go.sum key, where they were both oversized and
evicted by any unrelated dependency bump.
Dropped the fmt.Fprint errcheck exclusion: every bare fmt.Fprint call in the
module already discards its result explicitly, so the entry matched nothing.
Verified by removing it and re-running -- still 0 issues, while removing any
of the other five surfaces real findings.
Left alone deliberately: the two jobs stay separate rather than becoming a
matrix, since distinct job names are the signal; and RenderYAML keeps
`defer func() { _ = enc.Close() }()` rather than moving to a
(*gopkg.in/yaml.v3.Encoder).Close exclusion, which would put the reasoning in
config, away from the call site it explains.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review follow-ups.
The vulncheck job could not do the job its comment claimed. Both triggers
were path-filtered to cli/**, and a CVE landing in the database produces
neither a push nor a PR -- so on a module that sees 6% of commits, a new
advisory could sit unreported for weeks. Adds a daily schedule and a
workflow_dispatch, and skips lint on the scheduled run since a pinned linter
re-run is only noise.
govulncheck is now pinned like the linter. The "deliberately unpinned" note
was wrong about its own mechanism: govulncheck fetches the advisory database
from vuln.go.dev at run time (its -version output prints the DB URL), so
pinning the binary costs no freshness while removing an unreviewed upstream
fetch that could redden an unrelated PR.
Two errcheck exclusions were doing more than their comments described:
- fmt.Fprintf/Fprintln covered 9 sites, and only 5 were the stderr
diagnostics the comment justified. setup_apply.go:302-327 writes the
"Next steps (deployment credentials)" block to stdout, which told the
user which secret to add where and could be truncated silently while the
command exited 0.
- (*os.File).Close covered the 6 abandonment closes in the atomic-write
paths, but being module-wide it also disarmed the guard on the three
closes whose error decides whether the write reached disk before
os.Rename publishes it (config.go:152, state.go:96, envfile.go:112).
Both are replaced by the explicit `_ =` the module already uses at ~40 sites,
so the linter keeps guarding everything else. Also drops the dead
`fmt.Fprint` entry, which matched nothing.
go.mod now uses a `toolchain` directive rather than raising the `go` line.
pkg/client is documented as importable by other programs, and the `go` line
is a hard floor for consumers -- forcing every importer to go1.26.6 over a
CVE in the *build* toolchain is the wrong lever. `toolchain go1.26.6` pins
what builds the binary and leaves the consumer floor at 1.26.2.
Two scope gaps closed: test/smoke (13 files behind //go:build smoke) was
invisible to the linter and had an unchecked write of its own; and the
default max-same-issues cap of 3 was hiding real hits, which is not what a
gate should do.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
FrameAutomata
pushed a commit
that referenced
this pull request
Aug 26, 2026
Cleanup pass over the previous two commits. Removed three comments citing `.github/workflows/cli-lint.yml`, which does not exist on main and never has -- it is added by the still-open #307. They claimed cli/ was already gated (it is not; the CLI's only vulncheck is the local `just vulncheck`) and described a cron stagger against a schedule that does not exist. This one is the worst of the set: it disguises a real gap rather than just being noise. Dropped the `if [ -n "$TAGS" ]` branch. Verified against this module that `govulncheck -tags "" ./...` is identical to the untagged form -- 0 called vulnerabilities, exit 0 -- so the conditional was dead code with two paths, one of which two of the three matrix legs never took. Added `cli/**` to the path filters. backend/go.mod carries `replace github.com/tracewayapp/traceway/cli => ../cli`, so the scan compiles working-tree CLI source; a cli-only PR could change what is reachable and never trigger this gate. cli.yml already carries the reciprocal filter. Added `concurrency` (the repo already uses it in four workflows) and `timeout-minutes: 15`. govulncheck's advisory-database client has no HTTP timeout, and its fetch is the first thing it does, so a network hang would otherwise sit at GitHub's 360-minute default across three legs on an unattended cron. Corrected two overclaims. "These three are the only combinations that compile" is true of the storage axis only -- the orthogonal `oxc` symbolicator tag also builds and is not scanned. And the toolchain pin does not reach the Docker images at all: the official golang bases set GOTOOLCHAIN=local, so a `toolchain` directive is ignored there and the images build with whatever the floating golang:1.26-* tag ships. That is still >= what CI scans, so the images are not at risk, but the comment claimed coverage it does not have. Comments trimmed 42/104 -> 29/93, toward the repo's convention (cli.yml 0/52, cli-contract.yml 4/41, release-traceway.yml 9/344). README.md and docs/pages/learn/contributing.mdx still said Go 1.25, below the module's own floor -- contributing.mdx states it as an install prerequisite. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LW8APoZZwVdoHkPeUYGnwW
FrameAutomata
pushed a commit
that referenced
this pull request
Aug 26, 2026
CI caught this: both `contract` and `build-test` failed on `go: updates to go.mod needed; to update it: go mod tidy`. cli/test/contract requires `github.com/tracewayapp/traceway/backend v0.0.0` through a local `replace`, so it records the backend's transitive requirements in its own go.mod. Bumping grpc, x/text, quic-go and the AWS SDK in the backend left those stale, which is a hard error rather than an auto-fix in a build. `go mod tidy` propagates the same versions and nothing else. The `go 1.26.2` line is unchanged and no `toolchain` directive is added here, so this stays off the line #307 edits in this file. Verified: `cd cli/test/contract && go test ./...` passes (ok, 5.5s -- it boots the backend in SQLite mode and asserts the wire contract), and `go mod tidy -diff` is clean afterwards. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LW8APoZZwVdoHkPeUYGnwW
dusanstanojeviccs
pushed a commit
that referenced
this pull request
Aug 26, 2026
* deps: pin backend toolchain to go1.26.6 and bump 5 vulnerable modules
govulncheck against backend/ at the current `go 1.26.2` pin reports 21
reachable vulnerabilities: 16 in the standard library and 5 in required
modules. The shipped artifact is the worst of it -- `./cmd/traceway-runner`
alone has 9 reachable stdlib vulnerabilities, and that binary runs on user
infrastructure with an outbound HTTPS long-poll as its main loop, so
crypto/tls, crypto/x509, net/http and net/textproto are all on its hot path.
Toolchain: `toolchain go1.26.6`, not a raise of the `go` line. Since Go 1.21
the `go` line is a hard floor propagated to consumers, and the backend is
importable as tracewaybackend; forcing every importer up over a CVE in the
build toolchain is the wrong lever. Same reasoning as the CLI module.
go1.26.6 is the highest fixed-in version across all 16 stdlib findings.
setup-go@v5 reads only the `go` line and never sets GOTOOLCHAIN, so it
installs the 1.26.2 floor and the go command then switches to 1.26.6 on
first use in the module. release-traceway.yml builds the runner binaries
through exactly that path.
Modules, which no toolchain bump can fix:
GO-2026-6061 google.golang.org/grpc v1.81.1 -> v1.82.1
GO-2026-5970 golang.org/x/text v0.37.0 -> v0.39.0
GO-2026-5764 aws-sdk-go-v2/service/s3 v1.96.0 -> v1.97.3
aws-sdk-go-v2/.../eventstream v1.7.4 -> v1.7.8
GO-2026-5676 github.com/quic-go/quic-go v0.54.0 -> v0.59.1
GO-2025-4233 github.com/quic-go/quic-go (same bump)
Verified: all three supported build-tag combinations (default dual-SQLite,
telemetry_duckdb with CGO, transactional_pg telemetry_ch) build, test and
scan clean -- govulncheck reports 0 reachable vulnerabilities and exits 0
for each. The one failing package under telemetry_duckdb
(app/controllers, a telemetry-migration dialect error in setup_test.go)
fails identically before this change and is untouched by it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LW8APoZZwVdoHkPeUYGnwW
* ci: add a govulncheck gate for the backend module
Nothing scanned backend/ -- the only govulncheck in the repo was the CLI's,
and backend/ is the module that ships a binary to users.
One job per supported build-tag combination rather than a single default
scan. The tags are not cosmetic: they select which db/ and repositories/
packages compile in, and each backend brings its own driver stack. Measured
on this tree, `go list -deps ./cmd/traceway` returns 0 ClickHouse packages
on the default build and 11 under transactional_pg,telemetry_ch, so a
default-only scan would leave the driver stacks the published Docker images
actually run completely unscanned. The three in the matrix are the only
combinations that compile; app/db/guard_*.go rejects the rest.
A daily schedule plus workflow_dispatch, because both push and pull_request
are path-filtered and a CVE lands in the database without anyone pushing a
commit. Staggered off the CLI workflow's 06:17. govulncheck is pinned:
freshness comes from the schedule, since the advisory database is fetched
at run time regardless of binary version.
No -test flag -- the gate is about what ships, and failing a release on a
test-only dependency would train people to ignore the job.
Scope is stated in the workflow rather than left implied: this scans
linux/amd64 source, while release also cross-compiles the runner for darwin
and windows. Every finding so far is either a stdlib symbol the toolchain
pin resolves for all targets or a platform-independent module version, so a
GOOS axis is a deliberate follow-up rather than an oversight.
Verified green for all three combinations on this tree.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LW8APoZZwVdoHkPeUYGnwW
* ci: correct and tighten the backend vulncheck workflow
Cleanup pass over the previous two commits.
Removed three comments citing `.github/workflows/cli-lint.yml`, which does
not exist on main and never has -- it is added by the still-open #307. They
claimed cli/ was already gated (it is not; the CLI's only vulncheck is the
local `just vulncheck`) and described a cron stagger against a schedule that
does not exist. This one is the worst of the set: it disguises a real gap
rather than just being noise.
Dropped the `if [ -n "$TAGS" ]` branch. Verified against this module that
`govulncheck -tags "" ./...` is identical to the untagged form -- 0 called
vulnerabilities, exit 0 -- so the conditional was dead code with two paths,
one of which two of the three matrix legs never took.
Added `cli/**` to the path filters. backend/go.mod carries
`replace github.com/tracewayapp/traceway/cli => ../cli`, so the scan compiles
working-tree CLI source; a cli-only PR could change what is reachable and
never trigger this gate. cli.yml already carries the reciprocal filter.
Added `concurrency` (the repo already uses it in four workflows) and
`timeout-minutes: 15`. govulncheck's advisory-database client has no HTTP
timeout, and its fetch is the first thing it does, so a network hang would
otherwise sit at GitHub's 360-minute default across three legs on an
unattended cron.
Corrected two overclaims. "These three are the only combinations that
compile" is true of the storage axis only -- the orthogonal `oxc` symbolicator
tag also builds and is not scanned. And the toolchain pin does not reach the
Docker images at all: the official golang bases set GOTOOLCHAIN=local, so a
`toolchain` directive is ignored there and the images build with whatever the
floating golang:1.26-* tag ships. That is still >= what CI scans, so the
images are not at risk, but the comment claimed coverage it does not have.
Comments trimmed 42/104 -> 29/93, toward the repo's convention (cli.yml 0/52,
cli-contract.yml 4/41, release-traceway.yml 9/344).
README.md and docs/pages/learn/contributing.mdx still said Go 1.25, below the
module's own floor -- contributing.mdx states it as an install prerequisite.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LW8APoZZwVdoHkPeUYGnwW
* deps: tidy cli/test/contract after the backend dependency bumps
CI caught this: both `contract` and `build-test` failed on
`go: updates to go.mod needed; to update it: go mod tidy`.
cli/test/contract requires `github.com/tracewayapp/traceway/backend v0.0.0`
through a local `replace`, so it records the backend's transitive
requirements in its own go.mod. Bumping grpc, x/text, quic-go and the AWS SDK
in the backend left those stale, which is a hard error rather than an
auto-fix in a build.
`go mod tidy` propagates the same versions and nothing else. The `go 1.26.2`
line is unchanged and no `toolchain` directive is added here, so this stays
off the line #307 edits in this file.
Verified: `cd cli/test/contract && go test ./...` passes (ok, 5.5s -- it
boots the backend in SQLite mode and asserts the wire contract), and
`go mod tidy -diff` is clean afterwards.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LW8APoZZwVdoHkPeUYGnwW
---------
Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
CLAUDE.md calls
cd cli && just checkthe pre-commit gate. Nothing ran it, and it was red onmain: 19 lint issues plus a govulncheck failure.Lint policy
All 19 were unactionable-error noise rather than defects, so most of this is a policy file. The exclusions are kept deliberately narrow: where the module already checks a function's error somewhere, the unactionable call sites get an explicit
_ =(the idiom already used at ~40 sites) instead of a module-wide exclusion, so the linter keeps guarding the sites that matter.That distinction turned out to be load-bearing. Two broader exclusions I first wrote were silently disarming real guards:
fmt.Fprintf/Fprintlncovered 9 sites, but only 5 were the stderr diagnostics the rationale described.setup_apply.go:302-327writes the "Next steps (deployment credentials)" block to stdout — the text telling the user which secret to add where. Unchecked, it can truncate silently while the command exits 0.(*os.File).Closecovered the 6 abandonment closes in the atomic-write paths, but being module-wide it also disarmed the three closes whose error decides whether the write reached disk beforeos.Renamepublishes it (config.go:152,state.go:96,envfile.go:112).Also closed two scope gaps:
test/smoke(13 files behind//go:build smoke) was invisible to the linter and had an unchecked write of its own, and golangci-lint's defaultmax-same-issues: 3was hiding real hits — not what a gate should do.The staticcheck list is spelled out rather than
allminus one, because bareallis wider than the default: it enables ST1003, which wanted to renameId→IDacross the client's wire structs.One thing worth recording, because it looks like a bug and isn't.
defer enc.Close()inRenderYAMLdiscards an error — the textbook shape of "returns nil while emitting truncated output". It isn't:yaml.v3'sEncodeflushes each document in full (measured at 0 extra bytes and 0 extra writes atClosefor a 32KB document and a multi-document stream). The call site says so now.Go toolchain
govulncheckfailed on 4 stdlib CVEs —net/url,crypto/tls,encoding/asn1,net/http— all fixed in go1.26.6, againstgo 1.26.2.Fixed with a
toolchaindirective, not by raising thegoline.pkg/clientis documented as importable by other Go programs, and since Go 1.21 thegoline is a hard floor propagated to consumers — forcing every importer up over a CVE in the build toolchain is the wrong lever.toolchain go1.26.6pins what builds the binary and leaves the consumer floor at 1.26.2.This mattered beyond local tooling:
release-cli.ymlbuilds published binaries from this samego-version-file.CI
Own workflow rather than more jobs in
cli.yml, because the triggers genuinely differ —cli.ymlruns onbackend/**so its contract tests can catch backend drift, but nothing inbackend/can change the result of linting the CLI module. Over the last 300 commits, 42% touchedbackend/against 6% touchingcli/.schedule+workflow_dispatch. Without it the vulncheck job was structurally unable to do its job: both triggers were path-filtered, and a CVE landing in the database produces neither a push nor a PR, so a new advisory could sit unreported for weeks on a module that changes 6% of the time.@latestreasoning that freshness required it — that was wrong about the mechanism.govulncheck -versionprintsDB: https://vuln.go.dev; the advisory database is fetched at run time regardless of binary version, so the schedule provides freshness and the pin removes an unreviewed upstream fetch on every PR.golangci-lint-actionin its default prebuilt-binary mode, rather thango install. Compiling it from source pulled 320 modules and ~950MB of sources plus build objects on every run (26-32s measured on an 8-core box, slower on a 2-4 vCPU runner), and wrote those artifacts into the cachesetup-gosaves under acli/go.sumkey.Verification
just checkgreen end to end (lint, 322 tests, vulncheck, skill drift, contract tests), exit 0toolchaindirective confirmed to build with 1.26.6 while leaving the module floor at 1.26.2Known gap, deliberately not in this PR
backend/go.modis stillgo 1.26.2, andrelease-traceway.yml:167builds the shippedtraceway-runnerbinaries from it — so those still carry the same 4 stdlib CVEs, two of which (crypto/tls,net/http) are directly on the runner's outbound long-poll path. There is nogovulncheckanywhere outsidecli/.Left out because the backend has three build-tag combinations and CGO/DuckDB, so it deserves its own change with its own verification rather than riding along here. Tracked as #308.
🤖 Generated with Claude Code