Current templates: safe.directory, golangci-lint v2.14.0, fetch-depth 0, the policy's last stage (closes #50) #56

Merged
clawbot merged 1 commits from issue-50-current-templates into next 2026-10-04 08:59:09 +02:00
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
# 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.
# 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/modules/**/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.
+3
View File
@@ -6,4 +6,7 @@ jobs:
steps:
# actions/checkout v4.2.2, 2026-02-22
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
# All history and tags, which `git describe --tags` needs.
fetch-depth: 0
- run: script/cibuild
+1
View File
@@ -17,6 +17,7 @@ linters:
disable:
# Genuinely incompatible with project patterns
- exhaustruct # Requires all struct fields
- exhaustruct_v5 # Requires all struct fields (successor to exhaustruct)
- godot # Requires comments to end with periods
- wrapcheck # Too verbose for internal packages
- 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
# script/test each build one phase alone; a plain `docker build .` builds
# both, because the build stage copies a file from each. Formatting is
# checked on the host by script/fmt-check, not here.
# The lint phase, the test phase and a development environment.
# script/lint and script/test each build one phase alone; a plain
# `docker build .` builds both, because the last stage copies a file from
# each. Formatting is checked on the host by script/fmt-check, not here.
# Lint phase
# golangci/golangci-lint:v2.12.2, 2026-09-07
FROM golangci/golangci-lint@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240 AS lint
# golangci/golangci-lint:v2.14.0, 2026-10-04
FROM golangci/golangci-lint@sha256:ad862ba6b3798cbe0fd9fd7408d498fd74fbd2623a92406b2fd3898faf0bf98f AS lint
WORKDIR /src
@@ -16,13 +16,12 @@ COPY . .
RUN golangci-lint run --config .golangci.yml ./...
# Test phase
# golang:1.26-alpine, 2026-09-07. It carries Go 1.26.8, the version
# script/bootstrap installs on the host; change both together.
FROM golang@sha256:ce864e7223ac17b1775e6fd0b4c0db580c2eb50e7953a427916379e4b92a1628 AS test
# -race needs cgo, and cgo needs a C toolchain.
RUN apk add --no-cache gcc musl-dev
# Test phase. -race needs cgo and so a C compiler, which the Debian Go
# image ships.
# golang:1.26.8-trixie, 2026-10-04. It carries Go 1.26.8, the version
# script/bootstrap installs on the host; change both together, and the
# same image in the last stage.
FROM golang@sha256:eae2aaa6add2936cbf350dd0d2628b363461542f0c4b3c0b558957e0f2997379 AS test
WORKDIR /src
@@ -35,21 +34,27 @@ RUN go test -timeout 90s -race -cover ./... || \
{ echo "--- Rerunning with -v for details ---"; \
go test -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 this stage cannot run
# unless lint and test passed.
# golang:1.26-alpine, 2026-09-07
FROM golang@sha256:ce864e7223ac17b1775e6fd0b4c0db580c2eb50e7953a427916379e4b92a1628 AS builder
# Development environment, and the last stage: a plain `docker build .`
# builds this one. It holds the source tree in /src, what
# script/bootstrap installs, and keyfunc built from that tree on the
# PATH. Nothing is wanted from either phase above; the copies are what
# 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=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
COPY go.mod go.sum ./
RUN go mod download
# script/bootstrap needs only script/ and the dependency manifests.
COPY script/ script/
COPY go.mod go.sum package.json yarn.lock ./
RUN script/bootstrap
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; \
exit 1; \
fi; \
make build VERSION="$version"
# alpine:3.23, 2026-09-07
FROM alpine@sha256:fd791d74b68913cbb027c6546007b3f0d3bc45125f797758156952bc2d6daf40
COPY --from=builder /src/keyfunc /usr/local/bin/keyfunc
ENTRYPOINT ["keyfunc"]
make build VERSION="$version" && mv keyfunc /usr/local/bin/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
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
`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
`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
+55 -36
View File
@@ -1,6 +1,6 @@
---
title: Repository Policies
last_modified: 2026-10-02
last_modified: 2026-10-04
---
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 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,
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
@@ -173,8 +173,9 @@ style conventions are in separate documents:
COPY . .
RUN golangci-lint run --config .golangci.yml ./...
# Test phase
# golang:1.x-alpine, YYYY-MM-DD
# Test phase. -race needs cgo and so a C compiler, which the Debian Go
# image ships and the alpine one does not.
# golang:1.x, YYYY-MM-DD
FROM golang@sha256:... AS test
WORKDIR /src
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=test /src/go.sum /dev/null
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
COPY go.mod go.sum ./
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.
`vips-dev`), install them in the lint phase with `apk add`.
- `.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.
`.git/config` and each submodule's `config` under `.git/modules/` at any
depth (`.git/modules/**/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. The stage that compiles also marks its working directory
safe for git (`git config --system --add safe.directory /src`): a context
sent as a tar stream keeps the sender's file owners, and git refuses a
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
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_
cache, so the target cannot report a pass it did not earn, and the rerun
reproduces a failure instead of replaying it. It leaves the build cache
alone, so it costs the runtime of the suite and no recompilation.
cache, so neither run can report a stored pass in place of running the
tests. It leaves the build cache alone, so it costs the runtime of the suite
and no recompilation.
Note that this is a second, independent cache, stacked below the Docker
layer cache that [issue #26](https://git.eeqj.de/sneak/prompts/issues/26)
addresses. `CHECK_EPOCH` guarantees the `RUN make test` _step_ re-executes;
it does not guarantee `go test` inside that step does any work, because the
`GOCACHE` baked into earlier image layers survives into the re-executed
step. They are two separate defects requiring two separate fixes, and a fix
for one must not be recorded as covering the other.
That cache is Go's own, separate from Docker's layer cache. Go stores a
passing result in its cache directory (`GOCACHE`), and when the same tests
run again on unchanged code it prints that result, marked `(cached)`,
without running them. That matters on a developer's machine, where this
target runs and the directory lasts from one run to the next. The `test`
phase of the `Dockerfile` needs no `-count=1`: its base image holds no
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:
@@ -451,12 +460,18 @@ style conventions are in separate documents:
`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
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
(`golangci/golangci-lint@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240`,
which reports `2.12.2 built with go1.26.2 from c0d3ddc9`). That digest is the
only pin, since no repo installs golangci-lint on the host: bumping the
version means changing it and nothing else.
(`golangci/golangci-lint@sha256:ad862ba6b3798cbe0fd9fd7408d498fd74fbd2623a92406b2fd3898faf0bf98f`,
which reports `2.14.0 built with go1.27.0 from 114493f9`). A module's `go`
directive must not name a newer Go minor version than the one golangci-lint
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
testing presence.** An `if ! command -v <tool>; then install; fi` guard tests
@@ -592,10 +607,10 @@ style conventions are in separate documents:
settings.
- Avoid putting files in the repo root unless necessary. Root should contain
only project-level config files (`README.md`, `Makefile`, `Dockerfile`,
`LICENSE`, `.gitignore`, `.editorconfig`, `REPO_POLICIES.md`, and
language-specific config). Everything else goes in a subdirectory. Canonical
subdirectory names:
only project-level config files (`README.md`, `AGENTS.md`, `Makefile`,
`Dockerfile`, `LICENSE`, `.gitignore`, `.editorconfig`, `REPO_POLICIES.md`,
and language-specific config). Everything else goes in a subdirectory.
Canonical subdirectory names:
- `bin/` — executable scripts and tools
- `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
@@ -626,3 +641,7 @@ style conventions are in separate documents:
- Go: `go.mod`, `go.sum`, `.golangci.yml`
- JS: `package.json`, `yarn.lock`, `.prettierrc`, `.prettierignore`
- 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
}
var passed ssh.StatusError
if errors.As(err, &passed) {
if passed, ok := errors.AsType[ssh.StatusError](err); ok {
return passed.Status
}
+1 -2
View File
@@ -88,8 +88,7 @@ func connect(ctx context.Context, argv []string) error {
return nil
}
var ended *exec.ExitError
if errors.As(err, &ended) {
if ended, ok := errors.AsType[*exec.ExitError](err); ok {
status := ended.ExitCode()
if status < 0 {
// A signal ended ssh, and a signal has no status of its