Current templates: safe.directory, golangci-lint v2.14.0, fetch-depth 0, the policy's last stage (closes #50)
check / check (push) Failing after 3s

REPO_POLICIES.md and .golangci.yml are the sneak/prompts next copies, and
.dockerignore takes the template's .git/modules/**/config line. The lint
phase runs golangci-lint v2.14.0; its two errors.AsType findings are
fixed. The stage that builds keyfunc marks /src safe for git, so a
context sent as a tar stream still yields a version. That stage is now
the last one and a development environment on the Debian Go image: what
script/bootstrap installs, the source in /src, keyfunc on the PATH. The
test phase uses the same image, so one Go image stays matched to
script/bootstrap. The CI checkout fetches all history and tags.

Model: opus-5-5
This commit is contained in:
2026-10-04 06:27:16 +00:00
parent d4fbcbc83d
commit 72cb05b5a8
8 changed files with 95 additions and 70 deletions
+3
View File
@@ -17,7 +17,10 @@
# stage that compiles runs `git describe --tags --always` on .git, which # 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 # 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. # password in a remote URL or the token the CI checkout step stores there.
# Each submodule keeps a config with the same exposure in its git directory
# under .git/modules/, nested again for a submodule's own submodules.
.git/config .git/config
.git/modules/**/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.
+3
View File
@@ -6,4 +6,7 @@ jobs:
steps: steps:
# actions/checkout v4.2.2, 2026-02-22 # actions/checkout v4.2.2, 2026-02-22
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
# All history and tags, which `git describe --tags` needs.
fetch-depth: 0
- run: script/cibuild - run: script/cibuild
+1
View File
@@ -17,6 +17,7 @@ linters:
disable: disable:
# Genuinely incompatible with project patterns # Genuinely incompatible with project patterns
- exhaustruct # Requires all struct fields - exhaustruct # Requires all struct fields
- exhaustruct_v5 # Requires all struct fields (successor to exhaustruct)
- godot # Requires comments to end with periods - godot # Requires comments to end with periods
- wrapcheck # Too verbose for internal packages - wrapcheck # Too verbose for internal packages
- varnamelen # Short names like db, id are idiomatic Go - varnamelen # Short names like db, id are idiomatic Go
+27 -29
View File
@@ -1,11 +1,11 @@
# The lint phase, the test phase and the build. script/lint and # The lint phase, the test phase and a development environment.
# script/test each build one phase alone; a plain `docker build .` builds # script/lint and script/test each build one phase alone; a plain
# both, because the build stage copies a file from each. Formatting is # `docker build .` builds both, because the last stage copies a file from
# checked on the host by script/fmt-check, not here. # each. Formatting is checked on the host by script/fmt-check, not here.
# Lint phase # Lint phase
# golangci/golangci-lint:v2.12.2, 2026-09-07 # golangci/golangci-lint:v2.14.0, 2026-10-04
FROM golangci/golangci-lint@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240 AS lint FROM golangci/golangci-lint@sha256:ad862ba6b3798cbe0fd9fd7408d498fd74fbd2623a92406b2fd3898faf0bf98f AS lint
WORKDIR /src WORKDIR /src
@@ -16,13 +16,12 @@ COPY . .
RUN golangci-lint run --config .golangci.yml ./... RUN golangci-lint run --config .golangci.yml ./...
# Test phase # Test phase. -race needs cgo and so a C compiler, which the Debian Go
# golang:1.26-alpine, 2026-09-07. It carries Go 1.26.8, the version # image ships.
# script/bootstrap installs on the host; change both together. # golang:1.26.8-trixie, 2026-10-04. It carries Go 1.26.8, the version
FROM golang@sha256:ce864e7223ac17b1775e6fd0b4c0db580c2eb50e7953a427916379e4b92a1628 AS test # script/bootstrap installs on the host; change both together, and the
# same image in the last stage.
# -race needs cgo, and cgo needs a C toolchain. FROM golang@sha256:eae2aaa6add2936cbf350dd0d2628b363461542f0c4b3c0b558957e0f2997379 AS test
RUN apk add --no-cache gcc musl-dev
WORKDIR /src WORKDIR /src
@@ -35,21 +34,27 @@ RUN go test -timeout 90s -race -cover ./... || \
{ echo "--- Rerunning with -v for details ---"; \ { echo "--- Rerunning with -v for details ---"; \
go test -timeout 90s -race -v ./...; exit 1; } go test -timeout 90s -race -v ./...; exit 1; }
# Build stage. Nothing is wanted from either phase above; the copies # Development environment, and the last stage: a plain `docker build .`
# are what make BuildKit build them first, so this stage cannot run # builds this one. It holds the source tree in /src, what
# unless lint and test passed. # script/bootstrap installs, and keyfunc built from that tree on the
# golang:1.26-alpine, 2026-09-07 # PATH. Nothing is wanted from either phase above; the copies are what
FROM golang@sha256:ce864e7223ac17b1775e6fd0b4c0db580c2eb50e7953a427916379e4b92a1628 AS builder # make BuildKit build them first, so this stage cannot run unless lint
# and test passed.
# golang:1.26.8-trixie, 2026-10-04
FROM golang@sha256:eae2aaa6add2936cbf350dd0d2628b363461542f0c4b3c0b558957e0f2997379
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 make git # A tar-stream context keeps the sender's file owners, which git refuses.
RUN git config --system --add safe.directory /src
WORKDIR /src WORKDIR /src
COPY go.mod go.sum ./ # script/bootstrap needs only script/ and the dependency manifests.
RUN go mod download COPY script/ script/
COPY go.mod go.sum package.json yarn.lock ./
RUN script/bootstrap
COPY . . COPY . .
@@ -64,11 +69,4 @@ RUN version="${VERSION:-$(git describe --tags --always || echo dev)}"; \
echo "no version could be derived although the build context carries .git" >&2; \ echo "no version could be derived although the build context carries .git" >&2; \
exit 1; \ exit 1; \
fi; \ fi; \
make build VERSION="$version" make build VERSION="$version" && mv keyfunc /usr/local/bin/keyfunc
# alpine:3.23, 2026-09-07
FROM alpine@sha256:fd791d74b68913cbb027c6546007b3f0d3bc45125f797758156952bc2d6daf40
COPY --from=builder /src/keyfunc /usr/local/bin/keyfunc
ENTRYPOINT ["keyfunc"]
+4 -1
View File
@@ -331,7 +331,10 @@ standard: most Makefile targets are thin shims over an executable in `script/`
- `script/docker` builds the Docker image, uncached, tagged with the project - `script/docker` builds the Docker image, uncached, tagged with the project
name and stamped with the version `git describe` gives on the host. The image name and stamped with the version `git describe` gives on the host. The image
cannot be built unless the `lint` and `test` phases pass, so a plain cannot be built unless the `lint` and `test` phases pass, so a plain
`docker build .` runs them too. `docker build .` runs them too. The image is a development environment, not a
runtime image: the Debian Go image with what `script/bootstrap` installs, the
source tree in `/src`, and `keyfunc` built from it on the `PATH`.
`docker run --rm -it keyfunc` opens a shell in it.
- `script/cibuild` is the CI build the Gitea workflow calls: it runs - `script/cibuild` is the CI build the Gitea workflow calls: it runs
`bootstrap`, then `check`, then builds the image as `script/docker` does. `bootstrap`, then `check`, then builds the image as `script/docker` does.
- `script/precommit` is what the git pre-commit hook runs: `go mod tidy` and - `script/precommit` is what the git pre-commit hook runs: `go mod tidy` and
+55 -36
View File
@@ -1,6 +1,6 @@
--- ---
title: Repository Policies title: Repository Policies
last_modified: 2026-10-02 last_modified: 2026-10-04
--- ---
This document covers repository structure, tooling, and workflow standards. Code This document covers repository structure, tooling, and workflow standards. Code
@@ -160,7 +160,7 @@ style conventions are in separate documents:
- **The gate phases are separate stages, and the build stage depends on both.** - **The gate phases are separate stages, and the build stage depends on both.**
The lint phase is based on the `golangci/golangci-lint` image (pinned by The lint phase is based on the `golangci/golangci-lint` image (pinned by
hash), so lint failures surface in seconds rather than after a full compile, hash), so lint failures surface in seconds rather than after a full compile,
and the test phase is based on the Go image. The canonical Go repo and the test phase is based on the Debian Go image. The canonical Go repo
`Dockerfile`: `Dockerfile`:
```dockerfile ```dockerfile
@@ -173,8 +173,9 @@ style conventions are in separate documents:
COPY . . COPY . .
RUN golangci-lint run --config .golangci.yml ./... RUN golangci-lint run --config .golangci.yml ./...
# Test phase # Test phase. -race needs cgo and so a C compiler, which the Debian Go
# golang:1.x-alpine, YYYY-MM-DD # image ships and the alpine one does not.
# golang:1.x, YYYY-MM-DD
FROM golang@sha256:... AS test FROM golang@sha256:... AS test
WORKDIR /src WORKDIR /src
COPY go.mod go.sum ./ COPY go.mod go.sum ./
@@ -192,6 +193,8 @@ style conventions are in separate documents:
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 RUN apk add --no-cache git
# A tar-stream context keeps the sender's file owners, which git refuses.
RUN git config --system --add safe.directory /src
WORKDIR /src WORKDIR /src
COPY go.mod go.sum ./ COPY go.mod go.sum ./
RUN go mod download RUN go mod download
@@ -236,19 +239,23 @@ style conventions are in separate documents:
- 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`.
- `.dockerignore` lets `.git` into the build context. It keeps out - `.dockerignore` lets `.git` into the build context. It keeps out
`.git/config`, which `git describe` does not need and which can hold a `.git/config` and each submodule's `config` under `.git/modules/` at any
credential: a password in a remote URL, or the token the CI checkout step depth (`.git/modules/**/config`), which `git describe` does not need and
stores there. The stage that compiles has `git` (the Debian Go image has which can hold a credential: a password in a remote URL, or the token the
it; an alpine one needs `apk add --no-cache git`) and takes the version CI checkout step stores there. The stage that compiles has `git` (the
from the `VERSION` build argument when one is given, otherwise from Debian Go image has it; an alpine one needs `apk add --no-cache git`) and
`git describe --tags --always`. That gives the tag on a tagged commit; on takes the version from the `VERSION` build argument when one is given,
a later commit, the tag, the number of commits since it and the short otherwise from `git describe --tags --always`. That gives the tag on a
commit (`v1.2.3-4-gabc1234`); and the short commit when no tag is tagged commit; on a later commit, the tag, the number of commits since it
reachable. `ARG VERSION` has no default, and the build fails if the and the short commit (`v1.2.3-4-gabc1234`); and the short commit when no
context carries `.git` and the version still comes out empty, `dev` or tag is reachable. The stage that compiles also marks its working directory
`unknown`. A plain `docker build .` with no build arguments must succeed; safe for git (`git config --system --add safe.directory /src`): a context
a Dockerfile that refuses an empty build argument drops that refusal and sent as a tar stream keeps the sender's file owners, and git refuses a
keeps the argument. checkout owned by another user, so the version would come out empty.
`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.
@@ -310,17 +317,19 @@ style conventions are in separate documents:
``` ```
`-count=1` is required on both invocations: it defeats Go's test _result_ `-count=1` is required on both invocations: it defeats Go's test _result_
cache, so the target cannot report a pass it did not earn, and the rerun cache, so neither run can report a stored pass in place of running the
reproduces a failure instead of replaying it. It leaves the build cache tests. It leaves the build cache alone, so it costs the runtime of the suite
alone, so it costs the runtime of the suite and no recompilation. and no recompilation.
Note that this is a second, independent cache, stacked below the Docker That cache is Go's own, separate from Docker's layer cache. Go stores a
layer cache that [issue #26](https://git.eeqj.de/sneak/prompts/issues/26) passing result in its cache directory (`GOCACHE`), and when the same tests
addresses. `CHECK_EPOCH` guarantees the `RUN make test` _step_ re-executes; run again on unchanged code it prints that result, marked `(cached)`,
it does not guarantee `go test` inside that step does any work, because the without running them. That matters on a developer's machine, where this
`GOCACHE` baked into earlier image layers survives into the re-executed target runs and the directory lasts from one run to the next. The `test`
step. They are two separate defects requiring two separate fixes, and a fix phase of the `Dockerfile` needs no `-count=1`: its base image holds no
for one must not be recorded as covering the other. result for this repo's tests and nothing before its `go test` step runs a
test, so there is nothing to replay. `--no-cache` (above) is what makes that
step run on an unchanged tree.
Python example: Python example:
@@ -451,12 +460,18 @@ style conventions are in separate documents:
`test-support` depguard rule, where a repo names its own test-support packages `test-support` depguard rule, where a repo names its own test-support packages
by full import path. A repo adds entries there and changes nothing else, and a by full import path. A repo adds entries there and changes nothing else, and a
re-vendor carries its entries forward. The canonical golangci-lint version is re-vendor carries its entries forward. The canonical golangci-lint version is
v2.12.2 (released 2026-05-06), pinned as the digest of the lint phase's base v2.14.0 (released 2026-09-24), pinned as the digest of the lint phase's base
image image
(`golangci/golangci-lint@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240`, (`golangci/golangci-lint@sha256:ad862ba6b3798cbe0fd9fd7408d498fd74fbd2623a92406b2fd3898faf0bf98f`,
which reports `2.12.2 built with go1.26.2 from c0d3ddc9`). That digest is the which reports `2.14.0 built with go1.27.0 from 114493f9`). A module's `go`
only pin, since no repo installs golangci-lint on the host: bumping the directive must not name a newer Go minor version than the one golangci-lint
version means changing it and nothing else. was built with, or golangci-lint refuses to lint it: this release lints
`go 1.27.1` but not `go 1.28`. That digest is the only pin, since no repo
installs golangci-lint on the host. A repo sets the lint phase digest to the
one named here and re-vendors `.golangci.yml` in the same commit, whichever of
the two prompted the change: the canonical copy can name linters that an older
golangci-lint rejects, and a newer golangci-lint can add linters that
`default: all` switches on until the canonical copy disables them.
- **`script/bootstrap` installs a pinned tool by comparing versions, never by - **`script/bootstrap` installs a pinned tool by comparing versions, never by
testing presence.** An `if ! command -v <tool>; then install; fi` guard tests testing presence.** An `if ! command -v <tool>; then install; fi` guard tests
@@ -592,10 +607,10 @@ style conventions are in separate documents:
settings. settings.
- Avoid putting files in the repo root unless necessary. Root should contain - Avoid putting files in the repo root unless necessary. Root should contain
only project-level config files (`README.md`, `Makefile`, `Dockerfile`, only project-level config files (`README.md`, `AGENTS.md`, `Makefile`,
`LICENSE`, `.gitignore`, `.editorconfig`, `REPO_POLICIES.md`, and `Dockerfile`, `LICENSE`, `.gitignore`, `.editorconfig`, `REPO_POLICIES.md`,
language-specific config). Everything else goes in a subdirectory. Canonical and language-specific config). Everything else goes in a subdirectory.
subdirectory names: Canonical subdirectory names:
- `bin/` — executable scripts and tools - `bin/` — executable scripts and tools
- `cmd/` — Go command entrypoints; thin only: one `main.go` per binary whose - `cmd/` — Go command entrypoints; thin only: one `main.go` per binary whose
body is a single call into `internal/` or `pkg/`, no project logic in body is a single call into `internal/` or `pkg/`, no project logic in
@@ -626,3 +641,7 @@ style conventions are in separate documents:
- Go: `go.mod`, `go.sum`, `.golangci.yml` - Go: `go.mod`, `go.sum`, `.golangci.yml`
- JS: `package.json`, `yarn.lock`, `.prettierrc`, `.prettierignore` - JS: `package.json`, `yarn.lock`, `.prettierrc`, `.prettierignore`
- Python: `pyproject.toml` - Python: `pyproject.toml`
- Guidance for coding agents lives in one `AGENTS.md` at the repository root. It
is never committed under a file or directory named after one agent tool, such
as `CLAUDE.md` or `.claude/`, and never split into separate memory files.
+1 -2
View File
@@ -86,8 +86,7 @@ func Main() int {
return 0 return 0
} }
var passed ssh.StatusError if passed, ok := errors.AsType[ssh.StatusError](err); ok {
if errors.As(err, &passed) {
return passed.Status return passed.Status
} }
+1 -2
View File
@@ -88,8 +88,7 @@ func connect(ctx context.Context, argv []string) error {
return nil return nil
} }
var ended *exec.ExitError if ended, ok := errors.AsType[*exec.ExitError](err); ok {
if errors.As(err, &ended) {
status := ended.ExitCode() status := ended.ExitCode()
if status < 0 { if status < 0 {
// A signal ended ssh, and a signal has no status of its // A signal ended ssh, and a signal has no status of its