1 Commits
Author SHA1 Message Date
clawbot c460b0d6a5 Derive the image version from git; send .git to the build (closes #69)
check / check (push) Successful in 28s
The canonical documents told every repo to exclude .git from the build
context, default ARG VERSION to dev and never run git describe in a
build stage, so an image built from a clone with no build argument
reported dev. .dockerignore now sends .git. The Dockerfile example in
REPO_POLICIES.md installs git, takes the VERSION build argument when one
is given and otherwise git describe --tags --always, and fails when .git
is present but the version is empty, dev or unknown. The checklists, the
Go docs' Makefile comments, the README, and this repo's Dockerfile and
scripts now say the same.

Model: opus-5-5
2026-10-02 00:49:56 +00:00
5 changed files with 38 additions and 61 deletions
+2 -5
View File
@@ -13,11 +13,8 @@
# `/myapp`, never `**/myapp`, which also matches `cmd/myapp/` and # `/myapp`, never `**/myapp`, which also matches `cmd/myapp/` and
# deletes the package directory from the context. # deletes the package directory from the context.
# .git is sent without its config. Without a VERSION build argument the # .git is sent: without a VERSION build argument, the stage that compiles
# stage that compiles runs `git describe --tags --always` on .git, which # takes the version from `git describe --tags --always` on it.
# does not need .git/config; that file can hold a credential, such as a
# password in a remote URL or the token the CI checkout step stores there.
.git/config
# Agent scratch: one full checkout of the repo per in-flight agent. # Agent scratch: one full checkout of the repo per in-flight agent.
# Anchored because it occurs once where agents run at the repo root. # Anchored because it occurs once where agents run at the repo root.
+1 -1
View File
@@ -52,7 +52,7 @@ last_modified: 2026-10-02
# where a build stage invokes make, `ARG VERSION` puts it in the # where a build stage invokes make, `ARG VERSION` puts it in the
# environment and `?=` defers to it. Otherwise `git describe` runs, in a # environment and `?=` defers to it. Otherwise `git describe` runs, in a
# build stage on the `.git` the build context carries. # build stage on the `.git` the build context carries.
VERSION ?= $(shell git describe --tags --always) VERSION ?= $(shell git describe --always --dirty)
GOLDFLAGS += -X main.Version=$(VERSION) GOLDFLAGS += -X main.Version=$(VERSION)
+10 -16
View File
@@ -59,22 +59,16 @@ with your task.
here run anywhere other than the repo root, the anchored entry misses here run anywhere other than the repo root, the anchored entry misses
`services/api/.claude/`: add anchored entries for those directories. `services/api/.claude/`: add anchored entries for those directories.
- [ ] If the repo embeds a version in a binary: `.dockerignore` lets `.git` into - [ ] If the repo embeds a version in a binary: `.dockerignore` lets `.git` into
the build context. It keeps out `.git/config`, which `git describe` does the build context. The stage that compiles has `git` (the Debian Go image
not need and which can hold a credential: a password in a remote URL, or has it; an alpine one needs `apk add --no-cache git`) and takes the
the token the CI checkout step stores there. The stage that compiles has version from the `VERSION` build argument when one is given, otherwise
`git` (the Debian Go image has it; an alpine one needs from `git describe --tags --always` (a tag when the commit has one,
`apk add --no-cache git`) and takes the version from the `VERSION` build otherwise the short commit). `ARG VERSION` has no default, and the build
argument when one is given, otherwise from `git describe --tags --always`. fails if the context carries `.git` and the version still comes out empty,
That gives the tag on a tagged commit; on a later commit, the tag, the `dev` or `unknown`. `script/docker` and `script/cibuild` pass the version
number of commits since it and the short commit (`v1.2.3-4-gabc1234`); and they compute on the host; it takes precedence. A tag-derived version
the short commit when no tag is reachable. `ARG VERSION` has no default, additionally needs `fetch-depth: 0` on the CI checkout step, which clones
and the build fails if the context carries `.git` and the version still shallow and fetches no tags by default.
comes out empty, `dev` or `unknown`. A plain `docker build .` with no
build arguments must succeed; a Dockerfile that refuses an empty build
argument drops that refusal and keeps the argument. `script/docker` and
`script/cibuild` pass the version they compute on the host; it takes
precedence. A tag-derived version additionally needs `fetch-depth: 0` on
the CI checkout step, which clones shallow and fetches no tags by default.
- [ ] Gitea Actions workflow in `.gitea/workflows/` runs `script/cibuild` on - [ ] Gitea Actions workflow in `.gitea/workflows/` runs `script/cibuild` on
push — reference push — reference
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitea/workflows/check.yml` `https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitea/workflows/check.yml`
+7 -13
View File
@@ -68,19 +68,13 @@ Template files can be fetched from:
will run them in subdirectories, `services/api/.claude/` needs its own will run them in subdirectories, `services/api/.claude/` needs its own
anchored entry. anchored entry.
- If the image embeds a version in a binary: `.dockerignore` lets `.git` - If the image embeds a version in a binary: `.dockerignore` lets `.git`
into the build context. It keeps out `.git/config`, which `git describe` into the build context. The stage that compiles has `git` (the Debian Go
does not need and which can hold a credential: a password in a remote URL, image has it; an alpine one needs `apk add --no-cache git`) and takes the
or the token the CI checkout step stores there. The stage that compiles version from the `VERSION` build argument when one is given, otherwise
has `git` (the Debian Go image has it; an alpine one needs from `git describe --tags --always` (a tag when the commit has one,
`apk add --no-cache git`) and takes the version from the `VERSION` build otherwise the short commit). `ARG VERSION` has no default, and the build
argument when one is given, otherwise from `git describe --tags --always`. fails if the context carries `.git` and the version still comes out empty,
That gives the tag on a tagged commit; on a later commit, the tag, the `dev` or `unknown`.
number of commits since it and the short commit (`v1.2.3-4-gabc1234`); and
the short commit when no tag is reachable. `ARG VERSION` has no default,
and the build fails if the context carries `.git` and the version still
comes out empty, `dev` or `unknown`. A plain `docker build .` with no
build arguments must succeed; a Dockerfile that refuses an empty build
argument drops that refusal and keeps the argument.
- The Dockerfile carries a `lint` phase and a `test` phase, each invoking - The Dockerfile carries a `lint` phase and a `test` phase, each invoking
its tool directly rather than through `make` or `script/`, and the final its tool directly rather than through `make` or `script/`, and the final
stage carries a `COPY --from=` of a harmless file from each so the image stage carries a `COPY --from=` of a harmless file from each so the image
+18 -26
View File
@@ -197,13 +197,13 @@ style conventions are in separate documents:
RUN go mod download RUN go mod download
COPY . . COPY . .
# The VERSION build arg when one is given, otherwise # The VERSION build arg when one is given, otherwise the tag or short
# `git describe --tags --always` on the .git in the build context. With # commit from the .git in the build context. With .git present, a
# .git present, a version that is still empty, dev or unknown fails the # version that is still empty, dev or unknown fails the build: git is
# build: git is missing or could not read the checkout. # missing or could not read the checkout.
ARG VERSION ARG VERSION
RUN VERSION="${VERSION:-$(git describe --tags --always)}"; \ RUN VERSION="${VERSION:-$(git describe --tags --always)}"; \
if [ -e .git ]; then \ if [ -d .git ]; then \
case "$VERSION" in ""|dev|unknown) \ case "$VERSION" in ""|dev|unknown) \
echo "version is '$VERSION' although .git is present" >&2; \ echo "version is '$VERSION' although .git is present" >&2; \
exit 1 ;; \ exit 1 ;; \
@@ -235,20 +235,13 @@ style conventions are in separate documents:
`RUN mkdir -p web/dist && touch web/dist/index.html web/dist/style.css`. `RUN mkdir -p web/dist && touch web/dist/index.html web/dist/style.css`.
- If the project requires CGO or system libraries for linting (e.g. - If the project requires CGO or system libraries for linting (e.g.
`vips-dev`), install them in the lint phase with `apk add`. `vips-dev`), install them in the lint phase with `apk add`.
- `.dockerignore` lets `.git` into the build context. It keeps out - `.dockerignore` lets `.git` into the build context. The stage that
`.git/config`, which `git describe` does not need and which can hold a compiles has `git` (the Debian Go image has it; an alpine one needs
credential: a password in a remote URL, or the token the CI checkout step `apk add --no-cache git`) and takes the version from the `VERSION` build
stores there. The stage that compiles has `git` (the Debian Go image has argument when one is given, otherwise from `git describe --tags --always`
it; an alpine one needs `apk add --no-cache git`) and takes the version (a tag when the commit has one, otherwise the short commit). `ARG VERSION`
from the `VERSION` build argument when one is given, otherwise from has no default, and the build fails if the context carries `.git` and the
`git describe --tags --always`. That gives the tag on a tagged commit; on version still comes out empty, `dev` or `unknown`.
a later commit, the tag, the number of commits since it and the short
commit (`v1.2.3-4-gabc1234`); and the short commit when no tag is
reachable. `ARG VERSION` has no default, and the build fails if the
context carries `.git` and the version still comes out empty, `dev` or
`unknown`. A plain `docker build .` with no build arguments must succeed;
a Dockerfile that refuses an empty build argument drops that refusal and
keeps the argument.
- Every repo should have a Gitea Actions workflow (`.gitea/workflows/`) that - Every repo should have a Gitea Actions workflow (`.gitea/workflows/`) that
runs `script/cibuild` on push, and checks out the repo as its only other step. runs `script/cibuild` on push, and checks out the repo as its only other step.
@@ -389,13 +382,12 @@ style conventions are in separate documents:
directory, so a repo running agents in subdirectories still ships directory, so a repo running agents in subdirectories still ships
`services/api/.claude/` and must add its own anchored entry there. `services/api/.claude/` and must add its own anchored entry there.
- **A plain `docker build .` of a clone stamps the version that - **A plain `docker build .` of a clone stamps the tag or short commit**,
`git describe --tags --always` gives**, derived from the `.git` in the build derived from the `.git` in the build context as the canonical `Dockerfile`
context as the canonical `Dockerfile` above shows. Without its failure check, above shows. Without its failure check, a missing `git` or an unreadable
a missing `git` or an unreadable checkout would leave `-X main.Version=` empty checkout would leave `-X main.Version=` empty and the build would still
and the build would still exit 0. `script/docker` and `script/cibuild` pass exit 0. `script/docker` and `script/cibuild` pass the version they compute on
the version they compute on the host; it takes precedence. They do this the host; it takes precedence. They do this byte-identically across repos:
byte-identically across repos:
```sh ```sh
# Own line: a failing command substitution inside an argument does not # Own line: a failing command substitution inside an argument does not