diff --git a/.dockerignore b/.dockerignore index fc4fd74..968019b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -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. diff --git a/Dockerfile b/Dockerfile index da25dee..e37890c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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}" diff --git a/README.md b/README.md index 68dca9c..370a06e 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/prompts/CODE_STYLEGUIDE_GO.md b/prompts/CODE_STYLEGUIDE_GO.md index d91a35c..2b409cf 100644 --- a/prompts/CODE_STYLEGUIDE_GO.md +++ b/prompts/CODE_STYLEGUIDE_GO.md @@ -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) diff --git a/prompts/EXISTING_REPO_CHECKLIST.md b/prompts/EXISTING_REPO_CHECKLIST.md index 70b5f58..a8bc4e2 100644 --- a/prompts/EXISTING_REPO_CHECKLIST.md +++ b/prompts/EXISTING_REPO_CHECKLIST.md @@ -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 diff --git a/prompts/GO_HTTP_SERVER_CONVENTIONS.md b/prompts/GO_HTTP_SERVER_CONVENTIONS.md index 72d528c..0aa35b5 100644 --- a/prompts/GO_HTTP_SERVER_CONVENTIONS.md +++ b/prompts/GO_HTTP_SERVER_CONVENTIONS.md @@ -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: diff --git a/prompts/NEW_REPO_CHECKLIST.md b/prompts/NEW_REPO_CHECKLIST.md index 4789182..59476e0 100644 --- a/prompts/NEW_REPO_CHECKLIST.md +++ b/prompts/NEW_REPO_CHECKLIST.md @@ -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 diff --git a/prompts/REPO_POLICIES.md b/prompts/REPO_POLICIES.md index 2256291..c14aabd 100644 --- a/prompts/REPO_POLICIES.md +++ b/prompts/REPO_POLICIES.md @@ -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,15 +191,27 @@ 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 \ - -ldflags="-s -w -X main.Version=${VERSION}" \ - -o /app ./cmd/app/ + # 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/ # Runtime stage, and the last one FROM alpine@sha256:... @@ -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 diff --git a/script/cibuild b/script/cibuild index 688299f..d8d3200 100755 --- a/script/cibuild +++ b/script/cibuild @@ -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 \ diff --git a/script/docker b/script/docker index c4688e8..07b626c 100755 --- a/script/docker +++ b/script/docker @@ -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 \