1 Commits
Author SHA1 Message Date
sneak 9272826fc7 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 what Go prints for a hang, and exits 1 with no rerun.
Any other failure 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.

Model: opus-5-5
2026-11-03 20:37:22 +00:00
19 changed files with 698 additions and 2092 deletions
-6
View File
@@ -35,12 +35,6 @@
# `services/api/.claude/` and must add its own anchored entry. # `services/api/.claude/` and must add its own anchored entry.
.claude .claude
# yarn's install state and cache stay out, and its release file stays
# in. Docker obeys the last matching line, so these two lines sit above
# the secret patterns, which must win under .yarn/releases/.
.yarn/*
!.yarn/releases
# Environment files. `*.env` covers bare `.env` and the `prod.env` # Environment files. `*.env` covers bare `.env` and the `prod.env`
# convention. Re-include a committed template with a negation if the # convention. Re-include a committed template with a negation if the
# build needs one: `!docs/example.env`. # build needs one: `!docs/example.env`.
-3
View File
@@ -2,6 +2,3 @@
# both sides instead of conflicting. Git never reports a conflict here: read # both sides instead of conflicting. Git never reports a conflict here: read
# the merged entries after every merge or rebase. # the merged entries after every merge or rebase.
TODO.md merge=union TODO.md merge=union
# yarn's release file is one opaque file in diffs and merges.
.yarn/releases/** binary
+3 -5
View File
@@ -19,12 +19,10 @@ Thumbs.db
# Node # Node
node_modules/ node_modules/
.yarn/*
!.yarn/releases
# Secrets. Unanchored, so each matches at every depth. Matching is # Secrets. Unanchored like every entry above, so each matches at every
# case-sensitive on Linux, so names use character ranges rather than a # depth. Matching is case-sensitive on Linux, so names use character
# lowercase form that misses `Server.Key`. # ranges rather than a lowercase form that misses `Server.Key`.
# Environment files. `*.env` covers bare `.env` and the `prod.env` # Environment files. `*.env` covers bare `.env` and the `prod.env`
# convention. Only the templates `example.env` and `sample.env` are # convention. Only the templates `example.env` and `sample.env` are
+12 -22
View File
@@ -3,9 +3,6 @@ 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
@@ -19,15 +16,17 @@ linters:
- gomodguard_v2 - gomodguard_v2
disable: disable:
# Genuinely incompatible with project patterns # Genuinely incompatible with project patterns
- exhaustruct_v5 # 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
# 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
# Misses findings at random in v2.14.0; back once a pinned release fixes it
- canonicalheader
settings: settings:
lll: lll:
line-length: 88 line-length: 88
@@ -43,10 +42,7 @@ 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. A package # ends in `test`, are where that code belongs, and are exempt.
# 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
@@ -61,25 +57,24 @@ linters:
files: files:
- "$all" - "$all"
- "!$test" - "!$test"
- "!${base-path}**/*test/*" - "!**/*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.
# Each entry names a module not to use and what to use instead. Every # Only decisions already recorded in the Go package defaults are
# entry matches the module path exactly. # listed here. Every 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."
# These entries cover only the github.com/go-redis/redis module paths, # One entry per pre-fork module path, because the later releases
# one entry per path, because the later releases are separate paths. # are separate paths. A prefix match would be shorter but would
# A prefix match would be shorter but would also reach # also reach github.com/go-redis/redismock, the test double for
# github.com/go-redis/redismock/v9, the test double for the successor # the successor these entries recommend.
# 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
@@ -92,10 +87,6 @@ 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
@@ -119,4 +110,3 @@ 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
-1
View File
@@ -1,3 +1,2 @@
node_modules/ node_modules/
.yarn/
yarn.lock yarn.lock
-1000
View File
File diff suppressed because one or more lines are too long
-5
View File
@@ -1,5 +0,0 @@
enableTelemetry: false
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.18.1.cjs
+13 -24
View File
@@ -2,24 +2,17 @@
# lint` or `script/lint`, which are themselves a docker build and would # lint` or `script/lint`, which are themselves a docker build and would
# recurse into a daemon that does not exist in a build step. # recurse into a daemon that does not exist in a build step.
# #
# In every stage, ARG CACHEBUST comes right after the step that installs # Every stage declares ARG CACHEBUST first: script/ passes a new value on
# system packages. script/ passes a new value on every build. That value # every build, which reruns every RUN step after it.
# reruns every RUN step below the ARG line, and the install step keeps
# its layer. node and yarn come from the distribution, which is trusted,
# so they need no version and no hash. That yarn only starts the release
# file in .yarn/releases/, so every stage copies that file and
# .yarnrc.yml before it runs script/bootstrap.
# #
# alpine 3.24.3, 2026-11-04 # node 22-alpine, 2026-02-22
FROM alpine@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a AS lint FROM node@sha256:e4bf2a82ad0a4037d28035ae71529873c069b13eb0455466ae0bc13363826e34 AS lint
RUN apk add --no-cache git make nodejs-current yarn
ARG CACHEBUST ARG CACHEBUST
WORKDIR /app WORKDIR /app
COPY script/ script/ COPY script/ script/
COPY package.json yarn.lock .yarnrc.yml ./ COPY package.json yarn.lock ./
COPY .yarn/releases/ .yarn/releases/
RUN script/bootstrap RUN script/bootstrap
COPY . . COPY . .
@@ -28,16 +21,14 @@ RUN yarn run prettier --check '**/*.md' --tab-width 4 --prose-wrap always
# Test phase, same shape and for the same reason. # Test phase, same shape and for the same reason.
# #
# alpine 3.24.3, 2026-11-04 # node 22-alpine, 2026-02-22
FROM alpine@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a AS test FROM node@sha256:e4bf2a82ad0a4037d28035ae71529873c069b13eb0455466ae0bc13363826e34 AS test
RUN apk add --no-cache git make nodejs-current yarn
ARG CACHEBUST ARG CACHEBUST
WORKDIR /app WORKDIR /app
COPY script/ script/ COPY script/ script/
COPY package.json yarn.lock .yarnrc.yml ./ COPY package.json yarn.lock ./
COPY .yarn/releases/ .yarn/releases/
RUN script/bootstrap RUN script/bootstrap
COPY . . COPY . .
@@ -50,9 +41,8 @@ RUN echo "No tests defined."
# this image cannot be produced unless lint and test passed. A stage # this image cannot be produced unless lint and test passed. A stage
# appended after this one would drop all three out of a plain build. # appended after this one would drop all three out of a plain build.
# #
# alpine 3.24.3, 2026-11-04 # node 22-alpine, 2026-02-22
FROM alpine@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a FROM node@sha256:e4bf2a82ad0a4037d28035ae71529873c069b13eb0455466ae0bc13363826e34
RUN apk add --no-cache git make nodejs-current yarn
ARG CACHEBUST ARG CACHEBUST
WORKDIR /app WORKDIR /app
@@ -60,11 +50,10 @@ WORKDIR /app
COPY --from=lint /app/package.json /dev/null COPY --from=lint /app/package.json /dev/null
COPY --from=test /app/package.json /dev/null COPY --from=test /app/package.json /dev/null
# script/bootstrap finds the system packages above and installs the node # script/bootstrap installs all prerequisites. Manifests are copied
# packages. # first so that layer stays cached until dependencies change.
COPY script/ script/ COPY script/ script/
COPY package.json yarn.lock .yarnrc.yml ./ COPY package.json yarn.lock ./
COPY .yarn/releases/ .yarn/releases/
RUN script/bootstrap RUN script/bootstrap
COPY . . COPY . .
+4 -4
View File
@@ -111,7 +111,7 @@ development workflow, and the Makefile targets are thin shims that call them.
The scripts are POSIX sh (not bash) so they run in minimal containers such as The scripts are POSIX sh (not bash) so they run in minimal containers such as
alpine. We provide: alpine. We provide:
- `script/bootstrap` — install all dependencies (`yarn install --immutable`) - `script/bootstrap` — install all dependencies (yarn install)
- `script/setup` — set up the repo for development after a fresh clone: runs - `script/setup` — set up the repo for development after a fresh clone: runs
`script/bootstrap`, then `script/install-precommit` `script/bootstrap`, then `script/install-precommit`
- `script/projectname` — output the project name (our own extension); used by - `script/projectname` — output the project name (our own extension); used by
@@ -156,9 +156,9 @@ A stage with none of these steps declares `ARG CACHEBUST` right after `FROM`.
The `script/` builds pass a new `CACHEBUST` value on every build, which reruns The `script/` builds pass a new `CACHEBUST` value on every build, which reruns
every `RUN` step after that line and leaves the layers above it and the cache every `RUN` step after that line and leaves the layers above it and the cache
mounts in place; `--no-cache` would build those layers again and empty the mounts in place; `--no-cache` would build those layers again and empty the
mounts. This repo's own `Dockerfile` installs git, make, node and yarn right mounts. This repo's own `Dockerfile` declares `ARG CACHEBUST` right after `FROM`
after `FROM` in every stage, above `ARG CACHEBUST`. `script/bootstrap` installs in every stage: each stage installs system packages and node packages together
the node packages below that line, because check builds keep system packages but in one `script/bootstrap` step, and check builds may keep system packages but
not node packages. not node packages.
## Rationale ## Rationale
+2 -79
View File
@@ -21,82 +21,6 @@ fmt-check, and commit.
# Completed Steps # Completed Steps
- 2026-11-04: Each repository now commits yarn 4 and installs with
`yarn install --immutable` (https://git.eeqj.de/sneak/prompts/issues/149), on
sneak's ruling at
https://git.eeqj.de/sneak/prompts/issues/149#issuecomment-167558. Product
repositories convert when they next have work. These changed:
- This repository carries `.yarn/releases/yarn-4.18.1.cjs`, and its
`yarn.lock` is in yarn 4's format.
- `script/bootstrap` no longer stops on yarn's major version.
- Node comes from the distribution's newest Node package, which on Alpine
3.24 is `nodejs-current`.
- A `Dockerfile` stage that needs yarn also copies `.yarnrc.yml` and
`.yarn/releases/`, and may start from any distribution that packages node
and yarn.
- Both checklists give the steps, and the ignore files keep yarn's install
state out of git and the build context.
- 2026-11-04: An agent whose work is confined to one package may run `go test`
directly on that package's suite, or on individual tests in it, while
iterating (https://git.eeqj.de/sneak/prompts/issues/51), on sneak's ruling at
https://git.eeqj.de/sneak/prompts/issues/51#issuecomment-167600. The full
`make check` is still required before every push, and the `Makefile` gains
nothing. The rule on Docker now names the lint and test gates, because a run
while iterating is not one.
- 2026-11-04: node, yarn, npm, python, pip and venv now come from the
distribution with no version and no hash
(https://git.eeqj.de/sneak/prompts/issues/146), on sneak's ruling at
https://git.eeqj.de/sneak/prompts/issues/146#issuecomment-166033. Third-party
packages stay pinned by hash. These changed:
- `script/bootstrap` no longer uses nvm or corepack, and stops when yarn's
major version is not 1.
- `script/bootstrap` installs node, yarn and the node packages only when
`yarn.lock` is in the tree.
- `script/bootstrap`, `script/fmt` and `script/fmt-check` run `yarn` or
`yarnpkg`, whichever is present.
- This repository's `Dockerfile` starts from Alpine 3.24.
- The Python `test` phase example starts from `debian:trixie-slim`.
- A Python repository's `script/bootstrap` creates its virtual environment
at `$HOME/.pipenv`, so it works on a host and in a `Dockerfile`.
- `PIP_ONLY_BINARY=:all:` stops a package with no wheel, so pip downloads no
build tools without a hash check.
- 2026-11-04: Rewrote `prompts/REPO_POLICIES.md` sentence by sentence
(https://git.eeqj.de/sneak/prompts/issues/136), on sneak's yes at
https://git.eeqj.de/sneak/prompts/issues/136#issuecomment-165193. Every rule
keeps its meaning and its place, and `must` and `should` stay as they were.
Long sentences are split, comma-chained lists are now bulleted lists, and
every reason stays. Code blocks, digests, URLs, file names, commands and
quoted strings are unchanged.
- 2026-11-03: The canonical golangci-lint is now v2.15.0, still built with
go1.27.0, so the `go` directive rule is unchanged (issue 142). Repositories
pick this up on their next re-vendor. Each item in the canonical
`.golangci.yml` was tried on a scratch module. These changed:
- `canonicalheader` is enabled again, because v2.15.0 fixes the random
misses behind issue 105.
- The `depguard` exemption covers only the files directly inside a directory
whose name ends in `test`. It used to reach every directory below that one
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 - 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), on sneak's approval at
https://git.eeqj.de/sneak/prompts/issues/61#issuecomment-165195. The Go recipe https://git.eeqj.de/sneak/prompts/issues/61#issuecomment-165195. The Go recipe
@@ -104,9 +28,8 @@ fmt-check, and commit.
`panic: test timed out` or `*** Test killed`, and exits 1 when either is `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` 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 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 5-minute rule now say a hung package uses the backstop once. Consuming
both repo checklists now asks for this, so a repository with the old recipe repositories pick this up on their next re-vendor.
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
+1 -2
View File
@@ -2,6 +2,5 @@
"private": true, "private": true,
"devDependencies": { "devDependencies": {
"prettier": "3.8.1" "prettier": "3.8.1"
}, }
"packageManager": "yarn@4.18.1"
} }
+2 -3
View File
@@ -1,6 +1,6 @@
--- ---
title: Code Styleguide — JavaScript title: Code Styleguide — JavaScript
last_modified: 2026-11-04 last_modified: 2026-02-22
--- ---
1. Use `const` for everything. If you need to reassign, use `let`. Never use 1. Use `const` for everything. If you need to reassign, use `let`. Never use
@@ -8,8 +8,7 @@ last_modified: 2026-11-04
1. Use yarn for package management, avoid using npm. 1. Use yarn for package management, avoid using npm.
1. Install node from the distribution. If its version is too old, use a newer 1. Use LTS node versions.
distribution.
1. Use `prettier` for code formatting, with four spaces for indentation. 1. Use `prettier` for code formatting, with four spaces for indentation.
+68 -135
View File
@@ -1,6 +1,6 @@
--- ---
title: Existing Repo Checklist title: Existing Repo Checklist
last_modified: 2026-11-04 last_modified: 2026-10-26
--- ---
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
@@ -56,45 +56,34 @@ with your task.
- the step that runs `script/bootstrap` to install a Python `test` phase's - the step that runs `script/bootstrap` to install a Python `test` phase's
packages from `Pipfile.lock` packages from `Pipfile.lock`
- [ ] A stage with none of these steps declares `ARG CACHEBUST` right after - [ ] A stage with none of these steps declares `ARG CACHEBUST` right after
`FROM`. A step that also installs anything else comes after `FROM`. A step that installs anything else as well, such as a
`ARG CACHEBUST` too. A `script/bootstrap` that also installs node packages `script/bootstrap` that also installs node packages, comes after it too.
is one. The `script/` builds pass a new `CACHEBUST` value on every build, The `script/` builds pass a new `CACHEBUST` value on every build, which
which reruns every `RUN` step after that line and leaves the layers above reruns every `RUN` step after that line and leaves the layers above it and
it and the cache mounts in place; `--no-cache` would build those layers the cache mounts in place; `--no-cache` would build those layers again and
again and empty the mounts. A stage without the `ARG` line is served from empty the mounts. A stage without the `ARG` line is served from cache on
cache on an unchanged tree, so the scripts and the `ARG` lines are an unchanged tree, so the scripts and the `ARG` lines are re-vendored in
re-vendored in one commit. one commit.
- [ ] Go: each Go stage downloads the modules (`COPY go.mod go.sum ./`, - [ ] Go: each Go stage downloads the modules (`COPY go.mod go.sum ./`,
`RUN go mod download`) above `ARG CACHEBUST`; the steps that compile `RUN go mod download`) above `ARG CACHEBUST`; the steps that compile
(`golangci-lint run`, `go test`, `go build`) mount Go's build cache (`golangci-lint run`, `go test`, `go build`) mount Go's build cache
(`--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`; a first run mounts anything else; both `go test` runs carry `-count=1`, as in the
that timed out gets no rerun, as in the canonical Go `Dockerfile` in canonical Go `Dockerfile` in `REPO_POLICIES.md`
`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
`script/bootstrap` runs in that step, it installs nothing beyond these: `script/bootstrap` runs in that step, it installs nothing beyond system
- system packages packages, pipenv and its dependencies, and the packages from
- pipenv and its dependencies `Pipfile.lock`. It installs pipenv and its dependencies by hash, with
- the packages from `Pipfile.lock` `pip install --require-hashes`. It installs the packages from
`Pipfile.lock` with `pipenv install --deploy`, plus `--dev` when the tests
It creates a virtual environment at `$HOME/.pipenv` with `python3 -m venv` need the development packages. It installs no Python there, because the
when it is missing, then uses that environment's `pip` and `pipenv`. On a tests use the base image's. The tests run after `ARG CACHEBUST`, and every
host, it first installs the distribution's python and venv packages when other stage installs its Python packages after it, as in the Python `test`
they are missing. It installs pipenv and its dependencies by hash, with phase example in `REPO_POLICIES.md`
`pip install --require-hashes`. It installs the packages from `Pipfile.lock`
with `pipenv install --deploy`, plus `--dev` when the tests need the
development packages. It sets `PIP_ONLY_BINARY=:all:` for both installs, so
a package with no wheel for the image stops the install. In that step it
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`
- [ ] The gate phases invoke their tools directly, never through `make lint` or - [ ] The gate phases invoke their tools directly, never through `make lint` or
`script/test` — those are themselves a `docker build` and would recurse `script/test` — those are themselves a `docker build` and would recurse
inside a build step inside a build step
@@ -115,43 +104,37 @@ with your task.
`services/api/.claude/`: add anchored entries for those directories. `services/api/.claude/`: add anchored entries for those directories.
- [ ] If the repo embeds a version in a binary: `.dockerignore` lets `.git` into - [ ] If the repo embeds a version in a binary: `.dockerignore` lets `.git` into
the build context. It keeps out every git `config` at any depth the build context. It keeps out every git `config` at any depth
(`**/.git/config`, `**/.git/modules/**/config`): (`**/.git/config`, `**/.git/modules/**/config`): the repository's own,
- the repository's own each submodule's under `.git/modules/`, and that of a submodule keeping
- each submodule's under `.git/modules/` its own `.git` directory. `git describe` does not need them, and each can
- that of a submodule keeping its own `.git` directory hold a credential: a password in a remote URL, or the token the CI
checkout step stores there. A submodule whose name has a `config` segment
`git describe` does not need them, and each can hold a credential: a (`config`, `deploy/config`, `config/lib`) loses its whole git directory to
password in a remote URL, or the token the CI checkout step stores there. `**/.git/modules/**/config`, and Go's version stamping then fails the
Some submodule names have a `config` segment, as in `config`, build: give it a name without that segment (`git submodule add --name`).
`deploy/config` or `config/lib`. Such a submodule loses its whole git The stage that compiles has `git` (the Debian Go image has it; an alpine
directory to `**/.git/modules/**/config`, and Go's version stamping then one needs `apk add --no-cache git`) and takes the version from the
fails the build. Give it a name without that segment `VERSION` build argument when one is given, otherwise from
(`git submodule add --name`). The stage that compiles has `git`: the Debian `git describe --tags --always`. That gives the tag on a tagged commit; on
Go image has it, and an alpine one needs `apk add --no-cache git`. That a later commit, the tag, the number of commits since it and the short
stage takes the version from the `VERSION` build argument when one is given, commit (`v1.2.3-4-gabc1234`); and the short commit when no tag is
otherwise from `git describe --tags --always`. That gives: reachable. The stage that compiles also marks its working directory safe
- the tag, on a tagged commit for git (`git config --system --add safe.directory /src`): a context sent
- the tag, the number of commits since it and the short commit as a tar stream keeps the sender's file owners, and git refuses a checkout
(`v1.2.3-4-gabc1234`), on a later commit owned by another user, so the version would come out empty. `ARG VERSION`
- the short commit, when no tag is reachable has no default, and the build fails if the context carries `.git` and the
version still comes out empty, `dev` or `unknown`. A plain
The stage that compiles also marks its working directory safe for git `docker build .` with no build arguments must succeed; a Dockerfile that
(`git config --system --add safe.directory /src`). A context sent as a tar refuses an empty build argument drops that refusal and keeps the argument.
stream keeps the sender's file owners, and git refuses a checkout owned by A checkout whose `.git` is a file (a linked worktree, or a repository
another user, so the version would come out empty. `ARG VERSION` has no checked out as a submodule) is the exception: that file points to a git
default. The build fails if the context carries `.git` and the version still directory outside the build context, so the build cannot read the version
comes out empty, `dev` or `unknown`. A plain `docker build .` with no build and a plain `docker build .` fails; pass the version with
arguments must succeed; a Dockerfile that refuses an empty build argument `--build-arg VERSION=...`. `script/docker` and `script/cibuild` already
drops that refusal and keeps the argument. A checkout whose `.git` is a file pass the version they compute on the host; it takes precedence. The
is the exception: a linked worktree, or a repository checked out as a canonical `.gitea/workflows/check.yml` sets `fetch-depth: 0` on its
submodule. That file points to a git directory outside the build context, so checkout step, which otherwise clones shallow and fetches no tags, so a CI
the build cannot read the version and a plain `docker build .` fails. Pass build finds the tag too.
the version with `--build-arg VERSION=...`. `script/docker` and
`script/cibuild` already pass the version they compute on the host; it takes
precedence. The canonical `.gitea/workflows/check.yml` sets `fetch-depth: 0`
on its checkout step, which otherwise clones shallow and fetches no tags, so
a CI build finds the tag too.
- [ ] Gitea Actions workflow in `.gitea/workflows/` runs `script/cibuild` on - [ ] Gitea Actions workflow in `.gitea/workflows/` runs `script/cibuild` on
push, checks out with `persist-credentials: false` and with push, checks out with `persist-credentials: false` and with
`fetch-depth: 0` (which fetches the tags `git describe` needs), carries `fetch-depth: 0` (which fetches the tags `git describe` needs), carries
@@ -164,39 +147,10 @@ with your task.
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.golangci.yml` and, `https://git.eeqj.de/sneak/prompts/raw/branch/main/.golangci.yml` and,
in the same commit, set the lint phase digest to the one named in the in the same commit, set the lint phase digest to the one named in the
`.golangci.yml` paragraph of `REPO_POLICIES.md`) `.golangci.yml` paragraph of `REPO_POLICIES.md`)
- [ ] JS: `package.json`, `yarn.lock`, `.yarnrc.yml`, `.yarn/releases/`, - [ ] JS: `package.json`, `yarn.lock`, `.prettierrc`, `.prettierignore`
`.gitattributes`, `.prettierrc`, `.prettierignore` (fetch the last two (fetch from
from `https://git.eeqj.de/sneak/prompts/raw/branch/main/.prettierrc` `https://git.eeqj.de/sneak/prompts/raw/branch/main/.prettierrc` and
and `https://git.eeqj.de/sneak/prompts/raw/branch/main/.prettierignore`)
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.prettierignore`).
- [ ] Any repository with `yarn.lock`: that file is in yarn 4's format. If
its second line says `yarn lockfile v1`, convert it with the steps
below. Do them in one commit with the re-vendored scripts, because
yarn 1 ignores `--immutable` and exits 0 on a tree with no
`.yarnrc.yml`. Run them with any distribution's node and yarn, and use
`yarnpkg` on Debian and Ubuntu.
1. Read the version from `yarnPath` in
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.yarnrc.yml`.
2. Run `yarn set version <version>`. It writes
`.yarn/releases/yarn-<version>.cjs`, `yarnPath` in `.yarnrc.yml`, and
`packageManager` in `package.json`.
3. Add `nodeLinker: node-modules` and `enableTelemetry: false` to
`.yarnrc.yml`.
4. Run `yarn install` once. It converts `yarn.lock`, and each package
keeps its version and gains a `checksum`.
5. Delete `approvedGitRepositories`, `enableScripts` and
`npmMinimalAgeGate` from `.yarnrc.yml`. The conversion wrote them.
They allow install scripts, any git repository and packages of any
age, which yarn otherwise refuses.
6. In `package.json`, name any package that must run its install script,
as `"dependenciesMeta": { "<name>": { "built": true } }`, and run
`yarn install` again.
7. Add `.yarn/*` and `!.yarn/releases` above the secret patterns in
`.gitignore` and `.dockerignore`. Docker obeys the last matching
line, and the secret patterns must win. Add `.yarn/` to
`.prettierignore`, and `.yarn/releases/** binary` to
`.gitattributes`.
8. Run `yarn install --immutable`. It must pass and change nothing.
- [ ] Python: `pyproject.toml` - [ ] Python: `pyproject.toml`
- [ ] Docs/writing: `.prettierrc`, `.prettierignore` (same URLs as above) - [ ] Docs/writing: `.prettierrc`, `.prettierignore` (same URLs as above)
@@ -231,20 +185,13 @@ with your task.
`docker build` in `script/`. Without the bootstrap the CI run dies in `docker build` in `script/`. Without the bootstrap the CI run dies in
`script/fmt-check`, which runs the formatter on the host and finds nothing `script/fmt-check`, which runs the formatter on the host and finds nothing
installed. installed.
- [ ] `script/bootstrap` installs node, yarn and the node packages only when - [ ] `script/fmt` and `script/fmt-check` pick their `yarn` with the same test
`yarn.lock` is in the tree. node and yarn come from the distribution when as `script/bootstrap`: the `yarn` on `PATH` when the node on `PATH` has
they are missing. It installs the node packages with the pinned major version, and otherwise yarn under the pinned node, with
`yarn install --immutable`. The distribution's yarn only starts the nvm loaded through `$HOME/.nvm/nvm.sh`. `script/bootstrap` leaves the node
release file that `yarnPath` in `.yarnrc.yml` names, and that file does and yarn it installs under nvm off the `PATH` of the shell that called it,
the work. so a bare `yarn` exits 127 on a runner carrying nothing but docker and
- [ ] A `Dockerfile` stage that needs yarn may start from any distribution that git, or runs under another node.
packages node and yarn. It copies `package.json`, `yarn.lock`,
`.yarnrc.yml` and `.yarn/releases/` before it runs `script/bootstrap`. A
stage that needs no yarn copies none of them.
- [ ] `script/bootstrap`, `script/fmt` and `script/fmt-check` run yarn by
whichever name is present: `yarn`, or `yarnpkg` on Debian and Ubuntu. With
neither name on `PATH`, `script/fmt` and `script/fmt-check` print one line
saying to run `script/bootstrap`, and exit 1.
- [ ] `script/bootstrap` installs no linter of its own — delete the block, its - [ ] `script/bootstrap` installs no linter of its own — delete the block, its
version variables and its call site. A JS repo's `yarn install` stays; it version variables and its call site. A JS repo's `yarn install` stays; it
brings a linter along with every other dependency, and no verdict is taken brings a linter along with every other dependency, and no verdict is taken
@@ -268,20 +215,7 @@ 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. The one exception pinned by cryptographic hash with version/date comment
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, and the host's package manager stands for it, nix and brew
included. A package source that a `Dockerfile` adds itself does not get
this exception
- [ ] node, yarn, npm, python, pip and venv come from the distribution wherever
possible, never from nvm, pyenv, corepack or `npm install -g yarn`. If the
distribution's version is too old, use a newer distribution. node means
the distribution's newest Node package, such as `nodejs-current` on Alpine
3.24
- [ ] yarn's release file is in git under `.yarn/releases/`, written once by
`yarn set version` with an exact version (repositories with `yarn.lock`)
- [ ] Using `yarn`, not `npm` (JS projects) - [ ] Using `yarn`, not `npm` (JS projects)
# Directory Structure # Directory Structure
@@ -310,12 +244,11 @@ with your task.
# Final # Final
- [ ] `make check` passes - [ ] `make check` passes
- [ ] `script/cibuild` succeeds in a fresh clone on a host carrying what CI has. - [ ] `script/cibuild` succeeds in a fresh clone on a host carrying what CI has
That host is the runner image (the runner image `docker.gitea.com/runner-images:ubuntu-latest`: docker,
`docker.gitea.com/runner-images:ubuntu-latest`, which has docker, git and git and node 24, with no yarn on `PATH`), and demonstrably executed the
node 24. Take any yarn off `PATH` first. The run must show that the checks checks — a sub-second build, or `CACHED` on a step that lints or tests,
executed. A sub-second build means nothing ran. So does `CACHED` on a step one of the `RUN` steps below `ARG CACHEBUST`, means nothing ran. Above
that lints or tests, one of the `RUN` steps below `ARG CACHEBUST`. Above
that line, these steps are expected to show `CACHED`: that line, these steps are expected to show `CACHED`:
- the steps that install system packages - the steps that install system packages
- the steps that download Go's modules - the steps that download Go's modules
+70 -125
View File
@@ -1,6 +1,6 @@
--- ---
title: New Repo Checklist title: New Repo Checklist
last_modified: 2026-11-04 last_modified: 2026-10-26
--- ---
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
@@ -75,42 +75,34 @@ Template files can be fetched from:
anchored entry. anchored entry.
- If the image embeds a version in a binary: `.dockerignore` lets `.git` - If the image embeds a version in a binary: `.dockerignore` lets `.git`
into the build context. It keeps out every git `config` at any depth into the build context. It keeps out every git `config` at any depth
(`**/.git/config`, `**/.git/modules/**/config`): (`**/.git/config`, `**/.git/modules/**/config`): the repository's own,
- the repository's own each submodule's under `.git/modules/`, and that of a submodule keeping
- each submodule's under `.git/modules/` its own `.git` directory. `git describe` does not need them, and each can
- that of a submodule keeping its own `.git` directory hold a credential: a password in a remote URL, or the token the CI
checkout step stores there. A submodule whose name has a `config` segment
`git describe` does not need them, and each can hold a credential: a (`config`, `deploy/config`, `config/lib`) loses its whole git directory to
password in a remote URL, or the token the CI checkout step stores `**/.git/modules/**/config`, and Go's version stamping then fails the
there. Some submodule names have a `config` segment, as in `config`, build: give it a name without that segment (`git submodule add --name`).
`deploy/config` or `config/lib`. Such a submodule loses its whole git The stage that compiles has `git` (the Debian Go image has it; an alpine
directory to `**/.git/modules/**/config`, and Go's version stamping then one needs `apk add --no-cache git`) and takes the version from the
fails the build. Give it a name without that segment `VERSION` build argument when one is given, otherwise from
(`git submodule add --name`). The stage that compiles has `git`: the `git describe --tags --always`. That gives the tag on a tagged commit; on
Debian Go image has it, and an alpine one needs a later commit, the tag, the number of commits since it and the short
`apk add --no-cache git`. That stage takes the version from the commit (`v1.2.3-4-gabc1234`); and the short commit when no tag is
`VERSION` build argument when one is given, otherwise from reachable. The stage that compiles also marks its working directory safe
`git describe --tags --always`. That gives: for git (`git config --system --add safe.directory /src`): a context sent
- the tag, on a tagged commit as a tar stream keeps the sender's file owners, and git refuses a checkout
- the tag, the number of commits since it and the short commit owned by another user, so the version would come out empty. `ARG VERSION`
(`v1.2.3-4-gabc1234`), on a later commit has no default, and the build fails if the context carries `.git` and the
- the short commit, when no tag is reachable version still comes out empty, `dev` or `unknown`. A plain
`docker build .` with no build arguments must succeed; a Dockerfile that
The stage that compiles also marks its working directory safe for git refuses an empty build argument drops that refusal and keeps the argument.
(`git config --system --add safe.directory /src`). A context sent as a A checkout whose `.git` is a file (a linked worktree, or a repository
tar stream keeps the sender's file owners, and git refuses a checkout checked out as a submodule) is the exception: that file points to a git
owned by another user, so the version would come out empty. directory outside the build context, so the build cannot read the version
`ARG VERSION` has no default. The build fails if the context carries and a plain `docker build .` fails; pass the version with
`.git` and the version still comes out empty, `dev` or `unknown`. A `--build-arg VERSION=...`, as `script/docker` and `script/cibuild` already
plain `docker build .` with no build arguments must succeed; a do.
Dockerfile that refuses an empty build argument drops that refusal and
keeps the argument. A checkout whose `.git` is a file is the exception:
a linked worktree, or a repository checked out as a submodule. That file
points to a git directory outside the build context, so the build cannot
read the version and a plain `docker build .` fails. Pass the version
with `--build-arg VERSION=...`, as `script/docker` and `script/cibuild`
already do.
- The Dockerfile carries a `lint` phase and a `test` phase, each invoking - The Dockerfile carries a `lint` phase and a `test` phase, each invoking
its tool directly rather than through `make` or `script/`, and the final its tool directly rather than through `make` or `script/`, and the final
stage carries a `COPY --from=` of a harmless file from each so the image stage carries a `COPY --from=` of a harmless file from each so the image
@@ -123,50 +115,37 @@ Template files can be fetched from:
- the step that runs `script/bootstrap` to install a Python `test` - the step that runs `script/bootstrap` to install a Python `test`
phase's packages from `Pipfile.lock` phase's packages from `Pipfile.lock`
- A stage with none of these steps declares `ARG CACHEBUST` right after - A stage with none of these steps declares `ARG CACHEBUST` right after
`FROM`. A step that also installs anything else comes after `FROM`. A step that installs anything else as well, such as a
`ARG CACHEBUST` too. A `script/bootstrap` that also installs node packages `script/bootstrap` that also installs node packages, comes after it too.
is one. The `script/` builds pass a new `CACHEBUST` value on every build, The `script/` builds pass a new `CACHEBUST` value on every build, which
which reruns every `RUN` step after that line and leaves the layers above reruns every `RUN` step after that line and leaves the layers above it and
it and the cache mounts in place; `--no-cache` would build those layers the cache mounts in place; `--no-cache` would build those layers again and
again and empty the mounts. A stage without the `ARG` line is served from empty the mounts. A stage without the `ARG` line is served from cache on
cache on an unchanged tree, so the scripts and every stage's an unchanged tree, so the scripts and every stage's `ARG CACHEBUST` line
`ARG CACHEBUST` line are added in the same commit. are added in the same commit.
- Go: each Go stage downloads the modules (`COPY go.mod go.sum ./`, - Go: each Go stage downloads the modules (`COPY go.mod go.sum ./`,
`RUN go mod download`) above `ARG CACHEBUST`; the steps that compile `RUN go mod download`) above `ARG CACHEBUST`; the steps that compile
(`golangci-lint run`, `go test`, `go build`) mount Go's build cache (`golangci-lint run`, `go test`, `go build`) mount Go's build cache
(`--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`; a first run mounts anything else; both `go test` runs carry `-count=1`, as in the
that timed out gets no rerun, as in the canonical Go `Dockerfile` in canonical Go `Dockerfile` in `REPO_POLICIES.md`
`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
`script/bootstrap` runs in that step, it installs nothing beyond these: `script/bootstrap` runs in that step, it installs nothing beyond system
- system packages packages, pipenv and its dependencies, and the packages from
- pipenv and its dependencies `Pipfile.lock`. It installs pipenv and its dependencies by hash, with
- the packages from `Pipfile.lock` `pip install --require-hashes`. It installs the packages from
`Pipfile.lock` with `pipenv install --deploy`, plus `--dev` when the tests
It creates a virtual environment at `$HOME/.pipenv` with need the development packages. It installs no Python there, because the
`python3 -m venv` when it is missing, then uses that environment's `pip` tests use the base image's. The tests run after `ARG CACHEBUST`, and every
and `pipenv`. On a host, it first installs the distribution's python and other stage installs its Python packages after it, as in the Python `test`
venv packages when they are missing. It installs pipenv and its phase example in `REPO_POLICIES.md`
dependencies by hash, with `pip install --require-hashes`. It installs
the packages from `Pipfile.lock` with `pipenv install --deploy`, plus
`--dev` when the tests need the development packages. It sets
`PIP_ONLY_BINARY=:all:` for both installs, so a package with no wheel
for the image stops the install. In that step it 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`
- Server: the final stage builds and runs the application - Server: the final stage builds and runs the application
- Non-server: the final stage brings up the dev environment - Non-server: the final stage brings up the dev environment
- Image pinned by sha256 hash with version/date comment - Image pinned by sha256 hash with version/date comment
- [ ] Gitea Actions workflow at `.gitea/workflows/check.yml` that runs - [ ] Gitea Actions workflow at `.gitea/workflows/check.yml` that runs
`script/cibuild` on push, checks out with `persist-credentials: false` and `script/cibuild` on push, checks out with `persist-credentials: false` and
with `fetch-depth: 0` (which fetches the tags `git describe` needs), with `fetch-depth: 0` (which fetches the tags `git describe` needs),
@@ -179,22 +158,7 @@ Template files can be fetched from:
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.golangci.yml` and, `https://git.eeqj.de/sneak/prompts/raw/branch/main/.golangci.yml` and,
in the same commit, set the lint phase digest to the one named in the in the same commit, set the lint phase digest to the one named in the
`.golangci.yml` paragraph of `REPO_POLICIES.md`) `.golangci.yml` paragraph of `REPO_POLICIES.md`)
- [ ] JS, or any repository that runs prettier through yarn: make the first - [ ] JS: `yarn init`, `yarn add --dev prettier`
`yarn.lock` with the steps below. `script/bootstrap` installs no yarn
until `yarn.lock` exists, so run them with any distribution's node and
yarn, such as Alpine's (`apk add nodejs-current yarn`). Use `yarnpkg`
on Debian and Ubuntu.
1. Read the version from `yarnPath` in
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.yarnrc.yml`.
2. Run `yarn set version <version>`. It writes
`.yarn/releases/yarn-<version>.cjs`, `.yarnrc.yml` and
`package.json`.
3. Add `nodeLinker: node-modules` and `enableTelemetry: false` to
`.yarnrc.yml`.
4. Add `"private": true` to `package.json`.
5. Run `yarn add --dev prettier`. It writes `yarn.lock`.
6. Add `.yarn/releases/** binary` to `.gitattributes`.
7. Run `yarn install --immutable`. It must pass and change nothing.
- [ ] Python: `pyproject.toml` - [ ] Python: `pyproject.toml`
## Configure script/ Entrypoints and Makefile ## Configure script/ Entrypoints and Makefile
@@ -205,23 +169,14 @@ are thin shims calling them. Model scripts:
- [ ] scripts are POSIX sh (`#!/bin/sh`, `set -eu`, no bashisms) so they run on - [ ] scripts are POSIX sh (`#!/bin/sh`, `set -eu`, no bashisms) so they run on
alpine images without bash alpine images without bash
- [ ] `script/bootstrap` / `make bootstrap` installs all dependencies, - [ ] `script/bootstrap` / `make bootstrap` — installs all dependencies,
idempotently, and assumes nothing is present: idempotently, assuming nothing (pkg manager detection nix/apt/brew/apk;
- It detects the package manager: nix, apt, brew or apk. node used if its major version is the pinned one, else pinned version via
- It installs node, yarn and the node packages only when `yarn.lock` is in nvm from a hash-verified archive, with yarn and the packages installed
the tree. node and yarn come from that package manager when they are under it; pinned yarn via corepack); a non-server repo's development
missing. environment stage runs it instead of inline installs; a gate phase or the
- It installs the node packages with `yarn install --immutable`. The build stage installs what its base image lacks either inline or by running
distribution's yarn only starts the release file that `yarnPath` in it
`.yarnrc.yml` names, and that file does the work.
- A non-server repo's development environment stage runs it, with only
system packages installed inline above it.
- A gate phase or the build stage installs what its base image lacks, either
inline or by running it.
- [ ] A `Dockerfile` stage that needs yarn may start from any distribution that
packages node and yarn. It copies `package.json`, `yarn.lock`,
`.yarnrc.yml` and `.yarn/releases/` before it runs `script/bootstrap`. A
stage that needs no yarn copies none of them.
- [ ] `script/setup` / `make setup` — readies a fresh clone: runs `bootstrap`, - [ ] `script/setup` / `make setup` — readies a fresh clone: runs `bootstrap`,
then `install-precommit`, plus repo-specific init then `install-precommit`, plus repo-specific init
- [ ] `script/test` / `make test` — - [ ] `script/test` / `make test` —
@@ -251,10 +206,13 @@ are thin shims calling them. Model scripts:
from `script/projectname`, each assigned on its own line before the build. from `script/projectname`, each assigned on its own line before the build.
The bootstrap is required: CI checks out and runs this alone, and The bootstrap is required: CI checks out and runs this alone, and
`script/fmt-check` runs the formatter on the host. `script/fmt-check` runs the formatter on the host.
- [ ] `script/bootstrap`, `script/fmt` and `script/fmt-check` run yarn by - [ ] `script/fmt` and `script/fmt-check` pick their `yarn` with the same test
whichever name is present: `yarn`, or `yarnpkg` on Debian and Ubuntu. With as `script/bootstrap`: the `yarn` on `PATH` when the node on `PATH` has
neither name on `PATH`, `script/fmt` and `script/fmt-check` print one line the pinned major version, and otherwise yarn under the pinned node, with
saying to run `script/bootstrap`, and exit 1. nvm loaded through `$HOME/.nvm/nvm.sh`. `script/bootstrap` leaves the node
and yarn it installs under nvm off the `PATH` of the shell that called it,
so a bare `yarn` exits 127 on a runner carrying nothing but docker and
git, or runs under another node.
- [ ] No `docker build` in `script/` leaves a dangling image behind: - [ ] No `docker build` in `script/` leaves a dangling image behind:
`script/lint` and `script/test` write no image, and `script/docker` and `script/lint` and `script/test` write no image, and `script/docker` and
`script/cibuild` tag theirs `script/cibuild` tag theirs
@@ -269,12 +227,11 @@ are thin shims calling them. Model scripts:
- [ ] `make check` passes - [ ] `make check` passes
- [ ] `make docker` succeeds - [ ] `make docker` succeeds
- [ ] `script/cibuild` succeeds in a fresh clone on a host carrying what CI has. - [ ] `script/cibuild` succeeds in a fresh clone on a host carrying what CI has
That host is the runner image (the runner image `docker.gitea.com/runner-images:ubuntu-latest`: docker,
`docker.gitea.com/runner-images:ubuntu-latest`, which has docker, git and git and node 24, with no yarn on `PATH`), and demonstrably executed the
node 24. Take any yarn off `PATH` first. The run must show that the checks checks — a sub-second build, or `CACHED` on a step that lints or tests,
executed. A sub-second build means nothing ran. So does `CACHED` on a step one of the `RUN` steps below `ARG CACHEBUST`, means nothing ran. Above
that lints or tests, one of the `RUN` steps below `ARG CACHEBUST`. Above
that line, these steps are expected to show `CACHED`: that line, these steps are expected to show `CACHED`:
- the steps that install system packages - the steps that install system packages
- the steps that download Go's modules - the steps that download Go's modules
@@ -285,19 +242,7 @@ 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. The one exception is a package from - [ ] No mutable image/package references
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, and the host's
package manager stands for it, nix and brew included. A package source
that a `Dockerfile` adds itself does not get this exception
- [ ] node, yarn, npm, python, pip and venv come from the distribution wherever
possible, never from nvm, pyenv, corepack or `npm install -g yarn`. If the
distribution's version is too old, use a newer distribution. node means
the distribution's newest Node package, such as `nodejs-current` on Alpine
3.24
- [ ] yarn's release file is in git under `.yarn/releases/`, written once by
`yarn set version` with an exact version (repositories with `yarn.lock`)
- [ ] 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
+373 -620
View File
File diff suppressed because it is too large Load Diff
+96 -23
View File
@@ -1,17 +1,24 @@
#!/bin/sh #!/bin/sh
# script/bootstrap: install all dependencies needed to build and develop # script/bootstrap: install all dependencies needed to build and develop
# this repo. Idempotent: every install is guarded by a check so already # this repo. Idempotent: every install is guarded by a check so already
# installed tools are skipped. Base tools, node and yarn come from nix, # installed tools are skipped. Base tooling comes from nix, apt, brew,
# apt, brew, or apk (detected in that order), with no version and no # or apk (detected in that order); assumes nothing is present. The
# hash, because the distribution is trusted. It installs node, yarn and # installed node is used only when its major version is the pinned one;
# the node packages only when yarn.lock is in the tree. The # otherwise (node missing, or another major) the pinned version is
# distribution's yarn only starts the release file that yarnPath in # installed via nvm (installing nvm itself first, from a hash-verified
# .yarnrc.yml names, and that file does the work. So git pins the yarn # release archive, never curl | sh), and yarn and the packages are
# that runs. Assumes nothing is present. # installed under it.
set -eu set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
# Pinned versions, 2026-07-06
NODE_VERSION="22.17.0"
NVM_VERSION="0.40.3"
# sha256 of https://github.com/nvm-sh/nvm/archive/refs/tags/v0.40.3.tar.gz
NVM_SHA256="5f4d6aaa04a177dc93c985e31dbc411ab6b8c6e1e21d8015dbc1372625fcd1d0"
YARN_VERSION="1.22.22"
PKGMGR="" PKGMGR=""
SUDO="" SUDO=""
APT_UPDATED="" APT_UPDATED=""
@@ -60,33 +67,99 @@ missing() {
! command -v "$1" >/dev/null 2>&1 ! command -v "$1" >/dev/null 2>&1
} }
# Debian and Ubuntu name the yarn command yarnpkg. # verify_sha256 <file> <expected-hash>
run_yarn() { verify_sha256() {
if command -v yarn >/dev/null 2>&1; then if command -v sha256sum >/dev/null 2>&1; then
yarn "$@" actual="$(sha256sum "$1" | cut -d' ' -f1)"
elif command -v yarnpkg >/dev/null 2>&1; then
yarnpkg "$@"
else else
echo "no yarn; run script/bootstrap first" >&2 actual="$(shasum -a 256 "$1" | cut -d' ' -f1)"
fi
if [ "$actual" != "$2" ]; then
echo "bootstrap: sha256 mismatch for $1" >&2
echo " expected: $2" >&2
echo " actual: $actual" >&2
exit 1 exit 1
fi fi
} }
# nvm is a bash script; run a command in a bash with nvm loaded
nvm_sh() {
bash -c ". \"\$HOME/.nvm/nvm.sh\" && $*"
}
ensure_nvm() {
[ -s "$HOME/.nvm/nvm.sh" ] && return 0
# nvm prerequisites; nvm itself requires bash
if missing bash; then pkg_install bash bash bash bash; fi
if missing curl; then pkg_install curl curl curl curl; fi
if missing git; then pkg_install git git git git; fi
tmp="$(mktemp -d)"
curl -fsSL -o "$tmp/nvm.tar.gz" \
"https://github.com/nvm-sh/nvm/archive/refs/tags/v${NVM_VERSION}.tar.gz"
verify_sha256 "$tmp/nvm.tar.gz" "$NVM_SHA256"
mkdir -p "$HOME/.nvm"
tar -xzf "$tmp/nvm.tar.gz" -C "$HOME/.nvm" --strip-components=1
rm -rf "$tmp"
}
# True when the node on PATH has the pinned major version. Only the
# major is compared: the Dockerfile stages start from a node 22 alpine
# image whose exact version is not the pin, and nvm cannot install a
# prebuilt node on alpine. Another major is not used: the pinned yarn 1
# prints a deprecation warning under node 24. script/fmt and
# script/fmt-check carry this function unchanged, to run the yarn
# installed here.
node_is_pinned_major() {
if ! command -v node >/dev/null 2>&1; then return 1; fi
installed="$(node --version)"
installed="${installed#v}"
[ "${installed%%.*}" = "${NODE_VERSION%%.*}" ]
}
ensure_node() {
if node_is_pinned_major; then
echo "bootstrap: using node $(node --version)"
return 0
fi
ensure_nvm
nvm_sh "nvm install $NODE_VERSION"
nvm_sh "nvm use $NODE_VERSION >/dev/null && \
echo \"bootstrap: using node \$(node --version)\""
}
ensure_yarn() {
if ! node_is_pinned_major; then
nvm_sh "nvm use $NODE_VERSION >/dev/null && corepack enable && \
corepack prepare yarn@$YARN_VERSION --activate"
return 0
fi
if ! missing yarn; then return 0; fi
if ! missing corepack; then
corepack enable
corepack prepare "yarn@$YARN_VERSION" --activate
else
npm install -g "yarn@$YARN_VERSION"
fi
}
install_js_deps() {
if node_is_pinned_major; then
yarn install --frozen-lockfile
else
nvm_sh "nvm use $NODE_VERSION >/dev/null && cd \"$ROOT\" && \
yarn install --frozen-lockfile"
fi
}
main() { main() {
cd "$ROOT" cd "$ROOT"
if missing make; then pkg_install gnumake make make make; fi if missing make; then pkg_install gnumake make make make; fi
if missing git; then pkg_install git git git git; fi if missing git; then pkg_install git git git git; fi
# A Dockerfile stage that needs no yarn copies no yarn.lock. ensure_node
if [ -f yarn.lock ]; then ensure_yarn
if missing node; then pkg_install nodejs_latest nodejs node nodejs-current; fi install_js_deps
if missing yarn && missing yarnpkg; then
pkg_install yarn yarnpkg yarn yarn
fi
run_yarn install --immutable
fi
echo "bootstrap complete" echo "bootstrap complete"
} }
+24 -7
View File
@@ -4,16 +4,33 @@ set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
# Debian and Ubuntu name the yarn command yarnpkg. # Must match the pin in script/bootstrap.
NODE_VERSION="22.17.0"
# True when the node on PATH has the pinned major version: the same
# function as in script/bootstrap, which uses it to decide where it
# installs yarn.
node_is_pinned_major() {
if ! command -v node >/dev/null 2>&1; then return 1; fi
installed="$(node --version)"
installed="${installed#v}"
[ "${installed%%.*}" = "${NODE_VERSION%%.*}" ]
}
# Run the yarn script/bootstrap installed: the yarn on PATH when the
# node on PATH has the pinned major version, and otherwise yarn under
# the pinned node in nvm, which bootstrap leaves off the PATH of the
# shell that called it. nvm is a bash script, hence the subshell.
run_yarn() { run_yarn() {
if command -v yarn >/dev/null 2>&1; then if node_is_pinned_major && command -v yarn >/dev/null 2>&1; then
yarn "$@" exec yarn "$@"
elif command -v yarnpkg >/dev/null 2>&1; then fi
yarnpkg "$@" if node_is_pinned_major || [ ! -s "$HOME/.nvm/nvm.sh" ]; then
else echo "fmt: no yarn; run script/bootstrap first" >&2
echo "no yarn; run script/bootstrap first" >&2
exit 1 exit 1
fi fi
exec bash -c '. "$HOME/.nvm/nvm.sh" && nvm use "$1" >/dev/null &&
shift && exec yarn "$@"' bash "$NODE_VERSION" "$@"
} }
main() { main() {
+24 -7
View File
@@ -4,16 +4,33 @@ set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
# Debian and Ubuntu name the yarn command yarnpkg. # Must match the pin in script/bootstrap.
NODE_VERSION="22.17.0"
# True when the node on PATH has the pinned major version: the same
# function as in script/bootstrap, which uses it to decide where it
# installs yarn.
node_is_pinned_major() {
if ! command -v node >/dev/null 2>&1; then return 1; fi
installed="$(node --version)"
installed="${installed#v}"
[ "${installed%%.*}" = "${NODE_VERSION%%.*}" ]
}
# Run the yarn script/bootstrap installed: the yarn on PATH when the
# node on PATH has the pinned major version, and otherwise yarn under
# the pinned node in nvm, which bootstrap leaves off the PATH of the
# shell that called it. nvm is a bash script, hence the subshell.
run_yarn() { run_yarn() {
if command -v yarn >/dev/null 2>&1; then if node_is_pinned_major && command -v yarn >/dev/null 2>&1; then
yarn "$@" exec yarn "$@"
elif command -v yarnpkg >/dev/null 2>&1; then fi
yarnpkg "$@" if node_is_pinned_major || [ ! -s "$HOME/.nvm/nvm.sh" ]; then
else echo "fmt-check: no yarn; run script/bootstrap first" >&2
echo "no yarn; run script/bootstrap first" >&2
exit 1 exit 1
fi fi
exec bash -c '. "$HOME/.nvm/nvm.sh" && nvm use "$1" >/dev/null &&
shift && exec yarn "$@"' bash "$NODE_VERSION" "$@"
} }
main() { main() {
+6 -21
View File
@@ -1,23 +1,8 @@
# This file is generated by running "yarn install" inside your project. # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# Manual changes might be lost - proceed with caution! # yarn lockfile v1
__metadata:
version: 10
cacheKey: 10c0
"prettier@npm:3.8.1": prettier@3.8.1:
version: 3.8.1 version "3.8.1"
resolution: "prettier@npm:3.8.1" resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.8.1.tgz#edf48977cf991558f4fcbd8a3ba6015ba2a3a173"
bin: integrity sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==
prettier: bin/prettier.cjs
checksum: 10c0/33169b594009e48f570471271be7eac7cdcf88a209eed39ac3b8d6d78984039bfa9132f82b7e6ba3b06711f3bfe0222a62a1bfb87c43f50c25a83df1b78a2c42
languageName: node
linkType: hard
"root-workspace-0b6124@workspace:.":
version: 0.0.0-use.local
resolution: "root-workspace-0b6124@workspace:."
dependencies:
prettier: "npm:3.8.1"
languageName: unknown
linkType: soft