check / check (push) Canceled after 0s
The ARG CACHEBUST rule and the CACHED expectation now list their steps as bullets, the same way in the policy, the README and both checklists. The checklists' Python item and the policy's Python point use shorter sentences with the verb early. The TODO entry no longer counts "no Python" as an install or says "except" where only a difference is meant. Model: opus-5-5
255 lines
14 KiB
Markdown
255 lines
14 KiB
Markdown
---
|
|
title: New Repo Checklist
|
|
last_modified: 2026-10-21
|
|
---
|
|
|
|
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/`, and 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. A submodule whose name has a `config` segment
|
|
(`config`, `deploy/config`, `config/lib`) 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; an alpine
|
|
one needs `apk add --no-cache git`) and 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; on
|
|
a later commit, the tag, the number of commits since it and the short
|
|
commit (`v1.2.3-4-gabc1234`); and 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, and 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 (a linked worktree, or a repository
|
|
checked out as a submodule) is the exception: 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 installs anything else as well, such as a
|
|
`script/bootstrap` that also installs node packages, comes after it too.
|
|
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`, 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 system
|
|
packages, pipenv and its dependencies, and the packages from
|
|
`Pipfile.lock`. 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 installs no Python there, because the
|
|
tests use the base image's. 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, assuming nothing (pkg manager detection nix/apt/brew/apk;
|
|
node used if its major version is the pinned one, else pinned version via
|
|
nvm from a hash-verified archive, with yarn and the packages installed
|
|
under it; pinned yarn via corepack); a non-server repo's development
|
|
environment stage runs it instead of inline installs; a gate phase or the
|
|
build stage installs what its base image lacks either inline or by running
|
|
it
|
|
- [ ] `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/fmt` and `script/fmt-check` pick their `yarn` with the same test
|
|
as `script/bootstrap`: the `yarn` on `PATH` when the node on `PATH` has
|
|
the pinned major version, and otherwise yarn under the pinned node, with
|
|
nvm loaded through `$HOME/.nvm/nvm.sh`. `script/bootstrap` leaves the node
|
|
and yarn it installs under nvm off the `PATH` of the shell that called it,
|
|
so a bare `yarn` exits 127 on a runner carrying nothing but docker and
|
|
git, or runs under another node.
|
|
- [ ] 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
|
|
(the runner image `docker.gitea.com/runner-images:ubuntu-latest`: docker,
|
|
git and node 24, with no yarn on `PATH`), and demonstrably executed the
|
|
checks — a sub-second build, or `CACHED` on a step that lints or tests,
|
|
one of the `RUN` steps below `ARG CACHEBUST`, means nothing ran. 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
|
|
- [ ] 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`
|