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
This commit was merged in pull request #151.
This commit is contained in:
2026-11-04 21:32:55 +01:00
parent 3066aae835
commit 9389bf0172
15 changed files with 1209 additions and 80 deletions
+59 -29
View File
@@ -35,7 +35,7 @@ style conventions are in separate documents:
- Docker: `@sha256:...`
- 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>`
This also means never `curl | bash` to install a tool such as rustup.
@@ -56,7 +56,8 @@ style conventions are in separate documents:
- `apk add`
These come from the distribution wherever possible:
- node
- node, meaning the distribution's newest Node package, such as
`nodejs-current` on Alpine 3.24
- yarn
- npm
- python
@@ -69,6 +70,9 @@ style conventions are in separate documents:
- 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:
- Docker Hub
- PyPI
@@ -123,9 +127,32 @@ style conventions are in separate documents:
`script/bootstrap` installs node, yarn and the node packages only when
`yarn.lock` is in the tree. With no `yarn.lock`, it installs no node and no
yarn, and it does not check yarn's version. Make a repository's first
`yarn.lock` with yarn 1, from a distribution that packages it, such as
Alpine (`apk add nodejs yarn`).
yarn. It installs the node packages with `yarn install --immutable`. That
install fails when a package's `checksum` is wrong, or when `package.json`
is out of step with `yarn.lock`.
Each repository commits the yarn that runs. `yarnPath` in `.yarnrc.yml`
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
@@ -133,18 +160,18 @@ style conventions are in separate documents:
and `script/fmt-check` print one line saying to run `script/bootstrap`, and
exit 1.
With `yarn.lock` in the tree, `script/bootstrap` stops with a one-line
message when yarn's major version is not 1, before it installs any node
packages. Debian 13 and Ubuntu 26.04 package yarn 4. With
`--frozen-lockfile`, yarn 4 refuses a yarn 1 `yarn.lock`. Without that flag,
it rewrites the file in its own format, with yarn 4 hashes in place of the
yarn 1 ones. A host whose distribution packages yarn 4 moves to one that
packages yarn 1, such as Alpine or Ubuntu 24.04.
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/`
A `Dockerfile` stage copies `yarn.lock` before it runs `script/bootstrap`
only if that stage needs yarn. A stage that needs yarn starts from an image
whose distribution packages yarn 1, such as Alpine. The Python and Go stages
below need no yarn and copy no `yarn.lock`, so they never reach the stop.
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 changes to the repo root, runs `script/bootstrap`, runs
@@ -210,8 +237,9 @@ style conventions are in separate documents:
running `script/bootstrap`. Only system packages may be installed inline
above it, so that check builds keep them; the `prompts` repo's own
`Dockerfile` does this. A stage that runs `script/bootstrap` COPYs `script/`
and the dependency manifests it needs (`package.json` + `yarn.lock`,
`go.mod` + `go.sum`, etc.) before running it.
and the dependency manifests it needs before running it. These are examples:
- `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`.**
There is no separate lint file. `script/lint` and `script/test` each build one
@@ -771,6 +799,7 @@ style conventions are in separate documents:
- editor files (`.swp`, `*~`)
- in-repo agent scratch directories (`.claude/`)
- `node_modules/`
- `.yarn/*`, with `!.yarn/releases` so that git keeps yarn's release file
- the repo's own build outputs
Fetch the standard `.gitignore` from
@@ -792,16 +821,16 @@ style conventions are in separate documents:
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
every depth-independent pattern the `**/` prefix, and leave only genuinely
root-anchored entries unprefixed. Those are `.claude`, and the repo's own
host-built binary. The binary is written `/myapp` and never `**/myapp`, which
would also match `cmd/myapp/` and delete the package directory from the
context. Matching is case-sensitive, and an all-caps copy of each pattern
still misses `Server.Key`. So secret names use character ranges:
`**/*.[kK][eE][yY]`, `**/*.[pP][eE][mM]`, and likewise for `.envrc` and the
extensionless SSH keys. Where such a pattern also catches something the build
needs, re-include it with a negation (`!docs/example.env`). Deleting the
pattern reopens the exposure for every other file it covers. Fetch the
standard `.dockerignore` from
root-anchored entries unprefixed. Those are `.claude`, `.yarn/*`,
`!.yarn/releases`, and the repo's own host-built binary. The binary is written
`/myapp` and never `**/myapp`, which would also match `cmd/myapp/` and delete
the package directory from the context. Matching is case-sensitive, and an
all-caps copy of each pattern still misses `Server.Key`. So secret names use
character ranges: `**/*.[kK][eE][yY]`, `**/*.[pP][eE][mM]`, and likewise for
`.envrc` and the extensionless SSH keys. Where such a pattern also catches
something the build needs, re-include it with a negation
(`!docs/example.env`). Deleting the pattern reopens the exposure for every
other file it covers. Fetch the standard `.dockerignore` from
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.dockerignore` and extend
it with the repo's own artifacts.
@@ -1105,7 +1134,8 @@ style conventions are in separate documents:
- `Dockerfile`, `.dockerignore`
- `.gitea/workflows/check.yml`
- Go: `go.mod`, `go.sum`, `.golangci.yml`
- JS: `package.json`, `yarn.lock`, `.prettierrc`, `.prettierignore`
- JS: `package.json`, `yarn.lock`, `.yarnrc.yml`, `.yarn/releases/`,
`.gitattributes`, `.prettierrc`, `.prettierignore`
- Python: `pyproject.toml`
- Guidance for coding agents lives in one `AGENTS.md` at the repository root. It