From 62b56a20245bf33e3263d0dccf613aa61d741e88 Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Mon, 9 Nov 2026 06:05:00 +0000 Subject: [PATCH] REPO_POLICIES: move the example Dockerfiles into examples/ Each fenced dockerfile block moves byte for byte into its own file under examples/, and one sentence links it where the block stood. Phrases that pointed at a block by position now name the file. Both checklists name the files. Rules keep their wording. As https://git.eeqj.de/sneak/prompts/pulls/159#issuecomment-171711 says. Model: opus-5-5 --- examples/go-lint-system-libraries.Dockerfile | 8 + examples/go.Dockerfile | 85 ++++++++++ examples/python-test-phase.Dockerfile | 24 +++ prompts/EXISTING_REPO_CHECKLIST.md | 8 +- prompts/NEW_REPO_CHECKLIST.md | 8 +- prompts/REPO_POLICIES.md | 159 +++---------------- 6 files changed, 146 insertions(+), 146 deletions(-) create mode 100644 examples/go-lint-system-libraries.Dockerfile create mode 100644 examples/go.Dockerfile create mode 100644 examples/python-test-phase.Dockerfile diff --git a/examples/go-lint-system-libraries.Dockerfile b/examples/go-lint-system-libraries.Dockerfile new file mode 100644 index 0000000..49699d7 --- /dev/null +++ b/examples/go-lint-system-libraries.Dockerfile @@ -0,0 +1,8 @@ +FROM golangci/golangci-lint@sha256:... AS lint +RUN apt-get update \ + && apt-get install -y --no-install-recommends libvips-dev \ + && rm -rf /var/lib/apt/lists/* +WORKDIR /src +COPY go.mod go.sum ./ +RUN go mod download +ARG CACHEBUST diff --git a/examples/go.Dockerfile b/examples/go.Dockerfile new file mode 100644 index 0000000..2128902 --- /dev/null +++ b/examples/go.Dockerfile @@ -0,0 +1,85 @@ +# In every stage ARG CACHEBUST comes right after the steps that install +# system packages and download Go's modules, or right after FROM in a +# stage with neither, and every step that lints, tests or compiles +# comes after it. script/ passes a new value on every build, which +# reruns every RUN step after it. Docker keeps the layers above it +# until the base image, their own lines or go.mod and go.sum change. +# The steps that compile mount Go's build cache, and golangci-lint run +# also mounts golangci-lint's; both survive from build to build. + +# Lint phase +# golangci/golangci-lint:v2.x.x, YYYY-MM-DD +FROM golangci/golangci-lint@sha256:... AS lint +WORKDIR /src +COPY go.mod go.sum ./ +RUN go mod download +ARG CACHEBUST +COPY . . +RUN --mount=type=cache,target=/root/.cache/go-build \ + --mount=type=cache,target=/root/.cache/golangci-lint \ + golangci-lint run --config .golangci.yml ./... + +# Test phase. -race needs cgo and so a C compiler, which the Debian Go +# image ships and the alpine one does not. -count=1 because the mounted +# cache also holds test results, which go test would otherwise replay. +# A first run that timed out gets no rerun, because a rerun would only +# wait for the same hang. +# golang:1.x, YYYY-MM-DD +FROM golang@sha256:... AS test +WORKDIR /src +COPY go.mod go.sum ./ +RUN go mod download +ARG CACHEBUST +COPY . . +RUN --mount=type=cache,target=/root/.cache/go-build \ + status=0; \ + output=$(go test -count=1 -timeout 90s -race -cover ./... 2>&1) || status=$?; \ + printf '%s\n' "$output"; \ + test "$status" -eq 0 && exit 0; \ + case "$output" in \ + *"panic: test timed out"*|*"*** Test killed"*) exit 1 ;; \ + esac; \ + echo "--- Rerunning with -v for details ---"; \ + go test -count=1 -timeout 90s -race -v ./...; \ + exit 1 + +# Build stage. Nothing is wanted from either phase above; the copies +# are what make BuildKit build them first, so nothing after them can +# run unless lint and test passed. +# golang:1.x-alpine, YYYY-MM-DD +FROM golang@sha256:... AS builder +# git comes from the base image's own distribution package index, which +# is trusted, so it needs no version and no hash. +RUN apk add --no-cache git +WORKDIR /src +COPY go.mod go.sum ./ +RUN go mod download +ARG CACHEBUST +COPY --from=lint /src/go.sum /dev/null +COPY --from=test /src/go.sum /dev/null +# A tar-stream context keeps the sender's file owners, which git refuses. +RUN git config --system --add safe.directory /src +COPY . . + +# 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 --mount=type=cache,target=/root/.cache/go-build \ + 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:... +ARG CACHEBUST +COPY --from=builder /app /usr/local/bin/app +ENTRYPOINT ["app"] diff --git a/examples/python-test-phase.Dockerfile b/examples/python-test-phase.Dockerfile new file mode 100644 index 0000000..e1209bb --- /dev/null +++ b/examples/python-test-phase.Dockerfile @@ -0,0 +1,24 @@ +# Test phase. Both pytest runs share one 90-second timeout, so a hang +# uses it once and gets no rerun. +# debian:trixie-slim, YYYY-MM-DD +FROM debian@sha256:... AS test +# Python, pip and venv come from the distribution, which is trusted, so +# they need no version and no hash. make and git are base tools that +# script/bootstrap installs when they are missing. Installed here, they +# stay in their own layer when script/ changes. +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates make git \ + python3 python3-venv \ + && rm -rf /var/lib/apt/lists/* +# script/bootstrap installs pipenv in a virtual environment at +# $HOME/.pipenv, and the tests below run that pipenv. +ENV PATH=/root/.pipenv/bin:$PATH +WORKDIR /app +COPY script/ script/ +COPY Pipfile Pipfile.lock ./ +RUN script/bootstrap +ARG CACHEBUST +COPY . . +RUN timeout 90 sh -c 'pipenv run pytest || { \ + echo "--- Rerunning with -v for details ---"; \ + pipenv run pytest -v; exit 1; }' diff --git a/prompts/EXISTING_REPO_CHECKLIST.md b/prompts/EXISTING_REPO_CHECKLIST.md index 31c3a6f..35130c0 100644 --- a/prompts/EXISTING_REPO_CHECKLIST.md +++ b/prompts/EXISTING_REPO_CHECKLIST.md @@ -71,8 +71,8 @@ with your task. also mounts golangci-lint's (`--mount=type=cache,target=/root/.cache/golangci-lint`), and no step mounts anything else; both `go test` runs carry `-count=1`; a first run - that timed out gets no rerun, as in the canonical Go `Dockerfile` in - `REPO_POLICIES.md` + that timed out gets no rerun, as in `examples/go.Dockerfile` in the + `prompts` repo - [ ] Python: a `test` phase that installs its packages from `Pipfile.lock` does it above `ARG CACHEBUST`, with `COPY script/ script/`, `COPY Pipfile Pipfile.lock ./` and `RUN script/bootstrap`. When @@ -92,8 +92,8 @@ with your task. installs no Python, because a step above it already did. `Pipfile` names no `python_version`, because `--deploy` stops when the distribution's Python differs from it. The tests run after `ARG CACHEBUST`, and every other stage - installs its Python packages after it, as in the Python `test` phase example - in `REPO_POLICIES.md` + installs its Python packages after it, as in + `examples/python-test-phase.Dockerfile` in the `prompts` repo - [ ] The gate phases invoke their tools directly, never through `make lint` or `script/test` — those are themselves a `docker build` and would recurse diff --git a/prompts/NEW_REPO_CHECKLIST.md b/prompts/NEW_REPO_CHECKLIST.md index 2bf6859..fafa2b1 100644 --- a/prompts/NEW_REPO_CHECKLIST.md +++ b/prompts/NEW_REPO_CHECKLIST.md @@ -140,8 +140,8 @@ Template files can be fetched from: also mounts golangci-lint's (`--mount=type=cache,target=/root/.cache/golangci-lint`), and no step mounts anything else; both `go test` runs carry `-count=1`; a first run - that timed out gets no rerun, as in the canonical Go `Dockerfile` in - `REPO_POLICIES.md` + that timed out gets no rerun, as in `examples/go.Dockerfile` in the + `prompts` repo - Python: a `test` phase that installs its packages from `Pipfile.lock` does it above `ARG CACHEBUST`, with `COPY script/ script/`, `COPY Pipfile Pipfile.lock ./` and `RUN script/bootstrap`. When @@ -162,8 +162,8 @@ Template files can be fetched from: because a step above it already did. `Pipfile` names no `python_version`, because `--deploy` stops when the distribution's Python differs from it. The tests run after `ARG CACHEBUST`, and every - other stage installs its Python packages after it, as in the Python - `test` phase example in `REPO_POLICIES.md` + other stage installs its Python packages after it, as in + `examples/python-test-phase.Dockerfile` in the `prompts` repo - Server: the final stage builds and runs the application - Non-server: the final stage brings up the dev environment diff --git a/prompts/REPO_POLICIES.md b/prompts/REPO_POLICIES.md index f63d4fc..c86cf26 100644 --- a/prompts/REPO_POLICIES.md +++ b/prompts/REPO_POLICIES.md @@ -198,10 +198,10 @@ package/suite summaries. Failure shows every test case and every assertion. hang. A hung package therefore uses the 90-second backstop once. The command lives in the `test` phase of the `Dockerfile`, since `script/test` -builds that phase. The canonical Go `Dockerfile` and the Python `test` phase -example below carry it. The Makefile form below is the same pattern for any -repo-local invocation. In a Makefile, each `$` of those two commands is written -`$$`. +builds that phase. `examples/go.Dockerfile` and +`examples/python-test-phase.Dockerfile` in the `prompts` repo carry it. The +Makefile form below is the same pattern for any repo-local invocation. In a +Makefile, each `$` of those two commands is written `$$`. ```makefile test: @@ -228,7 +228,9 @@ Use `yarn`, not `npm`. - `script/bootstrap` installs node, yarn and the node packages only when `yarn.lock` is in the tree. A `Dockerfile` stage that needs no yarn copies no `yarn.lock` before it runs `script/bootstrap`, so `script/bootstrap` installs - no node and no yarn there. The Go and Python stages below are such stages. + no node and no yarn there. The stages in the `prompts` repo's + `examples/go.Dockerfile` and `examples/python-test-phase.Dockerfile` are such + stages. - It installs the node packages with `yarn install --immutable`, which fails when a package's `checksum` is wrong, or when `package.json` is out of step with `yarn.lock`. @@ -345,7 +347,7 @@ What each stage installs: - The gate phases and the build stage start from their pinned base images. They install what those images lack in either of these ways: - - inline, as the canonical Go `Dockerfile` below does for `git` + - inline, as the `prompts` repo's `examples/go.Dockerfile` does for `git` - by running `script/bootstrap`, as the `prompts` repo's own `Dockerfile` does for its yarn packages - The development environment stage installs development prerequisites by @@ -391,8 +393,8 @@ step that also installs anything else comes after `ARG CACHEBUST` too. A repository re-vendors the scripts and adds the `ARG` lines in one commit. - The scripts do not pass `--no-cache`. That flag reruns every step too, but it also rebuilds the layers above the line and empties every cache mount. The - canonical Go `Dockerfile` below mounts Go's build cache and golangci-lint's so - that they survive between check builds. + `prompts` repo's `examples/go.Dockerfile` mounts Go's build cache and + golangci-lint's so that they survive between check builds. - A bare `docker build .` is not evidence that anything ran: a sub-second build reporting success is a cache hit, not a result. - Never invalidate by pruning: `docker builder prune` and friends destroy a @@ -409,93 +411,8 @@ so lint failures surface in seconds rather than after a full compile. That image has both Go and the linter, so nothing needs installing. The test phase is based on the Debian Go image. -```dockerfile -# In every stage ARG CACHEBUST comes right after the steps that install -# system packages and download Go's modules, or right after FROM in a -# stage with neither, and every step that lints, tests or compiles -# comes after it. script/ passes a new value on every build, which -# reruns every RUN step after it. Docker keeps the layers above it -# until the base image, their own lines or go.mod and go.sum change. -# The steps that compile mount Go's build cache, and golangci-lint run -# also mounts golangci-lint's; both survive from build to build. - -# Lint phase -# golangci/golangci-lint:v2.x.x, YYYY-MM-DD -FROM golangci/golangci-lint@sha256:... AS lint -WORKDIR /src -COPY go.mod go.sum ./ -RUN go mod download -ARG CACHEBUST -COPY . . -RUN --mount=type=cache,target=/root/.cache/go-build \ - --mount=type=cache,target=/root/.cache/golangci-lint \ - golangci-lint run --config .golangci.yml ./... - -# Test phase. -race needs cgo and so a C compiler, which the Debian Go -# image ships and the alpine one does not. -count=1 because the mounted -# cache also holds test results, which go test would otherwise replay. -# A first run that timed out gets no rerun, because a rerun would only -# wait for the same hang. -# golang:1.x, YYYY-MM-DD -FROM golang@sha256:... AS test -WORKDIR /src -COPY go.mod go.sum ./ -RUN go mod download -ARG CACHEBUST -COPY . . -RUN --mount=type=cache,target=/root/.cache/go-build \ - status=0; \ - output=$(go test -count=1 -timeout 90s -race -cover ./... 2>&1) || status=$?; \ - printf '%s\n' "$output"; \ - test "$status" -eq 0 && exit 0; \ - case "$output" in \ - *"panic: test timed out"*|*"*** Test killed"*) exit 1 ;; \ - esac; \ - echo "--- Rerunning with -v for details ---"; \ - go test -count=1 -timeout 90s -race -v ./...; \ - exit 1 - -# Build stage. Nothing is wanted from either phase above; the copies -# are what make BuildKit build them first, so nothing after them can -# run unless lint and test passed. -# golang:1.x-alpine, YYYY-MM-DD -FROM golang@sha256:... AS builder -# git comes from the base image's own distribution package index, which -# is trusted, so it needs no version and no hash. -RUN apk add --no-cache git -WORKDIR /src -COPY go.mod go.sum ./ -RUN go mod download -ARG CACHEBUST -COPY --from=lint /src/go.sum /dev/null -COPY --from=test /src/go.sum /dev/null -# A tar-stream context keeps the sender's file owners, which git refuses. -RUN git config --system --add safe.directory /src -COPY . . - -# 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 --mount=type=cache,target=/root/.cache/go-build \ - 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:... -ARG CACHEBUST -COPY --from=builder /app /usr/local/bin/app -ENTRYPOINT ["app"] -``` +The canonical Go `Dockerfile` is at +`https://git.eeqj.de/sneak/prompts/raw/branch/main/examples/go.Dockerfile`. Key points: @@ -543,18 +460,9 @@ Key points: where alpine says `vips-dev`), and delete the package lists in the same `RUN`, so the layer does not keep them. The test phase, on the Debian Go image, installs the libraries its tests need the same way. Either install goes right - after `FROM`, above `ARG CACHEBUST`: - - ```dockerfile - FROM golangci/golangci-lint@sha256:... AS lint - RUN apt-get update \ - && apt-get install -y --no-install-recommends libvips-dev \ - && rm -rf /var/lib/apt/lists/* - WORKDIR /src - COPY go.mod go.sum ./ - RUN go mod download - ARG CACHEBUST - ``` + after `FROM`, above `ARG CACHEBUST`. A lint phase that installs system + libraries is at + `https://git.eeqj.de/sneak/prompts/raw/branch/main/examples/go-lint-system-libraries.Dockerfile`. ## Python `test` phase @@ -645,37 +553,12 @@ not package `pip-tools`. Make the first `Pipfile.lock` with `$HOME/.pipenv/bin/pipenv lock`, after the `pip install` line has installed pipenv. -In the example below, `script/bootstrap` finds `make`, `git`, python and venv -already installed: +In `examples/python-test-phase.Dockerfile`, `script/bootstrap` finds `make`, +`git`, python and venv already installed. The Python `test` phase example is at +`https://git.eeqj.de/sneak/prompts/raw/branch/main/examples/python-test-phase.Dockerfile`. -```dockerfile -# Test phase. Both pytest runs share one 90-second timeout, so a hang -# uses it once and gets no rerun. -# debian:trixie-slim, YYYY-MM-DD -FROM debian@sha256:... AS test -# Python, pip and venv come from the distribution, which is trusted, so -# they need no version and no hash. make and git are base tools that -# script/bootstrap installs when they are missing. Installed here, they -# stay in their own layer when script/ changes. -RUN apt-get update \ - && apt-get install -y --no-install-recommends ca-certificates make git \ - python3 python3-venv \ - && rm -rf /var/lib/apt/lists/* -# script/bootstrap installs pipenv in a virtual environment at -# $HOME/.pipenv, and the tests below run that pipenv. -ENV PATH=/root/.pipenv/bin:$PATH -WORKDIR /app -COPY script/ script/ -COPY Pipfile Pipfile.lock ./ -RUN script/bootstrap -ARG CACHEBUST -COPY . . -RUN timeout 90 sh -c 'pipenv run pytest || { \ - echo "--- Rerunning with -v for details ---"; \ - pipenv run pytest -v; exit 1; }' -``` - -Run from a `make test` target on a host, that test command: +Run from a `make test` target on a host, the test command in +`examples/python-test-phase.Dockerfile`: - does not stop on Ctrl-C. It goes on until the tests end or the 90 seconds run out. @@ -693,7 +576,7 @@ Run from a `make test` target on a host, that test command: - the short commit, when no tag is reachable The stage that compiles derives it from the `.git` in the build context, as the -canonical Go `Dockerfile` above shows: +`prompts` repo's `examples/go.Dockerfile` shows: - It takes the version from the `VERSION` build argument when one is given, otherwise from `git describe`. `ARG VERSION` is declared in that stage,