How do you set a Go release policy that can prove, months later, which commit a binary was built from?
answer
- two sources of truth, different trust
- derived beats asserted
- the toolchain records source, not the build event
- a dirty tree should fail the release
- provenance a reviewer can refuse
basics
~20 sSplit it by who can know the fact. Let the toolchain record the source through its vcs settings, inject only what Go cannot know, such as a build time or pipeline identifier, and gate the release on a present revision and a clean tree rather than trusting the pipeline's own claims.
solid answer
~50 sThe policy question is which facts you are willing to have asserted by the build script and which must come from the toolchain. Source identity should come from the toolchain: `vcs.revision`, `vcs.time` and `vcs.modified` are derived from the tree the compiler actually read, so they cannot drift from reality the way a hash computed elsewhere and passed in with `-ldflags -X` can. Injection is right for facts Go deliberately does not record — a build timestamp, a pipeline run identifier, a release channel — because they describe the build event rather than the source. Then make it enforceable rather than aspirational: the release job verifies the artifact it is about to publish, refuses to publish when the revision is empty or the tree was dirty, and archives that record beside the artifact. The part you should expect to be overruled on is a scheme where provenance is self-asserted by the pipeline that produced it.
go deeper
Understand that a released binary is expected to say which source it came from, and that some of that comes from the build tool while some is supplied by the pipeline.
Explain the mechanical difference between a value the toolchain derives from the working tree and one passed in at link time, and why they can disagree.
Design the release check: verify the artifact about to be published, block an empty revision or a dirty tree, and archive the record next to the artifact.
Own the split between derived and asserted provenance, defend it to a reviewer who can overrule it, and name what the gate costs the build environments that cannot currently stamp.
## The decision, stated plainly There are two ways a Go binary can come to know what it is. The `go` command can record it — the `vcs`, `vcs.revision`, `vcs.time` and `vcs.modified` settings, plus the main module and the linked dependency modules. Or the build can inject it, passing values into string variables at link time with `-ldflags -X`. Most teams end up doing both, badly, and nobody owns the split. That split is the policy. ## Prefer what the toolchain derives The toolchain's version-control settings come from the working tree the compiler actually read. An injected value comes from whatever the build script computed, in a step that may run before a checkout is updated, may read an environment variable that was set for a different job, or may be copied into a second pipeline that no longer computes it at all. Both look identical in the output. Only one is derived from the thing that was compiled. So the default rule: **do not inject any fact the toolchain already records.** A commit hash your pipeline pastes in is strictly weaker evidence than the one the `go` command stamped, and having both invites the failure where they disagree and nobody knows which to believe. ## Inject what Go will not record Go records nothing about the build event. There is no wall-clock build time — omitted on purpose, so that two builds of identical source stay byte-identical — no pipeline run identifier, no release channel, no environment name, no artifact coordinates. If your organisation needs those, injection at link time is the only route, and that is the legitimate use of `-ldflags -X`. Keep the injected set small and clearly labelled as pipeline-asserted, so a reader can tell at a glance which half of the version output is derived and which half is claimed. ## Make it a gate, not a convention A policy that lives in a wiki page is not a policy. Three enforceable pieces: 1. The release job inspects the artifact it is about to publish — the exact file, not a rebuild — and reads its record. 2. Publication fails when `vcs.revision` is empty or `vcs.modified` is `true`. Dirty-tree builds must fail rather than warn: a warning becomes an artifact nobody can reconstruct, discovered months later during an incident. 3. The record is archived beside the artifact, so provenance survives even if the artifact does not. The cost is real and you should name it. Build environments that lack repository metadata, or where the version-control tool is unavailable or refuses the checkout, currently succeed with stamping silently omitted under the default `-buildvcs=auto`. Gating turns those into build failures that somebody has to fix — usually by making the checkout complete rather than by weakening the gate. Expect that trade to be argued, and expect at least one team to propose `-buildvcs=false` as the quick fix. That proposal is the moment the policy is actually tested. ## Where to be relaxed Developer builds off a laptop should not be forced through any of this. Let them be unstamped, and have the version output say so plainly — "local build, unstamped" — rather than fabricating something. The rule applies to published artifacts, where the question "what was this" will eventually be asked by someone who was not in the room. ## Who can overrule you This is the part that makes it a policy rather than a preference. A security reviewer or auditor is entitled to reject a scheme in which the only evidence of what a running binary contains is a string the build pipeline chose to write into it. The defensible position is that source identity is derived by the toolchain, build-event identity is injected and labelled as such, and the release gate proves both were present before anything shipped. The indefensible position — and it is common — is a `version` output that looks authoritative and is entirely self-asserted. ## The test to apply Pick any artifact from six months ago and ask one question: can someone holding only that file name the source that produced it, and reconstruct it? If the answer depends on a build log that has since expired, the policy has not done its job, whatever the version output prints.
- How do you answer a reviewer who wants every version field injected by the pipeline for consistency?Consistency of format is worth having; consistency of source is not. An injected value is asserted by the pipeline, while the toolchain's version-control settings are derived from the tree the compiler read. Standardise how the fields are printed, keep injection for facts Go cannot know, and never inject something the toolchain already stamps.
- Why block a release on vcs.modified=true rather than warning?Because the flag means the compiled source was never committed. The hash beside it names what the tree was based on, not what was built, so the artifact is unreconstructable by anyone. A warning at build time becomes an unanswerable question during an incident, and the fix at build time costs minutes.
- What do you do about developer builds with no version-control stamp at all?Leave them unstamped and say so in the output. The policy governs published artifacts; forcing local builds through a release gate buys nothing and invites people to disable the gate. What matters is that an unstamped build is visibly unstamped rather than dressed up as a release.
saying these in an interview costs you the question
- Injects a commit hash the pipeline computed separately
- Treats a dirty-tree build as a warning, not a blocker
- Assumes Go stamps a wall-clock build time
- Verifies a rebuild instead of the published artifact
- Presents pipeline-asserted values as toolchain-derived provenance