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
10 changed files with 73 additions and 56 deletions
+2 -3
View File
@@ -13,9 +13,8 @@
# `/myapp`, never `**/myapp`, which also matches `cmd/myapp/` and
# deletes the package directory from the context.
# Excluding .git means `git describe` cannot run in any build stage and
# fails quietly there; pass the version in with --build-arg VERSION.
.git
# .git is sent: without a VERSION build argument, the stage that compiles
# takes the version from `git describe --tags --always` on it.
# Agent scratch: one full checkout of the repo per in-flight agent.
# Anchored because it occurs once where agents run at the repo root.
+4 -3
View File
@@ -52,7 +52,8 @@ RUN script/bootstrap
COPY . .
# The version is computed on the host and passed in, because
# .dockerignore excludes .git.
ARG VERSION=dev
# Nothing here is compiled and a LABEL cannot run git, so the version is
# the VERSION build argument that script/docker and script/cibuild pass;
# a plain `docker build .` leaves it empty.
ARG VERSION
LABEL org.opencontainers.image.version="${VERSION}"
+1 -2
View File
@@ -132,8 +132,7 @@ alpine. We provide:
`script/check`, compute `version` from `git describe`, then
`docker build --no-cache --build-arg VERSION="$version" -t prompts .` (what CI
runs; it bootstraps because CI checks out and runs this alone while
`script/fmt-check` is native, and the version is computed on the host because
`.dockerignore` excludes `.git`)
`script/fmt-check` is native)
- `script/precommit` — run by the git pre-commit hook (our own extension); calls
`script/check`
- `script/install-precommit` — installs the git pre-commit hook (our own
+5 -8
View File
@@ -1,6 +1,6 @@
---
title: Code Styleguide — Go
last_modified: 2026-09-08
last_modified: 2026-10-02
---
1. Try to hard wrap long lines at 77 characters or less.
@@ -48,13 +48,10 @@ last_modified: 2026-09-08
```
```make
# ?= rather than := because this `$(shell git describe ...)` is only
# correct on the host: `.dockerignore` excludes `.git`, so evaluated
# inside a build stage it expands to the empty string without failing
# and the binary reports no version. The version is computed on the
# host by `script/docker` / `script/cibuild` and passed with
# `--build-arg VERSION=...`; where a build stage invokes make,
# `ARG VERSION` puts it in the environment and `?=` defers to it.
# ?= rather than := so that a `VERSION` build argument takes precedence:
# where a build stage invokes make, `ARG VERSION` puts it in the
# environment and `?=` defers to it. Otherwise `git describe` runs, in a
# build stage on the `.git` the build context carries.
VERSION ?= $(shell git describe --always --dirty)
GOLDFLAGS += -X main.Version=$(VERSION)
+11 -5
View File
@@ -1,6 +1,6 @@
---
title: Existing Repo Checklist
last_modified: 2026-09-08
last_modified: 2026-10-02
---
Use this checklist when beginning work in a repo that may not yet conform to our
@@ -44,7 +44,7 @@ with your task.
`script/test` — those are themselves a `docker build` and would recurse
inside a build step
- [ ] Every depth-independent pattern in `.dockerignore` carries a `**/` prefix,
only genuinely root-anchored entries such as `.git` are unprefixed, and
only genuinely root-anchored entries such as `.claude` are unprefixed, and
`.gitignore`'s patterns have not been transplanted unmodified — the
transplanted form leaves `config/.env` and `certs/server.key` in the build
context while reading as solved
@@ -58,9 +58,15 @@ with your task.
can copy another session's unreviewed work into an image layer. If agents
here run anywhere other than the repo root, the anchored entry misses
`services/api/.claude/`: add anchored entries for those directories.
- [ ] If the repo embeds a version in a binary, that version is computed on the
host and passed with `--build-arg VERSION=...` by `script/docker` and
`script/cibuild`, and no stage calls `git describe`. A tag-derived version
- [ ] If the repo embeds a version in a binary: `.dockerignore` lets `.git` into
the build context. The stage that compiles has `git` (the Debian Go image
has it; an alpine one needs `apk add --no-cache git`) and takes the
version from the `VERSION` build argument when one is given, otherwise
from `git describe --tags --always` (a tag when the commit has one,
otherwise the short commit). `ARG VERSION` has no default, and the build
fails if the context carries `.git` and the version still comes out empty,
`dev` or `unknown`. `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
+5 -8
View File
@@ -1,6 +1,6 @@
---
title: Go HTTP Server Conventions
last_modified: 2026-09-08
last_modified: 2026-10-02
---
This document defines the architectural patterns, design decisions, and
@@ -984,13 +984,10 @@ func main() {
Use ldflags to inject version information at build time:
```makefile
# ?= rather than := because this `$(shell git describe ...)` is only correct
# on the host: `.dockerignore` excludes `.git`, so evaluated inside a build
# stage it expands to the empty string without failing and the binary reports
# no version. The version is computed on the host by `script/docker` /
# `script/cibuild` and passed with `--build-arg VERSION=...`; where the build
# stage invokes make, `ARG VERSION` puts it in the environment and `?=` defers
# to it.
# ?= rather than := so that a `VERSION` build argument takes precedence:
# where a build stage invokes make, `ARG VERSION` puts it in the
# environment and `?=` defers to it. Otherwise `git describe` runs, in a
# build stage on the `.git` the build context carries.
VERSION ?= $(shell git describe --tags --always)
build:
+9 -6
View File
@@ -1,6 +1,6 @@
---
title: New Repo Checklist
last_modified: 2026-09-08
last_modified: 2026-10-02
---
Use this checklist when creating a new repository from scratch. Follow the steps
@@ -67,11 +67,14 @@ Template files can be fetched from:
note that it only covers agents running at the repo root — if this repo
will run them in subdirectories, `services/api/.claude/` needs its own
anchored entry.
- If the image embeds a version in a binary, the version is computed on the
host and passed with `--build-arg VERSION=...`, and `ARG VERSION=dev` is
declared in the stage that compiles. **No stage calls `git describe`** —
`.dockerignore` excludes `.git`, so it yields an empty version without
failing the build.
- If the image embeds a version in a binary: `.dockerignore` lets `.git`
into the build context. The stage that compiles has `git` (the Debian Go
image has it; an alpine one needs `apk add --no-cache git`) and takes the
version from the `VERSION` build argument when one is given, otherwise
from `git describe --tags --always` (a tag when the commit has one,
otherwise the short commit). `ARG VERSION` has no default, and the build
fails if the context carries `.git` and the version still comes out empty,
`dev` or `unknown`.
- The Dockerfile carries a `lint` phase and a `test` phase, each invoking
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
+30 -13
View File
@@ -1,6 +1,6 @@
---
title: Repository Policies
last_modified: 2026-09-08
last_modified: 2026-10-02
---
This document covers repository structure, tooling, and workflow standards. Code
@@ -191,13 +191,25 @@ style conventions are in separate documents:
FROM golang@sha256:... AS builder
COPY --from=lint /src/go.sum /dev/null
COPY --from=test /src/go.sum /dev/null
RUN apk add --no-cache git
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
ARG VERSION=dev
RUN CGO_ENABLED=0 go build -trimpath \
# The VERSION build arg when one is given, otherwise the tag or short
# commit from the .git in the build context. With .git present, a
# version that is still empty, dev or unknown fails the build: git is
# missing or could not read the checkout.
ARG VERSION
RUN VERSION="${VERSION:-$(git describe --tags --always)}"; \
if [ -d .git ]; then \
case "$VERSION" in ""|dev|unknown) \
echo "version is '$VERSION' although .git is present" >&2; \
exit 1 ;; \
esac; \
fi; \
CGO_ENABLED=0 go build -trimpath \
-ldflags="-s -w -X main.Version=${VERSION}" \
-o /app ./cmd/app/
@@ -223,8 +235,13 @@ style conventions are in separate documents:
`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.
`vips-dev`), install them in the lint phase with `apk add`.
- `ARG VERSION=dev` is declared in the stage that compiles and supplied by
`script/docker` and `script/cibuild`; no stage may call `git describe`.
- `.dockerignore` lets `.git` into the build context. The stage that
compiles has `git` (the Debian Go image has it; an alpine one needs
`apk add --no-cache git`) and takes the version from the `VERSION` build
argument when one is given, otherwise from `git describe --tags --always`
(a tag when the commit has one, otherwise the short commit). `ARG VERSION`
has no default, and the build fails if the context carries `.git` and the
version still comes out empty, `dev` or `unknown`.
- 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.
@@ -340,7 +357,7 @@ style conventions are in separate documents:
— which is more dangerous than a short file with no secret patterns at all,
because it reads as solved and stops anyone looking. Give every
depth-independent pattern the `**/` prefix and leave only genuinely
root-anchored entries unprefixed: `.git`, and the repo's own host-built
root-anchored entries unprefixed: `.claude`, and the repo's own host-built
binary, written `/myapp` and never `**/myapp`, which would also match
`cmd/myapp/` and delete the package directory from the context. Matching is
case-sensitive, and an ALL-CAPS twin per pattern still misses `Server.Key`, so
@@ -365,12 +382,12 @@ style conventions are in separate documents:
directory, so a repo running agents in subdirectories still ships
`services/api/.claude/` and must add its own anchored entry there.
- **Excluding `.git` means `git describe` cannot run inside any build stage, and
it fails quietly there.** In a build stage there is no repository, so
`git describe` writes nothing to stdout, `-X main.Version=` comes out empty,
the binary reports no version at all, and the build still exits 0. Compute the
version on the host and thread it in as a build arg. `script/docker` and
`script/cibuild` do this, byte-identically across repos:
- **A plain `docker build .` of a clone stamps the tag or short commit**,
derived from the `.git` in the build context as the canonical `Dockerfile`
above shows. Without its failure check, a missing `git` or an unreadable
checkout would leave `-X main.Version=` empty and the build would still
exit 0. `script/docker` and `script/cibuild` pass the version they compute on
the host; it takes precedence. They do this byte-identically across repos:
```sh
# Own line: a failing command substitution inside an argument does not
@@ -387,7 +404,7 @@ style conventions are in separate documents:
fallback is applied — a live check that fires on a build from an export with
no `.git` and on a repository with no commits yet. Do not fold it into the
substitution as `|| echo unknown`, which makes the guard unreachable. The
Dockerfile's side is `ARG VERSION=dev` in the stage that compiles, declared
Dockerfile's side is `ARG VERSION` in the stage that compiles, declared
there because `ARG` is stage-scoped; passing `VERSION` to a repo whose
Dockerfile declares no such `ARG` is ignored and costs nothing, which is why
the scripts stay byte-identical. One consequence for CI: the standard
+2 -3
View File
@@ -16,9 +16,8 @@ main() {
"$SCRIPT_DIR/check"
# Own line: a failing command substitution inside an argument does
# not trip `set -e`, so the inline form degrades silently to an
# empty constant. VERSION is computed here because .dockerignore
# excludes .git, so `git describe` in a build stage yields an empty
# version without failing.
# empty constant. The VERSION build argument takes precedence over
# the version a build stage derives from the .git in the context.
version="$(git describe --tags --always --dirty 2>/dev/null || true)"
[ -n "$version" ] || version="unknown"
docker build --no-cache \
+2 -3
View File
@@ -12,9 +12,8 @@ main() {
cd "$ROOT"
# Own line: a failing command substitution inside an argument does
# not trip `set -e`, so the inline form degrades silently to an
# empty constant. VERSION is computed here because .dockerignore
# excludes .git, so `git describe` in a build stage yields an empty
# version without failing.
# empty constant. The VERSION build argument takes precedence over
# the version a build stage derives from the .git in the context.
version="$(git describe --tags --always --dirty 2>/dev/null || true)"
[ -n "$version" ] || version="unknown"
docker build --no-cache \