1 Commits
Author SHA1 Message Date
clawbot 3926fff07e Derive the image version from git; send .git without its config (closes #69, closes #71)
check / check (push) Successful in 33s
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 but keeps out .git/config,
which can hold a credential. The Dockerfile example installs git, takes
the VERSION build argument when one is given and otherwise
git describe --tags --always, and fails when .git exists but the version
is empty, dev or unknown. The policy and both checklists state the rule
in the same words, including that a plain docker build . with no build
arguments must succeed.

Model: opus-5-5
2026-10-02 02:24:08 +00:00
11 changed files with 108 additions and 59 deletions
+5 -3
View File
@@ -13,9 +13,11 @@
# `/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.
# Excluding .git means `git describe` cannot run in any build stage and # .git is sent without its config. Without a VERSION build argument the
# fails quietly there; pass the version in with --build-arg VERSION. # stage that compiles runs `git describe --tags --always` on .git, which
.git # 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.
+4 -3
View File
@@ -52,7 +52,8 @@ RUN script/bootstrap
COPY . . COPY . .
# The version is computed on the host and passed in, because # Nothing here is compiled and a LABEL cannot run git, so the version is
# .dockerignore excludes .git. # the VERSION build argument that script/docker and script/cibuild pass;
ARG VERSION=dev # a plain `docker build .` leaves it empty.
ARG VERSION
LABEL org.opencontainers.image.version="${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 `script/check`, compute `version` from `git describe`, then
`docker build --no-cache --build-arg VERSION="$version" -t prompts .` (what CI `docker build --no-cache --build-arg VERSION="$version" -t prompts .` (what CI
runs; it bootstraps because CI checks out and runs this alone while 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 `script/fmt-check` is native)
`.dockerignore` excludes `.git`)
- `script/precommit` — run by the git pre-commit hook (our own extension); calls - `script/precommit` — run by the git pre-commit hook (our own extension); calls
`script/check` `script/check`
- `script/install-precommit` — installs the git pre-commit hook (our own - `script/install-precommit` — installs the git pre-commit hook (our own
+9
View File
@@ -21,6 +21,15 @@ fmt-check, and commit.
# Completed Steps # Completed Steps
- 2026-10-02: The image version now comes from git inside the build (issues 69
and 71), superseding the 2026-09-08 entry that excluded `.git`. The canonical
`.dockerignore` sends `.git` but keeps out `.git/config`, which can hold a
credential. 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` exists but the version
is empty, `dev` or `unknown`; a plain `docker build .` with no build arguments
must succeed. This repo's `script/docker` and `script/cibuild` still pass
`--build-arg VERSION`, since its own `Dockerfile` compiles nothing.
- 2026-09-08: Moved linting and testing into Docker as phases of the main - 2026-09-08: Moved linting and testing into Docker as phases of the main
`Dockerfile`, per the owner ruling on issue 40. `script/lint` and `Dockerfile`, per the owner ruling on issue 40. `script/lint` and
`script/test` build one phase each by name with `--no-cache` — the same answer `script/test` build one phase each by name with `--no-cache` — the same answer
+6 -9
View File
@@ -1,6 +1,6 @@
--- ---
title: Code Styleguide — Go 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. 1. Try to hard wrap long lines at 77 characters or less.
@@ -48,14 +48,11 @@ last_modified: 2026-09-08
``` ```
```make ```make
# ?= rather than := because this `$(shell git describe ...)` is only # ?= rather than := so that a `VERSION` build argument takes precedence:
# correct on the host: `.dockerignore` excludes `.git`, so evaluated # where a build stage invokes make, `ARG VERSION` puts it in the
# inside a build stage it expands to the empty string without failing # environment and `?=` defers to it. Otherwise `git describe` runs, in a
# and the binary reports no version. The version is computed on the # build stage on the `.git` the build context carries.
# host by `script/docker` / `script/cibuild` and passed with VERSION ?= $(shell git describe --tags --always)
# `--build-arg VERSION=...`; where a build stage invokes make,
# `ARG VERSION` puts it in the environment and `?=` defers to it.
VERSION ?= $(shell git describe --always --dirty)
GOLDFLAGS += -X main.Version=$(VERSION) GOLDFLAGS += -X main.Version=$(VERSION)
+19 -7
View File
@@ -1,6 +1,6 @@
--- ---
title: Existing Repo Checklist 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 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 `script/test` — those are themselves a `docker build` and would recurse
inside a build step inside a build step
- [ ] Every depth-independent pattern in `.dockerignore` carries a `**/` prefix, - [ ] 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 `.gitignore`'s patterns have not been transplanted unmodified — the
transplanted form leaves `config/.env` and `certs/server.key` in the build transplanted form leaves `config/.env` and `certs/server.key` in the build
context while reading as solved context while reading as solved
@@ -58,11 +58,23 @@ with your task.
can copy another session's unreviewed work into an image layer. If agents 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 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, that version is computed on the - [ ] If the repo embeds a version in a binary: `.dockerignore` lets `.git` into
host and passed with `--build-arg VERSION=...` by `script/docker` and the build context. It keeps out `.git/config`, which `git describe` does
`script/cibuild`, and no stage calls `git describe`. A tag-derived version not need and which can hold a credential: a password in a remote URL, or
additionally needs `fetch-depth: 0` on the CI checkout step, which clones the token the CI checkout step stores there. The stage that compiles has
shallow and fetches no tags by default. `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`.
That gives the tag on a tagged commit; on 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. `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`
+5 -8
View File
@@ -1,6 +1,6 @@
--- ---
title: Go HTTP Server Conventions title: Go HTTP Server Conventions
last_modified: 2026-09-08 last_modified: 2026-10-02
--- ---
This document defines the architectural patterns, design decisions, and This document defines the architectural patterns, design decisions, and
@@ -984,13 +984,10 @@ func main() {
Use ldflags to inject version information at build time: Use ldflags to inject version information at build time:
```makefile ```makefile
# ?= rather than := because this `$(shell git describe ...)` is only correct # ?= rather than := so that a `VERSION` build argument takes precedence:
# on the host: `.dockerignore` excludes `.git`, so evaluated inside a build # where a build stage invokes make, `ARG VERSION` puts it in the
# stage it expands to the empty string without failing and the binary reports # environment and `?=` defers to it. Otherwise `git describe` runs, in a
# no version. The version is computed on the host by `script/docker` / # build stage on the `.git` the build context carries.
# `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.
VERSION ?= $(shell git describe --tags --always) VERSION ?= $(shell git describe --tags --always)
build: build:
+15 -6
View File
@@ -1,6 +1,6 @@
--- ---
title: New Repo Checklist 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 Use this checklist when creating a new repository from scratch. Follow the steps
@@ -67,11 +67,20 @@ Template files can be fetched from:
note that it only covers agents running at the repo root — if this repo 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 will run them in subdirectories, `services/api/.claude/` needs its own
anchored entry. anchored entry.
- If the image embeds a version in a binary, the version is computed on the - If the image embeds a version in a binary: `.dockerignore` lets `.git`
host and passed with `--build-arg VERSION=...`, and `ARG VERSION=dev` is into the build context. It keeps out `.git/config`, which `git describe`
declared in the stage that compiles. **No stage calls `git describe`** — does not need and which can hold a credential: a password in a remote URL,
`.dockerignore` excludes `.git`, so it yields an empty version without or the token the CI checkout step stores there. The stage that compiles
failing the build. 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`.
That gives the tag on a tagged commit; on 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.
- 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
+38 -13
View File
@@ -1,6 +1,6 @@
--- ---
title: Repository Policies title: Repository Policies
last_modified: 2026-09-08 last_modified: 2026-10-02
--- ---
This document covers repository structure, tooling, and workflow standards. Code 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 FROM golang@sha256:... AS builder
COPY --from=lint /src/go.sum /dev/null COPY --from=lint /src/go.sum /dev/null
COPY --from=test /src/go.sum /dev/null COPY --from=test /src/go.sum /dev/null
RUN apk add --no-cache git
WORKDIR /src WORKDIR /src
COPY go.mod go.sum ./ COPY go.mod go.sum ./
RUN go mod download RUN go mod download
COPY . . COPY . .
ARG VERSION=dev # The VERSION build arg when one is given, otherwise
RUN CGO_ENABLED=0 go build -trimpath \ # `git describe --tags --always` on 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 [ -e .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}" \ -ldflags="-s -w -X main.Version=${VERSION}" \
-o /app ./cmd/app/ -o /app ./cmd/app/
@@ -223,8 +235,20 @@ 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`.
- `ARG VERSION=dev` is declared in the stage that compiles and supplied by - `.dockerignore` lets `.git` into the build context. It keeps out
`script/docker` and `script/cibuild`; no stage may call `git describe`. `.git/config`, which `git describe` does not need and which can hold a
credential: a password in a remote URL, or the token the CI checkout step
stores there. 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`. That gives the tag on a tagged commit; on
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.
@@ -340,7 +364,7 @@ style conventions are in separate documents:
— which is more dangerous than a short file with no secret patterns at all, — 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 because it reads as solved and stops anyone looking. Give every
depth-independent pattern the `**/` prefix and leave only genuinely 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 binary, written `/myapp` and never `**/myapp`, which would also match
`cmd/myapp/` and delete the package directory from the context. Matching is `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 case-sensitive, and an ALL-CAPS twin per pattern still misses `Server.Key`, so
@@ -365,12 +389,13 @@ 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.
- **Excluding `.git` means `git describe` cannot run inside any build stage, and - **A plain `docker build .` of a clone stamps the version that
it fails quietly there.** In a build stage there is no repository, so `git describe --tags --always` gives**, derived from the `.git` in the build
`git describe` writes nothing to stdout, `-X main.Version=` comes out empty, context as the canonical `Dockerfile` above shows. Without its failure check,
the binary reports no version at all, and the build still exits 0. Compute the a missing `git` or an unreadable checkout would leave `-X main.Version=` empty
version on the host and thread it in as a build arg. `script/docker` and and the build would still exit 0. `script/docker` and `script/cibuild` pass
`script/cibuild` do this, byte-identically across repos: the version they compute on the host; it takes precedence. They do this
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
@@ -387,7 +412,7 @@ style conventions are in separate documents:
fallback is applied — a live check that fires on a build from an export with 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 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 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 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 Dockerfile declares no such `ARG` is ignored and costs nothing, which is why
the scripts stay byte-identical. One consequence for CI: the standard the scripts stay byte-identical. One consequence for CI: the standard
+2 -3
View File
@@ -16,9 +16,8 @@ main() {
"$SCRIPT_DIR/check" "$SCRIPT_DIR/check"
# Own line: a failing command substitution inside an argument does # Own line: a failing command substitution inside an argument does
# not trip `set -e`, so the inline form degrades silently to an # not trip `set -e`, so the inline form degrades silently to an
# empty constant. VERSION is computed here because .dockerignore # empty constant. The VERSION build argument takes precedence over
# excludes .git, so `git describe` in a build stage yields an empty # the version a build stage derives from the .git in the context.
# version without failing.
version="$(git describe --tags --always --dirty 2>/dev/null || true)" version="$(git describe --tags --always --dirty 2>/dev/null || true)"
[ -n "$version" ] || version="unknown" [ -n "$version" ] || version="unknown"
docker build --no-cache \ docker build --no-cache \
+2 -3
View File
@@ -12,9 +12,8 @@ main() {
cd "$ROOT" cd "$ROOT"
# Own line: a failing command substitution inside an argument does # Own line: a failing command substitution inside an argument does
# not trip `set -e`, so the inline form degrades silently to an # not trip `set -e`, so the inline form degrades silently to an
# empty constant. VERSION is computed here because .dockerignore # empty constant. The VERSION build argument takes precedence over
# excludes .git, so `git describe` in a build stage yields an empty # the version a build stage derives from the .git in the context.
# version without failing.
version="$(git describe --tags --always --dirty 2>/dev/null || true)" version="$(git describe --tags --always --dirty 2>/dev/null || true)"
[ -n "$version" ] || version="unknown" [ -n "$version" ] || version="unknown"
docker build --no-cache \ docker build --no-cache \