Follows sneak's ruling at #146 (comment). Runtimes and the installer tools bundled with python come from the distribution, with no version and no hash. Third-party packages stay pinned by hash. script/bootstrap drops nvm and corepack. It installs node, yarn and the node packages only when yarn.lock is in the tree, and stops there when yarn's major version is not 1. This repository's Dockerfile starts from Alpine 3.24. The Python example starts from debian:trixie-slim, keeps pipenv at $HOME/.pipenv on a host and in an image, and sets PIP_ONLY_BINARY=:all:, so pip downloads no build tools without a hash check. Model: opus-5-5
16 KiB
title, last_modified
| title | last_modified |
|---|---|
| New Repo Checklist | 2026-11-04 |
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, then add the repo's own build outputs, such as its binaries, at the end of the file, where a re-vendor keeps them. 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/.editorconfig, then 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 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 license -
REPO_POLICIES.md— fetch fromhttps://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/REPO_POLICIES.md -
Guidance for coding agents, if the repo has any, is one
AGENTS.mdat the root — never a file or directory named after one agent tool, such asCLAUDE.mdor.claude/, and never separate memory files -
Dockerfileand.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. The canonical file's.claudeentry is anchored for the same reason as a repo-root binary; leave it that way, but note that it only covers agents running at the repo root — if this repo will run them in subdirectories,services/api/.claude/needs its own anchored entry. -
If the image embeds a version in a binary:
.dockerignorelets.gitinto the build context. It keeps out every gitconfigat any depth (**/.git/config,**/.git/modules/**/config):- the repository's own
- each submodule's under
.git/modules/ - that of a submodule keeping its own
.gitdirectory
git describedoes not need them, and each can hold a credential: a password in a remote URL, or the token the CI checkout step stores there. Some submodule names have aconfigsegment, as inconfig,deploy/configorconfig/lib. Such a submodule loses its whole git directory to**/.git/modules/**/config, and Go's version stamping then fails the build. Give it a name without that segment (git submodule add --name). The stage that compiles hasgit: the Debian Go image has it, and an alpine one needsapk add --no-cache git. That stage takes the version from theVERSIONbuild argument when one is given, otherwise fromgit describe --tags --always. That gives:- the tag, on a tagged commit
- the tag, the number of commits since it and the short commit
(
v1.2.3-4-gabc1234), on a later commit - the short commit, when no tag is reachable
The stage that compiles also marks its working directory safe for git (
git config --system --add safe.directory /src). A context sent as a tar stream keeps the sender's file owners, and git refuses a checkout owned by another user, so the version would come out empty.ARG VERSIONhas no default. The build fails if the context carries.gitand the version still comes out empty,devorunknown. A plaindocker build .with no build arguments must succeed; a Dockerfile that refuses an empty build argument drops that refusal and keeps the argument. A checkout whose.gitis a file is the exception: a linked worktree, or a repository checked out as a submodule. That file points to a git directory outside the build context, so the build cannot read the version and a plaindocker build .fails. Pass the version with--build-arg VERSION=..., asscript/dockerandscript/cibuildalready do. -
The Dockerfile carries a
lintphase and atestphase, each invoking its tool directly rather than throughmakeorscript/, and the final stage carries aCOPY --from=of a harmless file from each so the image cannot be built unless both passed. Keep the final stage last: a stage nothing depends on is built only when--targetnames it. -
In every stage, every step that lints, tests or compiles comes after
ARG CACHEBUST.ARG CACHEBUSTcomes right after these steps:- the steps that install system packages
- the steps that download Go's modules
- the step that runs
script/bootstrapto install a Pythontestphase's packages fromPipfile.lock
-
A stage with none of these steps declares
ARG CACHEBUSTright afterFROM. A step that also installs anything else comes afterARG CACHEBUSTtoo. Ascript/bootstrapthat also installs node packages is one. Thescript/builds pass a newCACHEBUSTvalue on every build, which reruns everyRUNstep after that line and leaves the layers above it and the cache mounts in place;--no-cachewould build those layers again and empty the mounts. A stage without theARGline is served from cache on an unchanged tree, so the scripts and every stage'sARG CACHEBUSTline are added in the same commit. -
Go: each Go stage downloads the modules (
COPY go.mod go.sum ./,RUN go mod download) aboveARG CACHEBUST; the steps that compile (golangci-lint run,go test,go build) mount Go's build cache (--mount=type=cache,target=/root/.cache/go-build),golangci-lint runalso mounts golangci-lint's (--mount=type=cache,target=/root/.cache/golangci-lint), and no step mounts anything else; bothgo testruns carry-count=1; a first run that timed out gets no rerun, as in the canonical GoDockerfileinREPO_POLICIES.md -
Python: a
testphase that installs its packages fromPipfile.lockdoes it aboveARG CACHEBUST, withCOPY script/ script/,COPY Pipfile Pipfile.lock ./andRUN script/bootstrap. Whenscript/bootstrapruns in that step, it installs nothing beyond these:- system packages
- pipenv and its dependencies
- the packages from
Pipfile.lock
It creates a virtual environment at
$HOME/.pipenvwithpython3 -m venvwhen it is missing, then uses that environment'spipandpipenv. On a host, it first installs the distribution's python and venv packages when they are missing. It installs pipenv and its dependencies by hash, withpip install --require-hashes. It installs the packages fromPipfile.lockwithpipenv install --deploy, plus--devwhen the tests need the development packages. It setsPIP_ONLY_BINARY=:all:for both installs, so a package with no wheel for the image stops the install. It installs no Python there, because the tests use the distribution's, which a step above it installs.Pipfilenames nopython_version, because--deploystops when the distribution's Python differs from it. The tests run afterARG CACHEBUST, and every other stage installs its Python packages after it, as in the Pythontestphase example inREPO_POLICIES.md -
Server: the final stage builds and runs the application
-
Non-server: the final stage brings up the dev environment
-
Image pinned by sha256 hash with version/date comment
-
-
Gitea Actions workflow at
.gitea/workflows/check.ymlthat runsscript/cibuildon push, checks out withpersist-credentials: falseand withfetch-depth: 0(which fetches the tagsgit describeneeds), carries theconcurrencyblock that lets a new push cancel only the same branch's older run, and setstimeout-minutes: 20on thecheckjob so a hung build frees the shared runner — 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.ymland, in the same commit, set the lint phase digest to the one named in the.golangci.ymlparagraph ofREPO_POLICIES.md) - 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 bootstrapinstalls all dependencies, idempotently, and assumes nothing is present:- It detects the package manager: nix, apt, brew or apk.
- It installs node, yarn and the node packages only when
yarn.lockis in the tree. node and yarn come from that package manager when they are missing. - With
yarn.lockin the tree, it stops with a one-line message when yarn's major version is not 1. - A non-server repo's development environment stage runs it, with only system packages installed inline above it.
- A gate phase or the build stage installs what its base image lacks, either inline or by running it.
- A
Dockerfilestage copiesyarn.lockbefore it runsscript/bootstraponly if that stage needs yarn. A stage that needs yarn starts from an image whose distribution packages yarn 1, which today means Alpine. script/setup/make setup— readies a fresh clone: runsbootstrap, theninstall-precommit, plus repo-specific initscript/test/make test—docker build --build-arg CACHEBUST="$(date +%s)-$$" --target test --output type=cacheonly ., which writes no image; the phase runs real tests, not a no-op (90-second timeout, 60-second hard cap on wall time)script/lint/make lint—docker build --build-arg CACHEBUST="$(date +%s)-$$" --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 container)script/fmt-check/make fmt-check— checks formatting (read-only; native)script/check/make check— runstest,lint,fmt-check; must not modify filesscript/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); a newCACHEBUSTvalue on every build, plus the version as a build argscript/cibuild— cd to repo root, runscript/bootstrap, runscript/check, thendocker build --build-arg CACHEBUST="$(date +%s)-$$" --build-arg VERSION="$version" -t "$tag" .(what CI runs), with$versionfromgit describe --tags --always --dirty(unknownif empty) and$tagfromscript/projectname, each assigned on its own line before the build. The bootstrap is required: CI checks out and runs this alone, andscript/fmt-checkruns the formatter on the host.script/bootstrap,script/fmtandscript/fmt-checkrun yarn by whichever name is present:yarn, oryarnpkgon Debian and Ubuntu. With neither name onPATH, they print one line saying to runscript/bootstrap, and exit 1.- No
docker buildinscript/leaves a dangling image behind:script/lintandscript/testwrite no image, andscript/dockerandscript/cibuildtag theirs script/precommit— called by the pre-commit hook; runsscript/checkscript/install-precommit— installs the pre-commit hook that runsscript/precommitmake hooks— shims toscript/install-precommit- README Entrypoints section documents the scripts and links the standard
4. Verify
make checkpassesmake dockersucceedsscript/cibuildsucceeds in a fresh clone on a host carrying what CI has. That host is the runner imagedocker.gitea.com/runner-images:ubuntu-latest, which has docker, git and node 24. Take any yarn offPATHfirst. The run must show that the checks executed. A sub-second build means nothing ran. So doesCACHEDon a step that lints or tests, one of theRUNsteps belowARG CACHEBUST. Above that line, these steps are expected to showCACHED:- the steps that install system packages
- the steps that download Go's modules
- the step that runs
script/bootstrapto install a Pythontestphase's packages fromPipfile.lock
- Plant a lint violation and confirm both
make lintand a plaindocker build .fail on it; revert. A plain build that passes proves the final stage is missing itsCOPY --from=edge to the gate phases. - No secrets in repo, and none in the build context: enumerate a probe image
rather than reading
.dockerignore - No mutable image/package references. The one exception is a package from
the distribution's own package index, which needs no version and no hash.
Pulling that index is also allowed. In a
Dockerfile, the distribution is the pinned base image's. On a host, it is the host's, and the host's package manager stands for it, nix and brew included. A package source that aDockerfileadds itself does not get this exception - node, yarn, npm, python, pip and venv come from the distribution wherever
possible, never from nvm, pyenv, corepack or
npm install -g yarn. If the distribution's version is too old, use a newer distribution - 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