Author SHA1 Message Date
sneak ae926cf2b6 Install node, yarn and python from the distribution (closes #146)
check / check (push) Successful in 22s
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 08:21:17 +00:00
15 changed files with 1128 additions and 2206 deletions
-6
View File
@@ -35,12 +35,6 @@
# `services/api/.claude/` and must add its own anchored entry.
.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`
# convention. Re-include a committed template with a negation if the
# 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
# the merged entries after every merge or rebase.
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_modules/
.yarn/*
!.yarn/releases
# Secrets. Unanchored, so each matches at every depth. Matching is
# case-sensitive on Linux, so names use character ranges rather than a
# lowercase form that misses `Server.Key`.
# Secrets. Unanchored like every entry above, so each matches at every
# depth. Matching is case-sensitive on Linux, so names use character
# ranges rather than a lowercase form that misses `Server.Key`.
# Environment files. `*.env` covers bare `.env` and the `prod.env`
# convention. Only the templates `example.env` and `sample.env` are
-1
View File
@@ -1,3 +1,2 @@
node_modules/
.yarn/
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
+7 -12
View File
@@ -6,20 +6,17 @@
# 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.
# so they need no version and no hash.
#
# alpine 3.24.3, 2026-11-04
FROM alpine@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a AS lint
RUN apk add --no-cache git make nodejs-current yarn
RUN apk add --no-cache git make nodejs yarn
ARG CACHEBUST
WORKDIR /app
COPY script/ script/
COPY package.json yarn.lock .yarnrc.yml ./
COPY .yarn/releases/ .yarn/releases/
COPY package.json yarn.lock ./
RUN script/bootstrap
COPY . .
@@ -30,14 +27,13 @@ RUN yarn run prettier --check '**/*.md' --tab-width 4 --prose-wrap always
#
# alpine 3.24.3, 2026-11-04
FROM alpine@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a AS test
RUN apk add --no-cache git make nodejs-current yarn
RUN apk add --no-cache git make nodejs yarn
ARG CACHEBUST
WORKDIR /app
COPY script/ script/
COPY package.json yarn.lock .yarnrc.yml ./
COPY .yarn/releases/ .yarn/releases/
COPY package.json yarn.lock ./
RUN script/bootstrap
COPY . .
@@ -52,7 +48,7 @@ RUN echo "No tests defined."
#
# alpine 3.24.3, 2026-11-04
FROM alpine@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a
RUN apk add --no-cache git make nodejs-current yarn
RUN apk add --no-cache git make nodejs yarn
ARG CACHEBUST
WORKDIR /app
@@ -63,8 +59,7 @@ COPY --from=test /app/package.json /dev/null
# script/bootstrap finds the system packages above and installs the node
# packages.
COPY script/ script/
COPY package.json yarn.lock .yarnrc.yml ./
COPY .yarn/releases/ .yarn/releases/
COPY package.json yarn.lock ./
RUN script/bootstrap
COPY . .
+1 -1
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
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/bootstrap`, then `script/install-precommit`
- `script/projectname` — output the project name (our own extension); used by
-34
View File
@@ -21,40 +21,6 @@ fmt-check, and commit.
# Completed Steps
- 2026-11-06: Compacted `prompts/REPO_POLICIES.md` from 1,145 lines and 8,427
words to 1,066 lines and 7,812 words
(https://git.eeqj.de/sneak/prompts/issues/158). Every rule keeps its meaning.
The rules now sit under headings, the pinning rule first. Each rule that was
stated in several places is stated once. The Makefile copies of the Go and
Python test commands are gone, because the `Dockerfile` examples carry the
same commands. Both checklists name the `.golangci.yml` section.
- 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
+1 -2
View File
@@ -2,6 +2,5 @@
"private": true,
"devDependencies": {
"prettier": "3.8.1"
},
"packageManager": "yarn@4.18.1"
}
}
+21 -55
View File
@@ -1,6 +1,6 @@
---
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
@@ -163,40 +163,13 @@ with your task.
- [ ] Go: `go.mod`, `go.sum`, `.golangci.yml` (fetch from
`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
`.golangci.yml` section of `REPO_POLICIES.md`)
- [ ] JS: `package.json`, `yarn.lock`, `.yarnrc.yml`, `.yarn/releases/`,
`.gitattributes`, `.prettierrc`, `.prettierignore` (fetch the last two
from `https://git.eeqj.de/sneak/prompts/raw/branch/main/.prettierrc`
and
`.golangci.yml` paragraph of `REPO_POLICIES.md`)
- [ ] JS: `package.json`, `yarn.lock`, `.prettierrc`, `.prettierignore`
(fetch from
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.prettierrc` 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.
Make the first `yarn.lock` with yarn 1, from a distribution that
packages it, such as Alpine (`apk add nodejs yarn`).
- [ ] Python: `pyproject.toml`
- [ ] Docs/writing: `.prettierrc`, `.prettierignore` (same URLs as above)
@@ -233,14 +206,11 @@ with your task.
installed.
- [ ] `script/bootstrap` installs node, yarn and the node packages only when
`yarn.lock` is in the tree. node and yarn come from the distribution when
they are missing. It installs the node packages with
`yarn install --immutable`. The distribution's yarn only starts the
release file that `yarnPath` in `.yarnrc.yml` names, and that file does
the work.
- [ ] 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.
they are missing. With `yarn.lock` in the tree, `script/bootstrap` stops
with a one-line message when yarn's major version is not 1.
- [ ] 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.
- [ ] `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
@@ -267,21 +237,17 @@ with your task.
- [ ] Pre-commit hook is installed (`make hooks`)
- [ ] No secrets in the repo (`.env`, keys, credentials)
- [ ] No mutable references (tags, `@latest`) to what the repo downloads and
installs itself in scripts and Dockerfiles — all pinned by cryptographic
hash with version/date comment. Distribution packages are outside this
rule. They need no version and no hash, and updating and upgrading them is
fine too. 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 is
not the distribution's own, so its packages are pinned
- [ ] No mutable references in Dockerfiles or scripts (tags, `@latest`) — all
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`)
distribution's version is too old, use a newer distribution
- [ ] Using `yarn`, not `npm` (JS projects)
# Directory Structure
+17 -37
View File
@@ -1,6 +1,6 @@
---
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
@@ -178,23 +178,10 @@ Template files can be fetched from:
- [ ] Go: `go mod init sneak.berlin/go/<name>`, `.golangci.yml` (fetch from
`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
`.golangci.yml` section of `REPO_POLICIES.md`)
- [ ] 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.
`.golangci.yml` paragraph of `REPO_POLICIES.md`)
- [ ] JS: `yarn init`, `yarn add --dev prettier`. `script/bootstrap`
installs no yarn until `yarn.lock` exists, so use yarn 1 from a
distribution that packages it, such as Alpine (`apk add nodejs yarn`).
- [ ] Python: `pyproject.toml`
## Configure script/ Entrypoints and Makefile
@@ -211,17 +198,15 @@ are thin shims calling them. Model scripts:
- It installs node, yarn and the node packages only when `yarn.lock` is in
the tree. node and yarn come from that package manager when they are
missing.
- It installs the node packages with `yarn install --immutable`. The
distribution's yarn only starts the release file that `yarnPath` in
`.yarnrc.yml` names, and that file does the work.
- With `yarn.lock` in the tree, it stops with a one-line message when yarn's
major version is not 1.
- 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.
- [ ] 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.
- [ ] `script/setup` / `make setup` — readies a fresh clone: runs `bootstrap`,
then `install-precommit`, plus repo-specific init
- [ ] `script/test` / `make test` —
@@ -285,20 +270,15 @@ are thin shims calling them. Model scripts:
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
rather than reading `.dockerignore`
- [ ] No mutable references to what the repo downloads and installs itself in
scripts and Dockerfiles. Distribution packages are outside this rule. They
need no version and no hash, and updating and upgrading them is fine too.
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 is not the
distribution's own, so its packages are pinned
- [ ] 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`)
distribution's version is too old, use a newer distribution
- [ ] No unnecessary files in repo root
- [ ] All dates written as YYYY-MM-DD
+1059 -1018
View File
File diff suppressed because it is too large Load Diff
+13 -6
View File
@@ -4,10 +4,8 @@
# installed tools are skipped. Base tools, node and yarn come from nix,
# apt, brew, or apk (detected in that order), with no version and no
# hash, because the distribution is trusted. It installs node, yarn and
# the node packages only when yarn.lock is in the tree. The
# distribution's yarn only starts the release file that yarnPath in
# .yarnrc.yml names, and that file does the work. So git pins the yarn
# that runs. Assumes nothing is present.
# the node packages only when yarn.lock is in the tree. Assumes nothing
# is present.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
@@ -80,12 +78,21 @@ main() {
# A Dockerfile stage that needs no yarn copies no yarn.lock.
if [ -f yarn.lock ]; then
if missing node; then pkg_install nodejs_latest nodejs node nodejs-current; fi
if missing node; then pkg_install nodejs nodejs node nodejs; fi
if missing yarn && missing yarnpkg; then
pkg_install yarn yarnpkg yarn yarn
fi
run_yarn install --immutable
# Debian 13 and Ubuntu 26.04 package yarn 4, which refuses a yarn 1
# yarn.lock, or rewrites it in its own format.
yarn_version="$(run_yarn --version)"
if [ "${yarn_version%%.*}" != "1" ]; then
echo "bootstrap: yarn $yarn_version refuses this yarn 1 yarn.lock;" \
"use a distribution that packages yarn 1" >&2
exit 1
fi
run_yarn install --frozen-lockfile
fi
echo "bootstrap complete"
+6 -21
View File
@@ -1,23 +1,8 @@
# This file is generated by running "yarn install" inside your project.
# Manual changes might be lost - proceed with caution!
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
__metadata:
version: 10
cacheKey: 10c0
"prettier@npm:3.8.1":
version: 3.8.1
resolution: "prettier@npm:3.8.1"
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
prettier@3.8.1:
version "3.8.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.8.1.tgz#edf48977cf991558f4fcbd8a3ba6015ba2a3a173"
integrity sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==