Files
prompts/prompts/NEW_REPO_CHECKLIST.md
T
sneak b5521672c8
check / check (push) Canceled after 0s
Install node, yarn and python from the distribution (closes #146)
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
2026-11-04 06:24:24 +00:00

289 lines
16 KiB
Markdown

---
title: New Repo Checklist
last_modified: 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.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](https://sneak.berlin)
- [ ] `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`, 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 `.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`, 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 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`
- [ ] Guidance for coding agents, if the repo has any, is one `AGENTS.md` at the
root — never a file or directory named after one agent tool, such as
`CLAUDE.md` or `.claude/`, and never separate memory files
- [ ] `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. The canonical file's `.claude` entry 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: `.dockerignore` lets `.git`
into the build context. It keeps out every git `config` at any depth
(`**/.git/config`, `**/.git/modules/**/config`):
- the repository's own
- each submodule's under `.git/modules/`
- that of a submodule keeping its own `.git` directory
`git describe` does 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 a `config` segment, as in `config`,
`deploy/config` or `config/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 has `git`: the
Debian Go image has it, and an alpine one needs
`apk add --no-cache git`. That stage takes the version from the
`VERSION` build argument when one is given, otherwise from
`git 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 VERSION` has no default. The build fails if the context carries
`.git` and the version still comes out empty, `dev` or `unknown`. A
plain `docker 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 `.git` is 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 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
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
cannot be built unless both passed. Keep the final stage last: a stage
nothing depends on is built only when `--target` names it.
- In every stage, every step that lints, tests or compiles comes after
`ARG CACHEBUST`. `ARG CACHEBUST` comes right after these steps:
- the steps that install system packages
- the steps that download Go's modules
- the step that runs `script/bootstrap` to install a Python `test`
phase's packages from `Pipfile.lock`
- A stage with none of these steps declares `ARG CACHEBUST` right after
`FROM`. A step that also installs anything else comes after
`ARG CACHEBUST` too. A `script/bootstrap` that also installs node packages
is one. The `script/` builds pass a new `CACHEBUST` value on every build,
which reruns every `RUN` step after that line and leaves the layers above
it and the cache mounts in place; `--no-cache` would build those layers
again and empty the mounts. A stage without the `ARG` line is served from
cache on an unchanged tree, so the scripts and every stage's
`ARG CACHEBUST` line are added in the same commit.
- Go: each Go stage downloads the modules (`COPY go.mod go.sum ./`,
`RUN go mod download`) above `ARG 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 run`
also mounts golangci-lint's
(`--mount=type=cache,target=/root/.cache/golangci-lint`), and no step
mounts anything else; both `go test` runs carry `-count=1`; a first run
that timed out gets no rerun, as in the canonical Go `Dockerfile` in
`REPO_POLICIES.md`
- Python: a `test` phase that installs its packages from `Pipfile.lock` does
it above `ARG CACHEBUST`, with `COPY script/ script/`,
`COPY Pipfile Pipfile.lock ./` and `RUN script/bootstrap`. When
`script/bootstrap` runs 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/.pipenv` with
`python3 -m venv` when it is missing, then uses that environment's `pip`
and `pipenv`. 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, with `pip install --require-hashes`. It installs
the packages from `Pipfile.lock` with `pipenv install --deploy`, plus
`--dev` when the tests need the development packages. It sets
`PIP_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.
`Pipfile` names no `python_version`, because `--deploy` stops when the
distribution's Python differs from it. The tests run after
`ARG CACHEBUST`, and every other stage installs its Python packages
after it, as in the Python `test` phase example in `REPO_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.yml` that runs
`script/cibuild` on push, checks out with `persist-credentials: false` and
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`
- [ ] 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` and,
in the same commit, set the lint phase digest to the one named in the
`.golangci.yml` paragraph of `REPO_POLICIES.md`)
- [ ] 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, 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.lock` is in
the tree. node and yarn come from that package manager when they are
missing.
- With `yarn.lock` in 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 `Dockerfile` stage copies `yarn.lock` before it runs `script/bootstrap`
only 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: runs `bootstrap`,
then `install-precommit`, plus repo-specific init
- [ ] `script/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` — runs `test`, `lint`, `fmt-check`; must not
modify files
- [ ] `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); a new `CACHEBUST`
value on every build, plus the version as a build arg
- [ ] `script/cibuild` — cd to repo root, run `script/bootstrap`, run
`script/check`, then
`docker build --build-arg CACHEBUST="$(date +%s)-$$" --build-arg VERSION="$version" -t "$tag" .`
(what CI runs), with `$version` from
`git describe --tags --always --dirty` (`unknown` if empty) and `$tag`
from `script/projectname`, each assigned on its own line before the build.
The bootstrap is required: CI checks out and runs this alone, and
`script/fmt-check` runs the formatter on the host.
- [ ] `script/bootstrap`, `script/fmt` and `script/fmt-check` run yarn by
whichever name is present: `yarn`, or `yarnpkg` on Debian and Ubuntu. With
neither name on `PATH`, they print one line saying to run
`script/bootstrap`, and exit 1.
- [ ] 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/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 docker` succeeds
- [ ] `script/cibuild` succeeds in a fresh clone on a host carrying what CI has.
That host is the runner image
`docker.gitea.com/runner-images:ubuntu-latest`, which has docker, git and
node 24. Take any yarn off `PATH` first. The run must show that the checks
executed. A sub-second build means nothing ran. So does `CACHED` on a step
that lints or tests, one of the `RUN` steps below `ARG CACHEBUST`. Above
that line, these steps are expected to show `CACHED`:
- the steps that install system packages
- the steps that download Go's modules
- the step that runs `script/bootstrap` to install a Python `test` phase's
packages from `Pipfile.lock`
- [ ] 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
final stage is missing its `COPY --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 a `Dockerfile` adds 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 hooks` to install pre-commit hook
- [ ] Add remote and push
- [ ] Verify `main` passes `make check`