build: pin the gate's Go toolchain to the one go.mod names - #449
Merged
Conversation
The Makefile's claim is that `make gate` is what CI runs, and that is what makes a red gate worth believing. It was not true on a machine with a newer Go than CI's: golangci-lint's bundled staticcheck builds its own IR of every package it loads, the standard library included, so a stdlib it does not know panics it before it reaches a line of this repo. On Go 1.27, `make gate` failed at `lint` with five panics in `internal/poll`, and `coverage` then failed two rows that pin an encoding/json escape 1.27 spells differently — neither having anything to do with the change under test, which is the situation that teaches people to ignore a red gate. The gate now pins GOTOOLCHAIN, read from go.mod's own go directive so the version has one definition, and exported so the scripts and golangci-lint see it too — the linter reads the stdlib through `go list`. CI reads the same line through setup-go's go-version-file, replacing the literal that was a second copy of it. A GOTOOLCHAIN already set in the environment still wins and is reported, exactly as a local golangci-lint of the wrong version is. Measured on a Go 1.27 machine with nothing set: `make gate` exits 2 before this change and 0 after. This does not move the toolchain. Bumping it needs a golangci-lint whose staticcheck knows the newer stdlib and a rewrite of the two rawDivergences rows, which are now commented where each will be reached; that is a deliberate change of its own, not a side effect of this one. Closes #431 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016EHKV7ZYQJJXCPyynTWq4P
Wahbeh-Mohammad
approved these changes
Sep 10, 2026
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.
Closes #431.
Summary
The
Makefilesaysmake gateis what CI runs, and that property is why ared gate is worth believing. It was not true on a machine with a newer Go than
CI’s.
golangci-lint’s bundled staticcheck builds its own IR of every package it loads,
the standard library included, so a stdlib newer than that release knows panics
it before it examines a line of this repo. On Go 1.27 the gate failed at
lintwith five panics in
internal/poll(unexpected expr: *ast.KeyValueExpr,cascading into
fact_purity,nilness,typednessandSA5012), andcoveragethen failed two rows that pin anencoding/jsonescape 1.27 spellsdifferently. Neither has anything to do with the change under test, which is the
situation that teaches people to ignore a red gate.
The gate now pins
GOTOOLCHAIN, read from go.mod’s owngodirective so theversion has one definition rather than a copy, and exported so the scripts and
golangci-lint see it too — the linter reads the stdlib through
go list. CIreads the same line through setup-go’s
go-version-file, replacing the literalgo-version: "1.26.3"that was a second copy of it.makeerrors if thatdirective cannot be read, rather than silently exporting a meaningless pin.
A
GOTOOLCHAINalready set in the environment still wins, as an override should,and is reported — exactly how a local golangci-lint of the wrong version is
already handled. Reporting rather than refusing is the existing convention here:
a toolchain setting is not a reason to decline to run the gate at all.
What this does not do
It does not move the toolchain. Bumping it needs three things to move together,
or the gate fails on something other than the change under test:
godirective in go.mod,GOLANGCI_LINT_VERSION, to a release whose staticcheck knows the newer stdlib,rawDivergencesrows pinning anencoding/jsonescape.Both ends now carry a comment saying so, at the
Makefilepin and at thedivergence rows, so whoever attempts the bump reaches the constraint from either
direction. Those rows claim the old conversion lost a byte, not that a version
of
encoding/jsonspelled the loss one way — so they are over-specific relativeto their own stated purpose, and rewriting them belongs to the bump rather than
to this change.
Test plan
Measured on a Go 1.27 machine with nothing set in the environment:
main:make gate→ exit 2, failing atlintwith 5 staticcheckpanics.
make gate→ exit 0, no env var, full suite under-raceat 100% coverage.
Also checked:
make -s print-go-versionreports1.26.3from go.mod; anoverriding
GOTOOLCHAINemits the mismatch warning and still runs; the workflowparses and its setup-go step resolves to
go-version-file: go.mod.🤖 Generated with Claude Code
https://claude.ai/code/session_016EHKV7ZYQJJXCPyynTWq4P