Featured image of post The version pin you can't lower

The version pin you can't lower

A security fix landed in the Go standard library, the kind you don’t get to think very hard about. You bump go.mod to the patched toolchain, 1.26.4, and you move on. Or you would, if your release didn’t immediately fall over the moment you tagged it.

Here’s what greeted me when I cut the next krites release. The before-hooks, the go mod tidy and friends that run ahead of the actual build, died on the spot:

go.mod requires go >= 1.26.4 (running go 1.26.3; GOTOOLCHAIN=local)

The goreleaser image I was releasing from, a perfectly current v2.16.0, bundled Go 1.26.3. One patch version behind. And GOTOOLCHAIN=local, which is the sensible default for a release because it means “use exactly the Go that’s installed, don’t go fetching anything mid-build”, was doing precisely its job: it saw go.mod asking for a toolchain it didn’t have, and refused to pretend otherwise.

Now, the fix that’s quickest to reach for is the one you have to talk yourself out of. The error is a version mismatch, and the fastest way to make a version mismatch go away is to change one of the versions. The image has 1.26.3? Fine, drop go.mod back to 1.26.3 and the red goes green. Except think about what that actually does. The whole reason go.mod says 1.26.4 is that 1.26.4 carries a security fix I need. Lowering the pin to placate the build image doesn’t resolve the tension, it surrenders to it. It un-applies the security update to make a pipeline pass, which is about the worst trade in software, and it’s the one sitting right there with a green tick on it.

So that pin is one you can’t lower. Which leaves making the build find the right toolchain instead of making the requirement smaller.

The quick version is one flag: flip GOTOOLCHAIN from local to auto. Where local means “only ever use what’s installed”, auto means “if go.mod asks for a newer toolchain than you’ve got, go and fetch that one and use it for this build”. So the release runs on the image’s 1.26.3, hits the go.mod directive, fetches 1.26.4 and carries on. The mismatch is resolved by upgrading at build time rather than by downgrading the requirement, which is the right direction for it to resolve in.

The better version, which is where krites ended up, is to not have the gap at all. The release now runs on the dev-tools image I bake my CI tools into, and that image tracks the Go version my code actually targets, 1.26.4. When the toolchain in the image and the toolchain in go.mod are the same thing by construction, there’s nothing to fetch and no flag to set, because there’s no disagreement to paper over.

The toolchain your code requires and the toolchain your build image ships are two different facts, and they drift apart every time a point release with a security fix lands in one a good while before it reaches the other. When they disagree, the instinct to make them agree by lowering the requirement is exactly backwards: the requirement is the thing that’s right, and the environment is the thing that’s behind. The pin only ever moves one way. The build image is what had to catch up to it, not the code learning to want less.

Built with Hugo
Theme Stack designed by Jimmy