Skip to content

chore(go): Move to Go 1.26 and split minimum from build toolchain - #88

Merged
korya merged 3 commits into
masterfrom
korya-chore-go-1-26
Aug 11, 2026
Merged

chore(go): Move to Go 1.26 and split minimum from build toolchain#88
korya merged 3 commits into
masterfrom
korya-chore-go-1-26

Conversation

@korya

@korya korya commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Problem

Released binaries were built with a Go toolchain eight months stale, missing seven patch releases of crypto/tls and net/http security fixes.

The published v0.2.0 artifact reports go1.25.5, released 2025-12-02. Every release from go1.25.6 through go1.25.12 carried security fixes, several to the exact packages this tool is built on — crypto/tls, net/http, net/url.

The cause is that go.mod pinned an exact patch and the workflows install whatever version go.mod names. One value served two unrelated roles: the minimum Go this module supports, and the compiler that builds and ships it. Nothing signalled when they diverged, because GOTOOLCHAIN=auto quietly downloaded a second toolchain to paper over the gap.

That coupling also lets a third party's Go floor break the build. gosec raised its requirement to go1.25.8, and #67 — which bumps actions/setup-go to v7, where GOTOOLCHAIN=local disables the silent download — went red:

go: github.com/securego/gosec/v2@v2.28.0 requires go >= 1.25.8
    (running go 1.25.5; GOTOOLCHAIN=local)

Solution

Split go.mod's single version into two directives: go 1.26 for the minimum supported, toolchain go1.26.5 for what actually builds.

 module github.com/korya/http-assert

-go 1.25.5
+go 1.26
+
+toolchain go1.26.5
Role Before After
Minimum Go the module supports go 1.25.5 go 1.26
Toolchain CI and releases build with go 1.25.5 (same value) toolchain go1.26.5
What happens when a dev tool raises its Go floor project minimum moves, or CI breaks nothing

1.26 is the line to be on: Go supports a release until two newer ones exist, and go1.27rc2 is already published, so 1.25 stops receiving backports the moment 1.27 ships.

Why an exact patch rather than go-version: 'stable'. .goreleaser.yaml deliberately pursues reproducible builds — -trimpath and mod_timestamp exist so two builds of one commit produce one checksum. The Go version is stamped into the binary, so a compiler free to drift would undo that. Two builds at a fixed toolchain do produce identical sha256; letting the compiler float would have quietly dismantled a property the config's own comments defend.

The directive is a floor, not a ceiling. A machine with a newer Go runs that instead. Releases are exact anyway because setup-go installs the version the directive names and then sets GOTOOLCHAIN=local, disabling switching altogether — so that guarantee arrives with #67, and until then a release build tracks the newest 1.26 patch.

The pin cannot upgrade itself, which is the original failure in slower form. just toolchain-check reports when the pin trails its patch line, as a warning that exits zero — a new Go patch is not a defect in this repository, and a red build every six weeks teaches everyone to skip past it. It lives in the Justfile so CI and a developer's machine run one implementation, as every other check here already does.

Together these unblock #67: 1.26.5 clears gosec's floor, so that PR goes green on a rebase with no edits to it.

Other Changes

  • README.md now states the Go requirement on the source-install section, which previously sent readers to go install without saying what it needed. The --version sample's go1.25.5 was refreshed, since the pin makes it untrue of anything built from this commit.
  • No visual change — this is a CLI and CI change with no rendered UI.
  • version_test.go keeps its go1.25.5 fixtures deliberately: they are supplied debug.BuildInfo inputs, not reads of the live toolchain, so changing them would be diff noise.

Related:

🤖 Generated with Claude Code

korya and others added 3 commits August 10, 2026 16:31
Go supports a release until two newer ones exist, and go1.27rc2 is already
published -- so 1.25 stops receiving security backports the moment 1.27 ships.
1.26 is the line to be on.

The version arrives as two directives rather than one because they answer
different questions. `go 1.26` is the minimum this module supports, a policy
that moves on Go's EOL schedule. `toolchain go1.26.5` is what builds it, and it
names an exact patch because .goreleaser.yaml goes out of its way to make
builds reproducible -- -trimpath and mod_timestamp exist so that two builds of
one commit produce one checksum, and the toolchain version is stamped into the
binary, so a compiler free to drift would undo that.

The directive is a floor rather than a ceiling: a machine with a newer Go runs
that instead. Releases are exact anyway, because setup-go installs the version
the directive names and then sets GOTOOLCHAIN=local, which disables switching
altogether -- so the guarantee arrives with the bump to setup-go v6 or newer
(#67), and until then a release build tracks the newest 1.26 patch.

Until now a single `go 1.25.5` served both roles, which is why a gosec release
requiring go1.25.8 could redden CI: the workflows install the version go.mod
names, so the project's declared minimum was also its build toolchain, and a
third party's Go floor could move it. Separating the two ends that coupling.

`go install` now requires Go 1.26. Released binaries are unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CrknafJSP5hF8u865cbnqX
Pinning `toolchain` in go.mod keeps builds reproducible, and gives up the
automatic patch upgrade that GOTOOLCHAIN=auto used to perform. Every Go patch
release carries security fixes, and nothing in this repository bumps the pin,
so without a signal it would sit at whatever version last looked current --
which is the failure this pin was introduced to make impossible, in a slower
form.

The check warns and exits zero. A new Go patch is not a defect here, and a red
build every six weeks would teach everyone to skip past it; a warning arrives
at the same moment and survives being ignored once. An unreachable go.dev
skips the check rather than failing or claiming the pin is current.

It lives in the Justfile because CI and a developer's machine should be running
one implementation, which is already true of every other check here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CrknafJSP5hF8u865cbnqX
The README sent readers to `go install` without saying what it needs, which
was survivable while the requirement was whatever Go they already had and is
not now that the module asks for 1.26. The note also says what the release
binaries need, because "requires Go" next to an install command reads as a
requirement of the tool rather than of one way to obtain it.

The --version sample said go1.25.5, which the toolchain pin has just made
untrue of anything built from this commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CrknafJSP5hF8u865cbnqX
@korya
korya marked this pull request as ready for review August 11, 2026 03:21
@korya
korya merged commit 114dfca into master Aug 11, 2026
8 checks passed
@korya
korya deleted the korya-chore-go-1-26 branch August 11, 2026 03:21
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