8 Commits
Author SHA1 Message Date
clawbotandsneak ed5ed236b1 Migrate canonical .golangci.yml to gomodguard_v2, with a block list (#55)
check / check (push) Successful in 27s
Option-2 answer to sneak's ruling of 2026-08-19 on
sneak/homoicon#4: migrate to the successor, with
settings. Closes #25.

## The change

`golangci-lint` v2.12.0 deprecated `gomodguard`, and this config sets
`linters.default: all`, so it is enabled everywhere and warns on every run.

- `gomodguard` joins `wsl` in `linters.disable` under a shared "deprecated"
  comment. The warning is attached to the old name, so disabling it is what
  silences it.
- `gomodguard_v2` is named in `linters.enable`, a no-op under `default: all`
  that gives the settings block a visible owner.
- Blocked, each restating a decision already recorded in the Go package
  defaults: `rs/zerolog` → `log/slog`; the pre-fork `go-redis/redis` →
  `redis/go-redis/v9`; `sergi/go-diff` and `hexops/gotextdiff` → `go-udiff`.
  Every entry matches the module path exactly, so the pre-fork go-redis takes
  three: `go-redis/redis`, `/v7`, `/v8`. A prefix would also cover
  `go-redis/redismock`, the test double for the successor recommended here.

Deliberately absent: recorded rejections that vendoring repos still require
(`mattn/go-sqlite3`, `gorm.io/gorm`, `pmezard/go-difflib`), plus `urfave/cli`
and unversioned `go-chi/chi`. Blocking those would redden repos mid-migration
on their next re-vendor.

## After merge

The file's sha256 moves from
`d10f47ef5e0d8620efd62275b016a7de8fd5abedf333922eec86fe4abc06176e` to
`a79b63a254602a5318db5d0e9a06bc71b84bf0c1d896305229d8bfed1d1b1776`, so every
vendoring repo mismatches. #60 is the
propagation brief; it and the record on
#25 carry this value.

## Disclosures

- Judgement call: `wsl` moved two lines down to share the "deprecated" comment.
  No behaviour change, but it widens the diff.
- The `go.mod` survey and the settings-block probe are on
  #25.
- Unverified: the linter was not run against each vendoring repo; the per-repo
  claim rests on reading their `go.mod` files.
- `make check` exit 0.

Model: opus-5
Co-authored-by: Jeffrey Paul <sneak@noreply.example.org>
Reviewed-on: #55
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-09-09 14:04:18 +02:00
sneak 6c489067ce Milestone next: check-cache busting, build-context hygiene, lint in a container (#34)
check / check (push) Successful in 24s
Reviewed-on: #34
2026-09-09 14:01:54 +02:00
sneak c4d5546e86 Gate the build on Docker lint and test phases (closes #40, closes #30)
check / check (push) Successful in 5m12s
Per the owner ruling on issue 40, linting and testing are phases of the
main Dockerfile rather than a separate lint file. script/lint and
script/test build one phase each by name with caching disabled, and the
final stage copies a harmless file from each so the image cannot be built
unless both passed. A stage that is not the last is built only when
something depends on it or --target names it, so the gates are invoked by
name and the edges kept. script/check runs the gates and builds no image
of its own; script/cibuild bootstraps first, because CI runs it alone and
fmt-check is native. fmt and fmt-check source nvm for the pinned node
before calling yarn, which bootstrap installs but leaves off its caller's
PATH. Every build in script/ is tagged and uncached. Issue 30 closes too:
a container has its own lint cache and lock.

Model: opus-5
2026-09-09 11:44:59 +00:00
sneak 7b55c444ae Keep in-repo agent scratch out of the build context and out of git (closes #27)
The canonical .dockerignore and .gitignore both omitted the in-repo agent
scratch directory, which holds one worktree per in-flight agent, so under
`COPY . .` an entire extra checkout of the repo reached the image. The
two entries are deliberately different shapes: anchored in .dockerignore,
where the `**/` form would also delete a legitimately named nested
directory, and unanchored in .gitignore, where a pattern already matches
at every depth. Anchoring leaves a gap where agents run in
subdirectories, stated in the vendored file itself. The second half is
the consequence of excluding .git: `git describe` in a build stage yields
an empty version without erroring, so the version is now computed on the
host and passed in.

Model: opus-5
2026-09-09 11:44:59 +00:00
sneak c3a504f647 Keep secrets out of the Docker build context at every depth (closes #29)
The canonical .dockerignore was three lines while the canonical
Dockerfile does `COPY . .`, so a local .env, *.pem or *.key shipped into
the build context and could land in an image layer, invisible to every
git-based check. Copying .gitignore's patterns across is not the repair:
.dockerignore anchors an unprefixed pattern at the context root, so that
form protects only the repository root while reading as solved. Every
depth-independent pattern here carries `**/`, and secret names are
character ranges because matching is case-sensitive and an ALL-CAPS twin
still misses `Server.Key`. Public certificates are deliberately left in
as a legitimate build input. Verified by enumerating a probe image.

Model: opus-5
2026-09-09 11:44:58 +00:00
sneak 85bea7681e Compare versions when bootstrap installs a pinned tool (closes #28)
The canonical `if missing <tool>; then install; fi` guard tests PATH
presence and never version, so on any already-provisioned machine a pin
is inert and a version bump is a no-op, while the Dockerfile installs the
pinned version into a clean image and CI then disagrees with local about
what the tool is. Comparing versions alone is not enough either: an
installer writes to its own directory while callers resolve through PATH,
so a shadowing binary lets the install succeed and change nothing anyone
sees. REPO_POLICIES.md now states the whole form — exact whole-token
comparison, mis-parse falling through to a reinstall, re-resolution
through PATH after installing, and a call site that prints the version.

Model: opus-5
2026-09-09 11:44:58 +00:00
sneak 58f75147be Build with --no-cache so the check layer actually runs (closes #26)
script/cibuild was a plain `docker build .` and the Dockerfile does
`COPY . .` followed by `RUN make check`, so on an unchanged tree Docker
served the check layer from cache: the suite never ran and the build
still exited 0. Measured here before the change, a second run on a
byte-identical tree returned in 0.286s with `RUN make check` CACHED.
script/cibuild and script/docker now pass --no-cache. The canonical text
asserting that a bare `docker build .` proves the checks ran was wrong in
REPO_POLICIES.md, both checklists and the Go styleguide, and is corrected
in all of them.

Model: opus-5
2026-09-09 11:44:58 +00:00
58eafaf4c2 Add -count=1 to the canonical Go make test example (#45)
check / check (push) Successful in 7s
Answers #44: the canonical Go `make test` target omitted `-count=1`, so Go replayed cached successful results and the target could exit 0 having executed no test. Every repository that copied it inherited that false green.

The change adds `-count=1` to both `go test` invocations in the example in `REPO_POLICIES.md`, with a short paragraph saying why, and records the step in `TODO.md`. It defeats only the test-result cache, not the build cache, so it costs the suite's runtime and no recompilation. It is independent of the Docker layer cache that #26 addresses.

Rebased onto current `main`; the check is green. Repositories pick it up the next time each vendors the canonical files.

Model: opus-5 (change); fable-5-1 (this description)
Co-authored-by: sneak <sneak@sneak.berlin>
Co-authored-by: clawbot <cai2025@acidhou.se>
Co-authored-by: Jeffrey Paul <sneak@noreply.example.org>
Reviewed-on: #45
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-09-09 13:41:57 +02:00
11 changed files with 190 additions and 56 deletions
+40 -1
View File
@@ -10,13 +10,20 @@ run:
linters: linters:
default: all default: all
enable:
# Successor to the deprecated gomodguard. Named explicitly, rather than
# left to `default: all`, because it carries the module policy below.
- gomodguard_v2
disable: disable:
# Genuinely incompatible with project patterns # Genuinely incompatible with project patterns
- exhaustruct # Requires all struct fields - exhaustruct # Requires all struct fields
- godot # Requires comments to end with periods - godot # Requires comments to end with periods
- wsl # Deprecated, replaced by wsl_v5
- 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
# silenced by disabling that name, not by enabling the successor.
- wsl # Deprecated, replaced by wsl_v5
- gomodguard # Deprecated, replaced by gomodguard_v2
settings: settings:
lll: lll:
line-length: 88 line-length: 88
@@ -53,6 +60,38 @@ linters:
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.
# Only decisions already recorded in the Go package defaults are
# listed here. Every entry matches the module path exactly.
gomodguard_v2:
blocked:
- module: github.com/rs/zerolog
recommendations:
- log/slog
reason: "Structured logging is stdlib log/slog."
# One entry per pre-fork module path, because the later releases
# are separate paths. A prefix match would be shorter but would
# also reach github.com/go-redis/redismock, the test double for
# the successor these entries recommend.
- module: github.com/go-redis/redis
recommendations:
- github.com/redis/go-redis/v9
reason: "Pre-fork module; use the maintained go-redis v9."
- module: github.com/go-redis/redis/v7
recommendations:
- github.com/redis/go-redis/v9
reason: "Pre-fork module; use the maintained go-redis v9."
- module: github.com/go-redis/redis/v8
recommendations:
- github.com/redis/go-redis/v9
reason: "Pre-fork module; use the maintained go-redis v9."
- module: github.com/sergi/go-diff
recommendations:
- github.com/aymanbagabas/go-udiff
reason: "No unified diff output; use go-udiff."
- module: github.com/hexops/gotextdiff
recommendations:
- github.com/aymanbagabas/go-udiff
reason: "Unmaintained fork; use go-udiff."
issues: issues:
max-issues-per-linter: 0 max-issues-per-linter: 0
+8 -7
View File
@@ -123,16 +123,17 @@ alpine. We provide:
- `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)
- `script/check` — run all checks: `test`, `lint`, `fmt-check`, then - `script/check` — run all checks: `test`, `lint`, `fmt-check` (our own
`docker build -t prompts .` (our own extension; no `--no-cache` there, because extension); builds no image of its own
the two phases that build depends on were just built without it)
- `script/docker` - `script/docker`
`docker build --no-cache --build-arg VERSION="$version" -t prompts .`, the tag `docker build --no-cache --build-arg VERSION="$version" -t prompts .`, the tag
coming from `script/projectname` (byte-identical across repos) coming from `script/projectname` (byte-identical across repos)
- `script/cibuild` — cd to the repo root, compute `version` from `git describe`, - `script/cibuild` — cd to the repo root, run `script/bootstrap`, run
run `script/check`, then `script/check`, compute `version` from `git describe`, then
`docker build --build-arg VERSION="$version" -t prompts .` (what CI runs; the `docker build --no-cache --build-arg VERSION="$version" -t prompts .` (what CI
version is computed on the host because `.dockerignore` excludes `.git`) runs; it bootstraps because CI checks out and runs this alone while
`script/fmt-check` is native, and the version is computed on the host because
`.dockerignore` excludes `.git`)
- `script/precommit` — run by the git pre-commit hook (our own extension); calls - `script/precommit` — run by the git pre-commit hook (our own extension); calls
`script/check` `script/check`
- `script/install-precommit` — installs the git pre-commit hook (our own - `script/install-precommit` — installs the git pre-commit hook (our own
+11
View File
@@ -29,6 +29,8 @@ fmt-check, and commit.
they pass. This also closes issue 30: a container has its own result cache and they pass. This also closes issue 30: a container has its own result cache and
its own lock, so a lint verdict can no longer belong to another checkout. No its own lock, so a lint verdict can no longer belong to another checkout. No
separate lint Dockerfile, and no `golangci-lint config verify` step. separate lint Dockerfile, and no `golangci-lint config verify` step.
`script/check` runs the gates and nothing else, and `script/cibuild`
bootstraps first, since it is all CI runs and `script/fmt-check` is native.
- 2026-09-08: Kept in-repo agent scratch out of the Docker build context and out - 2026-09-08: Kept in-repo agent scratch out of the Docker build context and out
of version control: `.claude/` is one full checkout of the repo per in-flight of version control: `.claude/` is one full checkout of the repo per in-flight
agent, and under `COPY . .` all of it was reaching the image. Also closed the agent, and under `COPY . .` all of it was reaching the image. Also closed the
@@ -51,6 +53,15 @@ fmt-check, and commit.
layers cannot be served from cache on an unchanged tree, and the text claiming layers cannot be served from cache on an unchanged tree, and the text claiming
a bare `docker build .` proves the checks ran is corrected in a bare `docker build .` proves the checks ran is corrected in
`REPO_POLICIES.md`, both checklists and the Go styleguide. `REPO_POLICIES.md`, both checklists and the Go styleguide.
- 2026-09-03: Added `-count=1` to both `go test` invocations in the canonical Go
`make test` example in `REPO_POLICIES.md`, so the target cannot report a
cached pass it did not earn, and documented that Go's test-result cache is a
second, independent cache stacked below the Docker layer cache.
- 2026-08-31: Migrated the canonical `.golangci.yml` from the deprecated
`gomodguard` to `gomodguard_v2`: the old linter is disabled by name (which is
what silences the deprecation warning), the successor is named explicitly in
`linters.enable`, and it carries a `blocked` module list drawn only from
decisions already recorded in the Go package defaults.
- 2026-08-07: Set the canonical `.golangci.yml` to the org-standard v2-schema - 2026-08-07: Set the canonical `.golangci.yml` to the org-standard v2-schema
config already deployed byte-identical across the org's Go repos (settings config already deployed byte-identical across the org's Go repos (settings
under `linters.settings` so thresholds like lll/funlen/cyclop/dupl actually under `linters.settings` so thresholds like lll/funlen/cyclop/dupl actually
+13 -2
View File
@@ -99,6 +99,15 @@ with your task.
on the host. on the host.
- [ ] Every `docker build` in `script/` is tagged — an untagged one leaves a - [ ] Every `docker build` in `script/` is tagged — an untagged one leaves a
dangling image behind on every run, on every host and CI runner dangling image behind on every run, on every host and CI runner
- [ ] `script/cibuild` runs `script/bootstrap` before `script/check`, and builds
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
installed.
- [ ] `script/fmt` and `script/fmt-check` source nvm for the pinned node version
before invoking `yarn`, as `script/bootstrap`'s own install step does.
`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
nothing but docker and git.
- [ ] `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
@@ -151,8 +160,10 @@ with your task.
# Final # Final
- [ ] `make check` passes - [ ] `make check` passes
- [ ] `script/cibuild` succeeds and demonstrably executed the checks — a - [ ] `script/cibuild` succeeds in a fresh clone on a host carrying nothing but
sub-second build, or `CACHED` on a gate layer, means nothing ran docker and git, with no node or yarn on `PATH`, which is what CI has, and
demonstrably executed the checks — a sub-second build, or `CACHED` on a
gate layer, means nothing ran
- [ ] A planted lint violation fails both `make lint` and a plain - [ ] A planted lint violation fails both `make lint` and a plain
`docker build .`; revert it afterwards `docker build .`; revert it afterwards
- [ ] Commit and merge fixes before starting your actual task - [ ] Commit and merge fixes before starting your actual task
+16 -6
View File
@@ -113,15 +113,23 @@ are thin shims calling them. Model scripts:
container) container)
- [ ] `script/fmt-check` / `make fmt-check` — checks formatting (read-only; - [ ] `script/fmt-check` / `make fmt-check` — checks formatting (read-only;
native) native)
- [ ] `script/check` / `make check` — runs `test`, `lint`, `fmt-check`, then - [ ] `script/check` / `make check` — runs `test`, `lint`, `fmt-check`; must not
builds the image; must not modify files modify files
- [ ] `script/projectname` — outputs the project name (used by `script/docker` - [ ] `script/projectname` — outputs the project name (used by `script/docker`
for the image tag) for the image tag)
- [ ] `script/docker` / `make docker` — builds Docker image, tagged via - [ ] `script/docker` / `make docker` — builds Docker image, tagged via
`script/projectname` (byte-identical across repos); `--no-cache`, plus the `script/projectname` (byte-identical across repos); `--no-cache`, plus the
version as a build arg version as a build arg
- [ ] `script/cibuild` — cd to repo root, run `script/check`, then - [ ] `script/cibuild` — cd to repo root, run `script/bootstrap`, run
`docker build --build-arg VERSION="$version" .` (what CI runs) `script/check`, then
`docker build --no-cache --build-arg VERSION="$version" .` (what CI runs).
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
before invoking `yarn`, as `script/bootstrap`'s own install step does.
`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
nothing but docker and git.
- [ ] Every `docker build` in `script/` is tagged, so no invocation leaves a - [ ] Every `docker build` in `script/` is tagged, so no invocation leaves a
dangling image behind dangling image behind
- [ ] `script/precommit` — called by the pre-commit hook; runs `script/check` - [ ] `script/precommit` — called by the pre-commit hook; runs `script/check`
@@ -135,8 +143,10 @@ are thin shims calling them. Model scripts:
- [ ] `make check` passes - [ ] `make check` passes
- [ ] `make docker` succeeds - [ ] `make docker` succeeds
- [ ] `script/cibuild` succeeds and demonstrably executed the checks — a - [ ] `script/cibuild` succeeds in a fresh clone on a host carrying nothing but
sub-second build, or `CACHED` on a gate layer, means nothing ran docker and git, with no node or yarn on `PATH`, which is what CI has, and
demonstrably executed the checks — a sub-second build, or `CACHED` on a
gate layer, means nothing ran
- [ ] Plant a lint violation and confirm both `make lint` and a plain - [ ] Plant a lint violation and confirm both `make lint` and a plain
`docker build .` fail on it; revert. A plain build that passes proves the `docker build .` fail on it; revert. A plain build that passes proves the
final stage is missing its `COPY --from=` edge to the gate phases. final stage is missing its `COPY --from=` edge to the gate phases.
+47 -20
View File
@@ -60,10 +60,20 @@ style conventions are in separate documents:
prerequisite since nvm requires bash. yarn is then pinned via prerequisite since nvm requires bash. yarn is then pinned via
`corepack prepare yarn@<version> --activate`. Never install "latest" or "lts"; `corepack prepare yarn@<version> --activate`. Never install "latest" or "lts";
always exact versions. `script/cibuild` runs the CI build: it changes to the always exact versions. `script/cibuild` runs the CI build: it changes to the
repo root, runs `script/check`, and builds the image with the version; the repo root, runs `script/bootstrap`, runs `script/check`, and builds the image
Gitea workflow calls it. Four further scripts are our own extensions to the with the version; the Gitea workflow calls it. **`script/cibuild` runs
standard: `script/check` runs `script/test`, `script/lint` and `script/bootstrap` first**, because the workflow checks out the repo and runs
`script/fmt-check`, then builds the image; `script/precommit` is what the git nothing else, while `script/fmt-check` runs the formatter on the host: on a
pristine checkout with nothing installed the run dies there, after the
containerised gates have passed. **The bootstrap alone is not enough**:
`script/bootstrap` installs node and yarn under nvm and leaves neither on the
`PATH` of the shell that called it, so a bare `yarn` still exits 127. The host
entrypoints that need yarn — `script/fmt` and `script/fmt-check` — therefore
source nvm for the pinned node version before invoking it, exactly as
`script/bootstrap`'s own install step does. A runner carrying nothing but
docker and git then gets through `script/check`. Four further scripts are our
own extensions to the standard: `script/check` runs `script/test`,
`script/lint` and `script/fmt-check`; `script/precommit` is what the git
pre-commit hook runs, and it calls `script/check`; `script/install-precommit` pre-commit hook runs, and it calls `script/check`; `script/install-precommit`
installs the git pre-commit hook (the `make hooks` target shims to it); and installs the git pre-commit hook (the `make hooks` target shims to it); and
`script/projectname` (literally that filename) simply outputs the project's `script/projectname` (literally that filename) simply outputs the project's
@@ -115,7 +125,7 @@ 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 `script/check`, **Every `docker build` in `script/` is tagged**, here and in
`script/cibuild` and `script/docker`. An untagged build leaves a dangling `script/cibuild` and `script/docker`. An untagged build leaves a dangling
image behind on every invocation, on every developer host and every CI image behind on every invocation, on every developer host and every CI
runner; a tagged one replaces the previous image. runner; a tagged one replaces the previous image.
@@ -139,14 +149,13 @@ style conventions are in separate documents:
- **Any build that runs checks is built with `--no-cache`.** Docker invalidates - **Any build that runs checks is built with `--no-cache`.** Docker invalidates
a `COPY` layer only when the copied content changes, so on an unchanged tree a `COPY` layer only when the copied content changes, so on an unchanged tree
the check `RUN` is served from cache, nothing executes, and the build still the check `RUN` is served from cache, nothing executes, and the build still
exits 0. `script/lint`, `script/test`, `script/cibuild` and `script/docker` exits 0. Every `docker build` in `script/` therefore passes `--no-cache`:
therefore pass `--no-cache`, and a bare `docker build .` is not evidence that `script/lint`, `script/test`, `script/cibuild` and `script/docker` are the
anything ran: a sub-second build reporting success is a cache hit, not a four, and there is no fifth — `script/check` runs the two gate phases and
result. The one build that may go through the cache is the image build inside `script/fmt-check`, and builds no image of its own. A bare `docker build .` is
`script/check`, which runs immediately after the two gate phases were built not evidence that anything ran: a sub-second build reporting success is a
uncached and so reuses that run. Never invalidate by pruning — cache hit, not a result. Never invalidate by pruning — `docker builder prune`
`docker builder prune` and friends destroy a build cache shared with every and friends destroy a build cache shared with every other build on the host.
other build on the host.
- **The gate phases are separate stages, and the build stage depends on both.** - **The gate phases are separate stages, and the build stage depends on both.**
The lint phase is based on the `golangci/golangci-lint` image (pinned by The lint phase is based on the `golangci/golangci-lint` image (pinned by
@@ -218,10 +227,13 @@ style conventions are in separate documents:
`script/docker` and `script/cibuild`; no stage may call `git describe`. `script/docker` and `script/cibuild`; no stage may call `git describe`.
- Every repo should have a Gitea Actions workflow (`.gitea/workflows/`) that - Every repo should have a Gitea Actions workflow (`.gitea/workflows/`) that
runs `script/cibuild` on push. That script runs the gate phases and then the runs `script/cibuild` on push, and checks out the repo as its only other step.
image build, so a successful run means every check passed; a bare That script bootstraps, runs the gate phases, and then builds the image, so a
`docker build .` does not carry the same guarantee, because its gate phases successful run means every check passed; a bare `docker build .` does not
may come from the cache. carry the same guarantee, because its gate phases may come from the cache. The
image build is uncached and so runs the gate phases a second time. That is the
price of the rule above, and it is worth paying: the image that ships is built
from a run of its own gates rather than from a cache entry.
- Use platform-standard formatters: `black` for Python, `prettier` for - Use platform-standard formatters: `black` for Python, `prettier` for
JS/CSS/Markdown/HTML, `go fmt` for Go. Always use default configuration with JS/CSS/Markdown/HTML, `go fmt` for Go. Always use default configuration with
@@ -268,11 +280,24 @@ style conventions are in separate documents:
```makefile ```makefile
test: test:
@go test -timeout 90s -race -cover ./... || \ @go test -count=1 -timeout 90s -race -cover ./... || \
{ echo "--- Rerunning with -v for details ---"; \ { echo "--- Rerunning with -v for details ---"; \
go test -timeout 90s -race -v ./...; exit 1; } go test -count=1 -timeout 90s -race -v ./...; exit 1; }
``` ```
`-count=1` is required on both invocations: it defeats Go's test _result_
cache, so the target cannot report a pass it did not earn, and the rerun
reproduces a failure instead of replaying it. It leaves the build cache
alone, so it costs the runtime of the suite and no recompilation.
Note that this is a second, independent cache, stacked below the Docker
layer cache that [issue #26](https://git.eeqj.de/sneak/prompts/issues/26)
addresses. `CHECK_EPOCH` guarantees the `RUN make test` _step_ re-executes;
it does not guarantee `go test` inside that step does any work, because the
`GOCACHE` baked into earlier image layers survives into the re-executed
step. They are two separate defects requiring two separate fixes, and a fix
for one must not be recorded as covering the other.
Python example: Python example:
```makefile ```makefile
@@ -352,7 +377,9 @@ style conventions are in separate documents:
# trip `set -e`, so the inline form degrades to an empty constant. # trip `set -e`, so the inline form degrades to an empty constant.
version="$(git describe --tags --always --dirty 2>/dev/null || true)" version="$(git describe --tags --always --dirty 2>/dev/null || true)"
[ -n "$version" ] || version="unknown" [ -n "$version" ] || version="unknown"
docker build --no-cache --build-arg VERSION="$version" . docker build --no-cache \
--build-arg VERSION="$version" \
-t "$(script/projectname)" .
``` ```
`--always` makes an untagged repo yield an abbreviated commit hash rather `--always` makes an untagged repo yield an abbreviated commit hash rather
+4 -10
View File
@@ -1,22 +1,16 @@
#!/bin/sh #!/bin/sh
# script/check: run all checks (test, lint, fmt-check) and then the # script/check: run all checks (test, lint, fmt-check). Our own
# image build. Our own extension to scripts-to-rule-them-all. test and # extension to scripts-to-rule-them-all. test and lint are Docker
# lint are Docker phases; fmt-check is native, because a formatter # phases; fmt-check is native, because a formatter writes the working
# writes the working tree. Must not modify any files. # tree. Must not modify any files.
set -eu set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
main() { main() {
"$SCRIPT_DIR/test" "$SCRIPT_DIR/test"
"$SCRIPT_DIR/lint" "$SCRIPT_DIR/lint"
"$SCRIPT_DIR/fmt-check" "$SCRIPT_DIR/fmt-check"
# No --no-cache here: the two phases this build depends on were just
# built uncached above, so what it reuses is that run and not an
# older one.
cd "$ROOT"
docker build -t "$("$SCRIPT_DIR/projectname")" .
} }
main "$@" main "$@"
+9 -5
View File
@@ -1,7 +1,10 @@
#!/bin/sh #!/bin/sh
# script/cibuild: run the CI build. script/check runs the gates and # script/cibuild: run the CI build. It bootstraps first: a CI runner
# builds the image; this repeats the build with the version, which # checks out and runs this and nothing else, and script/fmt-check runs
# reuses the phases just built rather than re-running them. # the formatter on the host, which a pristine checkout cannot do.
# --no-cache for the same reason as script/docker: the gate phases the
# final stage depends on are RUN steps, and a cached one is a check that
# did not run.
set -eu set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
@@ -9,6 +12,8 @@ ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
main() { main() {
cd "$ROOT" cd "$ROOT"
"$SCRIPT_DIR/bootstrap"
"$SCRIPT_DIR/check"
# Own line: a failing command substitution inside an argument does # Own line: a failing command substitution inside an argument does
# not trip `set -e`, so the inline form degrades silently to an # not trip `set -e`, so the inline form degrades silently to an
# empty constant. VERSION is computed here because .dockerignore # empty constant. VERSION is computed here because .dockerignore
@@ -16,8 +21,7 @@ main() {
# version without failing. # version without failing.
version="$(git describe --tags --always --dirty 2>/dev/null || true)" version="$(git describe --tags --always --dirty 2>/dev/null || true)"
[ -n "$version" ] || version="unknown" [ -n "$version" ] || version="unknown"
"$SCRIPT_DIR/check" docker build --no-cache \
docker build \
--build-arg VERSION="$version" \ --build-arg VERSION="$version" \
-t "$("$SCRIPT_DIR/projectname")" . -t "$("$SCRIPT_DIR/projectname")" .
} }
+2 -3
View File
@@ -1,9 +1,8 @@
#!/bin/sh #!/bin/sh
# script/docker: build the Docker image tagged with the project name. # script/docker: build the Docker image tagged with the project name.
# Identical in all repos; the tag comes from script/projectname. # Identical in all repos; the tag comes from script/projectname.
# --no-cache because this is a standalone entrypoint: the lint and test # --no-cache because the gate phases the final stage depends on are RUN
# phases the final stage depends on must run rather than be served from # steps, and a cached one is a check that did not run.
# an older build.
set -eu set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
+20 -1
View File
@@ -4,9 +4,28 @@ set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
# Must match the pin in script/bootstrap.
NODE_VERSION="22.17.0"
# script/bootstrap installs node and yarn under nvm and leaves neither
# on the PATH of the shell that called it, so resolve the pinned
# toolchain here the way bootstrap's own install step does. nvm is a
# bash script, hence the subshell.
run_yarn() {
if command -v yarn >/dev/null 2>&1; then
exec yarn "$@"
fi
if [ ! -s "$HOME/.nvm/nvm.sh" ]; then
echo "fmt: no yarn; run script/bootstrap first" >&2
exit 1
fi
exec bash -c '. "$HOME/.nvm/nvm.sh" && nvm use "$1" >/dev/null &&
shift && exec yarn "$@"' bash "$NODE_VERSION" "$@"
}
main() { main() {
cd "$ROOT" cd "$ROOT"
yarn run prettier --write '**/*.md' --tab-width 4 --prose-wrap always run_yarn run prettier --write '**/*.md' --tab-width 4 --prose-wrap always
} }
main "$@" main "$@"
+20 -1
View File
@@ -4,9 +4,28 @@ set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
# Must match the pin in script/bootstrap.
NODE_VERSION="22.17.0"
# script/bootstrap installs node and yarn under nvm and leaves neither
# on the PATH of the shell that called it, so resolve the pinned
# toolchain here the way bootstrap's own install step does. nvm is a
# bash script, hence the subshell.
run_yarn() {
if command -v yarn >/dev/null 2>&1; then
exec yarn "$@"
fi
if [ ! -s "$HOME/.nvm/nvm.sh" ]; then
echo "fmt-check: no yarn; run script/bootstrap first" >&2
exit 1
fi
exec bash -c '. "$HOME/.nvm/nvm.sh" && nvm use "$1" >/dev/null &&
shift && exec yarn "$@"' bash "$NODE_VERSION" "$@"
}
main() { main() {
cd "$ROOT" cd "$ROOT"
yarn run prettier --check '**/*.md' --tab-width 4 --prose-wrap always run_yarn run prettier --check '**/*.md' --tab-width 4 --prose-wrap always
} }
main "$@" main "$@"