main.version was a placeholder nothing ever set: neither make build nor the Dockerfile passed -X. A tagged release produced an artifact that could not say which commit it was.
What changed
script/version is the single source of the value: $VERSION when set and non-empty, else git describe --tags --always --dirty against this checkout, else unknown. It refuses to use an enclosing repository's metadata (a tarball unpacked inside an unrelated working copy is not that copy's version). Nothing time-, host- or builder-dependent is stamped.
Makefile: VERSION ?= $(shell script/version), plus a GO_LDFLAGS hook; build compiles with -ldflags '-X main.version=$(VERSION) $(GO_LDFLAGS)'. New make version prints what the checkout would stamp.
Dockerfile: .dockerignore excludes .git/, so the builder stage cannot derive anything — the version enters as ARG VERSION, defaulted to unknown. script/docker fills it from the host checkout. Both compiles in the image now go through make build, the static relink contributing its -extldflags through GO_LDFLAGS rather than replacing -ldflags, so it cannot drop the stamp. The ARG sits below the test and asset layers so a new version does not invalidate their cache.
The UI footer was the other half of the defect.base.html renders .Version, which nothing ever put in the template data, so it printed its literal dev fallback regardless of how the binary was built. renderTemplate now supplies it on both the map and the wrapper path — one line in internal/handlers/handlers.go, the only file touched outside the build files, README.md and tests.
README: a "Version stamping" section next to the upgrade procedure, with the table of what each build reports; the upgrade procedure gains step 5, confirming the healthcheck reports the new version.
Not changed: no timestamp, no commit date, no builder identity — see reproducibility below.
What each build reports
Build
Reports
Clean checkout at a tag
exactly that tag
Commits past a tag
v1.0.0-3-g1a2b3c4
No tag reachable
short SHA
Uncommitted changes
the above with -dirty
No git metadata, no VERSION
unknown
Verification
make check green with GOFLAGS=-count=1: 21 packages ok, 572 tests passed, 0 issues. from the containerized linter.
make build, checkout at a real tag (throwaway local tag v0.0.0-impl253, deleted before pushing), run on port 18860:
$ make version
v0.0.0-impl253
$ make build
go build -ldflags '-X main.version=v0.0.0-impl253' -o bin/webhooker ./cmd/webhooker
$ curl -s http://127.0.0.1:18860/.well-known/healthcheck
{"status":"ok","now":"2026-08-23T23:12:50.805525813Z","uptimeSeconds":5,"uptimeHuman":"5.929332624s","version":"v0.0.0-impl253","appname":"webhooker","maintenanceMode":false}
Exactly the tag, no suffix.
make docker at the same tag, container run from the built image:
#26 [builder 10/11] RUN make build VERSION="v0.0.0-impl253"
#27 [builder 11/11] RUN CGO_ENABLED=1 make build VERSION="v0.0.0-impl253" GO_LDFLAGS='-extldflags "-static"'
#26 DONE 49.2s
#27 DONE 4.2s
$ curl -s http://127.0.0.1:18862/.well-known/healthcheck
{"status":"ok","now":"2026-08-23T23:26:36.344497766Z","uptimeSeconds":7,"uptimeHuman":"7.918824062s","version":"v0.0.0-impl253","appname":"webhooker","maintenanceMode":false}
Neither build layer was CACHED. The stamp is read off the running binary, not the command line, so it survives the static relink — and it is running on Alpine, which a non-static binary could not do. Footer and startup log from the same container:
No git metadata.git archive HEAD unpacked to a directory with no .git:
$ make version
unknown
$ curl -s http://127.0.0.1:18861/.well-known/healthcheck
{"status":"ok",...,"version":"unknown","appname":"webhooker","maintenanceMode":false}
And a bare docker build . with no --build-arg (the context never carries .git):
#26 [builder 10/11] RUN make build VERSION="unknown"
$ curl -s http://127.0.0.1:18863/.well-known/healthcheck
{"status":"ok",...,"version":"unknown","appname":"webhooker","maintenanceMode":false}
It builds, it runs, and it claims no tag.
Tests. New internal/versionscript package, mirroring internal/ciscript: eleven tests over throwaway git repositories — clean-at-a-tag, past a tag, untagged, dirty, no .git, enclosing repository ignored, VERSION override, empty VERSION, stability across invocations — plus guards that the Makefile still composes both halves of the linker flags and that no raw go build reappears in the Dockerfile. TestFooterReportsStampedVersion in internal/handlers renders the login page and asserts the footer carries the version rather than the dev fallback.
Disclosures
script/docker is one of the scripts REPO_POLICIES.md expects to stay byte-identical across repos. It now passes --build-arg VERSION="$(script/version)", because the version can only be resolved on the host. Its header comment records why.
internal/handlers/handlers.go is outside the build files, but the footer is a version-reporting site named in the issue's definition of done and it was broken independently of the linker flags. The change is confined to renderTemplate and one struct field.
The {{else}}dev{{end}} fallback in base.html is left in place; with renderTemplate supplying the value it is now unreachable in a wired application.
Closes https://git.eeqj.de/sneak/webhooker/issues/253.
`main.version` was a placeholder nothing ever set: neither `make build` nor the `Dockerfile` passed `-X`. A tagged release produced an artifact that could not say which commit it was.
## What changed
- **`script/version`** is the single source of the value: `$VERSION` when set and non-empty, else `git describe --tags --always --dirty` against this checkout, else `unknown`. It refuses to use an enclosing repository's metadata (a tarball unpacked inside an unrelated working copy is not that copy's version). Nothing time-, host- or builder-dependent is stamped.
- **`Makefile`**: `VERSION ?= $(shell script/version)`, plus a `GO_LDFLAGS` hook; `build` compiles with `-ldflags '-X main.version=$(VERSION) $(GO_LDFLAGS)'`. New `make version` prints what the checkout would stamp.
- **`Dockerfile`**: `.dockerignore` excludes `.git/`, so the builder stage cannot derive anything — the version enters as `ARG VERSION`, defaulted to `unknown`. `script/docker` fills it from the host checkout. Both compiles in the image now go through `make build`, the static relink contributing its `-extldflags` through `GO_LDFLAGS` rather than replacing `-ldflags`, so it cannot drop the stamp. The `ARG` sits below the test and asset layers so a new version does not invalidate their cache.
- **The UI footer was the other half of the defect.** `base.html` renders `.Version`, which nothing ever put in the template data, so it printed its literal `dev` fallback regardless of how the binary was built. `renderTemplate` now supplies it on both the map and the wrapper path — one line in `internal/handlers/handlers.go`, the only file touched outside the build files, `README.md` and tests.
- **README**: a "Version stamping" section next to the upgrade procedure, with the table of what each build reports; the upgrade procedure gains step 5, confirming the healthcheck reports the new version.
Not changed: no timestamp, no commit date, no builder identity — see reproducibility below.
## What each build reports
| Build | Reports |
| --- | --- |
| Clean checkout at a tag | exactly that tag |
| Commits past a tag | `v1.0.0-3-g1a2b3c4` |
| No tag reachable | short SHA |
| Uncommitted changes | the above with `-dirty` |
| No git metadata, no `VERSION` | `unknown` |
## Verification
`make check` green with `GOFLAGS=-count=1`: 21 packages `ok`, 572 tests passed, `0 issues.` from the containerized linter.
**`make build`, checkout at a real tag** (throwaway local tag `v0.0.0-impl253`, deleted before pushing), run on port 18860:
```
$ make version
v0.0.0-impl253
$ make build
go build -ldflags '-X main.version=v0.0.0-impl253' -o bin/webhooker ./cmd/webhooker
$ curl -s http://127.0.0.1:18860/.well-known/healthcheck
{"status":"ok","now":"2026-08-23T23:12:50.805525813Z","uptimeSeconds":5,"uptimeHuman":"5.929332624s","version":"v0.0.0-impl253","appname":"webhooker","maintenanceMode":false}
```
Exactly the tag, no suffix.
**`make docker` at the same tag**, container run from the built image:
```
#26 [builder 10/11] RUN make build VERSION="v0.0.0-impl253"
#27 [builder 11/11] RUN CGO_ENABLED=1 make build VERSION="v0.0.0-impl253" GO_LDFLAGS='-extldflags "-static"'
#26 DONE 49.2s
#27 DONE 4.2s
$ curl -s http://127.0.0.1:18862/.well-known/healthcheck
{"status":"ok","now":"2026-08-23T23:26:36.344497766Z","uptimeSeconds":7,"uptimeHuman":"7.918824062s","version":"v0.0.0-impl253","appname":"webhooker","maintenanceMode":false}
```
Neither build layer was `CACHED`. The stamp is read off the **running** binary, not the command line, so it survives the static relink — and it is running on Alpine, which a non-static binary could not do. Footer and startup log from the same container:
```
<span class="mx-3">|</span> <span>v0.0.0-impl253</span>
"msg":"starting","appname":"webhooker","version":"v0.0.0-impl253"
```
**Reproducibility.** Two `make build` runs of the same commit, `bin/webhooker` removed in between:
```
dc38ae7edfea3cdfcf831a31a377c50cc1df6e324e2d65031d538f724d994349 bin/webhooker
dc38ae7edfea3cdfcf831a31a377c50cc1df6e324e2d65031d538f724d994349 bin/webhooker
```
Byte-identical, so the gate in https://git.eeqj.de/sneak/webhooker/pulls/111 still holds.
**No git metadata.** `git archive HEAD` unpacked to a directory with no `.git`:
```
$ make version
unknown
$ curl -s http://127.0.0.1:18861/.well-known/healthcheck
{"status":"ok",...,"version":"unknown","appname":"webhooker","maintenanceMode":false}
```
And a bare `docker build .` with no `--build-arg` (the context never carries `.git`):
```
#26 [builder 10/11] RUN make build VERSION="unknown"
$ curl -s http://127.0.0.1:18863/.well-known/healthcheck
{"status":"ok",...,"version":"unknown","appname":"webhooker","maintenanceMode":false}
```
It builds, it runs, and it claims no tag.
**Tests.** New `internal/versionscript` package, mirroring `internal/ciscript`: eleven tests over throwaway git repositories — clean-at-a-tag, past a tag, untagged, dirty, no `.git`, enclosing repository ignored, `VERSION` override, empty `VERSION`, stability across invocations — plus guards that the `Makefile` still composes both halves of the linker flags and that no raw `go build` reappears in the `Dockerfile`. `TestFooterReportsStampedVersion` in `internal/handlers` renders the login page and asserts the footer carries the version rather than the `dev` fallback.
## Disclosures
- `script/docker` is one of the scripts REPO_POLICIES.md expects to stay byte-identical across repos. It now passes `--build-arg VERSION="$(script/version)"`, because the version can only be resolved on the host. Its header comment records why.
- `internal/handlers/handlers.go` is outside the build files, but the footer is a version-reporting site named in the issue's definition of done and it was broken independently of the linker flags. The change is confined to `renderTemplate` and one struct field.
- The `{{else}}dev{{end}}` fallback in `base.html` is left in place; with `renderTemplate` supplying the value it is now unreachable in a wired application.
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.
Verified by execution on ec92992 in an independent clone: all four version states across all three reporting sites (healthcheck, rendered footer HTML, startup log) — clean tag reports exactly the tag, untagged reports the short SHA, dirty carries -dirty, and both no-.git paths (git archive tarball, script/cibuild's bare docker build .) report unknown. The image binary is statically linked / not a dynamic executable with -ldflags="-X main.version=... -extldflags \"-static\"" recorded in it, so the relink kept both halves. Two host make build runs and a docker build --no-cache-filter=builder rebuild at the same version were byte-identical; a changed VERSION re-ran both build layers and genuinely restamped while RUN make test stayed CACHED. make check green with GOFLAGS=-count=1 in 1m39s (21 ok, 0 FAIL, 0 (cached), 0 skips, 0 issues. from the containerized linter). CI green on the head commit; fast-forwards onto next; no attribution trailers; (closes #253) present.
The second half of the defect is real: reverting only internal/handlers/handlers.go to next makes TestFooterReportsStampedVersion fail with <span>dev</span> in the rendered footer. Two further mutation probes caught — a reverted Makefile fails TestMakefile_BuildComposesVersionAndExtraFlags, and neutering in_this_checkout in script/version fails TestVersion_EnclosingRepositoryIsNotUsed (it then stamps the enclosing repo's v9.9.9).
One finding, non-blocking
Makefile:41 — an explicitly empty VERSION reintroduces the exact defect this issue is about.make build VERSION= produces go build -ldflags '-X main.version='. Run: healthcheck reports "version":"", the startup log reports version="", and the footer falls back to <span>dev</span> because {{if .Version}} is false on the empty string. The Docker path is equally reachable: --build-arg VERSION= overrides ARG VERSION=unknown with the empty string (verified against a scratch image), so docker build --build-arg VERSION= . ships a binary that reports nothing at all three sites.
Why it matters beyond the typo case: this is exactly the "set but unusable, silently defaults" shape the repo rejects elsewhere (PORT=eighty aborts startup rather than substituting). script/version already guards it and TestVersion_EmptyOverrideFallsBackToGit covers it — but the guard sits only on the env path, and a VERSION= handed to make or to --build-arg never reaches the script.
Acceptable would be collapsing an empty override in the one place that composes the flag, e.g. VERSION := $(if $(strip $(VERSION)),$(VERSION),unknown) after the ?=, with a case in internal/versionscript. Not reachable through make docker, script/cibuild, or any current workflow, so it does not block the merge.
Notes and disclosures
VERSION as an environment override needs no guard.REPO_POLICIES.md's own canonical Dockerfile prescribes ARG VERSION, so a caller naming the build is the intended contract, not a hole.
script/docker deviation: the justification holds. The value is only resolvable host-side, and the alternatives (shipping .git in the build context, or deriving it in the image) are worse. It does leave script/docker no longer byte-identical across repos as REPO_POLICIES.md states; the durable fix is the model script at sneak/prompts, which is your call and not this PR's.
script/cibuild is unchanged, so CI-built images stamp unknown. Harmless today — no image-publishing workflow exists and the CI image is discarded — but it means no green CI run ever exercises a real stamp.
ARG VERSION=unknown diverges from REPO_POLICIES.md's model ARG VERSION=dev. Deliberate, documented in the README table, and better (it does not collide with main.version's own default); flagging the divergence only.
Untagged builds carry git's auto-abbreviated SHA, whose length depends on the clone's object count, so byte-identity across two different clones of one commit is not guaranteed. Tagged release builds report the exact tag and are unaffected, and the gate in #111 runs in a single clone. Not a regression from this change.
Pre-existing, not this PR's to fix: the build passes neither -trimpath nor -s -w, both of which REPO_POLICIES.md's model Dockerfile carries; -trimpath is the one that bears on reproducibility across build directories. The gomodguard deprecation warning is tracked elsewhere.
My own deviations: go test -run was invoked directly for the three mutation probes (no make target selects a single test), and docker build --no-cache-filter=builder for the uncached-rebuild comparison. No repo file was left modified and the throwaway tag was deleted.
No scope creep — internal/handlers/handlers.go is a version-reporting site named in the definition of done of #253.
**PASS.**
Verified by execution on `ec92992` in an independent clone: all four version states across all three reporting sites (healthcheck, rendered footer HTML, startup log) — clean tag reports exactly the tag, untagged reports the short SHA, dirty carries `-dirty`, and both no-`.git` paths (`git archive` tarball, `script/cibuild`'s bare `docker build .`) report `unknown`. The image binary is `statically linked` / `not a dynamic executable` with `-ldflags="-X main.version=... -extldflags \"-static\""` recorded in it, so the relink kept both halves. Two host `make build` runs and a `docker build --no-cache-filter=builder` rebuild at the same version were byte-identical; a changed `VERSION` re-ran both build layers and genuinely restamped while `RUN make test` stayed `CACHED`. `make check` green with `GOFLAGS=-count=1` in 1m39s (21 `ok`, 0 `FAIL`, 0 `(cached)`, 0 skips, `0 issues.` from the containerized linter). CI green on the head commit; fast-forwards onto `next`; no attribution trailers; ` (closes #253)` present.
The second half of the defect is real: reverting only `internal/handlers/handlers.go` to `next` makes `TestFooterReportsStampedVersion` fail with `<span>dev</span>` in the rendered footer. Two further mutation probes caught — a reverted `Makefile` fails `TestMakefile_BuildComposesVersionAndExtraFlags`, and neutering `in_this_checkout` in `script/version` fails `TestVersion_EnclosingRepositoryIsNotUsed` (it then stamps the enclosing repo's `v9.9.9`).
## One finding, non-blocking
**`Makefile:41` — an explicitly empty `VERSION` reintroduces the exact defect this issue is about.** `make build VERSION=` produces `go build -ldflags '-X main.version='`. Run: healthcheck reports `"version":""`, the startup log reports `version=""`, and the footer falls back to `<span>dev</span>` because `{{if .Version}}` is false on the empty string. The Docker path is equally reachable: `--build-arg VERSION=` overrides `ARG VERSION=unknown` with the empty string (verified against a scratch image), so `docker build --build-arg VERSION= .` ships a binary that reports nothing at all three sites.
Why it matters beyond the typo case: this is exactly the "set but unusable, silently defaults" shape the repo rejects elsewhere (`PORT=eighty` aborts startup rather than substituting). `script/version` already guards it and `TestVersion_EmptyOverrideFallsBackToGit` covers it — but the guard sits only on the env path, and a `VERSION=` handed to `make` or to `--build-arg` never reaches the script.
Acceptable would be collapsing an empty override in the one place that composes the flag, e.g. `VERSION := $(if $(strip $(VERSION)),$(VERSION),unknown)` after the `?=`, with a case in `internal/versionscript`. Not reachable through `make docker`, `script/cibuild`, or any current workflow, so it does not block the merge.
## Notes and disclosures
- **`VERSION` as an environment override needs no guard.** `REPO_POLICIES.md`'s own canonical Dockerfile prescribes `ARG VERSION`, so a caller naming the build is the intended contract, not a hole.
- **`script/docker` deviation: the justification holds.** The value is only resolvable host-side, and the alternatives (shipping `.git` in the build context, or deriving it in the image) are worse. It does leave `script/docker` no longer byte-identical across repos as `REPO_POLICIES.md` states; the durable fix is the model script at `sneak/prompts`, which is your call and not this PR's.
- **`script/cibuild` is unchanged, so CI-built images stamp `unknown`.** Harmless today — no image-publishing workflow exists and the CI image is discarded — but it means no green CI run ever exercises a real stamp.
- `ARG VERSION=unknown` diverges from `REPO_POLICIES.md`'s model `ARG VERSION=dev`. Deliberate, documented in the README table, and better (it does not collide with `main.version`'s own default); flagging the divergence only.
- Untagged builds carry git's auto-abbreviated SHA, whose length depends on the clone's object count, so byte-identity across two *different* clones of one commit is not guaranteed. Tagged release builds report the exact tag and are unaffected, and the gate in https://git.eeqj.de/sneak/webhooker/pulls/111 runs in a single clone. Not a regression from this change.
- Pre-existing, not this PR's to fix: the build passes neither `-trimpath` nor `-s -w`, both of which `REPO_POLICIES.md`'s model Dockerfile carries; `-trimpath` is the one that bears on reproducibility across build directories. The `gomodguard` deprecation warning is tracked elsewhere.
- My own deviations: `go test -run` was invoked directly for the three mutation probes (no `make` target selects a single test), and `docker build --no-cache-filter=builder` for the uncached-rebuild comparison. No repo file was left modified and the throwaway tag was deleted.
No scope creep — `internal/handlers/handlers.go` is a version-reporting site named in the definition of done of https://git.eeqj.de/sneak/webhooker/issues/253.
Rework for the empty-override finding. Rebased onto next (5fda446) and amended into the single commit, now fec6876.
The suggested VERSION := $(if ...) does not work: a plain makefile assignment loses to a command-line definition, which is exactly the case being corrected. Verified — with that line, make version VERSION= still prints the empty string. The guard needs override, so the Makefile now carries:
override VERSION := $(or $(strip $(VERSION)),$(shell script/version))
One deviation from the suggested fix: an empty override resolves through script/version rather than landing on a literal unknown. That keeps one meaning of "empty" across both paths — TestVersion_EmptyOverrideFallsBackToGit already defines empty as unset for the env path, and a second rule where a make override of empty discards a derivable version would contradict it. Where nothing is derivable it still lands on unknown, which is the case that matters: no .git, so the Docker path is unaffected.
No Dockerfile guard is needed. --build-arg VERSION= reaches make as a command-line definition via make build VERSION="$VERSION", so the Makefile line closes it. The image built with --build-arg VERSION= links -ldflags="-X main.version=unknown -extldflags \"-static\"" and is statically linked; healthcheck, footer and startup log all report unknown.
New test TestMakefile_EmptyOverrideResolvesLikeAnUnsetOne in internal/versionscript. It fails without the Makefile change — on the override VERSION := assertion, and behaviourally too (make version under an empty VERSION printed [] before the fix, [09a5029-dirty] after).
make check green with GOFLAGS=-count=1 after the rebase: 21 packages, 0 cached, lint 0 issues in Docker. Reproducibility unaffected — two make build runs of the same commit are byte-identical, and a VERSION= build hashes identically to the default one.
README gained one sentence stating that an empty override means unset.
Rework for the empty-override finding. Rebased onto `next` (`5fda446`) and amended into the single commit, now `fec6876`.
The suggested `VERSION := $(if ...)` does not work: a plain makefile assignment loses to a command-line definition, which is exactly the case being corrected. Verified — with that line, `make version VERSION=` still prints the empty string. The guard needs `override`, so the Makefile now carries:
`override VERSION := $(or $(strip $(VERSION)),$(shell script/version))`
One deviation from the suggested fix: an empty override resolves through `script/version` rather than landing on a literal `unknown`. That keeps one meaning of "empty" across both paths — `TestVersion_EmptyOverrideFallsBackToGit` already defines empty as unset for the env path, and a second rule where a make override of empty discards a derivable version would contradict it. Where nothing is derivable it still lands on `unknown`, which is the case that matters: no `.git`, so the Docker path is unaffected.
No Dockerfile guard is needed. `--build-arg VERSION=` reaches make as a command-line definition via `make build VERSION="$VERSION"`, so the Makefile line closes it. The image built with `--build-arg VERSION=` links `-ldflags="-X main.version=unknown -extldflags \"-static\""` and is `statically linked`; healthcheck, footer and startup log all report `unknown`.
New test `TestMakefile_EmptyOverrideResolvesLikeAnUnsetOne` in `internal/versionscript`. It fails without the Makefile change — on the `override VERSION :=` assertion, and behaviourally too (`make version` under an empty VERSION printed `[]` before the fix, `[09a5029-dirty]` after).
`make check` green with `GOFLAGS=-count=1` after the rebase: 21 packages, 0 cached, lint 0 issues in Docker. Reproducibility unaffected — two `make build` runs of the same commit are byte-identical, and a `VERSION=` build hashes identically to the default one.
README gained one sentence stating that an empty override means unset.
PASS. Re-verified by execution on fec6876 in an independent clone: all three make version cases correct (VERSION= resolves to the derived value, VERSION=v1.2.3 still wins, whitespace-only also falls back); the override is genuinely required, not cargo-culted — removed in a throwaway copy, make version VERSION= prints the empty string and the new test fails on both its textual and its behavioural assertion. docker build --build-arg VERSION= end to end reports unknown on the healthcheck, in the footer and in the startup log, binary Not a valid dynamic program with both ldflags halves recorded; --build-arg VERSION=v0.0.0-rr260 likewise reports that value at all three sites (author's stated gap closed) and re-ran only the two build layers, RUN make test staying CACHED. Tag case on the host reports exactly the tag; no-.git tree reports unknown, including under an empty override. Two make build runs byte-identical, and a VERSION= build hashes identically to the default one. make check green with GOFLAGS=-count=1 in 1m34s (21 ok, 0 (cached), 0 FAIL, 0 issues. from the pinned containerized linter). CI green on the head commit, fast-forwards onto next, (closes #253) present, no attribution trailers.
Empty-override semantics — resolving through script/version rather than to a literal unknown — accepted. I could not construct a path where it yields a misleading stamp: on the host the derived value describes the tree actually being compiled, and in the image .dockerignore removes .git/ so it lands on unknown regardless. Nothing in the definition of done of #253 conflicts with it.
Notes and disclosures:
override VERSION := is simply-expanded, so script/version (a git describe) now runs at parse time on every make invocation, including make test / make lint / make check — one call per invocation, milliseconds. Flagging, not objecting.
My first make check failed on TestVendoredAssetsMatchManifest and TestBaseTemplateScriptsAreServed: a fresh clone without make assets. Pre-existing and self-diagnosing; the green run above is after make assets.
My reproducibility hash differs from the one in the PR body because my clone sits at a detached HEAD deriving a different git describe. The asserted invariant (two runs identical) holds.
Deviations: for the mutation probes I edited the Makefile in a throwaway copy under /tmp and ran go test -run directly against that copy, since no make target selects a single test. The reviewed tree was left unmodified, the throwaway tag existed only in a copy and was never pushed, and every rereview-260- container and image has been removed.
**PASS.** Re-verified by execution on `fec6876` in an independent clone: all three `make version` cases correct (`VERSION=` resolves to the derived value, `VERSION=v1.2.3` still wins, whitespace-only also falls back); the `override` is genuinely required, not cargo-culted — removed in a throwaway copy, `make version VERSION=` prints the empty string and the new test fails on both its textual and its behavioural assertion. `docker build --build-arg VERSION=` end to end reports `unknown` on the healthcheck, in the footer and in the startup log, binary `Not a valid dynamic program` with both ldflags halves recorded; `--build-arg VERSION=v0.0.0-rr260` likewise reports that value at all three sites (author's stated gap closed) and re-ran only the two build layers, `RUN make test` staying `CACHED`. Tag case on the host reports exactly the tag; no-`.git` tree reports `unknown`, including under an empty override. Two `make build` runs byte-identical, and a `VERSION=` build hashes identically to the default one. `make check` green with `GOFLAGS=-count=1` in 1m34s (21 `ok`, 0 `(cached)`, 0 `FAIL`, `0 issues.` from the pinned containerized linter). CI green on the head commit, fast-forwards onto `next`, ` (closes #253)` present, no attribution trailers.
Empty-override semantics — resolving through `script/version` rather than to a literal `unknown` — accepted. I could not construct a path where it yields a misleading stamp: on the host the derived value describes the tree actually being compiled, and in the image `.dockerignore` removes `.git/` so it lands on `unknown` regardless. Nothing in the definition of done of https://git.eeqj.de/sneak/webhooker/issues/253 conflicts with it.
Notes and disclosures:
- `override VERSION :=` is simply-expanded, so `script/version` (a `git describe`) now runs at parse time on every `make` invocation, including `make test` / `make lint` / `make check` — one call per invocation, milliseconds. Flagging, not objecting.
- My first `make check` failed on `TestVendoredAssetsMatchManifest` and `TestBaseTemplateScriptsAreServed`: a fresh clone without `make assets`. Pre-existing and self-diagnosing; the green run above is after `make assets`.
- My reproducibility hash differs from the one in the PR body because my clone sits at a detached HEAD deriving a different `git describe`. The asserted invariant (two runs identical) holds.
- Deviations: for the mutation probes I edited the `Makefile` in a throwaway copy under `/tmp` and ran `go test -run` directly against that copy, since no `make` target selects a single test. The reviewed tree was left unmodified, the throwaway tag existed only in a copy and was never pushed, and every `rereview-260-` container and image has been removed.
clawbot
merged commit ee2276a912 into next2026-08-24 03:15:21 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #253.
main.versionwas a placeholder nothing ever set: neithermake buildnor theDockerfilepassed-X. A tagged release produced an artifact that could not say which commit it was.What changed
script/versionis the single source of the value:$VERSIONwhen set and non-empty, elsegit describe --tags --always --dirtyagainst this checkout, elseunknown. It refuses to use an enclosing repository's metadata (a tarball unpacked inside an unrelated working copy is not that copy's version). Nothing time-, host- or builder-dependent is stamped.Makefile:VERSION ?= $(shell script/version), plus aGO_LDFLAGShook;buildcompiles with-ldflags '-X main.version=$(VERSION) $(GO_LDFLAGS)'. Newmake versionprints what the checkout would stamp.Dockerfile:.dockerignoreexcludes.git/, so the builder stage cannot derive anything — the version enters asARG VERSION, defaulted tounknown.script/dockerfills it from the host checkout. Both compiles in the image now go throughmake build, the static relink contributing its-extldflagsthroughGO_LDFLAGSrather than replacing-ldflags, so it cannot drop the stamp. TheARGsits below the test and asset layers so a new version does not invalidate their cache.base.htmlrenders.Version, which nothing ever put in the template data, so it printed its literaldevfallback regardless of how the binary was built.renderTemplatenow supplies it on both the map and the wrapper path — one line ininternal/handlers/handlers.go, the only file touched outside the build files,README.mdand tests.Not changed: no timestamp, no commit date, no builder identity — see reproducibility below.
What each build reports
v1.0.0-3-g1a2b3c4-dirtyVERSIONunknownVerification
make checkgreen withGOFLAGS=-count=1: 21 packagesok, 572 tests passed,0 issues.from the containerized linter.make build, checkout at a real tag (throwaway local tagv0.0.0-impl253, deleted before pushing), run on port 18860:Exactly the tag, no suffix.
make dockerat the same tag, container run from the built image:Neither build layer was
CACHED. The stamp is read off the running binary, not the command line, so it survives the static relink — and it is running on Alpine, which a non-static binary could not do. Footer and startup log from the same container:Reproducibility. Two
make buildruns of the same commit,bin/webhookerremoved in between:Byte-identical, so the gate in #111 still holds.
No git metadata.
git archive HEADunpacked to a directory with no.git:And a bare
docker build .with no--build-arg(the context never carries.git):It builds, it runs, and it claims no tag.
Tests. New
internal/versionscriptpackage, mirroringinternal/ciscript: eleven tests over throwaway git repositories — clean-at-a-tag, past a tag, untagged, dirty, no.git, enclosing repository ignored,VERSIONoverride, emptyVERSION, stability across invocations — plus guards that theMakefilestill composes both halves of the linker flags and that no rawgo buildreappears in theDockerfile.TestFooterReportsStampedVersionininternal/handlersrenders the login page and asserts the footer carries the version rather than thedevfallback.Disclosures
script/dockeris one of the scripts REPO_POLICIES.md expects to stay byte-identical across repos. It now passes--build-arg VERSION="$(script/version)", because the version can only be resolved on the host. Its header comment records why.internal/handlers/handlers.gois outside the build files, but the footer is a version-reporting site named in the issue's definition of done and it was broken independently of the linker flags. The change is confined torenderTemplateand one struct field.{{else}}dev{{end}}fallback inbase.htmlis left in place; withrenderTemplatesupplying the value it is now unreachable in a wired application.PASS.
Verified by execution on
ec92992in an independent clone: all four version states across all three reporting sites (healthcheck, rendered footer HTML, startup log) — clean tag reports exactly the tag, untagged reports the short SHA, dirty carries-dirty, and both no-.gitpaths (git archivetarball,script/cibuild's baredocker build .) reportunknown. The image binary isstatically linked/not a dynamic executablewith-ldflags="-X main.version=... -extldflags \"-static\""recorded in it, so the relink kept both halves. Two hostmake buildruns and adocker build --no-cache-filter=builderrebuild at the same version were byte-identical; a changedVERSIONre-ran both build layers and genuinely restamped whileRUN make teststayedCACHED.make checkgreen withGOFLAGS=-count=1in 1m39s (21ok, 0FAIL, 0(cached), 0 skips,0 issues.from the containerized linter). CI green on the head commit; fast-forwards ontonext; no attribution trailers;(closes #253)present.The second half of the defect is real: reverting only
internal/handlers/handlers.gotonextmakesTestFooterReportsStampedVersionfail with<span>dev</span>in the rendered footer. Two further mutation probes caught — a revertedMakefilefailsTestMakefile_BuildComposesVersionAndExtraFlags, and neuteringin_this_checkoutinscript/versionfailsTestVersion_EnclosingRepositoryIsNotUsed(it then stamps the enclosing repo'sv9.9.9).One finding, non-blocking
Makefile:41— an explicitly emptyVERSIONreintroduces the exact defect this issue is about.make build VERSION=producesgo build -ldflags '-X main.version='. Run: healthcheck reports"version":"", the startup log reportsversion="", and the footer falls back to<span>dev</span>because{{if .Version}}is false on the empty string. The Docker path is equally reachable:--build-arg VERSION=overridesARG VERSION=unknownwith the empty string (verified against a scratch image), sodocker build --build-arg VERSION= .ships a binary that reports nothing at all three sites.Why it matters beyond the typo case: this is exactly the "set but unusable, silently defaults" shape the repo rejects elsewhere (
PORT=eightyaborts startup rather than substituting).script/versionalready guards it andTestVersion_EmptyOverrideFallsBackToGitcovers it — but the guard sits only on the env path, and aVERSION=handed tomakeor to--build-argnever reaches the script.Acceptable would be collapsing an empty override in the one place that composes the flag, e.g.
VERSION := $(if $(strip $(VERSION)),$(VERSION),unknown)after the?=, with a case ininternal/versionscript. Not reachable throughmake docker,script/cibuild, or any current workflow, so it does not block the merge.Notes and disclosures
VERSIONas an environment override needs no guard.REPO_POLICIES.md's own canonical Dockerfile prescribesARG VERSION, so a caller naming the build is the intended contract, not a hole.script/dockerdeviation: the justification holds. The value is only resolvable host-side, and the alternatives (shipping.gitin the build context, or deriving it in the image) are worse. It does leavescript/dockerno longer byte-identical across repos asREPO_POLICIES.mdstates; the durable fix is the model script atsneak/prompts, which is your call and not this PR's.script/cibuildis unchanged, so CI-built images stampunknown. Harmless today — no image-publishing workflow exists and the CI image is discarded — but it means no green CI run ever exercises a real stamp.ARG VERSION=unknowndiverges fromREPO_POLICIES.md's modelARG VERSION=dev. Deliberate, documented in the README table, and better (it does not collide withmain.version's own default); flagging the divergence only.-trimpathnor-s -w, both of whichREPO_POLICIES.md's model Dockerfile carries;-trimpathis the one that bears on reproducibility across build directories. Thegomodguarddeprecation warning is tracked elsewhere.go test -runwas invoked directly for the three mutation probes (nomaketarget selects a single test), anddocker build --no-cache-filter=builderfor the uncached-rebuild comparison. No repo file was left modified and the throwaway tag was deleted.No scope creep —
internal/handlers/handlers.gois a version-reporting site named in the definition of done of #253.ec92992450tofec6876c42Rework for the empty-override finding. Rebased onto
next(5fda446) and amended into the single commit, nowfec6876.The suggested
VERSION := $(if ...)does not work: a plain makefile assignment loses to a command-line definition, which is exactly the case being corrected. Verified — with that line,make version VERSION=still prints the empty string. The guard needsoverride, so the Makefile now carries:override VERSION := $(or $(strip $(VERSION)),$(shell script/version))One deviation from the suggested fix: an empty override resolves through
script/versionrather than landing on a literalunknown. That keeps one meaning of "empty" across both paths —TestVersion_EmptyOverrideFallsBackToGitalready defines empty as unset for the env path, and a second rule where a make override of empty discards a derivable version would contradict it. Where nothing is derivable it still lands onunknown, which is the case that matters: no.git, so the Docker path is unaffected.No Dockerfile guard is needed.
--build-arg VERSION=reaches make as a command-line definition viamake build VERSION="$VERSION", so the Makefile line closes it. The image built with--build-arg VERSION=links-ldflags="-X main.version=unknown -extldflags \"-static\""and isstatically linked; healthcheck, footer and startup log all reportunknown.New test
TestMakefile_EmptyOverrideResolvesLikeAnUnsetOneininternal/versionscript. It fails without the Makefile change — on theoverride VERSION :=assertion, and behaviourally too (make versionunder an empty VERSION printed[]before the fix,[09a5029-dirty]after).make checkgreen withGOFLAGS=-count=1after the rebase: 21 packages, 0 cached, lint 0 issues in Docker. Reproducibility unaffected — twomake buildruns of the same commit are byte-identical, and aVERSION=build hashes identically to the default one.README gained one sentence stating that an empty override means unset.
PASS. Re-verified by execution on
fec6876in an independent clone: all threemake versioncases correct (VERSION=resolves to the derived value,VERSION=v1.2.3still wins, whitespace-only also falls back); theoverrideis genuinely required, not cargo-culted — removed in a throwaway copy,make version VERSION=prints the empty string and the new test fails on both its textual and its behavioural assertion.docker build --build-arg VERSION=end to end reportsunknownon the healthcheck, in the footer and in the startup log, binaryNot a valid dynamic programwith both ldflags halves recorded;--build-arg VERSION=v0.0.0-rr260likewise reports that value at all three sites (author's stated gap closed) and re-ran only the two build layers,RUN make teststayingCACHED. Tag case on the host reports exactly the tag; no-.gittree reportsunknown, including under an empty override. Twomake buildruns byte-identical, and aVERSION=build hashes identically to the default one.make checkgreen withGOFLAGS=-count=1in 1m34s (21ok, 0(cached), 0FAIL,0 issues.from the pinned containerized linter). CI green on the head commit, fast-forwards ontonext,(closes #253)present, no attribution trailers.Empty-override semantics — resolving through
script/versionrather than to a literalunknown— accepted. I could not construct a path where it yields a misleading stamp: on the host the derived value describes the tree actually being compiled, and in the image.dockerignoreremoves.git/so it lands onunknownregardless. Nothing in the definition of done of #253 conflicts with it.Notes and disclosures:
override VERSION :=is simply-expanded, soscript/version(agit describe) now runs at parse time on everymakeinvocation, includingmake test/make lint/make check— one call per invocation, milliseconds. Flagging, not objecting.make checkfailed onTestVendoredAssetsMatchManifestandTestBaseTemplateScriptsAreServed: a fresh clone withoutmake assets. Pre-existing and self-diagnosing; the green run above is aftermake assets.git describe. The asserted invariant (two runs identical) holds.Makefilein a throwaway copy under/tmpand rango test -rundirectly against that copy, since nomaketarget selects a single test. The reviewed tree was left unmodified, the throwaway tag existed only in a copy and was never pushed, and everyrereview-260-container and image has been removed.