Author SHA1 Message Date
sneak b910a1e808 Move the canonical golangci-lint to v2.15.0 and bring back canonicalheader
check / check (push) Canceled after 0s
v2.15.0 (released 2026-10-16, built with go1.27.0 like v2.14.0) bundles
canonicalheader with its upstream fix for non-deterministic results, the
defect behind issue 105. Linted 20 times each on a scratch package with
two planted findings beside a ResponseWriter.Header() call, v2.14.0
reported them on 8 runs and nothing on 12; v2.15.0 reported them on all
20. REPO_POLICIES.md names the new digest; .golangci.yml no longer
disables canonicalheader and is otherwise unchanged, since it verifies
against v2.15.0 as it was and default: all switches on no new linter.
2026-11-01 16:40:35 -08:00
11 changed files with 681 additions and 957 deletions

No files matched your search

+10 -22
View File
@@ -3,9 +3,6 @@ version: "2"
# Config schema uses the golangci-lint v2 layout (settings live under # Config schema uses the golangci-lint v2 layout (settings live under
# linters.settings, not top-level linters-settings) so that the # linters.settings, not top-level linters-settings) so that the
# thresholds below are actually applied by golangci-lint >= v2. # thresholds below are actually applied by golangci-lint >= v2.
#
# This file has no formatters section, so golangci-lint run does not run
# gofmt. A repository's script/fmt-check runs it, on the host.
run: run:
timeout: 5m timeout: 5m
@@ -19,13 +16,13 @@ linters:
- gomodguard_v2 - gomodguard_v2
disable: disable:
# Genuinely incompatible with project patterns # Genuinely incompatible with project patterns
- exhaustruct_v5 # Requires all struct fields - exhaustruct # Requires all struct fields
- exhaustruct_v5 # Requires all struct fields (successor to exhaustruct)
- godot # Requires comments to end with periods - godot # Requires comments to end with periods
- wrapcheck # Too verbose for internal packages - wrapcheck # Too verbose for internal packages
- varnamelen # Short names like db, id are idiomatic Go - varnamelen # Short names like db, id are idiomatic Go
# Deprecated: the warning is attached to the old name, so it is # Deprecated: the warning is attached to the old name, so it is
# silenced by disabling that name, not by enabling the successor. # silenced by disabling that name, not by enabling the successor.
- exhaustruct # Deprecated, replaced by exhaustruct_v5
- wsl # Deprecated, replaced by wsl_v5 - wsl # Deprecated, replaced by wsl_v5
- gomodguard # Deprecated, replaced by gomodguard_v2 - gomodguard # Deprecated, replaced by gomodguard_v2
settings: settings:
@@ -43,10 +40,7 @@ linters:
# test-support package exists to hand a test privileges the program # test-support package exists to hand a test privileges the program
# itself must never have, so a file that is not a test must not import # itself must never have, so a file that is not a test must not import
# one. Test files, and the files inside a package whose directory name # one. Test files, and the files inside a package whose directory name
# ends in `test`, are where that code belongs, and are exempt. A package # ends in `test`, are where that code belongs, and are exempt.
# in a subdirectory of that directory is not exempt. `${base-path}` is
# the directory holding this file, so the name of the directory a
# repository is cloned into does not count.
# #
# The deny list below is the one part of this file a repository is # The deny list below is the one part of this file a repository is
# expected to extend, and the only part it may. depguard matches an # expected to extend, and the only part it may. depguard matches an
@@ -61,25 +55,24 @@ linters:
files: files:
- "$all" - "$all"
- "!$test" - "!$test"
- "!${base-path}**/*test/*" - "!**/*test/**"
deny: deny:
- pkg: net/http/httptest - pkg: net/http/httptest
desc: >- desc: >-
Test-support code belongs in test files and in packages whose Test-support code belongs in test files and in packages whose
directory name ends in test, not in the shipped binary. directory name ends in test, not in the shipped binary.
# Each entry names a module not to use and what to use instead. Every # Only decisions already recorded in the Go package defaults are
# entry matches the module path exactly. # listed here. Every entry matches the module path exactly.
gomodguard_v2: gomodguard_v2:
blocked: blocked:
- module: github.com/rs/zerolog - module: github.com/rs/zerolog
recommendations: recommendations:
- log/slog - log/slog
reason: "Structured logging is stdlib log/slog." reason: "Structured logging is stdlib log/slog."
# These entries cover only the github.com/go-redis/redis module paths, # One entry per pre-fork module path, because the later releases
# one entry per path, because the later releases are separate paths. # are separate paths. A prefix match would be shorter but would
# A prefix match would be shorter but would also reach # also reach github.com/go-redis/redismock, the test double for
# github.com/go-redis/redismock/v9, the test double for the successor # the successor these entries recommend.
# these entries recommend.
- module: github.com/go-redis/redis - module: github.com/go-redis/redis
recommendations: recommendations:
- github.com/redis/go-redis/v9 - github.com/redis/go-redis/v9
@@ -92,10 +85,6 @@ linters:
recommendations: recommendations:
- github.com/redis/go-redis/v9 - github.com/redis/go-redis/v9
reason: "Pre-fork module; use the maintained go-redis v9." reason: "Pre-fork module; use the maintained go-redis v9."
- module: github.com/go-redis/redis/v9
recommendations:
- github.com/redis/go-redis/v9
reason: "Pre-fork module; use the maintained go-redis v9."
- module: github.com/sergi/go-diff - module: github.com/sergi/go-diff
recommendations: recommendations:
- github.com/aymanbagabas/go-udiff - github.com/aymanbagabas/go-udiff
@@ -119,4 +108,3 @@ linters:
issues: issues:
max-issues-per-linter: 0 max-issues-per-linter: 0
max-same-issues: 0 max-same-issues: 0
uniq-by-line: false
+10 -16
View File
@@ -2,15 +2,11 @@
# 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.
# #
# 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 yarn
ARG CACHEBUST ARG CACHEBUST
WORKDIR /app WORKDIR /app
@@ -25,9 +21,8 @@ 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 yarn
ARG CACHEBUST ARG CACHEBUST
WORKDIR /app WORKDIR /app
@@ -46,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 yarn
ARG CACHEBUST ARG CACHEBUST
WORKDIR /app WORKDIR /app
@@ -56,8 +50,8 @@ 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 ./ COPY package.json yarn.lock ./
RUN script/bootstrap RUN script/bootstrap
+3 -3
View File
@@ -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
+9 -64
View File
@@ -21,70 +21,15 @@ fmt-check, and commit.
# Completed Steps # Completed Steps
- 2026-11-04: node, yarn, npm, python, pip and venv now come from the - 2026-11-01: Moved the canonical golangci-lint to v2.15.0 (released
distribution with no version and no hash 2026-10-16), still built with go1.27.0, so the `go` directive rule is
(https://git.eeqj.de/sneak/prompts/issues/146), on sneak's ruling at unchanged. The canonical `.golangci.yml` passes `golangci-lint config verify`
https://git.eeqj.de/sneak/prompts/issues/146#issuecomment-166033. Third-party on v2.15.0, and `default: all` switches on no new linter. `canonicalheader` is
packages stay pinned by hash. These changed: enabled again (issue 105): v2.15.0 carries the upstream commit "fix:
- `script/bootstrap` no longer uses nvm or corepack, and stops when yarn's non-deterministic results", and on a scratch package with two planted findings
major version is not 1. beside a `ResponseWriter.Header()` call, linted 20 times with each pinned
- `script/bootstrap` installs node, yarn and the node packages only when image, v2.14.0 reported them on 8 runs and nothing on 12, while v2.15.0
`yarn.lock` is in the tree. reported them on all 20.
- `script/bootstrap`, `script/fmt` and `script/fmt-check` run `yarn` or
`yarnpkg`, whichever is present.
- This repository's `Dockerfile` starts from Alpine 3.24.
- The Python `test` phase example starts from `debian:trixie-slim`.
- A Python repository's `script/bootstrap` creates its virtual environment
at `$HOME/.pipenv`, so it works on a host and in a `Dockerfile`.
- `PIP_ONLY_BINARY=:all:` stops a package with no wheel, so pip downloads no
build tools without a hash check.
- 2026-11-04: Rewrote `prompts/REPO_POLICIES.md` sentence by sentence
(https://git.eeqj.de/sneak/prompts/issues/136), on sneak's yes at
https://git.eeqj.de/sneak/prompts/issues/136#issuecomment-165193. Every rule
keeps its meaning and its place, and `must` and `should` stay as they were.
Long sentences are split, comma-chained lists are now bulleted lists, and
every reason stays. Code blocks, digests, URLs, file names, commands and
quoted strings are unchanged.
- 2026-11-03: The canonical golangci-lint is now v2.15.0, still built with
go1.27.0, so the `go` directive rule is unchanged (issue 142). Repositories
pick this up on their next re-vendor. Each item in the canonical
`.golangci.yml` was tried on a scratch module. These changed:
- `canonicalheader` is enabled again, because v2.15.0 fixes the random
misses behind issue 105.
- The `depguard` exemption covers only the files directly inside a directory
whose name ends in `test`. It used to reach every directory below that one
too. The name of the directory a repository is cloned into no longer
counts.
- `gomodguard_v2` also blocks `github.com/go-redis/redis/v9`, whose
pre-releases can still be imported under that path.
- `uniq-by-line` is now `false`, so a line that trips two linters reports
both findings.
- Three comments are corrected:
- `exhaustruct` is listed as deprecated.
- The test double is named `github.com/go-redis/redismock/v9`.
- The claim that every blocked module is in the Go package defaults is
gone.
- A comment says the file has no `formatters` section, so
`golangci-lint run` does not run `gofmt`.
- 2026-11-03: A package from the distribution's own package index now needs no
version and no hash, and pulling that index is allowed
(https://git.eeqj.de/sneak/prompts/issues/72), on sneak's answer at
https://git.eeqj.de/sneak/prompts/issues/72#issuecomment-165196. The
distribution is the pinned base image's in a `Dockerfile` and the host's on a
host. It is trusted, and everything else stays pinned by hash. A package
source that a `Dockerfile` adds itself does not get the exception. The pinning
rule of `REPO_POLICIES.md` and both checklists say so, and the canonical Go
`Dockerfile`'s `apk add` line has a comment saying why it is allowed.
- 2026-11-03: A first test run that timed out now gets no verbose rerun
(https://git.eeqj.de/sneak/prompts/issues/61), on sneak's approval at
https://git.eeqj.de/sneak/prompts/issues/61#issuecomment-165195. The Go recipe
in `REPO_POLICIES.md` looks in the first run's output for
`panic: test timed out` or `*** Test killed`, and exits 1 when either is
there. The Python `make test` example takes the same `timeout 90 sh -c`
wrapper as its `Dockerfile` example. The 60 and 90 second rule and the
5-minute rule now say a hung package uses the backstop once. The Go item in
both repo checklists now asks for this, so a repository with the old recipe
fails it.
- 2026-10-27: Added `prompts/API_POLICIES.md` - 2026-10-27: Added `prompts/API_POLICIES.md`
(https://git.eeqj.de/sneak/prompts/issues/137). It holds sneak's rule from (https://git.eeqj.de/sneak/prompts/issues/137). It holds sneak's rule from
https://git.eeqj.de/sneak/routewatch/issues/199 that a value in an API https://git.eeqj.de/sneak/routewatch/issues/199 that a value in an API
+2 -2
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,7 +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, in whatever version it packages. 1. Use LTS node versions.
1. Use `prettier` for code formatting, with four spaces for indentation. 1. Use `prettier` for code formatting, with four spaces for indentation.
+64 -95
View File
@@ -1,6 +1,6 @@
--- ---
title: Existing Repo Checklist title: Existing Repo Checklist
last_modified: 2026-11-04 last_modified: 2026-10-26
--- ---
Use this checklist when beginning work in a repo that may not yet conform to our Use this checklist when beginning work in a repo that may not yet conform to our
@@ -56,45 +56,34 @@ with your task.
- the step that runs `script/bootstrap` to install a Python `test` phase's - the step that runs `script/bootstrap` to install a Python `test` phase's
packages from `Pipfile.lock` packages from `Pipfile.lock`
- [ ] A stage with none of these steps declares `ARG CACHEBUST` right after - [ ] A stage with none of these steps declares `ARG CACHEBUST` right after
`FROM`. A step that also installs anything else comes after `FROM`. A step that installs anything else as well, such as a
`ARG CACHEBUST` too. A `script/bootstrap` that also installs node packages `script/bootstrap` that also installs node packages, comes after it too.
is one. The `script/` builds pass a new `CACHEBUST` value on every build, The `script/` builds pass a new `CACHEBUST` value on every build, which
which reruns every `RUN` step after that line and leaves the layers above reruns every `RUN` step after that line and leaves the layers above it and
it and the cache mounts in place; `--no-cache` would build those layers the cache mounts in place; `--no-cache` would build those layers again and
again and empty the mounts. A stage without the `ARG` line is served from empty the mounts. A stage without the `ARG` line is served from cache on
cache on an unchanged tree, so the scripts and the `ARG` lines are an unchanged tree, so the scripts and the `ARG` lines are re-vendored in
re-vendored in one commit. one commit.
- [ ] Go: each Go stage downloads the modules (`COPY go.mod go.sum ./`, - [ ] Go: each Go stage downloads the modules (`COPY go.mod go.sum ./`,
`RUN go mod download`) above `ARG CACHEBUST`; the steps that compile `RUN go mod download`) above `ARG CACHEBUST`; the steps that compile
(`golangci-lint run`, `go test`, `go build`) mount Go's build cache (`golangci-lint run`, `go test`, `go build`) mount Go's build cache
(`--mount=type=cache,target=/root/.cache/go-build`), `golangci-lint run` (`--mount=type=cache,target=/root/.cache/go-build`), `golangci-lint run`
also mounts golangci-lint's also mounts golangci-lint's
(`--mount=type=cache,target=/root/.cache/golangci-lint`), and no step (`--mount=type=cache,target=/root/.cache/golangci-lint`), and no step
mounts anything else; both `go test` runs carry `-count=1`; a first run mounts anything else; both `go test` runs carry `-count=1`, as in the
that timed out gets no rerun, as in the canonical Go `Dockerfile` in canonical Go `Dockerfile` in `REPO_POLICIES.md`
`REPO_POLICIES.md`
- [ ] Python: a `test` phase that installs its packages from `Pipfile.lock` does - [ ] Python: a `test` phase that installs its packages from `Pipfile.lock` does
it above `ARG CACHEBUST`, with `COPY script/ script/`, it above `ARG CACHEBUST`, with `COPY script/ script/`,
`COPY Pipfile Pipfile.lock ./` and `RUN script/bootstrap`. When `COPY Pipfile Pipfile.lock ./` and `RUN script/bootstrap`. When
`script/bootstrap` runs in that step, it installs nothing beyond these: `script/bootstrap` runs in that step, it installs nothing beyond system
- system packages packages, pipenv and its dependencies, and the packages from
- pipenv and its dependencies `Pipfile.lock`. It installs pipenv and its dependencies by hash, with
- 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
It creates a virtual environment at `$HOME/.pipenv` with `python3 -m venv` need the development packages. It installs no Python there, because the
when it is missing, then uses that environment's `pip` and `pipenv`. On a tests use the base image's. The tests run after `ARG CACHEBUST`, and every
host, it first installs the distribution's python and venv packages when other stage installs its Python packages after it, as in the Python `test`
they are missing. It installs pipenv and its dependencies by hash, with phase example in `REPO_POLICIES.md`
`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`
- [ ] The gate phases invoke their tools directly, never through `make lint` or - [ ] The gate phases invoke their tools directly, never through `make lint` or
`script/test` — those are themselves a `docker build` and would recurse `script/test` — those are themselves a `docker build` and would recurse
inside a build step inside a build step
@@ -115,43 +104,37 @@ with your task.
`services/api/.claude/`: add anchored entries for those directories. `services/api/.claude/`: add anchored entries for those directories.
- [ ] If the repo embeds a version in a binary: `.dockerignore` lets `.git` into - [ ] If the repo embeds a version in a binary: `.dockerignore` lets `.git` into
the build context. It keeps out every git `config` at any depth the build context. It keeps out every git `config` at any depth
(`**/.git/config`, `**/.git/modules/**/config`): (`**/.git/config`, `**/.git/modules/**/config`): the repository's own,
- the repository's own each submodule's under `.git/modules/`, and that of a submodule keeping
- each submodule's under `.git/modules/` its own `.git` directory. `git describe` does not need them, and each can
- that of a submodule keeping its own `.git` directory hold a credential: a password in a remote URL, or the token the CI
checkout step stores there. A submodule whose name has a `config` segment
`git describe` does not need them, and each can hold a credential: a (`config`, `deploy/config`, `config/lib`) loses its whole git directory to
password in a remote URL, or the token the CI checkout step stores there. `**/.git/modules/**/config`, and Go's version stamping then fails the
Some submodule names have a `config` segment, as in `config`, build: give it a name without that segment (`git submodule add --name`).
`deploy/config` or `config/lib`. Such a submodule loses its whole git The stage that compiles has `git` (the Debian Go image has it; an alpine
directory to `**/.git/modules/**/config`, and Go's version stamping then one needs `apk add --no-cache git`) and takes the version from the
fails the build. Give it a name without that segment `VERSION` build argument when one is given, otherwise from
(`git submodule add --name`). The stage that compiles has `git`: the Debian `git describe --tags --always`. That gives the tag on a tagged commit; on
Go image has it, and an alpine one needs `apk add --no-cache git`. That a later commit, the tag, the number of commits since it and the short
stage takes the version from the `VERSION` build argument when one is given, commit (`v1.2.3-4-gabc1234`); and the short commit when no tag is
otherwise from `git describe --tags --always`. That gives: reachable. The stage that compiles also marks its working directory safe
- the tag, on a tagged commit for git (`git config --system --add safe.directory /src`): a context sent
- the tag, the number of commits since it and the short commit as a tar stream keeps the sender's file owners, and git refuses a checkout
(`v1.2.3-4-gabc1234`), on a later commit owned by another user, so the version would come out empty. `ARG VERSION`
- the short commit, when no tag is reachable has no default, and the build fails if the context carries `.git` and the
version still comes out empty, `dev` or `unknown`. A plain
The stage that compiles also marks its working directory safe for git `docker build .` with no build arguments must succeed; a Dockerfile that
(`git config --system --add safe.directory /src`). A context sent as a tar refuses an empty build argument drops that refusal and keeps the argument.
stream keeps the sender's file owners, and git refuses a checkout owned by A checkout whose `.git` is a file (a linked worktree, or a repository
another user, so the version would come out empty. `ARG VERSION` has no checked out as a submodule) is the exception: that file points to a git
default. The build fails if the context carries `.git` and the version still directory outside the build context, so the build cannot read the version
comes out empty, `dev` or `unknown`. A plain `docker build .` with no build and a plain `docker build .` fails; pass the version with
arguments must succeed; a Dockerfile that refuses an empty build argument `--build-arg VERSION=...`. `script/docker` and `script/cibuild` already
drops that refusal and keeps the argument. A checkout whose `.git` is a file pass the version they compute on the host; it takes precedence. The
is the exception: a linked worktree, or a repository checked out as a canonical `.gitea/workflows/check.yml` sets `fetch-depth: 0` on its
submodule. That file points to a git directory outside the build context, so checkout step, which otherwise clones shallow and fetches no tags, so a CI
the build cannot read the version and a plain `docker build .` fails. Pass build finds the tag too.
the version with `--build-arg VERSION=...`. `script/docker` and
`script/cibuild` already pass the version they compute on the host; it takes
precedence. The canonical `.gitea/workflows/check.yml` sets `fetch-depth: 0`
on its checkout step, which otherwise clones shallow and fetches no tags, so
a CI build finds the tag too.
- [ ] Gitea Actions workflow in `.gitea/workflows/` runs `script/cibuild` on - [ ] Gitea Actions workflow in `.gitea/workflows/` runs `script/cibuild` on
push, checks out with `persist-credentials: false` and with push, checks out with `persist-credentials: false` and with
`fetch-depth: 0` (which fetches the tags `git describe` needs), carries `fetch-depth: 0` (which fetches the tags `git describe` needs), carries
@@ -202,17 +185,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. With `yarn.lock` in the tree, `script/bootstrap` stops the pinned major version, and otherwise yarn under the pinned node, with
with a one-line message when yarn's major version is not 1. nvm loaded through `$HOME/.nvm/nvm.sh`. `script/bootstrap` leaves the node
- [ ] A `Dockerfile` stage copies `yarn.lock` before it runs `script/bootstrap` and yarn it installs under nvm off the `PATH` of the shell that called it,
only if that stage needs yarn. A stage that needs yarn starts from an so a bare `yarn` exits 127 on a runner carrying nothing but docker and
image whose distribution packages yarn 1, which today means Alpine. git, or runs under another node.
- [ ] `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`, they 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
@@ -236,16 +215,7 @@ with your task.
- [ ] Pre-commit hook is installed (`make hooks`) - [ ] Pre-commit hook is installed (`make hooks`)
- [ ] No secrets in the repo (`.env`, keys, credentials) - [ ] No secrets in the repo (`.env`, keys, credentials)
- [ ] No mutable references in Dockerfiles or scripts (tags, `@latest`) — all - [ ] No mutable references in Dockerfiles or scripts (tags, `@latest`) — all
pinned by cryptographic hash with version/date comment. The one exception pinned by cryptographic hash with version/date comment
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
- [ ] Using `yarn`, not `npm` (JS projects) - [ ] Using `yarn`, not `npm` (JS projects)
# Directory Structure # Directory Structure
@@ -274,12 +244,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
+68 -102
View File
@@ -1,6 +1,6 @@
--- ---
title: New Repo Checklist title: New Repo Checklist
last_modified: 2026-11-04 last_modified: 2026-10-26
--- ---
Use this checklist when creating a new repository from scratch. Follow the steps Use this checklist when creating a new repository from scratch. Follow the steps
@@ -75,42 +75,34 @@ Template files can be fetched from:
anchored entry. anchored entry.
- If the image embeds a version in a binary: `.dockerignore` lets `.git` - If the image embeds a version in a binary: `.dockerignore` lets `.git`
into the build context. It keeps out every git `config` at any depth into the build context. It keeps out every git `config` at any depth
(`**/.git/config`, `**/.git/modules/**/config`): (`**/.git/config`, `**/.git/modules/**/config`): the repository's own,
- the repository's own each submodule's under `.git/modules/`, and that of a submodule keeping
- each submodule's under `.git/modules/` its own `.git` directory. `git describe` does not need them, and each can
- that of a submodule keeping its own `.git` directory hold a credential: a password in a remote URL, or the token the CI
checkout step stores there. A submodule whose name has a `config` segment
`git describe` does not need them, and each can hold a credential: a (`config`, `deploy/config`, `config/lib`) loses its whole git directory to
password in a remote URL, or the token the CI checkout step stores `**/.git/modules/**/config`, and Go's version stamping then fails the
there. Some submodule names have a `config` segment, as in `config`, build: give it a name without that segment (`git submodule add --name`).
`deploy/config` or `config/lib`. Such a submodule loses its whole git The stage that compiles has `git` (the Debian Go image has it; an alpine
directory to `**/.git/modules/**/config`, and Go's version stamping then one needs `apk add --no-cache git`) and takes the version from the
fails the build. Give it a name without that segment
(`git submodule add --name`). The stage that compiles has `git`: the
Debian Go image has it, and an alpine one needs
`apk add --no-cache git`. That stage takes the version from the
`VERSION` build argument when one is given, otherwise from `VERSION` build argument when one is given, otherwise from
`git describe --tags --always`. That gives: `git describe --tags --always`. That gives the tag on a tagged commit; on
- the tag, on a tagged commit a later commit, the tag, the number of commits since it and the short
- the tag, the number of commits since it and the short commit commit (`v1.2.3-4-gabc1234`); and the short commit when no tag is
(`v1.2.3-4-gabc1234`), on a later commit reachable. The stage that compiles also marks its working directory safe
- the short commit, when no tag is reachable for git (`git config --system --add safe.directory /src`): a context sent
as a tar stream keeps the sender's file owners, and git refuses a checkout
The stage that compiles also marks its working directory safe for git owned by another user, so the version would come out empty. `ARG VERSION`
(`git config --system --add safe.directory /src`). A context sent as a has no default, and the build fails if the context carries `.git` and the
tar stream keeps the sender's file owners, and git refuses a checkout version still comes out empty, `dev` or `unknown`. A plain
owned by another user, so the version would come out empty. `docker build .` with no build arguments must succeed; a Dockerfile that
`ARG VERSION` has no default. The build fails if the context carries refuses an empty build argument drops that refusal and keeps the argument.
`.git` and the version still comes out empty, `dev` or `unknown`. A A checkout whose `.git` is a file (a linked worktree, or a repository
plain `docker build .` with no build arguments must succeed; a checked out as a submodule) is the exception: that file points to a git
Dockerfile that refuses an empty build argument drops that refusal and directory outside the build context, so the build cannot read the version
keeps the argument. A checkout whose `.git` is a file is the exception: and a plain `docker build .` fails; pass the version with
a linked worktree, or a repository checked out as a submodule. That file `--build-arg VERSION=...`, as `script/docker` and `script/cibuild` already
points to a git directory outside the build context, so the build cannot do.
read the version and a plain `docker build .` fails. Pass the version
with `--build-arg VERSION=...`, as `script/docker` and `script/cibuild`
already do.
- The Dockerfile carries a `lint` phase and a `test` phase, each invoking - The Dockerfile carries a `lint` phase and a `test` phase, each invoking
its tool directly rather than through `make` or `script/`, and the final its tool directly rather than through `make` or `script/`, and the final
stage carries a `COPY --from=` of a harmless file from each so the image stage carries a `COPY --from=` of a harmless file from each so the image
@@ -123,50 +115,37 @@ Template files can be fetched from:
- the step that runs `script/bootstrap` to install a Python `test` - the step that runs `script/bootstrap` to install a Python `test`
phase's packages from `Pipfile.lock` phase's packages from `Pipfile.lock`
- A stage with none of these steps declares `ARG CACHEBUST` right after - A stage with none of these steps declares `ARG CACHEBUST` right after
`FROM`. A step that also installs anything else comes after `FROM`. A step that installs anything else as well, such as a
`ARG CACHEBUST` too. A `script/bootstrap` that also installs node packages `script/bootstrap` that also installs node packages, comes after it too.
is one. The `script/` builds pass a new `CACHEBUST` value on every build, The `script/` builds pass a new `CACHEBUST` value on every build, which
which reruns every `RUN` step after that line and leaves the layers above reruns every `RUN` step after that line and leaves the layers above it and
it and the cache mounts in place; `--no-cache` would build those layers the cache mounts in place; `--no-cache` would build those layers again and
again and empty the mounts. A stage without the `ARG` line is served from empty the mounts. A stage without the `ARG` line is served from cache on
cache on an unchanged tree, so the scripts and every stage's an unchanged tree, so the scripts and every stage's `ARG CACHEBUST` line
`ARG CACHEBUST` line are added in the same commit. are added in the same commit.
- Go: each Go stage downloads the modules (`COPY go.mod go.sum ./`, - Go: each Go stage downloads the modules (`COPY go.mod go.sum ./`,
`RUN go mod download`) above `ARG CACHEBUST`; the steps that compile `RUN go mod download`) above `ARG CACHEBUST`; the steps that compile
(`golangci-lint run`, `go test`, `go build`) mount Go's build cache (`golangci-lint run`, `go test`, `go build`) mount Go's build cache
(`--mount=type=cache,target=/root/.cache/go-build`), `golangci-lint run` (`--mount=type=cache,target=/root/.cache/go-build`), `golangci-lint run`
also mounts golangci-lint's also mounts golangci-lint's
(`--mount=type=cache,target=/root/.cache/golangci-lint`), and no step (`--mount=type=cache,target=/root/.cache/golangci-lint`), and no step
mounts anything else; both `go test` runs carry `-count=1`; a first run mounts anything else; both `go test` runs carry `-count=1`, as in the
that timed out gets no rerun, as in the canonical Go `Dockerfile` in canonical Go `Dockerfile` in `REPO_POLICIES.md`
`REPO_POLICIES.md`
- Python: a `test` phase that installs its packages from `Pipfile.lock` does - Python: a `test` phase that installs its packages from `Pipfile.lock` does
it above `ARG CACHEBUST`, with `COPY script/ script/`, it above `ARG CACHEBUST`, with `COPY script/ script/`,
`COPY Pipfile Pipfile.lock ./` and `RUN script/bootstrap`. When `COPY Pipfile Pipfile.lock ./` and `RUN script/bootstrap`. When
`script/bootstrap` runs in that step, it installs nothing beyond these: `script/bootstrap` runs in that step, it installs nothing beyond system
- system packages packages, pipenv and its dependencies, and the packages from
- pipenv and its dependencies `Pipfile.lock`. It installs pipenv and its dependencies by hash, with
- 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
It creates a virtual environment at `$HOME/.pipenv` with need the development packages. It installs no Python there, because the
`python3 -m venv` when it is missing, then uses that environment's `pip` tests use the base image's. The tests run after `ARG CACHEBUST`, and every
and `pipenv`. On a host, it first installs the distribution's python and other stage installs its Python packages after it, as in the Python `test`
venv packages when they are missing. It installs pipenv and its phase example in `REPO_POLICIES.md`
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`
- Server: the final stage builds and runs the application - Server: the final stage builds and runs the application
- Non-server: the final stage brings up the dev environment - Non-server: the final stage brings up the dev environment
- Image pinned by sha256 hash with version/date comment - Image pinned by sha256 hash with version/date comment
- [ ] Gitea Actions workflow at `.gitea/workflows/check.yml` that runs - [ ] Gitea Actions workflow at `.gitea/workflows/check.yml` that runs
`script/cibuild` on push, checks out with `persist-credentials: false` and `script/cibuild` on push, checks out with `persist-credentials: false` and
with `fetch-depth: 0` (which fetches the tags `git describe` needs), with `fetch-depth: 0` (which fetches the tags `git describe` needs),
@@ -190,21 +169,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
- With `yarn.lock` in the tree, it stops with a one-line message when yarn's build stage installs what its base image lacks either inline or by running
major version is not 1. it
- 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`, - [ ] `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` —
@@ -234,10 +206,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`, they print one line saying to run the pinned major version, and otherwise yarn under the pinned node, with
`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
@@ -252,12 +227,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
@@ -268,15 +242,7 @@ are thin shims calling them. Model scripts:
final stage is missing its `COPY --from=` edge to the gate phases. final stage is missing its `COPY --from=` edge to the gate phases.
- [ ] No secrets in repo, and none in the build context: enumerate a probe image - [ ] No secrets in repo, and none in the build context: enumerate a probe image
rather than reading `.dockerignore` rather than reading `.dockerignore`
- [ ] No mutable image/package references. The one exception is a package from - [ ] No mutable image/package references
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
- [ ] 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
+371 -609
View File
File diff suppressed because it is too large. Load diff
+96 -30
View File
@@ -1,15 +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. Assumes nothing # otherwise (node missing, or another major) the pinned version is
# is present. # installed via nvm (installing nvm itself first, from a hash-verified
# release archive, never curl | sh), and yarn and the packages are
# 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=""
@@ -58,42 +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 nodejs node nodejs; fi install_js_deps
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 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" 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() {