Stamp the build version into the binary (closes #253)
All checks were successful
check / check (push) Successful in 3m6s

The binary reported "dev" in every deployment: main.version carried a
placeholder and nothing ever set it. Neither `make build` nor the
Dockerfile passed -X, so a tagged release produced an artifact that
could not say which commit it was, and the upgrade procedure's "confirm
the new build is live" step had nothing to confirm against.

script/version is now the single source of the value: $VERSION when
set, else `git describe --tags --always --dirty`, else "unknown". A
clean checkout at a tag reports exactly that tag; a tree with no git
metadata reports "unknown" rather than failing or naming a tag it may
not be at. Nothing time- or host-dependent is stamped, so two builds of
one commit stay byte-identical.

The Makefile's build target composes the flags -- `-X main.version` plus
whatever GO_LDFLAGS adds -- and every compile goes through it, including
the Dockerfile's static relink, which now contributes its -extldflags
through GO_LDFLAGS instead of replacing -ldflags wholesale. Since
.dockerignore excludes .git/, the image cannot derive the version: it
takes a VERSION build arg, defaulted to "unknown", that script/docker
fills in from the host checkout.

The UI footer needed the other half of the fix. It renders .Version,
which nothing ever put in the template data, so it printed its literal
"dev" fallback no matter what the binary was built as; renderTemplate
now supplies the value on both the map and the wrapper path.

An empty VERSION means unset in the Makefile too, not only in
script/version: `make build VERSION=` and a `--build-arg VERSION=`
reaching the Dockerfile's `make build VERSION="$VERSION"` both define
the variable as the empty string, which the script's guard never sees
and which would stamp no version at all -- putting the footer back on
its "dev" fallback, the defect this change exists to remove. The guard
needs `override`: a plain assignment loses to the command-line
definition it exists to correct.
This commit is contained in:
2026-08-23 23:11:56 +00:00
parent 5fda446c71
commit fec6876c42
9 changed files with 573 additions and 14 deletions

View File

@@ -184,6 +184,7 @@ type UserInfo struct {
type templateDataWrapper struct {
User *UserInfo
CSRFToken string
Version string
Data any
}
@@ -234,9 +235,16 @@ func (s *Handlers) renderTemplate(
userInfo := s.getUserInfo(r)
csrfToken := middleware.CSRFToken(r)
// The footer in base.html renders .Version. Every page reaches it
// through here, so this is the one place that has to supply it;
// left unset, the footer falls back to its literal "dev" and the
// UI reports a build that is not the one running.
version := s.params.Globals.Version
if m, ok := data.(map[string]any); ok {
m["User"] = userInfo
m["CSRFToken"] = csrfToken
m["Version"] = version
s.executeTemplate(w, tmpl, m)
return
@@ -245,6 +253,7 @@ func (s *Handlers) renderTemplate(
wrapper := templateDataWrapper{
User: userInfo,
CSRFToken: csrfToken,
Version: version,
Data: data,
}