Author SHA1 Message Date
sneak 9733cb7833 Update the canonical .golangci.yml for golangci-lint v2.15.0 (closes #142)
check / check (push) Successful in 25s
Moves the pin to v2.15.0 and enables canonicalheader again, because that
release fixes the random misses behind
#105.

The depguard exemption reached every directory below one ending in test,
and a whole checkout under such a directory. It now covers only the files
directly inside, and the directory a repository is cloned into does not
count. gomodguard_v2 also blocks github.com/go-redis/redis/v9, whose
pre-releases can still be imported.

uniq-by-line is now false, so a line that trips two linters reports both
findings.

Three comments are corrected. A new comment says the file has no
formatters section, so golangci-lint run does not run gofmt.

Model: opus-5-5
2026-11-03 23:49:17 +00:00
clawbot b656617183 Allow packages from the distribution's own package index unpinned (closes #72)
check / check (push) Canceled after 0s
A package from the distribution's own package index now needs no version
and no hash, and pulling that index is allowed, on sneak's answer at
#72 (comment). The
distribution is the pinned base image's in a Dockerfile and the host's
on a host. It is trusted, and everything else stays pinned by hash. A
package source that a Dockerfile adds itself does not get the exception.

The pinning rule of REPO_POLICIES.md and both checklists say so, and the
canonical Go Dockerfile's apk add line has a comment saying why it is
allowed.

Model: opus-5-5
2026-11-04 00:28:23 +01:00
clawbot 229ea67434 Skip the verbose test rerun after a timeout (closes #61)
check / check (push) Canceled after 0s
A hung test used the 90-second backstop twice, because the verbose
rerun waited for the same hang. The Go recipe now looks in the first
run's output for the lines Go prints for a hang, and exits 1 with no
rerun. A failure with neither line in its output still reruns with -v.

The Python make test example takes the same timeout wrapper as its
Dockerfile example. The 60 and 90 second rule and the 5-minute rule say
a hung package uses the backstop once. The Go item in both repo
checklists now asks for this.

Model: opus-5-5
2026-11-03 23:08:23 +01:00
5 changed files with 173 additions and 61 deletions

No files matched your search

+22 -10
View File
@@ -3,6 +3,9 @@ version: "2"
# Config schema uses the golangci-lint v2 layout (settings live under # Config schema uses the golangci-lint v2 layout (settings live under
# linters.settings, not top-level linters-settings) so that the # linters.settings, not top-level linters-settings) so that the
# thresholds below are actually applied by golangci-lint >= v2. # thresholds below are actually applied by golangci-lint >= v2.
#
# This file has no formatters section, so golangci-lint run does not run
# gofmt. A repository's script/fmt-check runs it, on the host.
run: run:
timeout: 5m timeout: 5m
@@ -16,13 +19,13 @@ linters:
- gomodguard_v2 - gomodguard_v2
disable: disable:
# Genuinely incompatible with project patterns # Genuinely incompatible with project patterns
- exhaustruct # Requires all struct fields - exhaustruct_v5 # 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
# Deprecated: the warning is attached to the old name, so it is # Deprecated: the warning is attached to the old name, so it is
# silenced by disabling that name, not by enabling the successor. # silenced by disabling that name, not by enabling the successor.
- exhaustruct # Deprecated, replaced by exhaustruct_v5
- wsl # Deprecated, replaced by wsl_v5 - wsl # Deprecated, replaced by wsl_v5
- gomodguard # Deprecated, replaced by gomodguard_v2 - gomodguard # Deprecated, replaced by gomodguard_v2
settings: settings:
@@ -40,7 +43,10 @@ linters:
# test-support package exists to hand a test privileges the program # test-support package exists to hand a test privileges the program
# itself must never have, so a file that is not a test must not import # itself must never have, so a file that is not a test must not import
# one. Test files, and the files inside a package whose directory name # one. Test files, and the files inside a package whose directory name
# ends in `test`, are where that code belongs, and are exempt. # ends in `test`, are where that code belongs, and are exempt. A package
# in a subdirectory of that directory is not exempt. `${base-path}` is
# the directory holding this file, so the name of the directory a
# repository is cloned into does not count.
# #
# The deny list below is the one part of this file a repository is # The deny list below is the one part of this file a repository is
# expected to extend, and the only part it may. depguard matches an # expected to extend, and the only part it may. depguard matches an
@@ -55,24 +61,25 @@ linters:
files: files:
- "$all" - "$all"
- "!$test" - "!$test"
- "!**/*test/**" - "!${base-path}**/*test/*"
deny: deny:
- pkg: net/http/httptest - pkg: net/http/httptest
desc: >- desc: >-
Test-support code belongs in test files and in packages whose Test-support code belongs in test files and in packages whose
directory name ends in test, not in the shipped binary. directory name ends in test, not in the shipped binary.
# Only decisions already recorded in the Go package defaults are # Each entry names a module not to use and what to use instead. Every
# listed here. Every entry matches the module path exactly. # entry matches the module path exactly.
gomodguard_v2: gomodguard_v2:
blocked: blocked:
- module: github.com/rs/zerolog - module: github.com/rs/zerolog
recommendations: recommendations:
- log/slog - log/slog
reason: "Structured logging is stdlib log/slog." reason: "Structured logging is stdlib log/slog."
# One entry per pre-fork module path, because the later releases # These entries cover only the github.com/go-redis/redis module paths,
# are separate paths. A prefix match would be shorter but would # one entry per path, because the later releases are separate paths.
# also reach github.com/go-redis/redismock, the test double for # A prefix match would be shorter but would also reach
# the successor these entries recommend. # github.com/go-redis/redismock/v9, the test double for the successor
# these entries recommend.
- module: github.com/go-redis/redis - module: github.com/go-redis/redis
recommendations: recommendations:
- github.com/redis/go-redis/v9 - github.com/redis/go-redis/v9
@@ -85,6 +92,10 @@ linters:
recommendations: recommendations:
- github.com/redis/go-redis/v9 - github.com/redis/go-redis/v9
reason: "Pre-fork module; use the maintained go-redis v9." reason: "Pre-fork module; use the maintained go-redis v9."
- module: github.com/go-redis/redis/v9
recommendations:
- github.com/redis/go-redis/v9
reason: "Pre-fork module; use the maintained go-redis v9."
- module: github.com/sergi/go-diff - module: github.com/sergi/go-diff
recommendations: recommendations:
- github.com/aymanbagabas/go-udiff - github.com/aymanbagabas/go-udiff
@@ -108,3 +119,4 @@ linters:
issues: issues:
max-issues-per-linter: 0 max-issues-per-linter: 0
max-same-issues: 0 max-same-issues: 0
uniq-by-line: false
+40 -9
View File
@@ -21,15 +21,46 @@ fmt-check, and commit.
# Completed Steps # Completed Steps
- 2026-11-01: Moved the canonical golangci-lint to v2.15.0 (released - 2026-11-03: The canonical golangci-lint is now v2.15.0, still built with
2026-10-16), still built with go1.27.0, so the `go` directive rule is go1.27.0, so the `go` directive rule is unchanged (issue 142). Repositories
unchanged. The canonical `.golangci.yml` passes `golangci-lint config verify` pick this up on their next re-vendor. Each item in the canonical
on v2.15.0, and `default: all` switches on no new linter. `canonicalheader` is `.golangci.yml` was tried on a scratch module. These changed:
enabled again (issue 105): v2.15.0 carries the upstream commit "fix: - `canonicalheader` is enabled again, because v2.15.0 fixes the random
non-deterministic results", and on a scratch package with two planted findings misses behind issue 105.
beside a `ResponseWriter.Header()` call, linted 20 times with each pinned - The `depguard` exemption covers only the files directly inside a directory
image, v2.14.0 reported them on 8 runs and nothing on 12, while v2.15.0 whose name ends in `test`. It used to reach every directory below that one
reported them on all 20. too. The name of the directory a repository is cloned into no longer
counts.
- `gomodguard_v2` also blocks `github.com/go-redis/redis/v9`, whose
pre-releases can still be imported under that path.
- `uniq-by-line` is now `false`, so a line that trips two linters reports
both findings.
- Three comments are corrected:
- `exhaustruct` is listed as deprecated.
- The test double is named `github.com/go-redis/redismock/v9`.
- The claim that every blocked module is in the Go package defaults is
gone.
- A comment says the file has no `formatters` section, so
`golangci-lint run` does not run `gofmt`.
- 2026-11-03: A package from the distribution's own package index now needs no
version and no hash, and pulling that index is allowed
(https://git.eeqj.de/sneak/prompts/issues/72), on sneak's answer at
https://git.eeqj.de/sneak/prompts/issues/72#issuecomment-165196. The
distribution is the pinned base image's in a `Dockerfile` and the host's on a
host. It is trusted, and everything else stays pinned by hash. A package
source that a `Dockerfile` adds itself does not get the exception. The pinning
rule of `REPO_POLICIES.md` and both checklists say so, and the canonical Go
`Dockerfile`'s `apk add` line has a comment saying why it is allowed.
- 2026-11-03: A first test run that timed out now gets no verbose rerun
(https://git.eeqj.de/sneak/prompts/issues/61), on sneak's approval at
https://git.eeqj.de/sneak/prompts/issues/61#issuecomment-165195. The Go recipe
in `REPO_POLICIES.md` looks in the first run's output for
`panic: test timed out` or `*** Test killed`, and exits 1 when either is
there. The Python `make test` example takes the same `timeout 90 sh -c`
wrapper as its `Dockerfile` example. The 60 and 90 second rule and the
5-minute rule now say a hung package uses the backstop once. The Go item in
both repo checklists now asks for this, so a repository with the old recipe
fails it.
- 2026-10-27: Added `prompts/API_POLICIES.md` - 2026-10-27: Added `prompts/API_POLICIES.md`
(https://git.eeqj.de/sneak/prompts/issues/137). It holds sneak's rule from (https://git.eeqj.de/sneak/prompts/issues/137). It holds sneak's rule from
https://git.eeqj.de/sneak/routewatch/issues/199 that a value in an API https://git.eeqj.de/sneak/routewatch/issues/199 that a value in an API
+10 -4
View File
@@ -1,6 +1,6 @@
--- ---
title: Existing Repo Checklist title: Existing Repo Checklist
last_modified: 2026-10-26 last_modified: 2026-11-03
--- ---
Use this checklist when beginning work in a repo that may not yet conform to our Use this checklist when beginning work in a repo that may not yet conform to our
@@ -70,8 +70,9 @@ with your task.
(`--mount=type=cache,target=/root/.cache/go-build`), `golangci-lint run` (`--mount=type=cache,target=/root/.cache/go-build`), `golangci-lint run`
also mounts golangci-lint's also mounts golangci-lint's
(`--mount=type=cache,target=/root/.cache/golangci-lint`), and no step (`--mount=type=cache,target=/root/.cache/golangci-lint`), and no step
mounts anything else; both `go test` runs carry `-count=1`, as in the mounts anything else; both `go test` runs carry `-count=1`; a first run
canonical Go `Dockerfile` in `REPO_POLICIES.md` that timed out gets no rerun, as in the canonical Go `Dockerfile` in
`REPO_POLICIES.md`
- [ ] Python: a `test` phase that installs its packages from `Pipfile.lock` does - [ ] Python: a `test` phase that installs its packages from `Pipfile.lock` does
it above `ARG CACHEBUST`, with `COPY script/ script/`, it above `ARG CACHEBUST`, with `COPY script/ script/`,
`COPY Pipfile Pipfile.lock ./` and `RUN script/bootstrap`. When `COPY Pipfile Pipfile.lock ./` and `RUN script/bootstrap`. When
@@ -215,7 +216,12 @@ with your task.
- [ ] Pre-commit hook is installed (`make hooks`) - [ ] Pre-commit hook is installed (`make hooks`)
- [ ] No secrets in the repo (`.env`, keys, credentials) - [ ] No secrets in the repo (`.env`, keys, credentials)
- [ ] No mutable references in Dockerfiles or scripts (tags, `@latest`) — all - [ ] No mutable references in Dockerfiles or scripts (tags, `@latest`) — all
pinned by cryptographic hash with version/date comment pinned by cryptographic hash with version/date comment. The one exception
is a package from the distribution's own package index, which needs no
version and no hash. Pulling that index is also allowed. In a
`Dockerfile`, the distribution is the pinned base image's. On a host, it
is the host's. A package source that a `Dockerfile` adds itself does not
get this exception
- [ ] Using `yarn`, not `npm` (JS projects) - [ ] Using `yarn`, not `npm` (JS projects)
# Directory Structure # Directory Structure
+9 -4
View File
@@ -1,6 +1,6 @@
--- ---
title: New Repo Checklist title: New Repo Checklist
last_modified: 2026-10-26 last_modified: 2026-11-03
--- ---
Use this checklist when creating a new repository from scratch. Follow the steps Use this checklist when creating a new repository from scratch. Follow the steps
@@ -129,8 +129,9 @@ Template files can be fetched from:
(`--mount=type=cache,target=/root/.cache/go-build`), `golangci-lint run` (`--mount=type=cache,target=/root/.cache/go-build`), `golangci-lint run`
also mounts golangci-lint's also mounts golangci-lint's
(`--mount=type=cache,target=/root/.cache/golangci-lint`), and no step (`--mount=type=cache,target=/root/.cache/golangci-lint`), and no step
mounts anything else; both `go test` runs carry `-count=1`, as in the mounts anything else; both `go test` runs carry `-count=1`; a first run
canonical Go `Dockerfile` in `REPO_POLICIES.md` that timed out gets no rerun, as in the canonical Go `Dockerfile` in
`REPO_POLICIES.md`
- Python: a `test` phase that installs its packages from `Pipfile.lock` does - Python: a `test` phase that installs its packages from `Pipfile.lock` does
it above `ARG CACHEBUST`, with `COPY script/ script/`, it above `ARG CACHEBUST`, with `COPY script/ script/`,
`COPY Pipfile Pipfile.lock ./` and `RUN script/bootstrap`. When `COPY Pipfile Pipfile.lock ./` and `RUN script/bootstrap`. When
@@ -242,7 +243,11 @@ are thin shims calling them. Model scripts:
final stage is missing its `COPY --from=` edge to the gate phases. final stage is missing its `COPY --from=` edge to the gate phases.
- [ ] No secrets in repo, and none in the build context: enumerate a probe image - [ ] No secrets in repo, and none in the build context: enumerate a probe image
rather than reading `.dockerignore` rather than reading `.dockerignore`
- [ ] No mutable image/package references - [ ] No mutable image/package references. The one exception is a package from
the distribution's own package index, which needs no version and no hash.
Pulling that index is also allowed. In a `Dockerfile`, the distribution is
the pinned base image's. On a host, it is the host's. A package source
that a `Dockerfile` adds itself does not get this exception
- [ ] No unnecessary files in repo root - [ ] No unnecessary files in repo root
- [ ] All dates written as YYYY-MM-DD - [ ] All dates written as YYYY-MM-DD
+92 -34
View File
@@ -1,6 +1,6 @@
--- ---
title: Repository Policies title: Repository Policies
last_modified: 2026-11-01 last_modified: 2026-11-03
--- ---
This document covers repository structure, tooling, and workflow standards. Code This document covers repository structure, tooling, and workflow standards. Code
@@ -20,19 +20,36 @@ style conventions are in separate documents:
`last_modified: YYYY-MM-DD` in the YAML front matter so it can be kept in sync `last_modified: YYYY-MM-DD` in the YAML front matter so it can be kept in sync
with the authoritative source as policies evolve. with the authoritative source as policies evolve.
- **ALL external references must be pinned by cryptographic hash.** This - **External references must be pinned by cryptographic hash.** This includes
includes Docker base images, Go modules, npm packages, GitHub Actions, and Docker base images, Go modules, npm packages, GitHub Actions, and anything
anything else fetched from a remote source. Version tags (`@v4`, `@latest`, else fetched from a remote source. Version tags (`@v4`, `@latest`, `:3.21`,
`:3.21`, etc.) are server-mutable and therefore remote code execution etc.) are server-mutable and therefore remote code execution vulnerabilities.
vulnerabilities. The ONLY acceptable way to reference an external dependency Reference an external dependency by its content hash (Docker `@sha256:...`, Go
is by its content hash (Docker `@sha256:...`, Go module hash in `go.sum`, npm module hash in `go.sum`, npm integrity hash in lockfile, GitHub Actions
integrity hash in lockfile, GitHub Actions `@<commit-sha>`). No exceptions. `@<commit-sha>`). This also means never `curl | bash` to install tools like
This also means never `curl | bash` to install tools like pyenv, nvm, rustup, pyenv, nvm, rustup, etc. Instead, download a specific release archive from
etc. Instead, download a specific release archive from GitHub, verify its hash GitHub, verify its hash (hardcoded in the Dockerfile or script), and only then
(hardcoded in the Dockerfile or script), and only then install. Unverified install. Unverified install scripts are arbitrary remote code execution. This
install scripts are arbitrary remote code execution. This is the single most is the single most important rule in this document. Double-check every
important rule in this document. Double-check every external reference in external reference in every file before committing.
every file before committing. There are zero exceptions to this rule.
There is one exception: a package from the distribution's own package index
needs no version and no hash. Pulling that index is also allowed. The
distribution is trusted. In a `Dockerfile`, it is the pinned base image's
distribution. On a host, it is the host's. These commands are examples:
- `apt-get update`
- `apt-get install`
- `apk update`
- `apk add`
These are not trusted, so everything from them stays pinned by hash:
- Docker Hub
- PyPI
- npm
- `go get`
A package source that a `Dockerfile` adds itself is not the distribution's
own, so the exception does not cover it.
- Every repo with software must have a root `Makefile` with these targets: - Every repo with software must have a root `Makefile` with these targets:
`make bootstrap`, `make setup`, `make test`, `make lint`, `make fmt` (writes), `make bootstrap`, `make setup`, `make test`, `make lint`, `make fmt` (writes),
@@ -233,6 +250,8 @@ style conventions are in separate documents:
# Test phase. -race needs cgo and so a C compiler, which the Debian Go # 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 # image ships and the alpine one does not. -count=1 because the mounted
# cache also holds test results, which go test would otherwise replay. # 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 # golang:1.x, YYYY-MM-DD
FROM golang@sha256:... AS test FROM golang@sha256:... AS test
WORKDIR /src WORKDIR /src
@@ -241,15 +260,24 @@ style conventions are in separate documents:
ARG CACHEBUST ARG CACHEBUST
COPY . . COPY . .
RUN --mount=type=cache,target=/root/.cache/go-build \ RUN --mount=type=cache,target=/root/.cache/go-build \
go test -count=1 -timeout 90s -race -cover ./... || \ status=0; \
{ echo "--- Rerunning with -v for details ---"; \ output=$(go test -count=1 -timeout 90s -race -cover ./... 2>&1) || status=$?; \
go test -count=1 -timeout 90s -race -v ./...; exit 1; } 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 # Build stage. Nothing is wanted from either phase above; the copies
# are what make BuildKit build them first, so nothing after them can # are what make BuildKit build them first, so nothing after them can
# run unless lint and test passed. # run unless lint and test passed.
# golang:1.x-alpine, YYYY-MM-DD # golang:1.x-alpine, YYYY-MM-DD
FROM golang@sha256:... AS builder 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 RUN apk add --no-cache git
WORKDIR /src WORKDIR /src
COPY go.mod go.sum ./ COPY go.mod go.sum ./
@@ -385,11 +413,10 @@ style conventions are in separate documents:
`Pipfile.lock`. It installs no Python there: the tests use the base `Pipfile.lock`. It installs no Python there: the tests use the base
image's Python, which the image's hash pins. If it installs anything else image's Python, which the image's hash pins. If it installs anything else
there, the step goes after `ARG CACHEBUST`. there, the step goes after `ARG CACHEBUST`.
- Every external reference must be pinned by hash, so `script/bootstrap` - PyPI is not trusted, so `script/bootstrap` installs pipenv and its
installs pipenv and its dependencies by hash. It does so with dependencies by hash. It does so with `pip install --require-hashes -r`
`pip install --require-hashes -r` and a requirements file in `script/` and a requirements file in `script/` that lists each of them with `==` and
that lists each of them with `==` and its `--hash`. pip refuses to install its `--hash`. pip refuses to install a dependency the file does not list.
a dependency the file does not list.
- It then runs `pipenv install --deploy`, with `--dev` when the tests need - It then runs `pipenv install --deploy`, with `--dev` when the tests need
the development packages. `--deploy` installs only what `Pipfile.lock` the development packages. `--deploy` installs only what `Pipfile.lock`
names, checks every package against the lockfile's hashes, and fails when names, checks every package against the lockfile's hashes, and fails when
@@ -413,7 +440,8 @@ style conventions are in separate documents:
`pipenv install --dev --deploy`: `pipenv install --dev --deploy`:
```dockerfile ```dockerfile
# Test phase. The 90-second timeout covers both pytest runs. # Test phase. Both pytest runs share one 90-second timeout, so a hang
# uses it once and gets no rerun.
# python:3.x-slim, YYYY-MM-DD # python:3.x-slim, YYYY-MM-DD
FROM python@sha256:... AS test FROM python@sha256:... AS test
# make and git are base tools that script/bootstrap installs when they # make and git are base tools that script/bootstrap installs when they
@@ -484,15 +512,19 @@ style conventions are in separate documents:
improvement bug against that repo. Add a 90-second timeout to the test improvement bug against that repo. Add a 90-second timeout to the test
invocation (`go test -timeout 90s`). The backstop deliberately sits above the invocation (`go test -timeout 90s`). The backstop deliberately sits above the
hard cap so that it catches a genuinely hung test rather than a merely slow hard cap so that it catches a genuinely hung test rather than a merely slow
one. one. A hung package uses the backstop once, because a first run that timed out
gets no rerun. `-timeout` applies to each package, so several hung packages
can each use it.
- **The test command should use the conditional verbose rerun pattern.** Run - **The test command should use the conditional verbose rerun pattern.** Run
tests without `-v` (verbose) first. If tests fail, automatically rerun with tests without `-v` (verbose) first. If tests fail, automatically rerun with
`-v` to show full output. This keeps CI logs and `docker build` output clean `-v` to show full output. This keeps CI logs and `docker build` output clean
on success (just package/suite summaries) while providing full diagnostic on success (just package/suite summaries) while providing full diagnostic
detail on failure (every test case, every assertion). The command lives in the detail on failure (every test case, every assertion). A first run that timed
`test` phase of the `Dockerfile`, since `script/test` builds that phase; the out gets no rerun, because the rerun would only wait for the same hang. The
Makefile form below is the same pattern for any repo-local invocation: command lives in the `test` phase of the `Dockerfile`, since `script/test`
builds that phase; the Makefile form below is the same pattern for any
repo-local invocation:
```makefile ```makefile
test: test:
@@ -505,11 +537,29 @@ style conventions are in separate documents:
```makefile ```makefile
test: test:
@go test -count=1 -timeout 90s -race -cover ./... || \ @status=0; \
{ echo "--- Rerunning with -v for details ---"; \ output=$$(go test -count=1 -timeout 90s -race -cover ./... 2>&1) || status=$$?; \
go test -count=1 -timeout 90s -race -v ./...; exit 1; } 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
``` ```
The first run's output is kept so that the recipe can look in it for what Go
prints for a hang:
- `panic: test timed out` means the test's own timer fired.
- `*** Test killed` means `go test` stopped a test that ignored that timer,
about a minute later.
Go prints the goroutine stacks with either line, as long as the test can
still respond. A rerun would only wait for the same hang, so the recipe
exits 1. A failure with neither line in its output reruns with `-v`. The
kept output prints when the first run ends, not while it runs.
`-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 neither run can report a stored pass in place of running the 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 tests. It leaves the build cache alone, so it costs the runtime of the suite
@@ -528,17 +578,25 @@ style conventions are in separate documents:
```makefile ```makefile
test: test:
@python -m pytest || \ @timeout 90 sh -c 'python -m pytest || { \
{ echo "--- Rerunning with -v for details ---"; \ echo "--- Rerunning with -v for details ---"; \
python -m pytest -v; exit 1; } python -m pytest -v; exit 1; }'
``` ```
Both pytest runs share that one 90-second timeout, as in the `Dockerfile`
example above, so a hang uses it once and gets no rerun. Ctrl-C does not
stop a running `make test`, which goes on until the tests end or the 90
seconds run out. On a Mac without `timeout`, the target fails until it is
installed.
The `exit 1` ensures the target always fails after a rerun — the first run The `exit 1` ensures the target always fails after a rerun — the first run
already proved the tests are broken, so the build must not pass even if a already proved the tests are broken, so the build must not pass even if a
flaky test happens to succeed on the second attempt. The rerun exists solely flaky test happens to succeed on the second attempt. The rerun exists solely
for diagnostic output. for diagnostic output.
- Docker builds must complete in under 5 minutes. - Docker builds must complete in under 5 minutes. A hung package uses the
90-second test backstop once, because a first run that timed out gets no
rerun.
- `make check` must not modify any files in the repo. Tests may use temporary - `make check` must not modify any files in the repo. Tests may use temporary
directories. directories.