Derive the image's version from the .git in the build context (closes #366)
check / check (push) Waiting to run

upaas uploads its clone as a tar context, which .dockerignore does not filter, so its builds already carried .git; the unknown default of the VERSION build arg is what stamped them. The image now derives its version itself: ARG VERSION has no default, so make build falls back to script/version, which runs git describe on the copied .git; a VERSION build arg still wins.

.dockerignore sends .git without its config in a directory context and no longer leaves out tracked files, which would mark the tree -dirty. The builder installs git, trusts /build as a safe.directory, and fails when .git is present but the version comes out unknown. A shallow single-branch clone stamps its short commit.

Model: opus-5-5
This commit was merged in pull request #410.
This commit is contained in:
2026-10-02 08:41:20 +02:00
parent 2416528b77
commit cb7bafab17
8 changed files with 90 additions and 68 deletions
+32 -21
View File
@@ -1133,13 +1133,29 @@ build itself.
| 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.
The image derives it the same way, from the `.git` that the build
context carries, so any `docker build .` of a clone, with no build
arguments, stamps the commit it was built from; a shallow clone of one
branch has no tags and stamps the short SHA. `.dockerignore` must
therefore leave out neither `.git` nor any tracked file, which git in
the build would see as deleted, marking the version `-dirty`. It does
leave `.git/config`, which can hold a remote URL carrying a credential
and which `git describe` does not need, out of a directory context. A
context sent as a tar is not filtered by `.dockerignore`, so it carries
`.git/config` unless its sender leaves it out; for upaas, that is
https://git.eeqj.de/sneak/upaas/issues/274. git in the build
reads the checkout whoever owns its files, since a context sent as a tar
archive keeps the sender's owners and git otherwise refuses a checkout
owned by another user. A `VERSION` build arg (`--build-arg VERSION=...`)
takes precedence; `script/docker` (and so `make docker`) passes the one
`script/version` resolves on the host. The image build fails if its
context carries `.git` and the version still comes out `unknown`, which
means git is missing from the build or could not read the checkout.
`unknown` is what a source tarball, or a `docker build` with no `.git`
in its context and no `VERSION` build arg, reports. 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
@@ -3233,8 +3249,9 @@ version is fixed independently of the compiler's:
rebuilds the binary with `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
stamps the version. The version is the `VERSION` build arg if one is
given, otherwise derived from the `.git` in the context, and the
stage fails if a context with `.git` would stamp `unknown` (see
[Version stamping](#version-stamping)).
3. **Runtime stage** (`alpine:3.21`) — copies the static binary and
`deploy/docker-entrypoint.sh`, creates the `/var/lib/webhooker`
@@ -3266,19 +3283,13 @@ A layer cache lets `docker build .` exit 0 in seconds with the lint and
test stages replayed rather than executed, which would make a green
check meaningless. The `check` workflow therefore writes
`.ci-fingerprint` into the build context before building. Its value is
the hash of the last commit that touched the build context, so:
the hash of the commit being checked, so every commit, docs-only ones
and a squash merge whose tree matches an already-built branch included,
gets a new fingerprint, invalidates the `COPY . .` layer of both check
stages, and really runs `make fmt-check`, `golangci-lint`, `make test`,
and `make build`. A run that reports success ran them.
- Any commit that changes code (including a squash merge whose tree
matches an already-built branch) gets a new fingerprint, invalidates
the `COPY . .` layer of both check stages, and really runs
`make fmt-check`, `golangci-lint`, `make test`, and `make build`. A
run that reports success ran them.
- A docs-only commit leaves the fingerprint unchanged — `.dockerignore`
excludes `*.md`, `LICENSE` and `.editorconfig` from the context
anyway — so the image replays from cache and costs seconds.
The module download layer sits above `COPY . .` and stays cached either
way.
The module download layer sits above `COPY . .` and stays cached.
A separate workflow step, run before the fingerprint is written, covers
a second way the gate lied: Gitea cancels an in-flight run when a newer