check / check (push) Canceled after 0s
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
289 lines
16 KiB
Markdown
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`
|