The product version is written into the binary by the linker:
go build -ldflags "-X github.com/sapedb/sapedb/internal/build.Version=1.0.0".
internal/build.Version is a variable with the default "dev", and nothing in
the program ever assigns it.
-X aimed at a symbol that does not exist does nothing, and says nothing.
No warning, no error, no non-zero exit. The build succeeds, the binary runs, and it reports
dev. So version stamping does not fail loudly on the day somebody renames the package or
the variable — it stops happening, and every artifact built after that claims to be an
unstamped development build.
That day is not hypothetical, because the path has to be spelled by hand in two places that
cannot read a Go constant: Makefile:20 and Dockerfile:32. Go source
has one spelling of it, build.Path (internal/build/build.go, the last line),
and make and docker have two more. Three spellings, two of which no compiler checks.
Measured at sapedb 248fa02, Go 1.27.1, darwin/arm64 — by building three binaries and
running each:
| Stamp | Build | sapedb version says |
|---|---|---|
| …/internal/build.Version=9.9.9 | exit 0, silent | sapedb 9.9.9 |
| …/internal/build.Verzion=9.9.9 | exit 0, silent | sapedb dev |
| …/internal/buildinfo.Version=9.9.9 | exit 0, silent | sapedb dev |
A misspelled variable and a wrong package path are indistinguishable from a correct build at build time. The only observable difference is what the binary says afterwards, which is exactly what nobody checks on a release artifact until somebody asks.
The failure mode is documented in three places, in the same words, by people who hit it:
internal/build/build.go, on Path: "a rename of this package that
misses them does not break the build, it silently stops stamping, and the binary quietly goes back
to saying 'dev'."Makefile:15-19, on STAMP: "Moving or renaming that package without
changing this line does not fail the build — the linker accepts an -X for a
symbol that does not exist and does nothing."Makefile:10-12, on VERSION: the fallback is
git describe --tags --always --dirty falling back to the literal
dev, chosen so a repository with no tags still produces something (a commit)
rather than an empty string — because an empty -X value also stamps nothing,
silently. Two silent-nothing paths, guarded differently.What guards it today — two tests, deliberately approaching from different directions:
TestTheBuildFilesStampTheSymbolThisPackageActuallyExports
(internal/build/build_test.go:38) reads the Makefile and the
Dockerfile as text and requires each to contain -X + Path +
=. Its own comment is precise about why this is not a tautology: "It is not a
string compared against itself. Path is Go source; the two files read below are make
and docker source, written independently, and the only thing making them agree today is somebody
having kept them in step — which is exactly the thing that stops happening."TestPathNamesThisPackagesOwnVariable (build_test.go:60) checks the half
the first cannot: that Path is right in the first place. A wrong Path
would keep all three files in perfect agreement and stamp nothing. The check is assembled by
construction, from a different direction than Path itself.TestAStampedBuildSaysWhatItWasStampedWith (version_live_test.go:60)
closes it end to end: it builds a binary through the same flag and refuses the default.TestTheDefaultIsNotSomethingAReleaseCouldBeCalled (build_test.go:77)
keeps "dev" unmistakable, and skips rather than failing if the test binary was itself
stamped — a test that says so instead of assuming.
Does it lose data, break a promise on a public surface, or stop somebody installing and
running? No. A binary saying dev serves correctly; nothing depends on the
string, and a stranger installs and runs it either way.
Defer — and the reason matters more than the label. This is deferred because it is already guarded, not because it is unimportant. Those are different deferrals and they age differently. An unimportant issue can be left alone; a guarded one is only safe while the guard holds, and the thing to keep is the guard, not the calm.
So the standing condition is explicit: the four tests above must stay green and must stay
non-vacuous. Specifically — if build_test.go's file reads ever start
failing soft, if Path is ever inlined back into a string literal at its use sites, or if
TestAStampedBuildSaysWhatItWasStampedWith ever becomes conditional on a toolchain being
present, this issue is promoted, because at that point nothing is watching the two hand-written
spellings.
The residual risk the guard does not cover is worth naming rather than implying: it checks the two build files in this repository. Any other place that stamps a sapedb binary — a downstream packaging script, a CI workflow, a distro recipe — carries a fourth and fifth spelling that nothing here can see.
dev — which is to
say, the failure lands precisely on SAPE-1's whole purpose, and it lands
where it is least visible: on a tagged build, on somebody else's machine.dev turns a silent stamp
failure into a failed release. That is the check that does not depend on anybody remembering to
keep three spellings in step, and it belongs with
SAPE-4.-X argument from
the Go constant — a tiny go run that prints build.Path, which
make and docker consume — so the hand-written copies stop existing rather than being
watched.