1 Commits
Author SHA1 Message Date
sneak 75c4011a2e Rewrite REPO_POLICIES.md sentence by sentence (closes #136)
check / check (push) Successful in 33s
Every sentence of prompts/REPO_POLICIES.md is rewritten against the
sentence rules: one idea per sentence, about 30 words at most, lists as
bulleted lists, subject and verb early, the main rule before any
exception. No rule's meaning changed: nothing is added, removed,
loosened or tightened, every reason stays, and code blocks, digests,
URLs, file names, commands and quoted strings are unchanged. Both repo
checklists carry the same rewrite of the sentences they quote word for
word.

Model: fable-5-1
2026-11-04 01:55:14 +00:00
18 changed files with 325 additions and 1495 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
-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
-44
View File
@@ -21,50 +21,6 @@ fmt-check, and commit.
# Completed Steps # Completed Steps
- 2026-11-06: The pinning rule in `prompts/REPO_POLICIES.md` now opens with its
scope: what we download and install ourselves in scripts and Dockerfiles
(https://git.eeqj.de/sneak/prompts/issues/154). Distribution packages are
outside the rule, and updating and upgrading them is allowed. Both checklists
say the same.
- 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 - 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), on sneak's yes at
https://git.eeqj.de/sneak/prompts/issues/136#issuecomment-165193. Every rule https://git.eeqj.de/sneak/prompts/issues/136#issuecomment-165193. Every rule
+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.
+27 -78
View File
@@ -1,6 +1,6 @@
--- ---
title: Existing Repo Checklist title: Existing Repo Checklist
last_modified: 2026-11-06 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
@@ -81,17 +81,11 @@ with your task.
- pipenv and its dependencies - pipenv and its dependencies
- the packages from `Pipfile.lock` - the packages from `Pipfile.lock`
It creates a virtual environment at `$HOME/.pipenv` with `python3 -m venv` It installs pipenv and its dependencies by hash, with
when it is missing, then uses that environment's `pip` and `pipenv`. On a
host, it first installs the distribution's python and 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` `pip install --require-hashes`. It installs the packages from `Pipfile.lock`
with `pipenv install --deploy`, plus `--dev` when the tests need the with `pipenv install --deploy`, plus `--dev` when the tests need the
development packages. It sets `PIP_ONLY_BINARY=:all:` for both installs, so development packages. It installs no Python there, because the tests use the
a package with no wheel for the image stops the install. In that step it base image's. The tests run after `ARG CACHEBUST`, and every other stage
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 installs its Python packages after it, as in the Python `test` phase example
in `REPO_POLICIES.md` in `REPO_POLICIES.md`
@@ -164,39 +158,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 +196,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
@@ -267,21 +225,13 @@ 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 (tags, `@latest`) to what the repo downloads and - [ ] No mutable references in Dockerfiles or scripts (tags, `@latest`) — all
installs itself in scripts and Dockerfiles — all pinned by cryptographic pinned by cryptographic hash with version/date comment. The one exception
hash with version/date comment. Distribution packages are outside this is a package from the distribution's own package index, which needs no
rule. They need no version and no hash, and updating and upgrading them is version and no hash. Pulling that index is also allowed. In a
fine too. In a `Dockerfile`, the distribution is the pinned base image's. `Dockerfile`, the distribution is the pinned base image's. On a host, it
On a host, it is the host's, and the host's package manager stands for it, is the host's. A package source that a `Dockerfile` adds itself does not
nix and brew included. A package source that a `Dockerfile` adds itself is get this exception
not the distribution's own, so its packages are pinned
- [ ] 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 +260,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
+34 -72
View File
@@ -1,6 +1,6 @@
--- ---
title: New Repo Checklist title: New Repo Checklist
last_modified: 2026-11-06 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
@@ -148,20 +148,13 @@ Template files can be fetched from:
- pipenv and its dependencies - pipenv and its dependencies
- the packages from `Pipfile.lock` - the packages from `Pipfile.lock`
It creates a virtual environment at `$HOME/.pipenv` with It installs pipenv and its dependencies by hash, with
`python3 -m venv` when it is missing, then uses that environment's `pip` `pip install --require-hashes`. It installs the packages from
and `pipenv`. On a host, it first installs the distribution's python and `Pipfile.lock` with `pipenv install --deploy`, plus `--dev` when the
venv packages when they are missing. It installs pipenv and its tests need the development packages. It installs no Python there,
dependencies by hash, with `pip install --require-hashes`. It installs because the tests use the base image's. The tests run after
the packages from `Pipfile.lock` with `pipenv install --deploy`, plus `ARG CACHEBUST`, and every other stage installs its Python packages
`--dev` when the tests need the development packages. It sets after it, as in the Python `test` phase example in `REPO_POLICIES.md`
`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
@@ -179,22 +172,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 +183,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 +220,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 +241,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,20 +256,11 @@ are thin shims calling them. Model scripts:
final stage is missing its `COPY --from=` edge to the gate phases. final stage is missing its `COPY --from=` edge to the gate phases.
- [ ] No secrets in repo, and none in the build context: enumerate a probe image - [ ] No secrets in repo, and none in the build context: enumerate a probe image
rather than reading `.dockerignore` rather than reading `.dockerignore`
- [ ] No mutable references to what the repo downloads and installs itself in - [ ] No mutable image/package references. The one exception is a package from
scripts and Dockerfiles. Distribution packages are outside this rule. They the distribution's own package index, which needs no version and no hash.
need no version and no hash, and updating and upgrading them is fine too. Pulling that index is also allowed. In a `Dockerfile`, the distribution is
In a `Dockerfile`, the distribution is the pinned base image's. On a host, the pinned base image's. On a host, it is the host's. A package source
it is the host's, and the host's package manager stands for it, nix and that a `Dockerfile` adds itself does not get this exception
brew included. A package source that a `Dockerfile` adds itself is not the
distribution's own, so its packages are pinned
- [ ] 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
+91 -190
View File
@@ -1,6 +1,6 @@
--- ---
title: Repository Policies title: Repository Policies
last_modified: 2026-11-06 last_modified: 2026-11-04
--- ---
This document covers repository structure, tooling, and workflow standards. Code This document covers repository structure, tooling, and workflow standards. Code
@@ -21,68 +21,46 @@ style conventions are in separate documents:
with the authoritative source as policies evolve. with the authoritative source as policies evolve.
- **External references must be pinned by cryptographic hash.** This is the - **External references must be pinned by cryptographic hash.** This is the
single most important rule in this document. It covers what we download and single most important rule in this document. It covers everything fetched from
install ourselves in scripts and Dockerfiles: a remote source, including:
- Docker base images - Docker base images
- Go modules - Go modules
- Python packages from PyPI
- npm packages - npm packages
- GitHub Actions - GitHub Actions
- release archives fetched with `curl` or `wget`
Packages from the distribution's own package manager are outside this rule.
They need no version and no hash, because the distribution is trusted. In a
`Dockerfile`, it is the pinned base image's distribution. On a host, it is
the host's. The host's package manager stands for its distribution, and that
includes nix and brew. Updating the package index and upgrading installed
packages are fine too. This command, for example, is allowed:
```sh
apt update && apt upgrade -y && apt install -y build-essential
```
A package source that a `Dockerfile` adds itself is not the distribution's
own, so its packages are pinned like any other download.
A version tag such as `@v4`, `@latest` or `:3.21` is server-mutable, and A version tag such as `@v4`, `@latest` or `:3.21` is server-mutable, and
therefore a remote code execution vulnerability. Reference an external therefore a remote code execution vulnerability. Reference an external
dependency by its content hash: dependency by its content hash:
- Docker: `@sha256:...` - Docker: `@sha256:...`
- Go: the module hash in `go.sum` - Go: the module hash in `go.sum`
- Python: the package hash in `Pipfile.lock` - npm: the integrity hash in the lockfile
- npm: the `checksum` in `yarn.lock`
- GitHub Actions: `@<commit-sha>` - GitHub Actions: `@<commit-sha>`
This also means never `curl | bash` to install a tool such as rustup. This also means never `curl | bash` to install tools like pyenv, nvm,
Instead, download a specific release archive from GitHub, verify its hash, rustup, etc. Instead, download a specific release archive from GitHub,
and only then install. The hash is hardcoded in the Dockerfile or script. verify its hash, and only then install. The hash is hardcoded in the
Unverified install scripts are arbitrary remote code execution. Double-check Dockerfile or script. Unverified install scripts are arbitrary remote code
every external reference in every file before committing. execution. Double-check every external reference in every file before
committing.
These come from the distribution wherever possible: There is one exception: a package from the distribution's own package index
- node, meaning the distribution's newest Node package, such as needs no version and no hash. Pulling that index is also allowed. The
`nodejs-current` on Alpine 3.24 distribution is trusted. In a `Dockerfile`, it is the pinned base image's
- yarn distribution. On a host, it is the host's. These commands are examples:
- npm - `apt-get update`
- python - `apt-get install`
- pip and venv, the installer tools bundled with python - `apk update`
- `apk add`
If the distribution's version is too old, use a newer distribution, never a
download. That rules out all of these:
- nvm
- pyenv
- corepack
- `npm install -g yarn`
yarn's release file is third-party code, and git pins it. `yarn set version`
writes it to `.yarn/releases/` once, given an exact version.
These are not trusted, so everything from them stays pinned by hash: These are not trusted, so everything from them stays pinned by hash:
- Docker Hub - Docker Hub
- PyPI - PyPI
- the npm registry - npm
- `go get` - `go get`
A package source that a `Dockerfile` adds itself is not the distribution's
own, so the exception does not cover it.
- Every repo with software must have a root `Makefile` with these targets: - Every repo with software must have a root `Makefile` with these targets:
- `make bootstrap` - `make bootstrap`
- `make setup` - `make setup`
@@ -121,58 +99,18 @@ style conventions are in separate documents:
initialization. initialization.
`script/bootstrap` installs all dependencies idempotently and assumes `script/bootstrap` installs all dependencies idempotently and assumes
nothing is present. Base tools, node and yarn come from nix, apt, brew, or nothing is present. Base tools come from nix, apt, brew, or apk, detected in
apk, detected in that order; apt runs noninteractive. Each one is installed that order; apt runs noninteractive. For node it uses the installed node
only when it is missing. A tool from outside the distribution is never only when its major version is the pinned one. When node is missing or has
installed as "latest" or "lts"; it is always an exact version. another major, it installs the PINNED node version via nvm, and installs
yarn and the packages under it. The CI runner image ships node 24, for
`script/bootstrap` installs node, yarn and the node packages only when example. nvm itself is installed first if missing, from a hash-verified
`yarn.lock` is in the tree. With no `yarn.lock`, it installs no node and no GitHub release archive, never `curl | sh`. bash is installed as an explicit
yarn. It installs the node packages with `yarn install --immutable`. That prerequisite, since nvm requires bash. Only the major version is compared
install fails when a package's `checksum` is wrong, or when `package.json` because the `Dockerfile` stages start from a node image whose exact version
is out of step with `yarn.lock`. is not the pin, and nvm cannot install a prebuilt node on alpine. yarn is
pinned via `corepack prepare yarn@<version> --activate`. Never install
Each repository commits the yarn that runs. `yarnPath` in `.yarnrc.yml` "latest" or "lts"; always exact versions.
points at the release file in `.yarn/releases/`. The distribution's yarn
only starts that file, and that file does the work. Alpine packages yarn 1,
and Debian 13 packages yarn 4.1. The same committed yarn does the work on
both. Corepack never runs, and no yarn is downloaded.
`.yarnrc.yml` holds these settings:
- `yarnPath`
- `nodeLinker: node-modules`
- `enableTelemetry: false`, because yarn otherwise sends usage reports from
any host that is not CI
By default, yarn runs no dependency's install script. A dependency that must
run one is named in `package.json`, as
`"dependenciesMeta": { "<name>": { "built": true } }`.
`.gitattributes` carries `.yarn/releases/** binary`, so diffs and merges
treat the release file as one opaque file.
yarn 1 ignores `--immutable` and exits 0, so on a tree with no `.yarnrc.yml`
the install is not frozen. A repository therefore re-vendors
`script/bootstrap` and converts its `yarn.lock` in one commit.
Debian and Ubuntu name the yarn command `yarnpkg`. `script/bootstrap`,
`script/fmt` and `script/fmt-check` each carry the same small function that
runs whichever name is present. With neither name on `PATH`, `script/fmt`
and `script/fmt-check` print one line saying to run `script/bootstrap`, and
exit 1.
A `Dockerfile` stage that needs yarn may start from any distribution that
packages node and yarn. It copies all of these before it runs
`script/bootstrap`:
- `package.json`
- `yarn.lock`
- `.yarnrc.yml`
- `.yarn/releases/`
With only `package.json` and `yarn.lock`, the install fails. Alpine's yarn 1
then says to enable corepack; never do that, and copy the missing files
instead. The Python and Go stages below need no yarn and copy no
`yarn.lock`, so `script/bootstrap` installs no node and no yarn there.
`script/cibuild` runs the CI build, and the Gitea workflow calls it. The `script/cibuild` runs the CI build, and the Gitea workflow calls it. The
script changes to the repo root, runs `script/bootstrap`, runs script changes to the repo root, runs `script/bootstrap`, runs
@@ -180,7 +118,15 @@ style conventions are in separate documents:
runs `script/bootstrap` first**, because the workflow checks out the repo runs `script/bootstrap` first**, because the workflow checks out the repo
and runs nothing else, while `script/fmt-check` runs the formatter on the and runs nothing else, while `script/fmt-check` runs the formatter on the
host. On a pristine checkout with nothing installed, the run dies there, host. On a pristine checkout with nothing installed, the run dies there,
after the containerised gates have passed. after the containerised gates have passed. **The bootstrap alone is not
enough.** When `script/bootstrap` installs node and yarn under nvm, it
leaves neither on the `PATH` of the shell that called it. A bare `yarn` then
either exits 127 or runs under another node. The host entrypoints that need
yarn, `script/fmt` and `script/fmt-check`, therefore use the same test as
`script/bootstrap`. When the node on `PATH` has the pinned major version,
they run the `yarn` on `PATH`. Otherwise they load nvm through
`$HOME/.nvm/nvm.sh` and run yarn under the pinned node. A runner carrying
nothing but docker and git then gets through `script/check`.
Four further scripts are our own extensions to the standard: Four further scripts are our own extensions to the standard:
- `script/check` runs `script/test`, `script/lint` and `script/fmt-check`. - `script/check` runs `script/test`, `script/lint` and `script/fmt-check`.
@@ -200,16 +146,9 @@ style conventions are in separate documents:
README must document the provided scripts in an **Entrypoints** section (see README must document the provided scripts in an **Entrypoints** section (see
the README requirements below). the README requirements below).
- Run the repo's own entrypoints, never the underlying tools directly. The - Always use Makefile targets (`make fmt`, `make test`, `make lint`, etc.)
`script/` scripts are the single source of truth for how each operation runs. instead of invoking the underlying tools directly. The Makefile is the single
Calling them directly is fine, and so is calling the Makefile targets. The source of truth for how these operations are run.
Makefile targets must still exist, as the thin shims described above.
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. This is
the only exception to this rule. `go build`, `go vet` and the linter still
go through the entrypoints. A run while iterating is not a gate, so the full
`make check` is still required before every push.
- The Makefile is authoritative documentation for how the repo is used. Beyond - The Makefile is authoritative documentation for how the repo is used. Beyond
the required targets above, it should have targets for every common operation: the required targets above, it should have targets for every common operation:
@@ -236,16 +175,14 @@ style conventions are in separate documents:
does for its yarn packages does for its yarn packages
The development environment stage installs development prerequisites by The development environment stage installs development prerequisites by
running `script/bootstrap`. Only system packages may be installed inline running `script/bootstrap` rather than duplicating its installs inline. A
above it, so that check builds keep them; the `prompts` repo's own stage that runs `script/bootstrap` COPYs `script/` and the dependency
`Dockerfile` does this. A stage that runs `script/bootstrap` COPYs `script/` manifests (`package.json` + `yarn.lock`, `go.mod` + `go.sum`, etc.) before
and the dependency manifests it needs before running it. These are examples: running it.
- `package.json`, `yarn.lock`, `.yarnrc.yml` and `.yarn/releases/`
- `go.mod` and `go.sum`
- **The lint and test gates run in Docker, as phases of the `Dockerfile`.** - **Linting and testing run in Docker, as phases of the `Dockerfile`.** There is
There is no separate lint file. `script/lint` and `script/test` each build one no separate lint file. `script/lint` and `script/test` each build one phase
phase and nothing else: and nothing else:
```sh ```sh
docker build --build-arg CACHEBUST="$(date +%s)-$$" \ docker build --build-arg CACHEBUST="$(date +%s)-$$" \
@@ -554,18 +491,10 @@ style conventions are in separate documents:
- pipenv and its dependencies - pipenv and its dependencies
- the packages from `Pipfile.lock` - the packages from `Pipfile.lock`
`script/bootstrap` installs no Python there: the tests use the `script/bootstrap` installs no Python there: the tests use the base
distribution's Python, which a step above it installs. If it installs image's Python, which the image's hash pins. If it installs anything
anything else there, the step goes after `ARG CACHEBUST`. else there, the step goes after `ARG CACHEBUST`.
- Debian's Python refuses a system-wide `pip install`, so pipenv goes in a
virtual environment at `$HOME/.pipenv`. `script/bootstrap` creates it with
`python3 -m venv` when it is missing, then uses that environment's `pip`
and `pipenv`. On a host, it first installs the distribution's python and
venv packages when they are missing. One script then works on a host and
in a `Dockerfile`. `Pipfile` names no `python_version`, because
`pipenv install --deploy` stops when the distribution's Python differs
from it.
- PyPI is not trusted, so `script/bootstrap` installs pipenv and its - PyPI is not trusted, so `script/bootstrap` installs pipenv and its
dependencies by hash. It does so with `pip install --require-hashes -r` dependencies by hash. It does so with `pip install --require-hashes -r`
and a requirements file in `script/` that lists each of them with `==` and and a requirements file in `script/` that lists each of them with `==` and
@@ -575,65 +504,37 @@ style conventions are in separate documents:
- installs only what `Pipfile.lock` names - installs only what `Pipfile.lock` names
- checks every package against the lockfile's hashes - checks every package against the lockfile's hashes
- fails when the lockfile is out of date with `Pipfile` - fails when the lockfile is out of date with `Pipfile`
- `script/bootstrap` sets `PIP_ONLY_BINARY=:all:` for both installs, and
pipenv passes it on to pip. A package with no ready-built wheel for the
image then stops the install. Without the setting, pip builds that package
from source and downloads the build tools without a hash check.
- A kept layer holds nothing a new install would refuse: pip checked the - A kept layer holds nothing a new install would refuse: pip checked the
hashes of pipenv and its dependencies, and pipenv checked the hashes of hashes of pipenv and its dependencies, and pipenv checked the hashes of
the lockfile's packages. pipenv creates its virtual environment with the the lockfile's packages. pipenv creates its virtual environment with the
`virtualenv` package. The packages that `virtualenv` adds come from wheels `virtualenv` package. The packages that `virtualenv` adds come from wheels
inside that package, so the hash pip checked for it covers them. System inside that package, so the hash pip checked for it covers them. Two
packages, Python among them, are not fixed by a hash. They keep the things in the layer are not fixed by a hash:
versions the package manager gave when the layer was built; a new install - A package with no ready-built wheel for the image is built from
may get newer ones. source. pip downloads the build tools without a hash check, so a kept
layer's build of such a package may differ from a new one.
- System packages keep the versions the package manager gave when the
layer was built; a new install may get newer ones.
- As with Go's modules, the packages are kept in a layer, not a cache mount. - As with Go's modules, the packages are kept in a layer, not a cache mount.
A cache mount is shared by every build on the host, and nothing checks its A cache mount is shared by every build on the host, and nothing checks its
files again. files again.
A Python repository's `script/bootstrap` adds these lines in `main`, above In the example below, `script/bootstrap` does all of these:
the last `echo`. The `ensurepip` test fails when python is missing, and also - finds `make` and `git` already installed
when Debian's venv package is missing: - installs pipenv and its dependencies by hash
- runs `pipenv install --dev --deploy`
```sh
if [ ! -x "$HOME/.pipenv/bin/pip" ]; then
if ! python3 -m ensurepip --version >/dev/null 2>&1; then
pkg_install python3 python3-venv python3 python3
fi
python3 -m venv "$HOME/.pipenv"
fi
export PIP_ONLY_BINARY=:all:
"$HOME/.pipenv/bin/pip" install --require-hashes -r script/requirements.txt
"$HOME/.pipenv/bin/pipenv" install --dev --deploy
```
Make `script/requirements.txt` once, with
`echo pipenv | pip-compile --generate-hashes --allow-unsafe -o script/requirements.txt -`.
`pip-compile` comes from Alpine's `py3-pip-tools` package; Debian and Ubuntu
do not package `pip-tools`. Make the first `Pipfile.lock` with
`$HOME/.pipenv/bin/pipenv lock`, after the `pip install` line has installed
pipenv.
In the example below, `script/bootstrap` finds `make`, `git`, python and
venv already installed. The example copies no `yarn.lock`, so
`script/bootstrap` installs no node and no yarn.
```dockerfile ```dockerfile
# Test phase. Both pytest runs share one 90-second timeout, so a hang # Test phase. Both pytest runs share one 90-second timeout, so a hang
# uses it once and gets no rerun. # uses it once and gets no rerun.
# debian:trixie-slim, YYYY-MM-DD # python:3.x-slim, YYYY-MM-DD
FROM debian@sha256:... AS test FROM python@sha256:... AS test
# Python, pip and venv come from the distribution, which is trusted, so # make and git are base tools that script/bootstrap installs when they
# they need no version and no hash. make and git are base tools that # are missing. Installed here, they stay in their own layer when
# script/bootstrap installs when they are missing. Installed here, they # script/ changes.
# stay in their own layer when script/ changes.
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates make git \ && apt-get install -y --no-install-recommends make git \
python3 python3-venv \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# script/bootstrap installs pipenv in a virtual environment at
# $HOME/.pipenv, and the tests below run that pipenv.
ENV PATH=/root/.pipenv/bin:$PATH
WORKDIR /app WORKDIR /app
COPY script/ script/ COPY script/ script/
COPY Pipfile Pipfile.lock ./ COPY Pipfile Pipfile.lock ./
@@ -768,16 +669,16 @@ style conventions are in separate documents:
```makefile ```makefile
test: test:
@timeout 90 sh -c 'pipenv run pytest || { \ @timeout 90 sh -c 'python -m pytest || { \
echo "--- Rerunning with -v for details ---"; \ echo "--- Rerunning with -v for details ---"; \
pipenv run pytest -v; exit 1; }' python -m pytest -v; exit 1; }'
``` ```
Both pytest runs share that one 90-second timeout, as in the `Dockerfile` Both pytest runs share that one 90-second timeout, as in the `Dockerfile`
example above, so a hang uses it once and gets no rerun. Ctrl-C does not example above, so a hang uses it once and gets no rerun. Ctrl-C does not
stop a running `make test`, which goes on until the tests end or the 90 stop a running `make test`, which goes on until the tests end or the 90
seconds run out. On a Mac without `timeout`, the target fails until it is seconds run out. On a Mac without `timeout`, the target fails until it is
installed. On a host, the target needs `$HOME/.pipenv/bin` on `PATH`. installed.
The `exit 1` ensures the target always fails after a rerun. The first run The `exit 1` ensures the target always fails after a rerun. The first run
already proved the tests are broken, so the build must not pass even if a already proved the tests are broken, so the build must not pass even if a
@@ -801,7 +702,6 @@ style conventions are in separate documents:
- editor files (`.swp`, `*~`) - editor files (`.swp`, `*~`)
- in-repo agent scratch directories (`.claude/`) - in-repo agent scratch directories (`.claude/`)
- `node_modules/` - `node_modules/`
- `.yarn/*`, with `!.yarn/releases` so that git keeps yarn's release file
- the repo's own build outputs - the repo's own build outputs
Fetch the standard `.gitignore` from Fetch the standard `.gitignore` from
@@ -823,16 +723,16 @@ style conventions are in separate documents:
Such a `.dockerignore` is more dangerous than a short file with no secret Such a `.dockerignore` is more dangerous than a short file with no secret
patterns at all, because it reads as solved and stops anyone looking. Give patterns at all, because it reads as solved and stops anyone looking. Give
every depth-independent pattern the `**/` prefix, and leave only genuinely every depth-independent pattern the `**/` prefix, and leave only genuinely
root-anchored entries unprefixed. Those are `.claude`, `.yarn/*`, root-anchored entries unprefixed. Those are `.claude`, and the repo's own
`!.yarn/releases`, and the repo's own host-built binary. The binary is written host-built binary. The binary is written `/myapp` and never `**/myapp`, which
`/myapp` and never `**/myapp`, which would also match `cmd/myapp/` and delete would also match `cmd/myapp/` and delete the package directory from the
the package directory from the context. Matching is case-sensitive, and an context. Matching is case-sensitive, and an all-caps copy of each pattern
all-caps copy of each pattern still misses `Server.Key`. So secret names use still misses `Server.Key`. So secret names use character ranges:
character ranges: `**/*.[kK][eE][yY]`, `**/*.[pP][eE][mM]`, and likewise for `**/*.[kK][eE][yY]`, `**/*.[pP][eE][mM]`, and likewise for `.envrc` and the
`.envrc` and the extensionless SSH keys. Where such a pattern also catches extensionless SSH keys. Where such a pattern also catches something the build
something the build needs, re-include it with a negation needs, re-include it with a negation (`!docs/example.env`). Deleting the
(`!docs/example.env`). Deleting the pattern reopens the exposure for every pattern reopens the exposure for every other file it covers. Fetch the
other file it covers. Fetch the standard `.dockerignore` from standard `.dockerignore` from
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.dockerignore` and extend `https://git.eeqj.de/sneak/prompts/raw/branch/main/.dockerignore` and extend
it with the repo's own artifacts. it with the repo's own artifacts.
@@ -957,8 +857,10 @@ style conventions are in separate documents:
function defined and never invoked has the same exit status and the same function defined and never invoked has the same exit status and the same
empty output as one that worked. empty output as one that worked.
A tool from the distribution has no pin, so `script/bootstrap` installs it Node is the exception to the whole-token comparison. The canonical
when it is missing. make, git, node and yarn are such tools. `script/bootstrap` compares only node's major version. That is because the
`Dockerfile` stages start from a node 22 alpine image, which is not the
exact pin, and nvm cannot install a prebuilt node on alpine.
Keep it POSIX sh: no arrays, no `[[`, no `grep -P`. Keep it POSIX sh: no arrays, no `[[`, no `grep -P`.
@@ -1136,8 +1038,7 @@ style conventions are in separate documents:
- `Dockerfile`, `.dockerignore` - `Dockerfile`, `.dockerignore`
- `.gitea/workflows/check.yml` - `.gitea/workflows/check.yml`
- Go: `go.mod`, `go.sum`, `.golangci.yml` - Go: `go.mod`, `go.sum`, `.golangci.yml`
- JS: `package.json`, `yarn.lock`, `.yarnrc.yml`, `.yarn/releases/`, - JS: `package.json`, `yarn.lock`, `.prettierrc`, `.prettierignore`
`.gitattributes`, `.prettierrc`, `.prettierignore`
- Python: `pyproject.toml` - Python: `pyproject.toml`
- Guidance for coding agents lives in one `AGENTS.md` at the repository root. It - Guidance for coding agents lives in one `AGENTS.md` at the repository root. It
+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