From 2ae9391b26514923fa8da5b9142d556b4d61a44b Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Fri, 2 Oct 2026 01:03:38 +0200 Subject: [PATCH 1/2] Read the architecture at run time, not via a Buildarch ldflag (closes #66) The Go styleguide and the HTTP server conventions no longer pass the build architecture in through the Makefile. The Buildarch variable, globals field and BUILDARCH Makefile lines are removed from every example; the styleguide example prints runtime.GOARCH, and the logger's Identify logs "arch", runtime.GOARCH. The styleguide item gains one sentence saying so. Model: opus-5-5 --- prompts/CODE_STYLEGUIDE_GO.md | 13 +++++------ prompts/GO_HTTP_SERVER_CONVENTIONS.md | 32 ++++++++++----------------- 2 files changed, 17 insertions(+), 28 deletions(-) diff --git a/prompts/CODE_STYLEGUIDE_GO.md b/prompts/CODE_STYLEGUIDE_GO.md index 2869855..d91a35c 100644 --- a/prompts/CODE_STYLEGUIDE_GO.md +++ b/prompts/CODE_STYLEGUIDE_GO.md @@ -24,7 +24,8 @@ last_modified: 2026-09-08 1. Embed the git commit hash into the binary and include it in startup logs and in health check output. This is to make it easier to correlate running instances with their code. Do not include build time or build user, as these - will make the build nondeterministic. + will make the build nondeterministic. The architecture is not passed in at + build time; a program that reports it reads `runtime.GOARCH` at run time. Example relevant Makefile sections: @@ -35,16 +36,14 @@ last_modified: 2026-09-08 import ( "fmt" + "runtime" ) - var ( - Version string - Buildarch string - ) + var Version string func main() { fmt.Printf("Version: %s\n", Version) - fmt.Printf("Buildarch: %s\n", Buildarch) + fmt.Printf("Arch: %s\n", runtime.GOARCH) } ``` @@ -57,10 +56,8 @@ last_modified: 2026-09-08 # `--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) - BUILDARCH := $(shell uname -m) GOLDFLAGS += -X main.Version=$(VERSION) - GOLDFLAGS += -X main.Buildarch=$(BUILDARCH) # osx can't statically link apparently?! ifeq ($(UNAME_S),Darwin) diff --git a/prompts/GO_HTTP_SERVER_CONVENTIONS.md b/prompts/GO_HTTP_SERVER_CONVENTIONS.md index 3ee41d2..72d528c 100644 --- a/prompts/GO_HTTP_SERVER_CONVENTIONS.md +++ b/prompts/GO_HTTP_SERVER_CONVENTIONS.md @@ -118,15 +118,13 @@ import ( ) var ( - Appname string = "CHANGEME" - Version string - Buildarch string + Appname string = "CHANGEME" + Version string ) func main() { globals.Appname = Appname globals.Version = Version - globals.Buildarch = Buildarch fx.New( fx.Provide( @@ -826,7 +824,7 @@ func (l *Logger) Identify() { l.log.Info("starting", "appname", l.params.Globals.Appname, "version", l.params.Globals.Version, - "buildarch", l.params.Globals.Buildarch, + "arch", runtime.GOARCH, ) } ``` @@ -946,23 +944,20 @@ import "go.uber.org/fx" // Package-level variables (set from main) var ( - Appname string - Version string - Buildarch string + Appname string + Version string ) // Struct for DI type Globals struct { - Appname string - Version string - Buildarch string + Appname string + Version string } func New(lc fx.Lifecycle) (*Globals, error) { n := &Globals{ - Appname: Appname, - Buildarch: Buildarch, - Version: Version, + Appname: Appname, + Version: Version, } return n, nil } @@ -973,15 +968,13 @@ func New(lc fx.Lifecycle) (*Globals, error) { ```go // cmd/httpd/main.go var ( - Appname string = "CHANGEME" // Default, overridden by build - Version string // Set at build time - Buildarch string // Set at build time + Appname string = "CHANGEME" // Default, overridden by build + Version string // Set at build time ) func main() { globals.Appname = Appname globals.Version = Version - globals.Buildarch = Buildarch // ... } ``` @@ -999,10 +992,9 @@ Use ldflags to inject version information at build time: # stage invokes make, `ARG VERSION` puts it in the environment and `?=` defers # to it. VERSION ?= $(shell git describe --tags --always) -BUILDARCH := $(shell go env GOARCH) build: - go build -ldflags "-X main.Version=$(VERSION) -X main.Buildarch=$(BUILDARCH)" ./cmd/httpd + go build -ldflags "-X main.Version=$(VERSION)" ./cmd/httpd ``` --- -- 2.54.0 From 507a57e813d2f2d80f970d32f33f5c279a27d263 Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Fri, 2 Oct 2026 04:38:10 +0200 Subject: [PATCH 2/2] Derive the image version from git; send .git without its config (closes #69, closes #71) 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 --- .dockerignore | 8 ++-- Dockerfile | 7 ++-- README.md | 3 +- TODO.md | 9 +++++ prompts/CODE_STYLEGUIDE_GO.md | 15 +++----- prompts/EXISTING_REPO_CHECKLIST.md | 26 +++++++++---- prompts/GO_HTTP_SERVER_CONVENTIONS.md | 13 +++---- prompts/NEW_REPO_CHECKLIST.md | 21 +++++++--- prompts/REPO_POLICIES.md | 55 +++++++++++++++++++-------- script/cibuild | 5 +-- script/docker | 5 +-- 11 files changed, 108 insertions(+), 59 deletions(-) diff --git a/.dockerignore b/.dockerignore index fc4fd74..5c135d9 100644 --- a/.dockerignore +++ b/.dockerignore @@ -13,9 +13,11 @@ # `/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 its config. Without a VERSION build argument the +# stage that compiles runs `git describe --tags --always` on .git, which +# 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. # 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/TODO.md b/TODO.md index 77fc0f9..2a27314 100644 --- a/TODO.md +++ b/TODO.md @@ -21,6 +21,15 @@ fmt-check, and commit. # 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 `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 diff --git a/prompts/CODE_STYLEGUIDE_GO.md b/prompts/CODE_STYLEGUIDE_GO.md index d91a35c..5ce1289 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,14 +48,11 @@ 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. - VERSION ?= $(shell git describe --always --dirty) + # ?= 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) GOLDFLAGS += -X main.Version=$(VERSION) diff --git a/prompts/EXISTING_REPO_CHECKLIST.md b/prompts/EXISTING_REPO_CHECKLIST.md index 70b5f58..369b534 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,11 +58,23 @@ 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 - additionally needs `fetch-depth: 0` on the CI checkout step, which clones - shallow and fetches no tags by default. +- [ ] 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 + 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. `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 push — reference `https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitea/workflows/check.yml` 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..b4d8e5f 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,20 @@ 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. It keeps out `.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. - 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..ca05cb4 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 + # `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}" \ + -o /app ./cmd/app/ # Runtime stage, and the last one FROM alpine@sha256:... @@ -223,8 +235,20 @@ 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. It keeps out + `.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 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, 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 +389,13 @@ 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 version that + `git describe --tags --always` gives**, 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 +412,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 \ -- 2.54.0