Files
prompts/prompts/NEW_REPO_CHECKLIST.md
sneak 35858dab66
All checks were successful
check / check (push) Successful in 13s
Run every lint in a container via Dockerfile.lint (closes #40)
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.
2026-08-10 13:14:57 +00:00

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.md with 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 from https://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 .dockerignore form. The canonical file already carries .claude/ so agent worktrees cannot be committed by accident.
  • .editorconfig — fetch from https://git.eeqj.de/sneak/prompts/raw/branch/main/.editorconfig
  • Makefile — fetch from https://git.eeqj.de/sneak/prompts/raw/branch/main/Makefile, adapt targets for the project's language and tools
  • For JS/docs repos: .prettierrc and .prettierignore — fetch from https://git.eeqj.de/sneak/prompts/raw/branch/main/.prettierrc and https://git.eeqj.de/sneak/prompts/raw/branch/main/.prettierignore

Create Project Files

  • LICENSE file matching the chosen license
  • REPO_POLICIES.md — fetch from https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/REPO_POLICIES.md
  • Dockerfile and .dockerignore — fetch .dockerignore from https://git.eeqj.de/sneak/prompts/raw/branch/main/.dockerignore
    • Extend .dockerignore with the repo's own host-built artifacts, giving every depth-independent pattern a **/ prefix — but write a repo-root binary anchored, /myapp and never **/myapp, which would also match cmd/myapp/ and delete the package directory. Do not transplant .gitignore's patterns: .dockerignore anchors an unprefixed pattern at the context root, so the copied form leaves config/.env in the build context while reading as solved. See the .dockerignore rule in REPO_POLICIES.md. The canonical file's .claude entry 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=dev is declared in the stage that compiles, and no stage calls git describe.dockerignore excludes .git, so it yields an empty version without failing the build.
    • The Dockerfile runs the non-lint checks as build steps — script/test and script/fmt-check, never make check. script/lint is a docker build of Dockerfile.lint, so make check here nests a build inside a build step, where there is no daemon. Put a comment above those RUN lines saying so. Every stage containing a check-running RUN must declare ARG CHECK_EPOCH with the RUN [ -n "$CHECK_EPOCH" ] || exit 1 guard immediately below it — see the CHECK_EPOCH rule in REPO_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
  • Dockerfile.lint — the lint-only image that script/lint builds. Same ARG CHECK_EPOCH + guard + expanded-value discipline as above, with the ARG placed after the dependency layer so only the lint steps re-run. Base image pinned by sha256 with a version/date comment. Go repos use golangci/golangci-lint and run both golangci-lint config verify and golangci-lint run; other repos use the same pattern around their own linter (eslint, ruff, prettier). Copy the canonical file from REPO_POLICIES.md. The linter is invoked directly there, never via make lint, which would recurse.
  • Gitea Actions workflow at .gitea/workflows/check.yml that runs script/cibuild on push — reference https://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 from https://git.eeqj.de/sneak/prompts/raw/branch/main/.golangci.yml)
    • JS: yarn init, yarn add --dev prettier
    • Python: pyproject.toml

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: runs bootstrap, then install-precommit, plus repo-specific init

  • script/test / make test — runs real tests, not a no-op (30-second timeout)

  • script/lint / make lint — builds Dockerfile.lint and nothing else: epoch="$(date +%s%N)$$" on its own line, then docker build --build-arg CHECK_EPOCH="$epoch" -f Dockerfile.lint .. No lint verdict may come from a host invocation. Copy the canonical script from REPO_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.lint it 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 — runs test, lint, fmt-check; must not modify files. It needs a docker daemon, because script/lint is a container build, and it must never be called from inside a build stage

  • script/projectname — outputs the project name (used by script/docker for the image tag)

  • script/docker / make docker — builds Docker image, tagged via script/projectname (byte-identical across repos); carries the same three version lines as script/cibuild below, and passes --build-arg CHECK_EPOCH="$epoch" and --build-arg VERSION="$version"

  • script/cibuild — cd to repo root, run script/lint first 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/lint call 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 trip set -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 .git and on a repo with no commits — keep it, and do not collapse it into || echo unknown, which makes it unreachable. See the CHECK_EPOCH and git-describe rules in REPO_POLICIES.md for why each element is load-bearing. A bare docker build . fails closed by design, and so does a bare docker build -f Dockerfile.lint ..

  • script/precommit — called by the pre-commit hook; runs script/check

  • script/install-precommit — installs the pre-commit hook that runs script/precommit

  • make hooks — shims to script/install-precommit

  • README Entrypoints section documents the scripts and links the standard

4. Verify

  • make check passes
  • make lint demonstrably runs the linter rather than returning a cached build: run it twice on an unchanged tree and confirm the lint layer says DONE, never CACHED, both times
  • make docker succeeds
  • script/cibuild succeeds 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 hooks to install pre-commit hook
  • Add remote and push
  • Verify main passes make check