4 Commits
Author SHA1 Message Date
clawbot c55a0cb2f0 Tag the image build in the checklist's script/cibuild item (closes #125)
check / check (push) Waiting to run
The new-repository checklist gave script/cibuild's image build without a
tag, so a repository written from it left a dangling image behind on
every run, and it used $version without saying where it comes from. The
item now gives the build as the canonical script/cibuild runs it, with
-t "$tag", and names the two steps the script runs before it, each on
its own line: the version from git describe --tags --always --dirty
(unknown if empty) and the tag from script/projectname. The other docker
build commands the checklists and REPO_POLICIES.md give for script/
already matched their scripts.

Model: opus-5-5
2026-10-07 13:02:05 +02:00
clawbot 1d9b046b91 Stop the lint and test builds writing an image (closes #123)
check / check (push) Canceled after 0s
script/lint and script/test build their Dockerfile phase with
--output type=cacheonly in place of a tag. The phase still runs
uncached and a failing step still fails the build, but no image is
written: nothing used those images, and writing one out took about 16
seconds of a Go repository's test build. script/cibuild and
script/docker keep their tags. REPO_POLICIES.md, both checklists and
the README now say the gate builds write no image.

Model: opus-5-5
2026-10-07 12:02:02 +02:00
clawbot 0b20f18734 Set a time limit on the canonical check job (closes #120)
check / check (push) Canceled after 0s
The canonical check workflow gave its job no time limit, so a hung
script/cibuild held the shared runner until the runner's own default.
The check job now sets timeout-minutes: 20. script/cibuild runs three
Docker builds (the test phase, the lint phase, then the image, which
runs both again), each held to the 5-minute build limit, plus the
bootstrap. REPO_POLICIES.md and both checklists name the limit among
what the workflow sets.

Model: opus-5-5
2026-10-07 11:31:34 +02:00
clawbot a04a76d59c Make the canonical package.json private instead of declaring MIT (closes #119)
check / check (push) Canceled after 0s
Repositories copy package.json to get prettier, and with "license": "MIT"
each of them declared MIT whatever its own licence is. "private": true
keeps yarn from printing "No license field" and makes no licence claim.
This repository's LICENSE and README License section are unchanged.

Model: opus-5-5
2026-10-07 11:02:12 +02:00
8 changed files with 89 additions and 61 deletions
+8 -4
View File
@@ -116,10 +116,14 @@ alpine. We provide:
`script/bootstrap`, then `script/install-precommit` `script/bootstrap`, then `script/install-precommit`
- `script/projectname` — output the project name (our own extension); used by - `script/projectname` — output the project name (our own extension); used by
`script/docker` for the image tag `script/docker` for the image tag
- `script/test` — `docker build --no-cache --target test -t prompts-test .`, - `script/test` —
building the `test` phase of the `Dockerfile` (no tests defined here) `docker build --no-cache --target test --output type=cacheonly .`, building
- `script/lint` — `docker build --no-cache --target lint -t prompts-lint .`, the `test` phase of the `Dockerfile` without writing an image (no tests
building the `lint` phase, which runs prettier over the markdown files defined here)
- `script/lint` —
`docker build --no-cache --target lint --output type=cacheonly .`, building
the `lint` phase, which runs prettier over the markdown files, without writing
an image
- `script/fmt` — format all markdown files with prettier (writes; native, not in - `script/fmt` — format all markdown files with prettier (writes; native, not in
a container) a container)
- `script/fmt-check` — check formatting (read-only; native) - `script/fmt-check` — check formatting (read-only; native)
+21
View File
@@ -21,6 +21,23 @@ fmt-check, and commit.
# Completed Steps # Completed Steps
- 2026-10-07: The `script/cibuild` item in `NEW_REPO_CHECKLIST.md` now matches
the canonical `script/cibuild` (issue 125). Its image build carries the tag,
`-t "$tag"`, and the item says that `$version` comes from
`git describe --tags --always --dirty` (`unknown` if empty) and `$tag` from
`script/projectname`, each assigned on its own line before the build. A
repository written from the checklist got an untagged build, which leaves a
dangling image behind on every run, and was never told where `$version` comes
from. The other `docker build` commands the checklists and `REPO_POLICIES.md`
give for `script/` already matched their scripts.
- 2026-10-07: `script/lint` and `script/test` now build with
`--output type=cacheonly` in place of a tag (issue 123), so they still run
their phase uncached and fail on a failing step but write no image. Nothing
used those images, and writing one out cost about 16 seconds of a Go
repository's test build. `script/cibuild` and `script/docker` keep their tags.
`REPO_POLICIES.md`, both checklists and the README no longer say the gate
builds are tagged. Not yet tried on the shared runner. Repositories pick this
up on their next re-vendor.
- 2026-10-07: The canonical `.gitea/workflows/check.yml` now sets - 2026-10-07: The canonical `.gitea/workflows/check.yml` now sets
`timeout-minutes: 20` on its `check` job (issue 120), so a hung build frees `timeout-minutes: 20` on its `check` job (issue 120), so a hung build frees
the shared runner instead of holding it until the runner's own limit. the shared runner instead of holding it until the runner's own limit.
@@ -29,6 +46,10 @@ fmt-check, and commit.
follows it. `REPO_POLICIES.md` and both checklists name the limit among what follows it. `REPO_POLICIES.md` and both checklists name the limit among what
the workflow sets. Not yet tried on the shared runner. Repositories pick this the workflow sets. Not yet tried on the shared runner. Repositories pick this
up on their next re-vendor. up on their next re-vendor.
- 2026-10-07: The canonical `package.json` now has `"private": true` in place of
`"license": "MIT"` (issue 119), so a repository that copies it no longer
declares MIT whatever its own licence is. yarn does not print "No license
field" for a private package. This repository's own licence is unchanged.
- 2026-10-06: The canonical `.gitea/workflows/check.yml` now sets - 2026-10-06: The canonical `.gitea/workflows/check.yml` now sets
`fetch-depth: 0` on its checkout step (issue 110), so CI fetches the history `fetch-depth: 0` on its checkout step (issue 110), so CI fetches the history
and tags that `git describe --tags --always` needs, and a tagged repository and tags that `git describe --tags --always` needs, and a tagged repository
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"license": "MIT", "private": true,
"devDependencies": { "devDependencies": {
"prettier": "3.8.1" "prettier": "3.8.1"
} }
+11 -8
View File
@@ -132,16 +132,19 @@ with your task.
`script/install-precommit`, shimmed by `make hooks`) runs it `script/install-precommit`, shimmed by `make hooks`) runs it
- [ ] README has an **Entrypoints** section documenting the `script/` - [ ] README has an **Entrypoints** section documenting the `script/`
entrypoints and linking the standard entrypoints and linking the standard
- [ ] `script/lint` and `script/test` build their phase by name - [ ] `script/lint` and `script/test` each run
(`docker build --no-cache --target <phase> -t <name>-<phase> .`), and no `docker build --no-cache --target <phase> --output type=cacheonly .`,
host invocation anywhere in the repo can produce a lint verdict — grep for which builds their phase by name and writes no image, and no host
the linter's own name across `script/`, the `Makefile` and CI config, not invocation anywhere in the repo can produce a lint verdict — grep for the
just `script/lint`. A second path is likeliest here: a `make lint-fast`, linter's own name across `script/`, the `Makefile` and CI config, not just
an older host-versus-container branch, or a CI step calling the binary `script/lint`. A second path is likeliest here: a `make lint-fast`, an
older host-versus-container branch, or a CI step calling the binary
directly. `script/fmt` and `script/fmt-check` are expected hits and stay directly. `script/fmt` and `script/fmt-check` are expected hits and stay
on the host. on the host.
- [ ] Every `docker build` in `script/` is tagged — an untagged one leaves a - [ ] No `docker build` in `script/` leaves a dangling image behind:
dangling image behind on every run, on every host and CI runner `script/lint` and `script/test` write no image, and `script/docker` and
`script/cibuild` tag theirs. A build that writes an untagged image leaves
one behind on every run, on every host and CI runner.
- [ ] `script/cibuild` runs `script/bootstrap` before `script/check`, and builds - [ ] `script/cibuild` runs `script/bootstrap` before `script/check`, and builds
the image with `--no-cache`. Without the bootstrap the CI run dies in the image with `--no-cache`. 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
+17 -10
View File
@@ -143,11 +143,14 @@ are thin shims calling them. Model scripts:
it it
- [ ] `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` — `docker build --no-cache --target test .`, - [ ] `script/test` / `make test` —
tagged; the phase runs real tests, not a no-op (90-second timeout, `docker build --no-cache --target test --output type=cacheonly .`, which
60-second hard cap on wall time) writes no image; the phase runs real tests, not a no-op (90-second
- [ ] `script/lint` / `make lint` — `docker build --no-cache --target lint .`, timeout, 60-second hard cap on wall time)
tagged. No lint verdict may come from a host invocation of the linter. - [ ] `script/lint` / `make lint` —
`docker build --no-cache --target lint --output type=cacheonly .`, which
writes no image. No lint verdict may come from a host invocation of the
linter.
- [ ] `script/fmt` / `make fmt` — formats code (writes; native, never in a - [ ] `script/fmt` / `make fmt` — formats code (writes; native, never in a
container) container)
- [ ] `script/fmt-check` / `make fmt-check` — checks formatting (read-only; - [ ] `script/fmt-check` / `make fmt-check` — checks formatting (read-only;
@@ -161,16 +164,20 @@ are thin shims calling them. Model scripts:
version as a build arg version as a build arg
- [ ] `script/cibuild` — cd to repo root, run `script/bootstrap`, run - [ ] `script/cibuild` — cd to repo root, run `script/bootstrap`, run
`script/check`, then `script/check`, then
`docker build --no-cache --build-arg VERSION="$version" .` (what CI runs). `docker build --no-cache --build-arg VERSION="$version" -t "$tag" .` (what
The bootstrap is required: CI checks out and runs this alone, and CI runs), with `$version` from `git describe --tags --always --dirty`
`script/fmt-check` runs the formatter on the host. (`unknown` if empty) and `$tag` from `script/projectname`, each assigned
on its own line before the build. The bootstrap is required: CI checks out
and runs this alone, and `script/fmt-check` runs the formatter on the
host.
- [ ] `script/fmt` and `script/fmt-check` source nvm for the pinned node version - [ ] `script/fmt` and `script/fmt-check` source nvm for the pinned node version
before invoking `yarn`, as `script/bootstrap`'s own install step does. before invoking `yarn`, as `script/bootstrap`'s own install step does.
`script/bootstrap` leaves the node and yarn it installs off the `PATH` of `script/bootstrap` leaves the node and yarn it installs off the `PATH` of
the shell that called it, so a bare `yarn` exits 127 on a runner carrying the shell that called it, so a bare `yarn` exits 127 on a runner carrying
nothing but docker and git. nothing but docker and git.
- [ ] Every `docker build` in `script/` is tagged, so no invocation leaves a - [ ] No `docker build` in `script/` leaves a dangling image behind:
dangling image behind `script/lint` and `script/test` write no image, and `script/docker` and
`script/cibuild` tag theirs
- [ ] `script/precommit` — called by the pre-commit hook; runs `script/check` - [ ] `script/precommit` — called by the pre-commit hook; runs `script/check`
- [ ] `script/install-precommit` — installs the pre-commit hook that runs - [ ] `script/install-precommit` — installs the pre-commit hook that runs
`script/precommit` `script/precommit`
+25 -24
View File
@@ -118,9 +118,8 @@ style conventions are in separate documents:
and nothing else: and nothing else:
```sh ```sh
tag="$(script/projectname)" docker build --no-cache --target lint --output type=cacheonly .
docker build --no-cache --target lint -t "$tag-lint" . docker build --no-cache --target test --output type=cacheonly .
docker build --no-cache --target test -t "$tag-test" .
``` ```
**A stage that is not the last one in the file is built only when the final **A stage that is not the last one in the file is built only when the final
@@ -130,12 +129,15 @@ style conventions are in separate documents:
plain `docker build .` builds the last stage alone and exits 0 having linted plain `docker build .` builds the last stage alone and exits 0 having linted
and tested nothing. and tested nothing.
**Every `docker build` in `script/` is tagged**, here and in **The gate builds write no image.** With `--output type=cacheonly` the phase
`script/cibuild` and `script/docker`. An untagged build leaves a dangling runs and a failing step fails the build, but the result is not exported.
image behind on every invocation, on every developer host and every CI Nothing uses those images, and writing one out is slow: a Go test phase's
runner; a tagged one replaces the previous image. Each script assigns the image holds the toolchain and every compiled package. A build given neither
tag on its own line before the build, so `set -e` stops it where `--output` nor `-t` writes an untagged image and leaves it dangling, on
`script/projectname` fails. every developer host and every CI runner. `script/cibuild` and
`script/docker` build the image that ships and tag it, so each build
replaces the previous image; each assigns the tag on its own line before the
build, so `set -e` stops it where `script/projectname` fails.
Inside a phase the tool is invoked directly — `golangci-lint`, `go test`, Inside a phase the tool is invoked directly — `golangci-lint`, `go test`,
`eslint`, `prettier` — never through `make lint` or `script/test`, which are `eslint`, `prettier` — never through `make lint` or `script/test`, which are
@@ -298,21 +300,20 @@ style conventions are in separate documents:
workflow's `concurrency` block groups runs by workflow and branch workflow's `concurrency` block groups runs by workflow and branch
(`${{ github.workflow }}-${{ github.ref }}`) with `cancel-in-progress: true`, (`${{ github.workflow }}-${{ github.ref }}`) with `cancel-in-progress: true`,
so a new push cancels the older run on the same branch, queued or running, and so a new push cancels the older run on the same branch, queued or running, and
no other: runs for replaced commits do not hold up the shared runner. The no other: runs for replaced commits do not hold up the shared runner.
`check` job sets `timeout-minutes: 20`, so a hung build frees the shared `script/cibuild` bootstraps, runs the gate phases, and then builds the image,
runner after 20 minutes. That allows for the three Docker builds so a successful run means every check passed; a bare `docker build .` does not
`script/cibuild` runs (the lint phase, the test phase, then the image, which carry the same guarantee, because its gate phases may come from the cache. The
runs both again), each held to the 5-minute Docker build limit below, plus the image build is uncached and so runs the gate phases a second time. That is the
bootstrap. `script/cibuild` bootstraps, runs the gate phases, and then builds price of the rule above, and it is worth paying: the image that ships is built
the image, so a successful run means every check passed; a bare from a run of its own gates rather than from a cache entry. The `check` job
`docker build .` does not carry the same guarantee, because its gate phases sets `timeout-minutes: 20`, so a hung build frees the shared runner after 20
may come from the cache. The image build is uncached and so runs the gate minutes. That allows for the three Docker builds described above (the test
phases a second time. That is the price of the rule above, and it is worth phase, the lint phase, then the image), each held to the 5-minute Docker build
paying: the image that ships is built from a run of its own gates rather than limit below, plus the bootstrap. A separate workflow limited to `main` by a
from a cache entry. A separate workflow limited to `main` by a `branches` list `branches` list under `on: push` cannot be checked by review: to try a change
under `on: push` cannot be checked by review: to try a change to it, add the to it, add the feature branch to that list and push, then remove the branch
feature branch to that list and push, then remove the branch from the list from the list again before merging. Keep any job in it that publishes behind
again before merging. Keep any job in it that publishes behind
`if: github.ref_name == 'main'`, so the run from the feature branch publishes `if: github.ref_name == 'main'`, so the run from the feature branch publishes
nothing. nothing.
+3 -7
View File
@@ -6,8 +6,8 @@
# #
# The phase is not the last stage in the file, so it is built only when # The phase is not the last stage in the file, so it is built only when
# --target names it. --no-cache because a cached lint layer is a lint # --target names it. --no-cache because a cached lint layer is a lint
# that did not run. The tag makes each build replace the previous image # that did not run. --output type=cacheonly writes no image, since
# instead of leaving a dangling one behind. # nothing uses one.
set -eu set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
@@ -15,13 +15,9 @@ ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
main() { main() {
cd "$ROOT" cd "$ROOT"
# The tag gets its own line: a failing command substitution inside
# an argument does not trip `set -e`, so the inline form degrades
# silently to an empty constant.
tag="$("$SCRIPT_DIR/projectname")"
docker build --no-cache \ docker build --no-cache \
--target lint \ --target lint \
-t "$tag-lint" . --output type=cacheonly .
} }
main "$@" main "$@"
+3 -7
View File
@@ -2,8 +2,8 @@
# script/test: run the test suite. Testing is a phase of the Dockerfile # script/test: run the test suite. Testing is a phase of the Dockerfile
# and this builds that phase alone, on the same terms as script/lint: # and this builds that phase alone, on the same terms as script/lint:
# --target because a phase that is not the last stage is built only when # --target because a phase that is not the last stage is built only when
# named, --no-cache because a cached test layer is a test that did not # named, and --no-cache because a cached test layer is a test that did
# run, and a tag so each build replaces the previous image. # not run. --output type=cacheonly writes no image, since nothing uses one.
set -eu set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
@@ -11,13 +11,9 @@ ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
main() { main() {
cd "$ROOT" cd "$ROOT"
# The tag gets its own line: a failing command substitution inside
# an argument does not trip `set -e`, so the inline form degrades
# silently to an empty constant.
tag="$("$SCRIPT_DIR/projectname")"
docker build --no-cache \ docker build --no-cache \
--target test \ --target test \
-t "$tag-test" . --output type=cacheonly .
} }
main "$@" main "$@"