The linter is no longer installed on the host and no longer invoked there. script/lint is now `docker build -f Dockerfile.lint .` and nothing else, with the linter running as a build step, so a successful build of that file is a clean lint — and it works unchanged where the docker daemon is remote and bind mounts are impossible. That removes three host-only failure mechanisms rather than mitigating them: the result cache keyed on file content rather than location, which produced a confirmed false green and a string of findings reported against other checkouts; the host-global $TMPDIR/golangci-lint.lock, which fails a run with `parallel golangci-lint is running` in a way no caller can distinguish from findings; and host/container version skew, which hid thirteen findings on one repo. A container per run has its own cache, its own lock and a binary pinned by digest. Resolving the recursion this creates. script/lint is a docker build, so a Dockerfile that runs `make check` would nest a build inside a build step where there is no daemon. Fixed by direction, not detection: the main Dockerfile runs script/test and script/fmt-check individually, with a comment saying why `make check` must not come back, and script/cibuild runs script/lint first for fail-fast feedback. script/check still runs all three, so developers and the pre-commit hook are unaffected. Dockerfile.lint carries the same CHECK_EPOCH guard as the main image, with the ARG placed below the dependency layer so only the lint steps re-run. Blanket --no-cache was rejected: it re-runs the dependency install on every lint and makes linting network-dependent. golangci-lint config verify is kept, on measurement rather than preference. Under the pinned v2.12.2, a bogus top-level key and a bogus key nested under linters.settings.lll both pass `golangci-lint run` with exit 0 and `0 issues` while config verify exits 3 and names them; an unknown linter name fails run and passes config verify. The two catch disjoint classes, and `run` alone silently ignores the class where a threshold reads as configured and is not applied. The concern that config verify fetches its JSON schema over live HTTPS does not hold for this version: every case reproduced byte-identically under `docker run --network none`, in a container where `getent hosts golangci-lint.run` exits 2. The schema is embedded in the pinned binary. Two canonical forms are superseded and deleted rather than left standing beside the new one, because consuming repos read these documents literally and two contradictory canonical script/lint forms is worse than either. The script/bootstrap golangci-lint install landed for #28 is removed: nothing invokes a host linter now, so it can only reintroduce the skew it was written to close. Its version-enforcement principle — compare version not presence, re-resolve through PATH after installing, let a mis-parse fall through to reinstall, and call it — stays documented for any other pinned host tool. The per-checkout GOLANGCI_LINT_CACHE/TMPDIR wrapper is removed with it; its entire subject was making a host run trustworthy. Adopting repos delete .lint-cache/ from .gitignore and .dockerignore too. The Go multistage lint stage and its COPY --from=lint ordering trick go the same way: that stage ran `make lint`, which is now a docker build. Corrected everywhere the claim that a successful docker build implies lint passed — REPO_POLICIES.md, both repo checklists, the Go styleguide and the README. The guarantee now belongs to script/cibuild, which runs both container builds; a bare `docker build .` never lints at all. Verified in this repo, not only documented: two consecutive script/lint runs on a byte-identical tree both executed prettier (4.556s and 3.738s, lint layers DONE with a fresh epoch printed, dependency layers CACHED as intended); a planted violation failed the build naming the file, and reverting it went green; a bare `docker build -f Dockerfile.lint .` failed on the guard; make check, script/docker and script/cibuild all green with the check layers demonstrably executing; and the main image build completed without attempting a nested build. Rework, from independent review of this commit. The canonical text is the deliverable here, so a false sentence is a fleet-wide defect: the Dockerfile rule still said the build "fails if the branch is not green", which stopped being true when lint left that image, and the earlier sweep grepped one phrasing rather than the claim. Re-swept on the claim itself — green/red-branch wording, build-fails wording, entailment verbs near build/lint/check, and "linted" asserted as covered — across prompts/, README.md, TODO.md, both Dockerfiles and every script. Two absolutes are narrowed to what is actually true, because seventeen repos adopt this literally. The rule is that no lint VERDICT may come from a host invocation, not that the binary never exists on the host: a JS repo's `yarn install` puts its linter in node_modules on the host unavoidably, and in a repo whose formatter is its linter — this one — `script/fmt-check` runs the same command that Dockerfile.lint runs. That gap is now stated with its bound (the version is pinned in the repo's own node_modules, so no shared cache, no host lock, nothing to skew) and the audit grep keeps its reach, gaining a note on which two hits are expected rather than being weakened. script/lint conflates "found issues" with "could not run": docker build exits 1 for both. The exit-75 VOID machinery is deliberately not restored, and the reasoning is now recorded where a reader looking for it lands. The dangerous direction is already closed, since a build that cannot run fails closed and can never read as clean; BuildKit already names the failing step, where the old lock error went to stderr while findings went to stdout and was easy to lose; and the failure is not transient, so the retry that justified the old machinery would be wrong here. Rebuilding the distinction would mean per-invocation capture files and traps again plus matching on BuildKit's message format, which is not a stable interface, and a mis-match in the "treat as infrastructure" direction would be the false green this rule exists to prevent. What survives is binding as a reading rule: a run that did not reach the lint step is not a verdict. Also corrected: script/lint was listed above a CHECK_EPOCH snippet that does a bare `docker build .` with no -f, which would have built the main image and linted nothing; the canonical Go Dockerfile template used `make fmt-check` / `make test` where every prose rule in the same document says script/, one Makefile edit away from re-entering the recursion; README omitted the mandatory VERSION build arg; script/docker did not say lint had left its image, a comment that propagates fleet-wide; the "byte-identical across repos" claim for script/lint is narrowed to its executable lines; and "--no-cache makes linting network-dependent" is softened to the measured comparative claim.
9.7 KiB
title, last_modified
| title | last_modified |
|---|---|
| New Repo Checklist | 2026-08-10 |
Use this checklist when creating a new repository from scratch. Follow the steps
in order. Full policies are at
https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/REPO_POLICIES.md.
Template files can be fetched from:
https://git.eeqj.de/sneak/prompts/raw/branch/main/<path>
1. Initialize
git init- Ask the user for the license (MIT, GPL, or WTFPL)
2. First Commit (README only)
- Create
README.mdwith all required sections:- Description: name, purpose, category, license, author
- Getting Started: copy-pasteable code block
- Rationale: why does this exist?
- Design: how is it structured?
- TODO: initial task list
- License: matches chosen license
- Author: @sneak
git add README.md && git commit
3. Scaffolding (feature branch)
git checkout -b initial-scaffolding
Fetch Template Files
.gitignore— fetch fromhttps://git.eeqj.de/sneak/prompts/raw/branch/main/.gitignore, extend for language-specific artifacts. Extensions are written to.gitignore's own semantics, where an unanchored pattern already matches at every depth: never add a**/prefix here, which is a.dockerignoreform. The canonical file already carries.claude/so agent worktrees cannot be committed by accident..editorconfig— fetch fromhttps://git.eeqj.de/sneak/prompts/raw/branch/main/.editorconfigMakefile— fetch fromhttps://git.eeqj.de/sneak/prompts/raw/branch/main/Makefile, adapt targets for the project's language and tools- For JS/docs repos:
.prettierrcand.prettierignore— fetch fromhttps://git.eeqj.de/sneak/prompts/raw/branch/main/.prettierrcandhttps://git.eeqj.de/sneak/prompts/raw/branch/main/.prettierignore
Create Project Files
LICENSEfile matching the chosen licenseREPO_POLICIES.md— fetch fromhttps://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/REPO_POLICIES.mdDockerfileand.dockerignore— fetch.dockerignorefromhttps://git.eeqj.de/sneak/prompts/raw/branch/main/.dockerignore- Extend
.dockerignorewith the repo's own host-built artifacts, giving every depth-independent pattern a**/prefix — but write a repo-root binary anchored,/myappand never**/myapp, which would also matchcmd/myapp/and delete the package directory. Do not transplant.gitignore's patterns:.dockerignoreanchors an unprefixed pattern at the context root, so the copied form leavesconfig/.envin the build context while reading as solved. See the.dockerignorerule inREPO_POLICIES.md. The canonical file's.claudeentry is anchored for the same reason as a repo-root binary; leave it that way, but note it only covers agents running at the repo root — if this repo will run them in subdirectories,services/api/.claude/is not excluded and needs its own anchored entry. - If the image embeds a version in a binary, the version is computed on the
host and passed with
--build-arg VERSION=....ARG VERSION=devis declared in the stage that compiles, and no stage callsgit describe—.dockerignoreexcludes.git, so it yields an empty version without failing the build. - The
Dockerfileruns the non-lint checks as build steps —script/testandscript/fmt-check, nevermake check.script/lintis adocker buildofDockerfile.lint, somake checkhere nests a build inside a build step, where there is no daemon. Put a comment above thoseRUNlines saying so. Every stage containing a check-runningRUNmust declareARG CHECK_EPOCHwith theRUN [ -n "$CHECK_EPOCH" ] || exit 1guard immediately below it — see theCHECK_EPOCHrule inREPO_POLICIES.md. Without them the check layer is served from cache on an unchanged tree and the build reports a green it never ran. - Server: also builds and runs the application
- Non-server: brings up dev environment and runs those checks
- Image pinned by sha256 hash with version/date comment
- Extend
Dockerfile.lint— the lint-only image thatscript/lintbuilds. SameARG CHECK_EPOCH+ guard + expanded-value discipline as above, with theARGplaced after the dependency layer so only the lint steps re-run. Base image pinned by sha256 with a version/date comment. Go repos usegolangci/golangci-lintand run bothgolangci-lint config verifyandgolangci-lint run; other repos use the same pattern around their own linter (eslint, ruff, prettier). Copy the canonical file fromREPO_POLICIES.md. The linter is invoked directly there, never viamake lint, which would recurse.- Gitea Actions workflow at
.gitea/workflows/check.ymlthat runsscript/cibuildon push — referencehttps://git.eeqj.de/sneak/prompts/raw/branch/main/.gitea/workflows/check.yml - Language-specific:
- Go:
go mod init sneak.berlin/go/<name>,.golangci.yml(fetch fromhttps://git.eeqj.de/sneak/prompts/raw/branch/main/.golangci.yml) - JS:
yarn init,yarn add --dev prettier - Python:
pyproject.toml
- Go:
Configure script/ Entrypoints and Makefile
Implementations live in script/ (scripts-to-rule-them-all); Makefile targets
are thin shims calling them. Model scripts:
https://git.eeqj.de/sneak/prompts/raw/branch/main/script/<name>
-
scripts are POSIX sh (
#!/bin/sh,set -eu, no bashisms) so they run on alpine images without bash -
script/bootstrap/make bootstrap— installs all dependencies, idempotently, assuming nothing (pkg manager detection nix/apt/brew/apk; node used if present, else pinned version via nvm from a hash-verified archive; pinned yarn via corepack); Dockerfile runs it instead of inline installs -
script/setup/make setup— readies a fresh clone: runsbootstrap, theninstall-precommit, plus repo-specific init -
script/test/make test— runs real tests, not a no-op (30-second timeout) -
script/lint/make lint— buildsDockerfile.lintand nothing else:epoch="$(date +%s%N)$$"on its own line, thendocker build --build-arg CHECK_EPOCH="$epoch" -f Dockerfile.lint .. No lint verdict may come from a host invocation. Copy the canonical script fromREPO_POLICIES.md; its executable lines are identical across repos. Without the nonce this script exits 0 on an unchanged tree having linted nothing, and without-f Dockerfile.lintit builds the main image and lints nothing at all. -
script/fmt/make fmt— formats code (writes) -
script/fmt-check/make fmt-check— checks formatting (read-only) -
script/check/make check— runstest,lint,fmt-check; must not modify files. It needs a docker daemon, becausescript/lintis a container build, and it must never be called from inside a build stage -
script/projectname— outputs the project name (used byscript/dockerfor the image tag) -
script/docker/make docker— builds Docker image, tagged viascript/projectname(byte-identical across repos); carries the same three version lines asscript/cibuildbelow, and passes--build-arg CHECK_EPOCH="$epoch"and--build-arg VERSION="$version" -
script/cibuild— cd to repo root, runscript/lintfirst for fail-fast feedback, then, each on its own line:epoch="$(date +%s%N)$$" version="$(git describe --tags --always --dirty 2>/dev/null || true)" [ -n "$version" ] || version="unknown" docker build \ --build-arg CHECK_EPOCH="$epoch" \ --build-arg VERSION="$version" \ .(what CI runs). The
script/lintcall is not optional: the main image does not lint, so without it CI never lints. Both build args are mandatory, and both assignments must be on their own line: a failing command substitution inside an argument does not tripset -e, so the inline form degrades silently to an empty constant. The[ -n "$version" ]line is a live check that fires on an export with no.gitand on a repo with no commits — keep it, and do not collapse it into|| echo unknown, which makes it unreachable. See theCHECK_EPOCHand git-describe rules inREPO_POLICIES.mdfor why each element is load-bearing. A baredocker build .fails closed by design, and so does a baredocker build -f Dockerfile.lint .. -
script/precommit— called by the pre-commit hook; runsscript/check -
script/install-precommit— installs the pre-commit hook that runsscript/precommit -
make hooks— shims toscript/install-precommit -
README Entrypoints section documents the scripts and links the standard
4. Verify
make checkpassesmake lintdemonstrably runs the linter rather than returning a cached build: run it twice on an unchanged tree and confirm the lint layer saysDONE, neverCACHED, both timesmake dockersucceedsscript/cibuildsucceeds and runs both container builds- No secrets in repo
- No mutable image/package references
- No unnecessary files in repo root
- All dates written as YYYY-MM-DD
5. Merge and Set Up
- Commit, merge to
main make hooksto install pre-commit hook- Add remote and push
- Verify
mainpassesmake check