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

This commit was merged in pull request #260.
This commit is contained in:
2026-08-24 03:15:20 +02:00
parent 032f265d69
commit ee2276a912
9 changed files with 573 additions and 14 deletions

View File

@@ -57,7 +57,8 @@ make fmt-check # Fail if gofmt would change anything (writes nothing)
make lint # Run golangci-lint in Docker (Dockerfile.lint)
make test # Run tests with race detection
make check # test + lint + fmt-check (CI gate)
make build # Build binary to bin/webhooker
make build # Build binary to bin/webhooker (version-stamped)
make version # Print the version this checkout would stamp
make run # build, then run ./bin/webhooker
make dev # go run ./cmd/webhooker
make deps # go mod download + go mod tidy
@@ -691,6 +692,9 @@ Upgrade procedure:
3. Pull the new image and start it.
4. Confirm `database migrations completed` in the logs before putting
traffic back on it.
5. Confirm the new build is the one running:
`curl -s http://host:8080/.well-known/healthcheck` reports the
version it was stamped with (see [Version stamping](#version-stamping)).
**Downgrade is unsupported.** Once a newer binary has migrated the files
there is no way to move them back. `AutoMigrate` is additive — it adds
@@ -701,6 +705,42 @@ silent divergence, not a startup error. The only supported way back to
an older version is restoring the pre-upgrade backup, which discards
everything received since that backup was taken.
### Version stamping
The binary reports its version at `/.well-known/healthcheck` (the
`version` field), in the UI footer, and in the startup log line
(`msg=starting`, `version=...`). It is also the Sentry release name,
as `webhooker-{version}`. The value is stamped in at build time by the
linker; it is not read from a file at runtime, so it identifies the
build itself.
`script/version` produces the value and both build paths use it:
| Build | What it reports |
| --- | --- |
| Clean checkout at a tag | exactly that tag, e.g. `v1.0.0` |
| Commits past a tag | `v1.0.0-3-g1a2b3c4` — tag, commits since, short SHA |
| No tag reachable | the short SHA, e.g. `1a2b3c4` |
| Uncommitted changes | the above with a `-dirty` suffix |
| No git metadata | `unknown` |
`unknown` is what a source tarball or a `docker build .` with no
`--build-arg VERSION=...` reports. `.dockerignore` excludes `.git/`, so
the build context carries no git metadata and the image cannot derive
the version itself: `script/docker` (and so `make docker`) resolves it
on the host and passes it in as the `VERSION` build arg. A build that
reports `unknown` is a build nobody told what it was; it is not a
failure, but it cannot be traced back to a commit.
`make version` prints what the current checkout would stamp, and
`make build VERSION=v1.2.3` overrides it. An empty override — from
`make build VERSION=` or from `--build-arg VERSION=` — means unset
rather than `""`, and resolves the way an absent one does.
Nothing that varies between two builds of the same commit is stamped —
no timestamp, no hostname, no builder identity — so two builds of one
commit still produce a byte-identical binary.
### Backups contain secrets
Treat a backup with the same care as the credentials inside it. Encrypt
@@ -843,9 +883,11 @@ anything.
This repository adheres to the
[Scripts to Rule Them All](https://github.com/github/scripts-to-rule-them-all)
standard: normalized scripts in `script/` are the entrypoints for the
development workflow. Ten of the Makefile's sixteen targets are thin
shims that call them; `build`, `run`, `dev`, `deps`, `clean` and `css`
are inline commands with no script behind them. We provide:
development workflow. Ten of the Makefile's seventeen targets are thin
shims that call them; `build`, `run`, `dev`, `deps`, `clean`, `css` and
`version` are inline commands with no script behind them, though
`build` and `version` both take their value from `script/version`. We
provide:
- `script/bootstrap` — install all dependencies (idempotent)
- `script/setup` — make a fresh clone ready for development
@@ -858,7 +900,11 @@ are inline commands with no script behind them. We provide:
- `script/fmt` — format all code (writes)
- `script/fmt-check` — check formatting (read-only)
- `script/check` — run test, lint, and fmt-check
- `script/docker` — build the Docker image tagged via `script/projectname`
- `script/version` — output the version to stamp into the binary (see
[Version stamping](#version-stamping))
- `script/docker` — build the Docker image tagged via
`script/projectname`, passing `script/version`'s output in as the
`VERSION` build arg
- `script/cibuild` — CI entrypoint: `docker build .` (the Dockerfile
runs the checks, so a green build implies a green repo)
- `script/ci-mark-superseded` — CI helper: mark the commits whose run a
@@ -2430,7 +2476,7 @@ webhooker/
├── script/ # Scripts to Rule Them All entrypoints
├── Dockerfile # Three stages: lint, test+build, Alpine runtime
├── Dockerfile.lint # Lint-only image built by script/lint
├── Makefile # 10 of 16 targets shim script/; 6 are inline
├── Makefile # 10 of 17 targets shim script/; 7 are inline
├── go.mod / go.sum
└── .golangci.yml # Linter configuration
```
@@ -2735,7 +2781,11 @@ version is fixed independently of the compiler's:
stage passing (it copies a file from it), runs `script/fetch-assets`
to download and verify the third-party browser assets, then runs
`make test` and `make build`, and finally rebuilds the binary with
`CGO_ENABLED=1` and static linking so it runs on musl.
`CGO_ENABLED=1` and static linking so it runs on musl. Both builds
go through `make build`, the relink adding its `-extldflags` via
`GO_LDFLAGS`, so neither can drop the `-X` that stamps the version.
The version arrives as the `VERSION` build arg, since the context
has no `.git` (see [Version stamping](#version-stamping)).
3. **Runtime stage** (`alpine:3.21`) — copies the static binary,
creates the `/var/lib/webhooker` directory for all SQLite databases,
runs as the non-root `webhooker` user (UID 1000), exposes port 8080,