1 Commits
Author SHA1 Message Date
sneak f3d5deb71b Say where a repository's own .gitignore and .editorconfig entries go (closes #103)
check / check (push) Successful in 35s
The canonical `.gitignore` and `.editorconfig` now each end with a comment saying a repository's own entries go below it and a re-vendor keeps them, as `.dockerignore`'s header already does; without it a re-vendor dropped a repository's binaries and test outputs from `.gitignore`. `.editorconfig` gains `[*.go]` with tabs, since `gofmt` decides Go indentation in every repository. `prompts/REPO_POLICIES.md`, both checklists and the Go styleguide say the same in plain sentences. Common outputs such as `*.test` stay out of the canonical `.gitignore`. This also covers #104.

Model: opus-5-5
2026-10-06 00:46:49 +00:00
15 changed files with 137 additions and 365 deletions
-11
View File
@@ -1,20 +1,9 @@
name: check name: check
on: [push] on: [push]
# Free the shared runner: a new push cancels only the same branch's older run.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
check: check:
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Free the shared runner from a hung build.
timeout-minutes: 20
steps: steps:
# actions/checkout v4.2.2, 2026-02-22 # actions/checkout v4.2.2, 2026-02-22
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
# script/cibuild needs no token, so none is left in .git/config.
with:
persist-credentials: false
# All history and tags, so git describe finds the version tag.
fetch-depth: 0
- run: script/cibuild - run: script/cibuild
-2
View File
@@ -25,8 +25,6 @@ linters:
# silenced by disabling that name, not by enabling the successor. # silenced by disabling that name, not by enabling the successor.
- wsl # Deprecated, replaced by wsl_v5 - wsl # Deprecated, replaced by wsl_v5
- gomodguard # Deprecated, replaced by gomodguard_v2 - gomodguard # Deprecated, replaced by gomodguard_v2
# Misses findings at random in v2.14.0; back once a pinned release fixes it
- canonicalheader
settings: settings:
lll: lll:
line-length: 88 line-length: 88
+4 -8
View File
@@ -116,14 +116,10 @@ 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` — - `script/test` — `docker build --no-cache --target test -t prompts-test .`,
`docker build --no-cache --target test --output type=cacheonly .`, building building the `test` phase of the `Dockerfile` (no tests defined here)
the `test` phase of the `Dockerfile` without writing an image (no tests - `script/lint` — `docker build --no-cache --target lint -t prompts-lint .`,
defined here) building the `lint` phase, which runs prettier over the markdown files
- `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)
-84
View File
@@ -21,83 +21,6 @@ fmt-check, and commit.
# Completed Steps # Completed Steps
- 2026-10-08: The canonical `script/bootstrap` now uses the installed node only
when its major version is the pinned one (issue 118). Otherwise, as when node
is missing, it installs the pinned node under nvm and installs yarn and the
packages under it. It prints the version of the node it uses either way. The
CI runner image ships node 24, under which the pinned yarn 1.22.22 printed the
deprecation warning DEP0169 on every bootstrap. Only the major is compared
because 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; the
version-comparison rule in `REPO_POLICIES.md` names node as its exception.
`script/fmt` and `script/fmt-check` pick their yarn with the same test: the
`yarn` on `PATH` when the installed node is the pinned major, and otherwise
yarn under the pinned node through nvm. `REPO_POLICIES.md` and both checklists
say so, and the checklists' final `script/cibuild` check names what the CI
runner image has: node 24 and no yarn. Not yet tried on the shared runner.
Repositories pick this up on their next re-vendor.
- 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
`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.
`script/cibuild` runs three Docker builds, each held to the 5-minute Docker
build limit, plus the bootstrap; if that limit changes (issue 113), the value
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
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
`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
stamps the same version in CI as in a local build. `REPO_POLICIES.md` and both
checklists name `fetch-depth: 0` among what the workflow does, next to
`persist-credentials: false` and the `concurrency` block, instead of asking
each tagged repository to add it. Not yet tried on the shared runner, which is
out of disk space. Repositories pick this up on their next re-vendor.
- 2026-10-06: The canonical `script/bootstrap` now runs `apt-get update` once,
before the first `apt-get install` of a run (issue 115). The Gitea runner
image starts with empty package lists, so installing anything it lacks, such
as Go, failed with `Unable to locate package`. A repository's own section no
longer needs a refresh of its own; `sneak/bsfirehose` and `sneak/dnswatcher`
drop theirs at their next re-vendor.
- 2026-10-06: `REPO_POLICIES.md` and both checklists now say that a checkout
whose `.git` is a file, a linked worktree or a repository checked out as a
submodule, is the exception to a plain `docker build .` succeeding (issue
111): that file points to a git directory outside the build context, so the
build cannot read the version and the version check in the canonical
`Dockerfile` stops it. Such a build is given its version with
`--build-arg VERSION=...`, as `script/docker` and `script/cibuild` already do.
The check itself is unchanged.
- 2026-10-06: The canonical `.gitea/workflows/check.yml` now has a `concurrency`
block, so a new push cancels the older run on the same branch and no other,
and its checkout step sets `persist-credentials: false`, so the job's token is
not left in `.git/config` (issue 107). `REPO_POLICIES.md` and both checklists
describe the workflow as it now is. Not yet tried on the shared runner, which
is out of disk space. Repositories pick this up on their next re-vendor.
- 2026-10-06: The canonical `.golangci.yml` now disables `canonicalheader`
(issue 105). In golangci-lint v2.14.0 it misses findings at random in a
package that also calls `ResponseWriter.Header()`, so the same tree can fail
lint on one run and pass on the next. It comes back once a pinned
golangci-lint release fixes it.
- 2026-10-06: The canonical `.gitignore` and `.editorconfig` now each end with a - 2026-10-06: The canonical `.gitignore` and `.editorconfig` now each end with a
comment saying the repository's own entries go below it and a re-vendor keeps comment saying the repository's own entries go below it and a re-vendor keeps
them (issue 103, which took in issue 104), as `.dockerignore`'s header already them (issue 103, which took in issue 104), as `.dockerignore`'s header already
@@ -107,13 +30,6 @@ fmt-check, and commit.
repository's own entries, which a re-vendor keeps, and the Go styleguide puts repository's own entries, which a re-vendor keeps, and the Go styleguide puts
`*.log`, `*.out`, `*.test` and binaries among those entries. Common outputs `*.log`, `*.out`, `*.test` and binaries among those entries. Common outputs
stay out of the canonical `.gitignore`; each repository lists its own. stay out of the canonical `.gitignore`; each repository lists its own.
- 2026-10-05: `script/cibuild`, `script/docker`, `script/lint` and `script/test`
now assign the image tag from `script/projectname` on its own line before the
`docker build` (issue 101), so `set -e` stops the script where
`script/projectname` fails instead of running `docker build` with a broken
tag. The comment above it in each script says why, and the snippets in
`REPO_POLICIES.md` show the same form. Repositories pick this up on their next
re-vendor.
- 2026-10-04: Went through the fleet findings recorded on 2026-08-09 (issue 62) - 2026-10-04: Went through the fleet findings recorded on 2026-08-09 (issue 62)
and added the two rules `REPO_POLICIES.md` did not yet state: a new or changed and added the two rules `REPO_POLICIES.md` did not yet state: a new or changed
check is proven by planting a defect it must catch; and a change to a separate check is proven by planting a defect it must catch; and a change to a separate
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"private": true, "license": "MIT",
"devDependencies": { "devDependencies": {
"prettier": "3.8.1" "prettier": "3.8.1"
} }
+23 -38
View File
@@ -1,6 +1,6 @@
--- ---
title: Existing Repo Checklist title: Existing Repo Checklist
last_modified: 2026-10-08 last_modified: 2026-10-06
--- ---
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
@@ -91,21 +91,12 @@ with your task.
version still comes out empty, `dev` or `unknown`. A plain version still comes out empty, `dev` or `unknown`. A plain
`docker build .` with no build arguments must succeed; a Dockerfile that `docker build .` with no build arguments must succeed; a Dockerfile that
refuses an empty build argument drops that refusal and keeps the argument. refuses an empty build argument drops that refusal and keeps the argument.
A checkout whose `.git` is a file (a linked worktree, or a repository `script/docker` and `script/cibuild` pass the version they compute on the
checked out as a submodule) is the exception: that file points to a git host; it takes precedence. A tag-derived version additionally needs
directory outside the build context, so the build cannot read the version `fetch-depth: 0` on the CI checkout step, which clones shallow and fetches
and a plain `docker build .` fails; pass the version with no tags by default.
`--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 — reference
`fetch-depth: 0` (which fetches the tags `git describe` needs), carries
the `concurrency` block that lets a new push cancel only the same branch's
older run, and sets `timeout-minutes: 20` on the `check` job so a hung
build frees the shared runner — reference
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitea/workflows/check.yml` `https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitea/workflows/check.yml`
- [ ] Language-specific config: - [ ] Language-specific config:
- [ ] Go: `go.mod`, `go.sum`, `.golangci.yml` (fetch from - [ ] Go: `go.mod`, `go.sum`, `.golangci.yml` (fetch from
@@ -132,30 +123,25 @@ 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` each run - [ ] `script/lint` and `script/test` build their phase by name
`docker build --no-cache --target <phase> --output type=cacheonly .`, (`docker build --no-cache --target <phase> -t <name>-<phase> .`), and no
which builds their phase by name and writes no image, and no host host invocation anywhere in the repo can produce a lint verdict — grep for
invocation anywhere in the repo can produce a lint verdict — grep for the the linter's own name across `script/`, the `Makefile` and CI config, not
linter's own name across `script/`, the `Makefile` and CI config, not just just `script/lint`. A second path is likeliest here: a `make lint-fast`,
`script/lint`. A second path is likeliest here: a `make lint-fast`, an an older host-versus-container branch, or a CI step calling the binary
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.
- [ ] No `docker build` in `script/` leaves a dangling image behind: - [ ] Every `docker build` in `script/` is tagged — an untagged one leaves a
`script/lint` and `script/test` write no image, and `script/docker` and dangling image behind on every run, on every host and CI runner
`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
installed. installed.
- [ ] `script/fmt` and `script/fmt-check` pick their `yarn` with the same test - [ ] `script/fmt` and `script/fmt-check` source nvm for the pinned node version
as `script/bootstrap`: the `yarn` on `PATH` when the node on `PATH` has before invoking `yarn`, as `script/bootstrap`'s own install step does.
the pinned major version, and otherwise yarn under the pinned node, with `script/bootstrap` leaves the node and yarn it installs off the `PATH` of
nvm loaded through `$HOME/.nvm/nvm.sh`. `script/bootstrap` leaves the node the shell that called it, so a bare `yarn` exits 127 on a runner carrying
and yarn it installs under nvm off the `PATH` of the shell that called it, nothing but docker and git.
so a bare `yarn` exits 127 on a runner carrying nothing but docker and
git, or runs under another node.
- [ ] `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
@@ -208,11 +194,10 @@ 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 nothing but
(the runner image `docker.gitea.com/runner-images:ubuntu-latest`: docker, docker and git, with no node or yarn on `PATH`, which is what CI has, and
git and node 24, with no yarn on `PATH`), and demonstrably executed the demonstrably executed the checks — a sub-second build, or `CACHED` on a
checks — a sub-second build, or `CACHED` on a gate layer, means nothing gate layer, means nothing ran
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
+23 -44
View File
@@ -1,6 +1,6 @@
--- ---
title: New Repo Checklist title: New Repo Checklist
last_modified: 2026-10-08 last_modified: 2026-10-06
--- ---
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
@@ -97,12 +97,6 @@ Template files can be fetched from:
version still comes out empty, `dev` or `unknown`. A plain version still comes out empty, `dev` or `unknown`. A plain
`docker build .` with no build arguments must succeed; a Dockerfile that `docker build .` with no build arguments must succeed; a Dockerfile that
refuses an empty build argument drops that refusal and keeps the argument. refuses an empty build argument drops that refusal and keeps the argument.
A checkout whose `.git` is a file (a linked worktree, or a repository
checked out as a submodule) is the exception: that file points to a git
directory outside the build context, so the build cannot 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
@@ -112,11 +106,7 @@ Template files can be fetched from:
- 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 — reference
with `fetch-depth: 0` (which fetches the tags `git describe` needs),
carries the `concurrency` block that lets a new push cancel only the same
branch's older run, and sets `timeout-minutes: 20` on the `check` job so a
hung build frees the shared runner — reference
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitea/workflows/check.yml` `https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitea/workflows/check.yml`
- [ ] Language-specific: - [ ] Language-specific:
- [ ] Go: `go mod init sneak.berlin/go/<name>`, `.golangci.yml` (fetch from - [ ] Go: `go mod init sneak.berlin/go/<name>`, `.golangci.yml` (fetch from
@@ -136,22 +126,18 @@ are thin shims calling them. Model scripts:
alpine images without bash alpine images without bash
- [ ] `script/bootstrap` / `make bootstrap` — installs all dependencies, - [ ] `script/bootstrap` / `make bootstrap` — installs all dependencies,
idempotently, assuming nothing (pkg manager detection nix/apt/brew/apk; idempotently, assuming nothing (pkg manager detection nix/apt/brew/apk;
node used if its major version is the pinned one, else pinned version via node used if present, else pinned version via nvm from a hash-verified
nvm from a hash-verified archive, with yarn and the packages installed archive; pinned yarn via corepack); a non-server repo's development
under it; pinned yarn via corepack); a non-server repo's development
environment stage runs it instead of inline installs; a gate phase or the environment stage runs it instead of inline installs; a gate phase or the
build stage installs what its base image lacks either inline or by running build stage installs what its base image lacks either inline or by running
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` — - [ ] `script/test` / `make test` — `docker build --no-cache --target test .`,
`docker build --no-cache --target test --output type=cacheonly .`, which tagged; the phase runs real tests, not a no-op (90-second timeout,
writes no image; the phase runs real tests, not a no-op (90-second 60-second hard cap on wall time)
timeout, 60-second hard cap on wall time) - [ ] `script/lint` / `make lint` — `docker build --no-cache --target lint .`,
- [ ] `script/lint` / `make lint` — tagged. No lint verdict may come from a host invocation of the linter.
`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;
@@ -165,22 +151,16 @@ 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" -t "$tag" .` (what `docker build --no-cache --build-arg VERSION="$version" .` (what CI runs).
CI runs), with `$version` from `git describe --tags --always --dirty` The bootstrap is required: CI checks out and runs this alone, and
(`unknown` if empty) and `$tag` from `script/projectname`, each assigned `script/fmt-check` runs the formatter on the host.
on its own line before the build. The bootstrap is required: CI checks out - [ ] `script/fmt` and `script/fmt-check` source nvm for the pinned node version
and runs this alone, and `script/fmt-check` runs the formatter on the before invoking `yarn`, as `script/bootstrap`'s own install step does.
host. `script/bootstrap` leaves the node and yarn it installs off the `PATH` of
- [ ] `script/fmt` and `script/fmt-check` pick their `yarn` with the same test the shell that called it, so a bare `yarn` exits 127 on a runner carrying
as `script/bootstrap`: the `yarn` on `PATH` when the node on `PATH` has nothing but docker and git.
the pinned major version, and otherwise yarn under the pinned node, with - [ ] Every `docker build` in `script/` is tagged, so no invocation leaves a
nvm loaded through `$HOME/.nvm/nvm.sh`. `script/bootstrap` leaves the node dangling image behind
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:
`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`
@@ -192,11 +172,10 @@ 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 nothing but
(the runner image `docker.gitea.com/runner-images:ubuntu-latest`: docker, docker and git, with no node or yarn on `PATH`, which is what CI has, and
git and node 24, with no yarn on `PATH`), and demonstrably executed the demonstrably executed the checks — a sub-second build, or `CACHED` on a
checks — a sub-second build, or `CACHED` on a gate layer, means nothing gate layer, means nothing ran
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.
+46 -82
View File
@@ -1,6 +1,6 @@
--- ---
title: Repository Policies title: Repository Policies
last_modified: 2026-10-08 last_modified: 2026-10-06
--- ---
This document covers repository structure, tooling, and workflow standards. Code This document covers repository structure, tooling, and workflow standards. Code
@@ -54,39 +54,34 @@ style conventions are in separate documents:
`cibuild`. `script/bootstrap` installs all dependencies idempotently and `cibuild`. `script/bootstrap` installs all dependencies idempotently and
assumes nothing is present: base tools come from nix, apt, brew, or apk assumes nothing is present: base tools come from nix, apt, brew, or apk
(detected in that order; apt runs noninteractive). For node it uses the (detected in that order; apt runs noninteractive). For node it uses the
installed node only when its major version is the pinned one; otherwise (node installed node if present; otherwise it installs a PINNED node version via
missing, or another major, such as the node 24 the CI runner image ships) it nvm, first installing nvm itself if missing — from a hash-verified GitHub
installs the PINNED node version via nvm and installs yarn and the packages release archive (never `curl | sh`), with bash installed as an explicit
under it. nvm itself is installed first if missing, from a hash-verified prerequisite since nvm requires bash. yarn is then pinned via
GitHub release archive (never `curl | sh`), with bash installed as an explicit `corepack prepare yarn@<version> --activate`. Never install "latest" or "lts";
prerequisite since nvm requires bash. Only the major version is compared always exact versions. `script/cibuild` runs the CI build: it changes to the
because the `Dockerfile` stages start from a node image whose exact version is repo root, runs `script/bootstrap`, runs `script/check`, and builds the image
not the pin, and nvm cannot install a prebuilt node on alpine. yarn is pinned with the version; the Gitea workflow calls it. **`script/cibuild` runs
via `corepack prepare yarn@<version> --activate`. Never install "latest" or
"lts"; always exact versions. `script/cibuild` runs the CI build: it changes
to the repo root, runs `script/bootstrap`, runs `script/check`, and builds the
image with the version; the Gitea workflow calls it. **`script/cibuild` runs
`script/bootstrap` first**, because the workflow checks out the repo and runs `script/bootstrap` first**, because the workflow checks out the repo and runs
nothing else, while `script/fmt-check` runs the formatter on the host: on a 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 pristine checkout with nothing installed the run dies there, after the
containerised gates have passed. **The bootstrap alone is not enough**: when containerised gates have passed. **The bootstrap alone is not enough**:
`script/bootstrap` installs node and yarn under nvm it leaves neither on the `script/bootstrap` installs node and yarn under nvm and leaves neither on the
`PATH` of the shell that called it, so a bare `yarn` either exits 127 or runs `PATH` of the shell that called it, so a bare `yarn` still exits 127. The host
under another node. The host entrypoints that need yarn — `script/fmt` and entrypoints that need yarn — `script/fmt` and `script/fmt-check` — therefore
`script/fmt-check` — therefore use the same test as `script/bootstrap`: when source nvm for the pinned node version before invoking it, exactly as
the node on `PATH` has the pinned major version they run the `yarn` on `PATH`, `script/bootstrap`'s own install step does. A runner carrying nothing but
and otherwise they load nvm through `$HOME/.nvm/nvm.sh` and run yarn under the docker and git then gets through `script/check`. Four further scripts are our
pinned node. A runner carrying nothing but docker and git then gets through own extensions to the standard: `script/check` runs `script/test`,
`script/check`. Four further scripts are our own extensions to the standard: `script/lint` and `script/fmt-check`; `script/precommit` is what the git
`script/check` runs `script/test`, `script/lint` and `script/fmt-check`; pre-commit hook runs, and it calls `script/check`; `script/install-precommit`
`script/precommit` is what the git pre-commit hook runs, and it calls installs the git pre-commit hook (the `make hooks` target shims to it); and
`script/check`; `script/install-precommit` installs the git pre-commit hook `script/projectname` (literally that filename) simply outputs the project's
(the `make hooks` target shims to it); and `script/projectname` (literally name. Scripts that need the name call `script/projectname` — e.g.
that filename) simply outputs the project's name. Scripts that need the name `script/docker` assembles its image tag from it — so those scripts stay
call `script/projectname` — e.g. `script/docker` assembles its image tag from byte-identical across all repos. Repo-type-specific pre-commit extras (e.g.
it — so those scripts stay byte-identical across all repos. Repo-type-specific `go mod tidy` verification in Go repos) belong in `script/precommit`, not in
pre-commit extras (e.g. `go mod tidy` verification in Go repos) belong in the hook itself. Model scripts are at
`script/precommit`, not in the hook itself. Model scripts are at
`https://git.eeqj.de/sneak/prompts/raw/branch/main/script/<name>`. The README `https://git.eeqj.de/sneak/prompts/raw/branch/main/script/<name>`. The README
must document the provided scripts in an **Entrypoints** section (see the must document the provided scripts in an **Entrypoints** section (see the
README requirements below). README requirements below).
@@ -123,8 +118,8 @@ style conventions are in separate documents:
and nothing else: and nothing else:
```sh ```sh
docker build --no-cache --target lint --output type=cacheonly . docker build --no-cache --target lint -t "$(script/projectname)-lint" .
docker build --no-cache --target test --output type=cacheonly . docker build --no-cache --target test -t "$(script/projectname)-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
@@ -134,15 +129,10 @@ 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.
**The gate builds write no image.** With `--output type=cacheonly` the phase **Every `docker build` in `script/` is tagged**, here and in
runs and a failing step fails the build, but the result is not exported. `script/cibuild` and `script/docker`. An untagged build leaves a dangling
Nothing uses those images, and writing one out is slow: a Go test phase's image behind on every invocation, on every developer host and every CI
image holds the toolchain and every compiled package. A build given neither runner; a tagged one replaces the previous image.
`--output` nor `-t` writes an untagged image and leaves it dangling, on
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
@@ -288,39 +278,20 @@ style conventions are in separate documents:
`.git` and the version still comes out empty, `dev` or `unknown`. A plain `.git` and the version still comes out empty, `dev` or `unknown`. A plain
`docker build .` with no build arguments must succeed; a Dockerfile that `docker build .` with no build arguments must succeed; a Dockerfile that
refuses an empty build argument drops that refusal and keeps the argument. refuses an empty build argument drops that refusal and keeps the argument.
A checkout whose `.git` is a file (a linked worktree, or a repository
checked out as a submodule) is the exception: that file points to a git
directory outside the build context, so the build cannot read the version
and a plain `docker build .` fails; pass the version with
`--build-arg VERSION=...`, as `script/docker` and `script/cibuild` already
do.
- 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, and checks out the repo as its only other step, runs `script/cibuild` on push, and checks out the repo as its only other step.
with `persist-credentials: false`: `script/cibuild` needs no token, and That script bootstraps, runs the gate phases, and then builds the image, so a
without it the checkout leaves the job's token in `.git/config` for every successful run means every check passed; a bare `docker build .` does not
later step. The checkout step also sets `fetch-depth: 0`, which fetches the
tags `git describe` needs: by default it clones shallow with no tags, and a
tagged repository's CI build would stamp a bare short commit id. The
workflow's `concurrency` block groups runs by workflow and branch
(`${{ 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
no other: runs for replaced commits do not hold up the shared runner.
`script/cibuild` bootstraps, runs the gate phases, and then builds the image,
so a successful run means every check passed; a bare `docker build .` does not
carry the same guarantee, because its gate phases may come from the cache. The 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 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 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. The `check` job from a run of its own gates rather than from a cache entry. A separate
sets `timeout-minutes: 20`, so a hung build frees the shared runner after 20 workflow limited to `main` by a `branches` list under `on: push` cannot be
minutes. That allows for the three Docker builds described above (the test checked by review: to try a change to it, add the feature branch to that list
phase, the lint phase, then the image), each held to the 5-minute Docker build and push, then remove the branch from the list again before merging. Keep any
limit below, plus the bootstrap. A separate workflow limited to `main` by a job in it that publishes behind `if: github.ref_name == 'main'`, so the run
`branches` list under `on: push` cannot be checked by review: to try a change from the feature branch publishes nothing.
to it, add the feature branch to that list and push, then remove the branch
from the list again before merging. Keep any job in it that publishes behind
`if: github.ref_name == 'main'`, so the run from the feature branch publishes
nothing.
- 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
@@ -466,15 +437,13 @@ style conventions are in separate documents:
byte-identically across repos: byte-identically across repos:
```sh ```sh
# The version and the tag each get their own line: a failing command # Own line: a failing command substitution inside an argument does not
# substitution inside an argument does not trip `set -e`, so the inline # trip `set -e`, so the inline form degrades to an empty constant.
# 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"
tag="$(script/projectname)"
docker build --no-cache \ docker build --no-cache \
--build-arg VERSION="$version" \ --build-arg VERSION="$version" \
-t "$tag" . -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
@@ -486,8 +455,8 @@ style conventions are in separate documents:
there because `ARG` is stage-scoped; passing `VERSION` to a repo whose there because `ARG` is stage-scoped; passing `VERSION` to a repo whose
Dockerfile declares no such `ARG` is ignored and costs nothing, which is why Dockerfile declares no such `ARG` is ignored and costs nothing, which is why
the scripts stay byte-identical. One consequence for CI: the standard the scripts stay byte-identical. One consequence for CI: the standard
checkout action clones shallow and fetches no tags, so the canonical checkout action clones shallow and fetches no tags, so a repo that embeds a
`.gitea/workflows/check.yml` sets `fetch-depth: 0` on its checkout step. tag-derived version must set `fetch-depth: 0` on its checkout step.
- **Verify `.dockerignore` by enumerating the image, not by reading the - **Verify `.dockerignore` by enumerating the image, not by reading the
patterns.** Plant files at the root _and_ at least two directories deep, build patterns.** Plant files at the root _and_ at least two directories deep, build
@@ -554,11 +523,6 @@ style conventions are in separate documents:
function defined and never invoked has the same exit status and the same function defined and never invoked has the same exit status and the same
empty output as one that worked. empty output as one that worked.
Node is the exception to the whole-token comparison: the canonical
`script/bootstrap` compares only its major version, because the node 22
alpine image the `Dockerfile` stages start from is not the exact pin, and
nvm cannot install a prebuilt node on alpine.
Keep it POSIX sh: no arrays, no `[[`, no `grep -P`. Keep it POSIX sh: no arrays, no `[[`, no `grep -P`.
A Go tool a repo needs on the host is installed with `go install` pinned to A Go tool a repo needs on the host is installed with `go install` pinned to
+12 -43
View File
@@ -2,12 +2,10 @@
# 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 tooling comes from nix, apt, brew, # installed tools are skipped. Base tooling comes from nix, apt, brew,
# or apk (detected in that order); assumes nothing is present. The # or apk (detected in that order); assumes nothing is present. Node is
# installed node is used only when its major version is the pinned one; # used directly if installed; otherwise it is installed at a pinned
# otherwise (node missing, or another major) the pinned version is # version via nvm (installing nvm itself first, from a hash-verified
# installed via nvm (installing nvm itself first, from a hash-verified # release archive, never curl | sh).
# 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)"
@@ -21,7 +19,6 @@ YARN_VERSION="1.22.22"
PKGMGR="" PKGMGR=""
SUDO="" SUDO=""
APT_UPDATED=""
detect_pkgmgr() { detect_pkgmgr() {
[ -n "$PKGMGR" ] && return 0 [ -n "$PKGMGR" ] && return 0
@@ -50,14 +47,7 @@ pkg_install() {
detect_pkgmgr detect_pkgmgr
case "$PKGMGR" in case "$PKGMGR" in
nix) nix-env -iA "nixpkgs.$1" ;; nix) nix-env -iA "nixpkgs.$1" ;;
apt) apt) $SUDO env DEBIAN_FRONTEND=noninteractive apt-get install -y "$2" ;;
# Package lists may be empty (fresh images); refresh once per run.
if [ -z "$APT_UPDATED" ]; then
$SUDO env DEBIAN_FRONTEND=noninteractive apt-get update
APT_UPDATED=1
fi
$SUDO env DEBIAN_FRONTEND=noninteractive apt-get install -y "$2"
;;
brew) brew install "$3" ;; brew) brew install "$3" ;;
apk) apk add --no-cache "$4" ;; apk) apk add --no-cache "$4" ;;
esac esac
@@ -102,52 +92,31 @@ ensure_nvm() {
rm -rf "$tmp" 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() { ensure_node() {
if node_is_pinned_major; then if ! missing node; then return 0; fi
echo "bootstrap: using node $(node --version)"
return 0
fi
ensure_nvm ensure_nvm
nvm_sh "nvm install $NODE_VERSION" nvm_sh "nvm install $NODE_VERSION"
nvm_sh "nvm use $NODE_VERSION >/dev/null && \
echo \"bootstrap: using node \$(node --version)\""
} }
ensure_yarn() { 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 yarn; then return 0; fi
if ! missing corepack; then if ! missing corepack; then
corepack enable corepack enable
corepack prepare "yarn@$YARN_VERSION" --activate corepack prepare "yarn@$YARN_VERSION" --activate
elif [ -s "$HOME/.nvm/nvm.sh" ]; then
nvm_sh "nvm use $NODE_VERSION >/dev/null && corepack enable && \
corepack prepare yarn@$YARN_VERSION --activate"
else else
npm install -g "yarn@$YARN_VERSION" npm install -g "yarn@$YARN_VERSION"
fi fi
} }
install_js_deps() { install_js_deps() {
if node_is_pinned_major; then if missing yarn && [ -s "$HOME/.nvm/nvm.sh" ]; then
yarn install --frozen-lockfile
else
nvm_sh "nvm use $NODE_VERSION >/dev/null && cd \"$ROOT\" && \ nvm_sh "nvm use $NODE_VERSION >/dev/null && cd \"$ROOT\" && \
yarn install --frozen-lockfile" yarn install --frozen-lockfile"
else
yarn install --frozen-lockfile
fi fi
} }
+5 -7
View File
@@ -14,17 +14,15 @@ main() {
cd "$ROOT" cd "$ROOT"
"$SCRIPT_DIR/bootstrap" "$SCRIPT_DIR/bootstrap"
"$SCRIPT_DIR/check" "$SCRIPT_DIR/check"
# The version and the tag each get their own line: a failing # Own line: a failing command substitution inside an argument does
# command substitution inside an argument does not trip `set -e`, # not trip `set -e`, so the inline form degrades silently to an
# so the inline form degrades silently to an empty constant. The # empty constant. The VERSION build argument takes precedence over
# VERSION build argument takes precedence over the version a build # the version a build stage derives from the .git in the context.
# stage derives from the .git in the context.
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"
tag="$("$SCRIPT_DIR/projectname")"
docker build --no-cache \ docker build --no-cache \
--build-arg VERSION="$version" \ --build-arg VERSION="$version" \
-t "$tag" . -t "$("$SCRIPT_DIR/projectname")" .
} }
main "$@" main "$@"
+5 -7
View File
@@ -10,17 +10,15 @@ ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
main() { main() {
cd "$ROOT" cd "$ROOT"
# The version and the tag each get their own line: a failing # Own line: a failing command substitution inside an argument does
# command substitution inside an argument does not trip `set -e`, # not trip `set -e`, so the inline form degrades silently to an
# so the inline form degrades silently to an empty constant. The # empty constant. The VERSION build argument takes precedence over
# VERSION build argument takes precedence over the version a build # the version a build stage derives from the .git in the context.
# stage derives from the .git in the context.
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"
tag="$("$SCRIPT_DIR/projectname")"
docker build --no-cache \ docker build --no-cache \
--build-arg VERSION="$version" \ --build-arg VERSION="$version" \
-t "$tag" . -t "$("$SCRIPT_DIR/projectname")" .
} }
main "$@" main "$@"
+6 -16
View File
@@ -7,25 +7,15 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
# Must match the pin in script/bootstrap. # Must match the pin in script/bootstrap.
NODE_VERSION="22.17.0" NODE_VERSION="22.17.0"
# True when the node on PATH has the pinned major version: the same # script/bootstrap installs node and yarn under nvm and leaves neither
# function as in script/bootstrap, which uses it to decide where it # on the PATH of the shell that called it, so resolve the pinned
# installs yarn. # toolchain here the way bootstrap's own install step does. nvm is a
node_is_pinned_major() { # bash script, hence the subshell.
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 node_is_pinned_major && command -v yarn >/dev/null 2>&1; then if command -v yarn >/dev/null 2>&1; then
exec yarn "$@" exec yarn "$@"
fi fi
if node_is_pinned_major || [ ! -s "$HOME/.nvm/nvm.sh" ]; then if [ ! -s "$HOME/.nvm/nvm.sh" ]; then
echo "fmt: no yarn; run script/bootstrap first" >&2 echo "fmt: no yarn; run script/bootstrap first" >&2
exit 1 exit 1
fi fi
+6 -16
View File
@@ -7,25 +7,15 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
# Must match the pin in script/bootstrap. # Must match the pin in script/bootstrap.
NODE_VERSION="22.17.0" NODE_VERSION="22.17.0"
# True when the node on PATH has the pinned major version: the same # script/bootstrap installs node and yarn under nvm and leaves neither
# function as in script/bootstrap, which uses it to decide where it # on the PATH of the shell that called it, so resolve the pinned
# installs yarn. # toolchain here the way bootstrap's own install step does. nvm is a
node_is_pinned_major() { # bash script, hence the subshell.
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 node_is_pinned_major && command -v yarn >/dev/null 2>&1; then if command -v yarn >/dev/null 2>&1; then
exec yarn "$@" exec yarn "$@"
fi fi
if node_is_pinned_major || [ ! -s "$HOME/.nvm/nvm.sh" ]; then if [ ! -s "$HOME/.nvm/nvm.sh" ]; then
echo "fmt-check: no yarn; run script/bootstrap first" >&2 echo "fmt-check: no yarn; run script/bootstrap first" >&2
exit 1 exit 1
fi fi
+3 -3
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. --output type=cacheonly writes no image, since # that did not run. The tag makes each build replace the previous image
# nothing uses one. # instead of leaving a dangling one behind.
set -eu set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
@@ -17,7 +17,7 @@ main() {
cd "$ROOT" cd "$ROOT"
docker build --no-cache \ docker build --no-cache \
--target lint \ --target lint \
--output type=cacheonly . -t "$("$SCRIPT_DIR/projectname")-lint" .
} }
main "$@" main "$@"
+3 -3
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, and --no-cache because a cached test layer is a test that did # named, --no-cache because a cached test layer is a test that did not
# not run. --output type=cacheonly writes no image, since nothing uses one. # run, and a tag so each build replaces the previous image.
set -eu set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
@@ -13,7 +13,7 @@ main() {
cd "$ROOT" cd "$ROOT"
docker build --no-cache \ docker build --no-cache \
--target test \ --target test \
--output type=cacheonly . -t "$("$SCRIPT_DIR/projectname")-test" .
} }
main "$@" main "$@"