1 Commits
Author SHA1 Message Date
sneak b5521672c8 Install node, yarn and python from the distribution (closes #146)
check / check (push) Canceled after 0s
Follows sneak's ruling at
#146 (comment). Runtimes
and the installer tools bundled with python come from the distribution,
with no version and no hash. Third-party packages stay pinned by hash.

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

Model: opus-5-5
2026-11-04 06:24:24 +00:00
9 changed files with 157 additions and 79 deletions
+4 -4
View File
@@ -3,10 +3,10 @@
# recurse into a daemon that does not exist in a build step.
#
# In every stage, ARG CACHEBUST comes right after the step that installs
# system packages: script/ passes a new value on every build, which
# reruns every RUN step after it and keeps that step's layer. node and
# yarn come from the distribution, which is trusted, so they need no
# version and no hash.
# 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.
#
# alpine 3.24.3, 2026-11-04
FROM alpine@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a AS lint
+4
View File
@@ -28,10 +28,14 @@ fmt-check, and commit.
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
+2 -2
View File
@@ -1,6 +1,6 @@
---
title: Code Styleguide — JavaScript
last_modified: 2026-02-22
last_modified: 2026-11-04
---
1. Use `const` for everything. If you need to reassign, use `let`. Never use
@@ -8,7 +8,7 @@ last_modified: 2026-02-22
1. Use yarn for package management, avoid using npm.
1. Use LTS node versions.
1. Install node from the distribution, in whatever version it packages.
1. Use `prettier` for code formatting, with four spaces for indentation.
+25 -14
View File
@@ -81,15 +81,19 @@ with your task.
- pipenv and its dependencies
- the packages from `Pipfile.lock`
It installs pipenv and its dependencies by hash, with
It creates a virtual environment at `$HOME/.pipenv` 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. It installs pipenv and its dependencies by hash, with
`pip install --require-hashes`. It installs the packages from `Pipfile.lock`
with `pipenv install --deploy`, plus `--dev` when the tests need the
development packages. It sets `PIP_ONLY_BINARY=:all:` for both installs, so
a package with no wheel for the image stops the install. It installs no
Python there, because the tests use the distribution's, which a step above
it installs. 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`
it installs. `Pipfile` names no `python_version`, because `--deploy` stops
when the distribution's Python differs from it. The tests run after
`ARG CACHEBUST`, and every other stage installs its Python packages after
it, as in the Python `test` phase example in `REPO_POLICIES.md`
- [ ] The gate phases invoke their tools directly, never through `make lint` or
`script/test` — those are themselves a `docker build` and would recurse
@@ -198,11 +202,17 @@ with your task.
`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
installed.
- [ ] `script/bootstrap` installs node and yarn from the distribution when they
are missing, and stops with a one-line message 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. node and yarn come from the distribution when
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, which today means Alpine.
- [ ] `script/bootstrap`, `script/fmt` and `script/fmt-check` run yarn by
whichever name is present: `yarn`, or `yarnpkg` on Debian and Ubuntu.
whichever name is present: `yarn`, or `yarnpkg` on Debian and Ubuntu. With
neither name on `PATH`, they print one line saying to run
`script/bootstrap`, and exit 1.
- [ ] `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
brings a linter along with every other dependency, and no verdict is taken
@@ -264,12 +274,13 @@ with your task.
# Final
- [ ] `make check` passes
- [ ] `script/cibuild` succeeds in a fresh clone on a host carrying what CI has
(the runner image `docker.gitea.com/runner-images:ubuntu-latest`: docker,
git and node 24), with any yarn first taken off `PATH`, and demonstrably
executed the checks — a sub-second build, or `CACHED` on a step that lints
or tests, one of the `RUN` steps below `ARG CACHEBUST`, means nothing ran.
Above that line, these steps are expected to show `CACHED`:
- [ ] `script/cibuild` succeeds in a fresh clone on a host carrying what CI has.
That host is the runner image
`docker.gitea.com/runner-images:ubuntu-latest`, which has docker, git and
node 24. Take any yarn off `PATH` first. The run must show that the checks
executed. A sub-second build means nothing ran. So does `CACHED` on a step
that lints or tests, one of the `RUN` steps below `ARG CACHEBUST`. Above
that line, these steps are expected to show `CACHED`:
- the steps that install system packages
- the steps that download Go's modules
- the step that runs `script/bootstrap` to install a Python `test` phase's
+37 -21
View File
@@ -148,13 +148,18 @@ Template files can be fetched from:
- pipenv and its dependencies
- the packages from `Pipfile.lock`
It installs pipenv and its dependencies by hash, with
`pip install --require-hashes`. It installs the packages from
`Pipfile.lock` with `pipenv install --deploy`, plus `--dev` when the
tests need the development packages. It sets `PIP_ONLY_BINARY=:all:` for
both installs, so a package with no wheel for the image stops the
install. It installs no Python there, because the tests use the
distribution's, which a step above it installs. The tests run after
It creates a virtual environment at `$HOME/.pipenv` 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. It installs pipenv and its
dependencies by hash, with `pip install --require-hashes`. It installs
the packages from `Pipfile.lock` with `pipenv install --deploy`, plus
`--dev` when the tests need the development packages. It sets
`PIP_ONLY_BINARY=:all:` for both installs, so a package with no wheel
for the image stops the install. It installs no Python there, because
the tests use the distribution's, which a step above it installs.
`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`
@@ -185,13 +190,21 @@ are thin shims calling them. Model scripts:
- [ ] scripts are POSIX sh (`#!/bin/sh`, `set -eu`, no bashisms) so they run on
alpine images without bash
- [ ] `script/bootstrap` / `make bootstrap` — installs all dependencies,
idempotently, assuming nothing (pkg manager detection nix/apt/brew/apk;
node and yarn from that package manager when they are missing; a one-line
message and a stop 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
- [ ] `script/bootstrap` / `make bootstrap` installs all dependencies,
idempotently, and assumes nothing is present:
- It detects the package manager: nix, apt, brew or apk.
- 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.
- 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 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, which today means Alpine.
- [ ] `script/setup` / `make setup` — readies a fresh clone: runs `bootstrap`,
then `install-precommit`, plus repo-specific init
- [ ] `script/test` / `make test` —
@@ -222,7 +235,9 @@ are thin shims calling them. Model scripts:
The bootstrap is required: CI checks out and runs this alone, and
`script/fmt-check` runs the formatter on the host.
- [ ] `script/bootstrap`, `script/fmt` and `script/fmt-check` run yarn by
whichever name is present: `yarn`, or `yarnpkg` on Debian and Ubuntu.
whichever name is present: `yarn`, or `yarnpkg` on Debian and Ubuntu. With
neither name on `PATH`, they print one line saying to run
`script/bootstrap`, and exit 1.
- [ ] No `docker build` in `script/` leaves a dangling image behind:
`script/lint` and `script/test` write no image, and `script/docker` and
`script/cibuild` tag theirs
@@ -237,12 +252,13 @@ are thin shims calling them. Model scripts:
- [ ] `make check` passes
- [ ] `make docker` succeeds
- [ ] `script/cibuild` succeeds in a fresh clone on a host carrying what CI has
(the runner image `docker.gitea.com/runner-images:ubuntu-latest`: docker,
git and node 24), with any yarn first taken off `PATH`, and demonstrably
executed the checks — a sub-second build, or `CACHED` on a step that lints
or tests, one of the `RUN` steps below `ARG CACHEBUST`, means nothing ran.
Above that line, these steps are expected to show `CACHED`:
- [ ] `script/cibuild` succeeds in a fresh clone on a host carrying what CI has.
That host is the runner image
`docker.gitea.com/runner-images:ubuntu-latest`, which has docker, git and
node 24. Take any yarn off `PATH` first. The run must show that the checks
executed. A sub-second build means nothing ran. So does `CACHED` on a step
that lints or tests, one of the `RUN` steps below `ARG CACHEBUST`. Above
that line, these steps are expected to show `CACHED`:
- the steps that install system packages
- the steps that download Go's modules
- the step that runs `script/bootstrap` to install a Python `test` phase's
+53 -21
View File
@@ -121,15 +121,27 @@ style conventions are in separate documents:
only when it is missing. A tool from outside the distribution is never
installed as "latest" or "lts"; it is always an exact version.
`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.
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.
runs whichever name is present. With neither name on `PATH`, the function
prints one line saying to run `script/bootstrap`, and exits 1.
`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 and drops the hashes.
Alpine and Ubuntu 24.04 package yarn 1, so use one of those.
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 its own hashes in place of
these. Alpine and Ubuntu 24.04 package yarn 1, so a host uses one of those.
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, which today means Alpine. The Python and
Go stages below need no yarn, so they copy no `yarn.lock` before that step
and never reach the stop.
`script/cibuild` runs the CI build, and the Gitea workflow calls it. The
script changes to the repo root, runs `script/bootstrap`, runs
@@ -189,8 +201,8 @@ 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 (`package.json` + `yarn.lock`, `go.mod` +
`go.sum`, etc.) before running it.
and the dependency manifests it needs (`package.json` + `yarn.lock`,
`go.mod` + `go.sum`, etc.) before running it.
- **Linting and testing run in Docker, as phases of the `Dockerfile`.** There is
no separate lint file. `script/lint` and `script/test` each build one phase
@@ -507,6 +519,14 @@ style conventions are in separate documents:
distribution's Python, which a step above it installs. If it installs
anything 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
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
@@ -532,11 +552,25 @@ style conventions are in separate documents:
A cache mount is shared by every build on the host, and nothing checks its
files again.
In the example below, `script/bootstrap` does all of these:
- finds `make` and `git` already installed
- sets `PIP_ONLY_BINARY=:all:`
- installs pipenv and its dependencies by hash
- runs `pipenv install --dev --deploy`
A Python repository's `script/bootstrap` adds these lines. The `ensurepip`
test fails when python is missing, and also when Debian's venv package is
missing:
```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
```
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
# Test phase. Both pytest runs share one 90-second timeout, so a hang
@@ -551,11 +585,9 @@ style conventions are in separate documents:
&& apt-get install -y --no-install-recommends ca-certificates make git \
python3 python3-pip python3-venv \
&& rm -rf /var/lib/apt/lists/*
# Debian's Python refuses a system-wide pip install, so pipenv goes in a
# virtual environment. pipenv is not bundled with Python, so
# script/bootstrap installs it by hash, with the pip this puts on PATH.
RUN python3 -m venv /opt/pipenv
ENV PATH=/opt/pipenv/bin:$PATH
# 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
COPY script/ script/
COPY Pipfile Pipfile.lock ./
@@ -690,16 +722,16 @@ style conventions are in separate documents:
```makefile
test:
@timeout 90 sh -c 'python -m pytest || { \
@timeout 90 sh -c 'pipenv run pytest || { \
echo "--- Rerunning with -v for details ---"; \
python -m pytest -v; exit 1; }'
pipenv run pytest -v; exit 1; }'
```
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
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
installed.
installed. On a host, the target needs `$HOME/.pipenv/bin` on `PATH`.
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
+24 -15
View File
@@ -3,7 +3,9 @@
# this repo. Idempotent: every install is guarded by a check so already
# 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. Assumes nothing is present.
# hash, because the distribution is trusted. It installs node, yarn and
# the node packages only when yarn.lock is in the tree. Assumes nothing
# is present.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
@@ -60,8 +62,11 @@ missing() {
run_yarn() {
if command -v yarn >/dev/null 2>&1; then
yarn "$@"
else
elif command -v yarnpkg >/dev/null 2>&1; then
yarnpkg "$@"
else
echo "no yarn; run script/bootstrap first" >&2
exit 1
fi
}
@@ -70,21 +75,25 @@ main() {
if missing make; then pkg_install gnumake make make make; fi
if missing git; then pkg_install git git git git; 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
# Debian 13 and Ubuntu 26.04 package yarn 4, which refuses a yarn 1
# yarn.lock, or rewrites it and drops its hashes.
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
# A Dockerfile stage that needs no yarn copies no yarn.lock.
if [ -f yarn.lock ]; then
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 --frozen-lockfile
# 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"
}
+4 -1
View File
@@ -8,8 +8,11 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
run_yarn() {
if command -v yarn >/dev/null 2>&1; then
yarn "$@"
else
elif command -v yarnpkg >/dev/null 2>&1; then
yarnpkg "$@"
else
echo "no yarn; run script/bootstrap first" >&2
exit 1
fi
}
+4 -1
View File
@@ -8,8 +8,11 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
run_yarn() {
if command -v yarn >/dev/null 2>&1; then
yarn "$@"
else
elif command -v yarnpkg >/dev/null 2>&1; then
yarnpkg "$@"
else
echo "no yarn; run script/bootstrap first" >&2
exit 1
fi
}