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

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.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, 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