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
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:
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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"]
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user