1 Commits
Author SHA1 Message Date
sneak 3746261144 Say which Dockerfile stages run script/bootstrap (closes #90)
check / check (push) Failing after 1s
The bullet in `prompts/REPO_POLICIES.md` that requires a `Dockerfile` said every Dockerfile installs its prerequisites by running `script/bootstrap`, while the canonical Go `Dockerfile` never runs it.

The bullet now says the gate phases and the build stage start from their pinned base images and install what those images lack either inline, as the Go example does for `git`, or by running `script/bootstrap`, as the `prompts` repo's own `Dockerfile` does for its yarn packages. The development environment stage, the final stage of a non-server repo, runs `script/bootstrap`. The new repo checklist says the same.

Model: opus-5-5
2026-10-04 12:15:55 +00:00
12 changed files with 41 additions and 126 deletions
-6
View File
@@ -10,9 +10,3 @@ insert_final_newline = true
[Makefile] [Makefile]
indent_style = tab indent_style = tab
[*.go]
indent_style = tab
# This repository's own sections, such as one for another language it
# uses, go below this comment, and a re-vendor keeps them.
+1 -5
View File
@@ -27,7 +27,7 @@ node_modules/
# Environment files. `*.env` covers bare `.env` and the `prod.env` # Environment files. `*.env` covers bare `.env` and the `prod.env`
# convention. Only the templates `example.env` and `sample.env` are # convention. Only the templates `example.env` and `sample.env` are
# re-included below. A repository that commits any other template adds # re-included below. A repository that commits any other template adds
# its own negation at the end of this file, for example `!.env.example`. # its own negation after these lines, for example `!.env.example`.
*.[eE][nN][vV] *.[eE][nN][vV]
.[eE][nN][vV].* .[eE][nN][vV].*
.[eE][nN][vV][rR][cC] .[eE][nN][vV][rR][cC]
@@ -45,7 +45,3 @@ node_modules/
[iI][dD]_[eE][cC][dD][sS][aA]_[sS][kK] [iI][dD]_[eE][cC][dD][sS][aA]_[sS][kK]
[iI][dD]_[eE][dD]25519 [iI][dD]_[eE][dD]25519
[iI][dD]_[eE][dD]25519_[sS][kK] [iI][dD]_[eE][dD]25519_[sS][kK]
# This repository's own entries, such as its build outputs, go below
# this comment, and a re-vendor keeps them. Anchor a binary built at the
# root: `/myapp`, never `myapp`, which also ignores `cmd/myapp/`.
-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
-32
View File
@@ -21,38 +21,6 @@ fmt-check, and commit.
# Completed Steps # Completed Steps
- 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
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
does. `.editorconfig` gains a `[*.go]` section with tabs, since `gofmt`
decides Go indentation everywhere. `REPO_POLICIES.md` and both checklists say
that each of these two files is the canonical content followed by the
repository's own entries, which a re-vendor keeps, and the Go styleguide puts
`*.log`, `*.out`, `*.test` and binaries among those entries. Common outputs
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)
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
workflow limited to `main` is first run from the feature branch, added to that
workflow's `branches` list and removed again before merging. The other
findings were already stated, replaced by `--no-cache`, about git worktrees,
or about how agents work together. The warning against
`golangci-lint config verify` is dropped because sneak ruled on
https://git.eeqj.de/sneak/prompts/issues/40 (2026-08-10) that there is no
config check step and the config is assumed valid; a vendored `.golangci.yml`
stays byte-identical to the canonical copy. The issue gives each reason.
- 2026-10-04: `REPO_POLICIES.md` now says which `Dockerfile` stages run - 2026-10-04: `REPO_POLICIES.md` now says which `Dockerfile` stages run
`script/bootstrap` (issue 90). The gate phases and the build stage start from `script/bootstrap` (issue 90). The gate phases and the build stage start from
their pinned base images and install what those images lack either inline, as their pinned base images and install what those images lack either inline, as
+3 -4
View File
@@ -1,6 +1,6 @@
--- ---
title: Code Styleguide — Go title: Code Styleguide — Go
last_modified: 2026-10-06 last_modified: 2026-10-04
--- ---
1. Try to hard wrap long lines at 77 characters or less. 1. Try to hard wrap long lines at 77 characters or less.
@@ -148,9 +148,8 @@ last_modified: 2026-10-06
handle HTTP requests. Don't use methods or your top level functions as handle HTTP requests. Don't use methods or your top level functions as
handlers. handlers.
1. The repository's own entries at the end of `.gitignore`, which a re-vendor 1. Provide a .gitignore file that ignores at least `*.log`, `*.out`, and
keeps, ignore at least `*.log`, `*.out`, and `*.test` files, as well as any `*.test` files, as well as any binaries.
binaries.
1. Constructors **must** be called `New()`. `modulename.New()` works great if 1. Constructors **must** be called `New()`. `modulename.New()` works great if
you name the packages properly. If the constructor creates an instance from you name the packages properly. If the constructor creates an instance from
+5 -10
View File
@@ -1,6 +1,6 @@
--- ---
title: Existing Repo Checklist title: Existing Repo Checklist
last_modified: 2026-10-06 last_modified: 2026-10-04
--- ---
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
@@ -28,18 +28,13 @@ with your task.
root — never a file or directory named after one agent tool, such as root — never a file or directory named after one agent tool, such as
`CLAUDE.md` or `.claude/`, and never separate memory files. Move what any `CLAUDE.md` or `.claude/`, and never separate memory files. Move what any
such committed file says into `AGENTS.md` and delete it. such committed file says into `AGENTS.md` and delete it.
- [ ] `.gitignore` is comprehensive (OS, editor, agent scratch, secrets, the - [ ] `.gitignore` is comprehensive (OS, editor, agent scratch, language
repo's own build outputs) — fetch from artifacts, secrets) — fetch from
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitignore` if missing. `https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitignore` if missing.
An existing repo usually has a hand-written one that is never re-fetched, An existing repo usually has a hand-written one that is never re-fetched,
so check the entries rather than the file's presence. The file is the so check the entries rather than the file's presence.
canonical content followed by the repo's own entries, such as its
binaries; a re-vendor replaces the canonical part and keeps those entries.
- [ ] `.editorconfig` exists — fetch from - [ ] `.editorconfig` exists — fetch from
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.editorconfig`. The `https://git.eeqj.de/sneak/prompts/raw/branch/main/.editorconfig`
file is the canonical content followed by the repo's own sections, such as
one for another language it uses; a re-vendor replaces the canonical part
and keeps those sections.
- [ ] `Dockerfile` and `.dockerignore` exist; the Dockerfile carries a `lint` - [ ] `Dockerfile` and `.dockerignore` exist; the Dockerfile carries a `lint`
phase and a `test` phase, and the final stage carries a `COPY --from=` of phase and a `test` phase, and the final stage carries a `COPY --from=` of
a harmless file from each — fetch `.dockerignore` from a harmless file from each — fetch `.dockerignore` from
+8 -11
View File
@@ -1,6 +1,6 @@
--- ---
title: New Repo Checklist title: New Repo Checklist
last_modified: 2026-10-06 last_modified: 2026-10-04
--- ---
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
@@ -34,17 +34,14 @@ Template files can be fetched from:
## Fetch Template Files ## Fetch Template Files
- [ ] `.gitignore` — fetch from - [ ] `.gitignore` — fetch from
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitignore`, then add `https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitignore`, extend for
the repo's own build outputs, such as its binaries, at the end of the language-specific artifacts. Extensions are written to `.gitignore`'s own
file, where a re-vendor keeps them. Extensions are written to semantics, where an unanchored pattern already matches at every depth:
`.gitignore`'s own semantics, where an unanchored pattern already matches never add a `**/` prefix here, which is a `.dockerignore` form. The
at every depth: never add a `**/` prefix here, which is a `.dockerignore` canonical file already carries `.claude/` so agent worktrees cannot be
form. The canonical file already carries `.claude/` so agent worktrees committed by accident.
cannot be committed by accident.
- [ ] `.editorconfig` — fetch from - [ ] `.editorconfig` — fetch from
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.editorconfig`, then `https://git.eeqj.de/sneak/prompts/raw/branch/main/.editorconfig`
add the repo's own sections, such as one for another language it uses, at
the end of the file, where a re-vendor keeps them.
- [ ] `Makefile` — fetch from - [ ] `Makefile` — fetch from
`https://git.eeqj.de/sneak/prompts/raw/branch/main/Makefile`, adapt `https://git.eeqj.de/sneak/prompts/raw/branch/main/Makefile`, adapt
targets for the project's language and tools targets for the project's language and tools
+12 -32
View File
@@ -1,6 +1,6 @@
--- ---
title: Repository Policies title: Repository Policies
last_modified: 2026-10-06 last_modified: 2026-10-04
--- ---
This document covers repository structure, tooling, and workflow standards. Code This document covers repository structure, tooling, and workflow standards. Code
@@ -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 -t "$(script/projectname)-lint" .
docker build --no-cache --target lint -t "$tag-lint" . docker build --no-cache --target test -t "$(script/projectname)-test" .
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
@@ -133,9 +132,7 @@ style conventions are in separate documents:
**Every `docker build` in `script/` is tagged**, here and in **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. Each script assigns the runner; a tagged one replaces the previous image.
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
@@ -163,9 +160,6 @@ style conventions are in separate documents:
not evidence that anything ran: a sub-second build reporting success is a not evidence that anything ran: a sub-second build reporting success is a
cache hit, not a result. Never invalidate by pruning — `docker builder prune` cache hit, not a result. Never invalidate by pruning — `docker builder prune`
and friends destroy a build cache shared with every other build on the host. and friends destroy a build cache shared with every other build on the host.
When a check is added or changed, prove it works by planting a defect it must
catch and watching the run fail on it, then revert the defect. A green run
alone shows neither that the check ran nor that it covers what it should.
- **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
@@ -289,12 +283,7 @@ style conventions are in separate documents:
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. A separate from a run of its own gates rather than from a cache entry.
workflow limited to `main` by a `branches` list under `on: push` cannot be
checked by review: to try a change 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
@@ -387,12 +376,9 @@ style conventions are in separate documents:
- `.gitignore` should be comprehensive from the start: OS files (`.DS_Store`), - `.gitignore` should be comprehensive from the start: OS files (`.DS_Store`),
editor files (`.swp`, `*~`), in-repo agent scratch directories (`.claude/`), editor files (`.swp`, `*~`), in-repo agent scratch directories (`.claude/`),
`node_modules/`, and the repo's own build outputs. Fetch the standard language build artifacts, and `node_modules/`. Fetch the standard `.gitignore`
`.gitignore` from from `https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitignore` when
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitignore` when setting up setting up a new repo. These patterns are written to `.gitignore`'s own
a new repo. A repo's `.gitignore` is the standard file followed by the repo's
own entries, such as its binaries; a re-vendor replaces the standard part and
keeps those entries. These patterns are written to `.gitignore`'s own
semantics, in which an unanchored pattern already matches at every depth; they semantics, in which an unanchored pattern already matches at every depth; they
are not a `.dockerignore` and must not be transplanted into one unmodified. are not a `.dockerignore` and must not be transplanted into one unmodified.
@@ -440,15 +426,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
@@ -644,11 +628,7 @@ style conventions are in separate documents:
Never edit existing migrations after release. Never edit existing migrations after release.
- All repos should have an `.editorconfig` enforcing the project's indentation - All repos should have an `.editorconfig` enforcing the project's indentation
settings: the standard file from settings.
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.editorconfig`, which sets
tabs for `Makefile` and Go files, followed by the repo's own sections, such as
one for another language it uses. A re-vendor replaces the standard part and
keeps those sections.
- Avoid putting files in the repo root unless necessary. Root should contain - Avoid putting files in the repo root unless necessary. Root should contain
only project-level config files (`README.md`, `AGENTS.md`, `Makefile`, only project-level config files (`README.md`, `AGENTS.md`, `Makefile`,
+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 "$@"
+1 -5
View File
@@ -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" . -t "$("$SCRIPT_DIR/projectname")-lint" .
} }
main "$@" main "$@"
+1 -5
View File
@@ -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" . -t "$("$SCRIPT_DIR/projectname")-test" .
} }
main "$@" main "$@"