7 Commits
Author SHA1 Message Date
clawbot 9389bf0172 Commit yarn 4 in the tree and install with --immutable (closes #149)
check / check (push) Successful in 16s
Follows sneak's ruling at
#149 (comment). This
repository now carries yarn 4.18.1 under .yarn/releases. The
distribution's yarn only starts that file, and that file does the work.
yarn.lock is converted once, and script/bootstrap installs with
--immutable instead of stopping on yarn's major version. Node is the
distribution's newest Node package: nodejs-current on Alpine and
nodejs_latest on nix. Every Dockerfile stage that needs yarn also copies
.yarnrc.yml and .yarn/releases/. The policies and both checklists say
so, with the steps for converting and for a new repository.

Model: opus-5-5
2026-11-04 21:32:55 +01:00
clawbot 3066aae835 Allow go test on one package while iterating (closes #51)
check / check (push) Canceled after 0s
Follows sneak's ruling at
#51 (comment). 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. `go build`,
`go vet` and the linter still go through make targets. The full `make check`
is still required before every push. The `Makefile` gains nothing.

The rule on Docker now says the lint and test gates run there, because a run
while iterating is not a gate.

Model: opus-5-5
2026-11-04 19:32:37 +01:00
clawbot 86c7617f86 Install node, yarn and python from the distribution (closes #146)
check / check (push) Canceled after 0s
Follows sneak's ruling at
#146 (comment). Runtimes
and the installer tools bundled with python come from the distribution,
with no version and no hash. Third-party packages stay pinned by hash.

script/bootstrap drops nvm and corepack. It installs node, yarn and the
node packages only when yarn.lock is in the tree, and stops there when
yarn's major version is not 1. This repository's Dockerfile starts from
Alpine 3.24. The Python example starts from debian:trixie-slim, keeps
pipenv at $HOME/.pipenv on a host and in an image, and sets
PIP_ONLY_BINARY=:all:, so pip downloads no build tools without a hash
check.

Model: opus-5-5
2026-11-04 09:55:26 +01:00
clawbotandsneak b36ddacee1 Rewrite REPO_POLICIES.md sentence by sentence (closes #136) (#147)
check / check (push) Canceled after 0s
Rewrites every sentence of `prompts/REPO_POLICIES.md` against the sentence rules, for #136. No rule's meaning, `must`/`should` or order changed. Code blocks, digests, URLs and commands are byte-identical. Both checklists carry the same rewrite of the paragraph they quote.

## For sneak

Kept as they are: each looks false or contradicts another rule. Line numbers are this branch's.

- Lines 196-197: the final stage carries the `COPY --from=` lines; in the `Dockerfile` (345-346) the build stage does, the runtime stage none.
- Lines 629-641 and 670-675: `make test` runs `go test` and `pytest` on the host; 80-81 make targets shims, 183-185 make `script/test` a Docker build.
- Lines 23-37 pin by hash; 103-112 install node (nvm) and yarn (corepack) by version. Tracked at #146.
- Line 131 counts four extensions; `script/lint`, `script/fmt`, `script/fmt-check`, `script/docker` (85-89) are not in the standard either.
- Lines 149-151: the Makefile is the single source of truth; 80-81 (implementation in `script/`), 211 (tool invoked in a phase), 616 (test command in the `Dockerfile`).
- Lines 64-68 require `make test` and `make lint`, which 183-185 make Docker builds; line 167 only says a repo should have a `Dockerfile`.
- Line 759 says byte-identical; the block at 767 has `tag="$(script/projectname)"`, both scripts have `tag="$("$SCRIPT_DIR/projectname")"`.
- Lines 834-839: `script/bootstrap` never tests presence, 860 excepts only node; `ensure_yarn` in the canonical `script/bootstrap` skips the install when yarn is present, without a version check.

## Disclosures

- Judgement call: short name lists stay inline under about 15 words.

Model: fable-5-1
Co-authored-by: sneak <sneak@sneak.berlin>
Reviewed-on: #147
Co-authored-by: clawbot <35+clawbot@noreply.example.org>
2026-11-04 05:17:16 +01:00
clawbot 8400e4cd1f Update the canonical .golangci.yml for golangci-lint v2.15.0 (closes #142)
check / check (push) Canceled after 0s
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-04 01:28:09 +01: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
19 changed files with 2137 additions and 697 deletions
+6
View File
@@ -35,6 +35,12 @@
# `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,3 +2,6 @@
# 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
+5 -3
View File
@@ -19,10 +19,12 @@ Thumbs.db
# Node # Node
node_modules/ node_modules/
.yarn/*
!.yarn/releases
# Secrets. Unanchored like every entry above, so each matches at every # Secrets. Unanchored, so each matches at every depth. Matching is
# depth. Matching is case-sensitive on Linux, so names use character # case-sensitive on Linux, so names use character ranges rather than a
# ranges rather than a lowercase form that misses `Server.Key`. # 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
+22 -12
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,17 +19,15 @@ 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
# 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
@@ -42,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
@@ -57,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
@@ -87,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
@@ -110,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
+1
View File
@@ -1,2 +1,3 @@
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
@@ -0,0 +1,5 @@
enableTelemetry: false
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.18.1.cjs
+24 -13
View File
@@ -2,17 +2,24 @@
# 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.
# #
# Every stage declares ARG CACHEBUST first: script/ passes a new value on # In every stage, ARG CACHEBUST comes right after the step that installs
# every build, which reruns every RUN step after it. # system packages. script/ passes a new value on every build. That value
# 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.
# #
# node 22-alpine, 2026-02-22 # alpine 3.24.3, 2026-11-04
FROM node@sha256:e4bf2a82ad0a4037d28035ae71529873c069b13eb0455466ae0bc13363826e34 AS lint FROM alpine@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a 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 ./ COPY package.json yarn.lock .yarnrc.yml ./
COPY .yarn/releases/ .yarn/releases/
RUN script/bootstrap RUN script/bootstrap
COPY . . COPY . .
@@ -21,14 +28,16 @@ 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.
# #
# node 22-alpine, 2026-02-22 # alpine 3.24.3, 2026-11-04
FROM node@sha256:e4bf2a82ad0a4037d28035ae71529873c069b13eb0455466ae0bc13363826e34 AS test FROM alpine@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a 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 ./ COPY package.json yarn.lock .yarnrc.yml ./
COPY .yarn/releases/ .yarn/releases/
RUN script/bootstrap RUN script/bootstrap
COPY . . COPY . .
@@ -41,8 +50,9 @@ 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.
# #
# node 22-alpine, 2026-02-22 # alpine 3.24.3, 2026-11-04
FROM node@sha256:e4bf2a82ad0a4037d28035ae71529873c069b13eb0455466ae0bc13363826e34 FROM alpine@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a
RUN apk add --no-cache git make nodejs-current yarn
ARG CACHEBUST ARG CACHEBUST
WORKDIR /app WORKDIR /app
@@ -50,10 +60,11 @@ 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 installs all prerequisites. Manifests are copied # script/bootstrap finds the system packages above and installs the node
# first so that layer stays cached until dependencies change. # packages.
COPY script/ script/ COPY script/ script/
COPY package.json yarn.lock ./ COPY package.json yarn.lock .yarnrc.yml ./
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) - `script/bootstrap` — install all dependencies (`yarn install --immutable`)
- `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` declares `ARG CACHEBUST` right after `FROM` mounts. This repo's own `Dockerfile` installs git, make, node and yarn right
in every stage: each stage installs system packages and node packages together after `FROM` in every stage, above `ARG CACHEBUST`. `script/bootstrap` installs
in one `script/bootstrap` step, and check builds may keep system packages but the node packages below that line, because check builds keep system packages but
not node packages. not node packages.
## Rationale ## Rationale
+86
View File
@@ -21,6 +21,92 @@ 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
(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
+2 -1
View File
@@ -2,5 +2,6 @@
"private": true, "private": true,
"devDependencies": { "devDependencies": {
"prettier": "3.8.1" "prettier": "3.8.1"
} },
"packageManager": "yarn@4.18.1"
} }
+3 -2
View File
@@ -1,6 +1,6 @@
--- ---
title: Code Styleguide — JavaScript title: Code Styleguide — JavaScript
last_modified: 2026-02-22 last_modified: 2026-11-04
--- ---
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,7 +8,8 @@ last_modified: 2026-02-22
1. Use yarn for package management, avoid using npm. 1. Use yarn for package management, avoid using npm.
1. Use LTS node versions. 1. Install node from the distribution. If its version is too old, use a newer
distribution.
1. Use `prettier` for code formatting, with four spaces for indentation. 1. Use `prettier` for code formatting, with four spaces for indentation.
+135 -68
View File
@@ -1,6 +1,6 @@
--- ---
title: Existing Repo Checklist title: Existing Repo Checklist
last_modified: 2026-10-26 last_modified: 2026-11-04
--- ---
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,34 +56,45 @@ 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 installs anything else as well, such as a `FROM`. A step that also installs anything else comes after
`script/bootstrap` that also installs node packages, comes after it too. `ARG CACHEBUST` too. A `script/bootstrap` that also installs node packages
The `script/` builds pass a new `CACHEBUST` value on every build, which is one. The `script/` builds pass a new `CACHEBUST` value on every build,
reruns every `RUN` step after that line and leaves the layers above it and which reruns every `RUN` step after that line and leaves the layers above
the cache mounts in place; `--no-cache` would build those layers again and it and the cache mounts in place; `--no-cache` would build those layers
empty the mounts. A stage without the `ARG` line is served from cache on again and empty the mounts. A stage without the `ARG` line is served from
an unchanged tree, so the scripts and the `ARG` lines are re-vendored in cache on an unchanged tree, so the scripts and the `ARG` lines are
one commit. re-vendored in 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`, 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
`script/bootstrap` runs in that step, it installs nothing beyond system `script/bootstrap` runs in that step, it installs nothing beyond these:
packages, pipenv and its dependencies, and the packages from - system packages
`Pipfile.lock`. It installs pipenv and its dependencies by hash, with - pipenv and its dependencies
`pip install --require-hashes`. It installs the packages from - the packages from `Pipfile.lock`
`Pipfile.lock` with `pipenv install --deploy`, plus `--dev` when the tests
need the development packages. It installs no Python there, because the It creates a virtual environment at `$HOME/.pipenv` with `python3 -m venv`
tests use the base image's. The tests run after `ARG CACHEBUST`, and every when it is missing, then uses that environment's `pip` and `pipenv`. On a
other stage installs its Python packages after it, as in the Python `test` host, it first installs the distribution's python and venv packages when
phase example in `REPO_POLICIES.md` they are missing. It installs pipenv and its 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`
- [ ] 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
@@ -104,37 +115,43 @@ 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`): the repository's own, (`**/.git/config`, `**/.git/modules/**/config`):
each submodule's under `.git/modules/`, and that of a submodule keeping - the repository's own
its own `.git` directory. `git describe` does not need them, and each can - each submodule's under `.git/modules/`
hold a credential: a password in a remote URL, or the token the CI - that of a submodule keeping its own `.git` directory
checkout step stores there. A submodule whose name has a `config` segment
(`config`, `deploy/config`, `config/lib`) loses its whole git directory to `git describe` does not need them, and each can hold a credential: a
`**/.git/modules/**/config`, and Go's version stamping then fails the password in a remote URL, or the token the CI checkout step stores there.
build: give it a name without that segment (`git submodule add --name`). Some submodule names have a `config` segment, as in `config`,
The stage that compiles has `git` (the Debian Go image has it; an alpine `deploy/config` or `config/lib`. Such a submodule loses its whole git
one needs `apk add --no-cache git`) and takes the version from the directory to `**/.git/modules/**/config`, and Go's version stamping then
`VERSION` build argument when one is given, otherwise from fails the build. Give it a name without that segment
`git describe --tags --always`. That gives the tag on a tagged commit; on (`git submodule add --name`). The stage that compiles has `git`: the Debian
a later commit, the tag, the number of commits since it and the short Go image has it, and an alpine one needs `apk add --no-cache git`. That
commit (`v1.2.3-4-gabc1234`); and the short commit when no tag is stage takes the version from the `VERSION` build argument when one is given,
reachable. The stage that compiles also marks its working directory safe otherwise from `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 tar
A checkout whose `.git` is a file (a linked worktree, or a repository stream keeps the sender's file owners, and git refuses a checkout owned by
checked out as a submodule) is the exception: that file points to a git another user, so the version would come out empty. `ARG VERSION` has no
directory outside the build context, so the build cannot read the version default. The build fails if the context carries `.git` and the version still
and a plain `docker build .` fails; pass the version with comes out empty, `dev` or `unknown`. A plain `docker build .` with no build
`--build-arg VERSION=...`. `script/docker` and `script/cibuild` already arguments must succeed; a Dockerfile that refuses an empty build argument
pass the version they compute on the host; it takes precedence. The drops that refusal and keeps the argument. A checkout whose `.git` is a file
canonical `.gitea/workflows/check.yml` sets `fetch-depth: 0` on its is the exception: a linked worktree, or a repository checked out as a
checkout step, which otherwise clones shallow and fetches no tags, so a CI submodule. That file points to a git directory outside the build context, so
build finds the tag too. the build cannot read the version and a plain `docker build .` fails. Pass
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
@@ -147,10 +164,39 @@ 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`, `.prettierrc`, `.prettierignore` - [ ] JS: `package.json`, `yarn.lock`, `.yarnrc.yml`, `.yarn/releases/`,
(fetch from `.gitattributes`, `.prettierrc`, `.prettierignore` (fetch the last two
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.prettierrc` and from `https://git.eeqj.de/sneak/prompts/raw/branch/main/.prettierrc`
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.prettierignore`) and
`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)
@@ -185,13 +231,20 @@ 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/fmt` and `script/fmt-check` pick their `yarn` with the same test - [ ] `script/bootstrap` installs node, yarn and the node packages only when
as `script/bootstrap`: the `yarn` on `PATH` when the node on `PATH` has `yarn.lock` is in the tree. node and yarn come from the distribution when
the pinned major version, and otherwise yarn under the pinned node, with they are missing. It installs the node packages with
nvm loaded through `$HOME/.nvm/nvm.sh`. `script/bootstrap` leaves the node `yarn install --immutable`. The distribution's yarn only starts the
and yarn it installs under nvm off the `PATH` of the shell that called it, release file that `yarnPath` in `.yarnrc.yml` names, and that file does
so a bare `yarn` exits 127 on a runner carrying nothing but docker and the work.
git, or runs under another node. - [ ] 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/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
@@ -215,7 +268,20 @@ 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, 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
@@ -244,11 +310,12 @@ 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.
(the runner image `docker.gitea.com/runner-images:ubuntu-latest`: docker, That host is the runner image
git and node 24, with no yarn on `PATH`), and demonstrably executed the `docker.gitea.com/runner-images:ubuntu-latest`, which has docker, git and
checks — a sub-second build, or `CACHED` on a step that lints or tests, node 24. Take any yarn off `PATH` first. The run must show that the checks
one of the `RUN` steps below `ARG CACHEBUST`, means nothing ran. Above executed. A sub-second build means nothing ran. So does `CACHED` on a step
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
+125 -70
View File
@@ -1,6 +1,6 @@
--- ---
title: New Repo Checklist title: New Repo Checklist
last_modified: 2026-10-26 last_modified: 2026-11-04
--- ---
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,34 +75,42 @@ 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`): the repository's own, (`**/.git/config`, `**/.git/modules/**/config`):
each submodule's under `.git/modules/`, and that of a submodule keeping - the repository's own
its own `.git` directory. `git describe` does not need them, and each can - each submodule's under `.git/modules/`
hold a credential: a password in a remote URL, or the token the CI - that of a submodule keeping its own `.git` directory
checkout step stores there. A submodule whose name has a `config` segment
(`config`, `deploy/config`, `config/lib`) loses its whole git directory to `git describe` does not need them, and each can hold a credential: a
`**/.git/modules/**/config`, and Go's version stamping then fails the password in a remote URL, or the token the CI checkout step stores
build: give it a name without that segment (`git submodule add --name`). there. Some submodule names have a `config` segment, as in `config`,
The stage that compiles has `git` (the Debian Go image has it; an alpine `deploy/config` or `config/lib`. Such a submodule loses its whole git
one needs `apk add --no-cache git`) and takes the version from the directory to `**/.git/modules/**/config`, and Go's version stamping then
`VERSION` build argument when one is given, otherwise from fails the build. Give it a name without that segment
`git describe --tags --always`. That gives the tag on a tagged commit; on (`git submodule add --name`). The stage that compiles has `git`: the
a later commit, the tag, the number of commits since it and the short Debian Go image has it, and an alpine one needs
commit (`v1.2.3-4-gabc1234`); and the short commit when no tag is `apk add --no-cache git`. That stage takes the version from the
reachable. The stage that compiles also marks its working directory safe `VERSION` build argument when one is given, otherwise from
for git (`git config --system --add safe.directory /src`): a context sent `git describe --tags --always`. That gives:
as a tar stream keeps the sender's file owners, and git refuses a checkout - the tag, on a tagged commit
owned by another user, so the version would come out empty. `ARG VERSION` - the tag, the number of commits since it and the short commit
has no default, and the build fails if the context carries `.git` and the (`v1.2.3-4-gabc1234`), on a later commit
version still comes out empty, `dev` or `unknown`. A plain - the short commit, when no tag is reachable
`docker build .` with no build arguments must succeed; a Dockerfile that
refuses an empty build argument drops that refusal and keeps the argument. The stage that compiles also marks its working directory safe for git
A checkout whose `.git` is a file (a linked worktree, or a repository (`git config --system --add safe.directory /src`). A context sent as a
checked out as a submodule) is the exception: that file points to a git tar stream keeps the sender's file owners, and git refuses a checkout
directory outside the build context, so the build cannot read the version owned by another user, so the version would come out empty.
and a plain `docker build .` fails; pass the version with `ARG VERSION` has no default. The build fails if the context carries
`--build-arg VERSION=...`, as `script/docker` and `script/cibuild` already `.git` and the version still comes out empty, `dev` or `unknown`. A
do. plain `docker build .` with no build arguments must succeed; a
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
@@ -115,37 +123,50 @@ 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 installs anything else as well, such as a `FROM`. A step that also installs anything else comes after
`script/bootstrap` that also installs node packages, comes after it too. `ARG CACHEBUST` too. A `script/bootstrap` that also installs node packages
The `script/` builds pass a new `CACHEBUST` value on every build, which is one. The `script/` builds pass a new `CACHEBUST` value on every build,
reruns every `RUN` step after that line and leaves the layers above it and which reruns every `RUN` step after that line and leaves the layers above
the cache mounts in place; `--no-cache` would build those layers again and it and the cache mounts in place; `--no-cache` would build those layers
empty the mounts. A stage without the `ARG` line is served from cache on again and empty the mounts. A stage without the `ARG` line is served from
an unchanged tree, so the scripts and every stage's `ARG CACHEBUST` line cache on an unchanged tree, so the scripts and every stage's
are added in the same commit. `ARG CACHEBUST` line 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`, 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
`script/bootstrap` runs in that step, it installs nothing beyond system `script/bootstrap` runs in that step, it installs nothing beyond these:
packages, pipenv and its dependencies, and the packages from - system packages
`Pipfile.lock`. It installs pipenv and its dependencies by hash, with - pipenv and its dependencies
`pip install --require-hashes`. It installs the packages from - the packages from `Pipfile.lock`
`Pipfile.lock` with `pipenv install --deploy`, plus `--dev` when the tests
need the development packages. It installs no Python there, because the It creates a virtual environment at `$HOME/.pipenv` with
tests use the base image's. The tests run after `ARG CACHEBUST`, and every `python3 -m venv` when it is missing, then uses that environment's `pip`
other stage installs its Python packages after it, as in the Python `test` and `pipenv`. On a host, it first installs the distribution's python and
phase example in `REPO_POLICIES.md` venv packages when they are missing. It installs pipenv and its
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),
@@ -158,7 +179,22 @@ 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: `yarn init`, `yarn add --dev prettier` - [ ] JS, or any repository that runs prettier through yarn: make the first
`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
@@ -169,14 +205,23 @@ 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, assuming nothing (pkg manager detection nix/apt/brew/apk; idempotently, and assumes nothing is present:
node used if its major version is the pinned one, else pinned version via - It detects the package manager: nix, apt, brew or apk.
nvm from a hash-verified archive, with yarn and the packages installed - It installs node, yarn and the node packages only when `yarn.lock` is in
under it; pinned yarn via corepack); a non-server repo's development the tree. node and yarn come from that package manager when they are
environment stage runs it instead of inline installs; a gate phase or the missing.
build stage installs what its base image lacks either inline or by running - It installs the node packages with `yarn install --immutable`. The
it distribution's yarn only starts the release file that `yarnPath` in
`.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` —
@@ -206,13 +251,10 @@ 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/fmt` and `script/fmt-check` pick their `yarn` with the same test - [ ] `script/bootstrap`, `script/fmt` and `script/fmt-check` run yarn by
as `script/bootstrap`: the `yarn` on `PATH` when the node on `PATH` has whichever name is present: `yarn`, or `yarnpkg` on Debian and Ubuntu. With
the pinned major version, and otherwise yarn under the pinned node, with neither name on `PATH`, `script/fmt` and `script/fmt-check` print one line
nvm loaded through `$HOME/.nvm/nvm.sh`. `script/bootstrap` leaves the node saying to run `script/bootstrap`, and exit 1.
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
@@ -227,11 +269,12 @@ 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.
(the runner image `docker.gitea.com/runner-images:ubuntu-latest`: docker, That host is the runner image
git and node 24, with no yarn on `PATH`), and demonstrably executed the `docker.gitea.com/runner-images:ubuntu-latest`, which has docker, git and
checks — a sub-second build, or `CACHED` on a step that lints or tests, node 24. Take any yarn off `PATH` first. The run must show that the checks
one of the `RUN` steps below `ARG CACHEBUST`, means nothing ran. Above executed. A sub-second build means nothing ran. So does `CACHED` on a step
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
@@ -242,7 +285,19 @@ 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, 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
+658 -374
View File
File diff suppressed because it is too large Load Diff
+23 -96
View File
@@ -1,24 +1,17 @@
#!/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 tooling comes from nix, apt, brew, # installed tools are skipped. Base tools, node and yarn come from nix,
# or apk (detected in that order); assumes nothing is present. The # apt, brew, or apk (detected in that order), with no version and no
# installed node is used only when its major version is the pinned one; # hash, because the distribution is trusted. It installs node, yarn and
# otherwise (node missing, or another major) the pinned version is # the node packages only when yarn.lock is in the tree. The
# installed via nvm (installing nvm itself first, from a hash-verified # distribution's yarn only starts the release file that yarnPath in
# release archive, never curl | sh), and yarn and the packages are # .yarnrc.yml names, and that file does the work. So git pins the yarn
# installed under it. # that runs. Assumes nothing is present.
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=""
@@ -67,99 +60,33 @@ missing() {
! command -v "$1" >/dev/null 2>&1 ! command -v "$1" >/dev/null 2>&1
} }
# verify_sha256 <file> <expected-hash> # Debian and Ubuntu name the yarn command yarnpkg.
verify_sha256() { run_yarn() {
if command -v sha256sum >/dev/null 2>&1; then if command -v yarn >/dev/null 2>&1; then
actual="$(sha256sum "$1" | cut -d' ' -f1)" yarn "$@"
elif command -v yarnpkg >/dev/null 2>&1; then
yarnpkg "$@"
else else
actual="$(shasum -a 256 "$1" | cut -d' ' -f1)" echo "no yarn; run script/bootstrap first" >&2
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
ensure_node # A Dockerfile stage that needs no yarn copies no yarn.lock.
ensure_yarn if [ -f yarn.lock ]; then
install_js_deps if missing node; then pkg_install nodejs_latest nodejs node nodejs-current; fi
if missing yarn && missing yarnpkg; then
pkg_install yarn yarnpkg yarn yarn
fi
run_yarn install --immutable
fi
echo "bootstrap complete" echo "bootstrap complete"
} }
+7 -24
View File
@@ -4,33 +4,16 @@ set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
# Must match the pin in script/bootstrap. # Debian and Ubuntu name the yarn command yarnpkg.
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 node_is_pinned_major && command -v yarn >/dev/null 2>&1; then if command -v yarn >/dev/null 2>&1; then
exec yarn "$@" yarn "$@"
fi elif command -v yarnpkg >/dev/null 2>&1; then
if node_is_pinned_major || [ ! -s "$HOME/.nvm/nvm.sh" ]; then yarnpkg "$@"
echo "fmt: no yarn; run script/bootstrap first" >&2 else
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() {
+7 -24
View File
@@ -4,33 +4,16 @@ set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
# Must match the pin in script/bootstrap. # Debian and Ubuntu name the yarn command yarnpkg.
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 node_is_pinned_major && command -v yarn >/dev/null 2>&1; then if command -v yarn >/dev/null 2>&1; then
exec yarn "$@" yarn "$@"
fi elif command -v yarnpkg >/dev/null 2>&1; then
if node_is_pinned_major || [ ! -s "$HOME/.nvm/nvm.sh" ]; then yarnpkg "$@"
echo "fmt-check: no yarn; run script/bootstrap first" >&2 else
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() {
+21 -6
View File
@@ -1,8 +1,23 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. # This file is generated by running "yarn install" inside your project.
# yarn lockfile v1 # Manual changes might be lost - proceed with caution!
__metadata:
version: 10
cacheKey: 10c0
prettier@3.8.1: "prettier@npm:3.8.1":
version "3.8.1" version: 3.8.1
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.8.1.tgz#edf48977cf991558f4fcbd8a3ba6015ba2a3a173" resolution: "prettier@npm:3.8.1"
integrity sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg== bin:
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