The linter is no longer installed on the host and no longer invoked there. script/lint is now `docker build -f Dockerfile.lint .` and nothing else, with the linter running as a build step, so a successful build of that file is a clean lint — and it works unchanged where the docker daemon is remote and bind mounts are impossible. That removes three host-only failure mechanisms rather than mitigating them: the result cache keyed on file content rather than location, which produced a confirmed false green and a string of findings reported against other checkouts; the host-global $TMPDIR/golangci-lint.lock, which fails a run with `parallel golangci-lint is running` in a way no caller can distinguish from findings; and host/container version skew, which hid thirteen findings on one repo. A container per run has its own cache, its own lock and a binary pinned by digest. Resolving the recursion this creates. script/lint is a docker build, so a Dockerfile that runs `make check` would nest a build inside a build step where there is no daemon. Fixed by direction, not detection: the main Dockerfile runs script/test and script/fmt-check individually, with a comment saying why `make check` must not come back, and script/cibuild runs script/lint first for fail-fast feedback. script/check still runs all three, so developers and the pre-commit hook are unaffected. Dockerfile.lint carries the same CHECK_EPOCH guard as the main image, with the ARG placed below the dependency layer so only the lint steps re-run. Blanket --no-cache was rejected: it re-runs the dependency install on every lint and makes linting network-dependent. golangci-lint config verify is kept, on measurement rather than preference. Under the pinned v2.12.2, a bogus top-level key and a bogus key nested under linters.settings.lll both pass `golangci-lint run` with exit 0 and `0 issues` while config verify exits 3 and names them; an unknown linter name fails run and passes config verify. The two catch disjoint classes, and `run` alone silently ignores the class where a threshold reads as configured and is not applied. The concern that config verify fetches its JSON schema over live HTTPS does not hold for this version: every case reproduced byte-identically under `docker run --network none`, in a container where `getent hosts golangci-lint.run` exits 2. The schema is embedded in the pinned binary. Two canonical forms are superseded and deleted rather than left standing beside the new one, because consuming repos read these documents literally and two contradictory canonical script/lint forms is worse than either. The script/bootstrap golangci-lint install landed for #28 is removed: nothing invokes a host linter now, so it can only reintroduce the skew it was written to close. Its version-enforcement principle — compare version not presence, re-resolve through PATH after installing, let a mis-parse fall through to reinstall, and call it — stays documented for any other pinned host tool. The per-checkout GOLANGCI_LINT_CACHE/TMPDIR wrapper is removed with it; its entire subject was making a host run trustworthy. Adopting repos delete .lint-cache/ from .gitignore and .dockerignore too. The Go multistage lint stage and its COPY --from=lint ordering trick go the same way: that stage ran `make lint`, which is now a docker build. Corrected everywhere the claim that a successful docker build implies lint passed — REPO_POLICIES.md, both repo checklists, the Go styleguide and the README. The guarantee now belongs to script/cibuild, which runs both container builds; a bare `docker build .` never lints at all. Verified in this repo, not only documented: two consecutive script/lint runs on a byte-identical tree both executed prettier (4.556s and 3.738s, lint layers DONE with a fresh epoch printed, dependency layers CACHED as intended); a planted violation failed the build naming the file, and reverting it went green; a bare `docker build -f Dockerfile.lint .` failed on the guard; make check, script/docker and script/cibuild all green with the check layers demonstrably executing; and the main image build completed without attempting a nested build. Rework, from independent review of this commit. The canonical text is the deliverable here, so a false sentence is a fleet-wide defect: the Dockerfile rule still said the build "fails if the branch is not green", which stopped being true when lint left that image, and the earlier sweep grepped one phrasing rather than the claim. Re-swept on the claim itself — green/red-branch wording, build-fails wording, entailment verbs near build/lint/check, and "linted" asserted as covered — across prompts/, README.md, TODO.md, both Dockerfiles and every script. Two absolutes are narrowed to what is actually true, because seventeen repos adopt this literally. The rule is that no lint VERDICT may come from a host invocation, not that the binary never exists on the host: a JS repo's `yarn install` puts its linter in node_modules on the host unavoidably, and in a repo whose formatter is its linter — this one — `script/fmt-check` runs the same command that Dockerfile.lint runs. That gap is now stated with its bound (the version is pinned in the repo's own node_modules, so no shared cache, no host lock, nothing to skew) and the audit grep keeps its reach, gaining a note on which two hits are expected rather than being weakened. script/lint conflates "found issues" with "could not run": docker build exits 1 for both. The exit-75 VOID machinery is deliberately not restored, and the reasoning is now recorded where a reader looking for it lands. The dangerous direction is already closed, since a build that cannot run fails closed and can never read as clean; BuildKit already names the failing step, where the old lock error went to stderr while findings went to stdout and was easy to lose; and the failure is not transient, so the retry that justified the old machinery would be wrong here. Rebuilding the distinction would mean per-invocation capture files and traps again plus matching on BuildKit's message format, which is not a stable interface, and a mis-match in the "treat as infrastructure" direction would be the false green this rule exists to prevent. What survives is binding as a reading rule: a run that did not reach the lint step is not a verdict. Also corrected: script/lint was listed above a CHECK_EPOCH snippet that does a bare `docker build .` with no -f, which would have built the main image and linted nothing; the canonical Go Dockerfile template used `make fmt-check` / `make test` where every prose rule in the same document says script/, one Makefile edit away from re-entering the recursion; README omitted the mandatory VERSION build arg; script/docker did not say lint had left its image, a comment that propagates fleet-wide; the "byte-identical across repos" claim for script/lint is narrowed to its executable lines; and "--no-cache makes linting network-dependent" is softened to the measured comparative claim.
57 KiB
title, last_modified
| title | last_modified |
|---|---|
| Repository Policies | 2026-08-10 |
This document covers repository structure, tooling, and workflow standards. Code style conventions are in separate documents:
- Code Styleguide (general, bash, Docker)
- Go
- JavaScript
- Python
- Go HTTP Server Conventions
-
Cross-project documentation (such as this file) must include
last_modified: YYYY-MM-DDin the YAML front matter so it can be kept in sync with the authoritative source as policies evolve. -
ALL external references must be pinned by cryptographic hash. This includes Docker base images, Go modules, npm packages, GitHub Actions, and anything else fetched from a remote source. Version tags (
@v4,@latest,:3.21, etc.) are server-mutable and therefore remote code execution vulnerabilities. The ONLY acceptable way to reference an external dependency is by its content hash (Docker@sha256:..., Go module hash ingo.sum, npm integrity hash in lockfile, GitHub Actions@<commit-sha>). No exceptions. This also means nevercurl | bashto install tools like pyenv, nvm, rustup, etc. Instead, download a specific release archive from GitHub, verify its hash (hardcoded in the Dockerfile or script), and only then install. Unverified install scripts are arbitrary remote code execution. This is the single most important rule in this document. Double-check every external reference in every file before committing. There are zero exceptions to this rule. -
Every repo with software must have a root
Makefilewith these targets:make bootstrap,make setup,make test,make lint,make fmt(writes),make fmt-check(read-only),make check(runstest,lint,fmt-check),make docker, andmake hooks(installs pre-commit hook). A model Makefile is athttps://git.eeqj.de/sneak/prompts/raw/branch/main/Makefile. -
Repos follow the Scripts to Rule Them All pattern: the implementation of each Makefile target lives in an executable script in
script/(script/bootstrap,script/setup,script/test,script/lint,script/fmt,script/fmt-check,script/check,script/docker), and the Makefile targets are thin shims that call them. The scripts must be POSIX sh (#!/bin/sh,set -eu, no bashisms) so they run in minimal containers (e.g. alpine images have no bash); locate the repo root with$(cd "$(dirname "$0")/.." && pwd -P)andcdthere before acting. From the standard's canonical set we usebootstrap,setup(make the repo ready for development after a fresh clone: runsbootstrap, theninstall-precommit, plus any repo-specific initialization),test, andcibuild.script/bootstrapinstalls all dependencies idempotently and assumes nothing is present: base tools come from nix, apt, brew, or apk (detected in that order; apt runs noninteractive). For node it uses the installed node if present; otherwise it installs a PINNED node version via nvm, first installing nvm itself if missing — from a hash-verified GitHub release archive (nevercurl | sh), with bash installed as an explicit prerequisite since nvm requires bash. yarn is then pinned viacorepack prepare yarn@<version> --activate. Never install "latest" or "lts"; always exact versions.script/cibuildruns the CI build: it changes to the repo root, runsscript/lintfirst for fail-fast feedback (that is itself a container build — see the containerised-lint rule below), and then runsdocker build --build-arg CHECK_EPOCH="$epoch" --build-arg VERSION="$version" ., whereepochis a per-invocation nonce (see theCHECK_EPOCHrule below) andversionis computed on the host because.gitis not in the build context (see the git-describe rule below); the Gitea workflow calls it. Four further scripts are our own extensions to the standard:script/checkrunsscript/test,script/lint, andscript/fmt-check;script/precommitis what the git pre-commit hook runs, and it callsscript/check;script/install-precommitinstalls the git pre-commit hook (themake hookstarget shims to it); andscript/projectname(literally that filename) simply outputs the project's name. Scripts that need the name callscript/projectname— e.g.script/dockerassembles its image tag from it — so those scripts stay byte-identical across all repos. Repo-type-specific pre-commit extras (e.g.go mod tidyverification in Go repos) belong inscript/precommit, not in the hook itself. Model scripts are athttps://git.eeqj.de/sneak/prompts/raw/branch/main/script/<name>. The README must document the provided scripts in an Entrypoints section (see the README requirements below). -
Always use Makefile targets (
make fmt,make test,make lint, etc.) instead of invoking the underlying tools directly. The Makefile is the single source of truth for how these operations are run. -
The Makefile is authoritative documentation for how the repo is used. Beyond the required targets above, it should have targets for every common operation: running a local development server (
make run,make dev), re-initializing or migrating the database (make db-reset,make migrate), building artifacts (make build), generating code, seeding data, or anything else a developer would do regularly. If someone checks out the repo and typesmake<tab>, they should see every meaningful operation available. A new contributor should be able to understand the entire development workflow by reading the Makefile. -
Every repo should have a
Dockerfile. It must run the repo's non-lint checks as build steps, so the build fails on a branch those checks reject — which requiresARG CHECK_EPOCHand its guard in every stage containing a check-runningRUN, per theCHECK_EPOCHrule below. Without them a Dockerfile satisfies this criterion while its check layers are served from cache, so it cannot fail on a branch those checks would have rejected.A green
docker build .does not mean the branch is green, because this file does not lint. Onlyscript/cibuildcarries that meaning: it runsscript/lintand then this build. Do not restate the older, stronger claim anywhere — it was true only while lint ran inside this image.It runs the individual non-lint checks —
script/testandscript/fmt-check— and nevermake check.script/lintis itself adocker build(ofDockerfile.lint, per the containerised-lint rule below), so aRUN make checkin this file attempts a docker build inside a build step, where there is no daemon. Lint is not skipped by this: it runs in its own container, andscript/cibuildruns it first. Put a comment to that effect directly above thoseRUNlines, becausemake checkis what the next person will reach for.For non-server repos, the Dockerfile should bring up a development environment and run those checks. For server repos, they should run as an early build stage before the final image is assembled. Dockerfiles install development prerequisites by running
script/bootstraprather than duplicating installs inline; COPYscript/and the dependency manifests (package.json+yarn.lock,go.mod+go.sum, etc.) before running it so the bootstrap layer stays cached until dependencies change. -
Every check-running
RUNmust be cache-busted withCHECK_EPOCH. Docker invalidates aCOPYlayer only when the copied content changes, so on an unchanged tree the check layer is served from cache, the suite never runs, and the build still exits 0. A sub-seconddocker buildreporting success is a cache hit, not a result. This applies to every file that runs checks in a build step, which since linting moved into its own container meansDockerfileandDockerfile.lintboth — aDockerfile.lintwithout the cache-bust is a lint that never ran, reported as a pass. The canonical form, in every stage containing a check-runningRUN, placed after the dependency-install layer so that layer stays cached:ARG CHECK_EPOCH RUN [ -n "$CHECK_EPOCH" ] || exit 1 RUN echo "check epoch: ${CHECK_EPOCH}" && <the check command>and in both
script/cibuildandscript/docker:epoch="$(date +%s%N)$$" version="$(git describe --tags --always --dirty 2>/dev/null || true)" [ -n "$version" ] || version="unknown" docker build \ --build-arg CHECK_EPOCH="$epoch" \ --build-arg VERSION="$version" \ .script/lintneeds the same nonce but is not this command: it builds a different file and passes no version. Do not copy the block above into it — adocker buildwith no-f Dockerfile.lintbuilds the main image instead, which is a lint that silently lints nothing. Its canonical form is in the containerised-lint rule below; copy that one.The
VERSIONlines are there for a different reason, covered by the git-describe rule below; they are shown here so the two rules do not each document half a command. All fourCHECK_EPOCHelements are load-bearing; none is optional, and each guards a failure mode that otherwise fails green:ARGis stage-scoped, so a single declaration leaves the other check stages frozen while the fix reviews as complete. Declare it in every stage that runs checks, immediately above the first suchRUN.- Expand the value into the command. This makes the cache miss contractual
rather than dependent on BuildKit's handling of an unreferenced
ARG, and it puts the epoch in the build log. The guard is itself value-keyed, for the same reason: it references$CHECK_EPOCH, so BuildKit renders the epoch into that layer's description (rendered asRUN [ -n "<epoch>" ] || exit 1) and re-runs it whenever the value changes. Each stage therefore has two independent invalidation points, and the guard always precedes the checkRUN. Keep both: the expansion is defence in depth, and it is what makes the epoch visible in the build output. - The
[ -n ... ]guard is required: an unsetARGis empty, and empty is a stable cache key, so without it a baredocker build .still produces the false green. Failed steps are never cached, so the guard fails on every such invocation, loudly. A baredocker build .failing is by design. - Assign
epoch=on its own line, never inline in the--build-argargument: a failing command substitution inside an argument does not tripset -e, so the inline form silently degrades to an empty constant. The$$suffix is required because busyboxdatedrops%Nand exits 0, so on an alpine host the epoch would degrade to second granularity and concurrent invocations would collide.
This invalidates the check layers and everything after them while leaving
go mod download,script/bootstrap, and the pinned toolchain install cached, so it does not push against the five-minute Docker build ceiling. Blanket--no-cacheis not an acceptable substitute, onDockerfileor onDockerfile.lint: it re-runsgo mod download/yarn installon every invocation, so a lint that should take seconds pays a dependency install each time and reaches the network on every run rather than only on the first and after a manifest change. It is a difference of degree, not an absolute — a container build is never fully offline-independent — but it is the difference between a lint that usually needs nothing and one that always does. Never reach fordocker builder pruneto achieve the same end: the build cache is shared with every other build on the host, including other people's. -
Every lint run happens in a container, and
script/lintis that container build. The precise claim, because it is the one that has to survive contact with a repo whose formatter and linter are the same binary: no lint verdict may come from a host invocation. Noscript/, noMakefiletarget and no CI step may produce a lint result by running a linter on the host. That is stronger than it sounds and weaker than "the binary is never on the host" — see the scope note at the end of this rule, which says exactly which host invocations remain legitimate and why. Every repo carries aDockerfile.lintnext to itsDockerfile; the linter runs as a build step, so a successful build is a clean lint. Building rather than bind-mounting is deliberate: it is what makes the pattern work unchanged where the docker daemon is remote and bind mounts are impossible. Docker is assumed available in every environment. Discarding the linter's cache on every run is the point of this rule, not a cost it pays.This closes a family of defects, every one of them an artefact of running the linter on a shared host, and every one of them observed rather than hypothesised:
- A confirmed false green. An implementer reported
0 issueson a branch that was genuinely red with agoconstfinding. golangci-lint keys cached results on file content, not location, so a second checkout of the same commit holds byte-identical files and serves its result. Note what content-keying implies: moving agents from worktrees into their own clones does not help, because two clones are byte-identical exactly as two worktrees were. It removes the foreign-path symptom and leaves the mechanism live, which makes the defect quieter rather than rarer. - False reds, repeatedly: findings reported against
../wt82-lint/..., against another agent's checkout, and against a worktree that had already been deleted; in one case 399 issues returned to a clean clone that genuinely lints 0. - Lock contention that cannot be distinguished from findings.
golangci-lint flocks
$TMPDIR/golangci-lint.lock(pkg/commands/run.go,acquireFileLock()) — host-global, keyed on the temp directory, entirely independent ofGOLANGCI_LINT_CACHE, with a 5-second acquire timeout, so it fails precisely when the host is busiest. On failure it printsparallel golangci-lint is running, analyzes nothing, and exits non-zero. Proven not fixed by per-cache isolation: two concurrent runs with entirely separate cache directories still collided. - Version skew. A host linter differing from the pinned one, with the
container surfacing thirteen findings the host missed on one repo, and a
local
make checkgreen against amake dockerthat rejected the same commit with sixgoconstfindings.
A container per run has its own cache, its own
TMPDIRand therefore its own lock, and a binary pinned by digest, so none of the above is reachable. That is also why the per-checkoutGOLANGCI_LINT_CACHE/TMPDIRwrapper that used to be canonical here is gone rather than kept alongside this: its entire subject was making a host run trustworthy, and there are no host runs. Consuming repos delete it when they adopt this; see the adoption list at the end of this rule.The canonical
Dockerfile.lintfor a Go repo:# Lint-only image. `script/lint` builds this file and nothing else: the # linter runs as a build step, so a successful build IS a clean lint. # # The linter is invoked directly below rather than through `make lint`. # That is not a style choice: `script/lint` IS this build, so calling it # from inside would recurse into a docker build with no daemon. # # golangci/golangci-lint:v2.12.2 (Debian-based), 2026-08-07 FROM golangci/golangci-lint@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240 WORKDIR /src # Dependency layer first, and deliberately above the ARG below, so it # stays cached and only the lint steps re-run on every invocation. COPY go.mod go.sum ./ RUN go mod download COPY . . ARG CHECK_EPOCH RUN [ -n "$CHECK_EPOCH" ] || exit 1 RUN echo "lint epoch: ${CHECK_EPOCH}" && \ golangci-lint config verify --config .golangci.yml RUN golangci-lint run --config .golangci.yml ./...and the canonical
script/lint, whose executable lines are identical in every repo (only the comment wording is a repo's own):#!/bin/sh # script/lint: run the linter. This is the ONLY source of a lint verdict # in this repo — the linter runs in a container, one way, everywhere, so # a run cannot inherit another checkout's cache, another process's lock, # or a host toolchain that differs from the pinned one. # # A failure here that names no finding is NOT a lint result: docker # build exits 1 both for findings and for a build that never reached # the lint step. BuildKit names the failing step; read it, fix the # environment, and re-run. set -eu ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" main() { cd "$ROOT" # Own line: a failing command substitution inside an argument does # not trip `set -e`, and `$$` is required because busybox `date` # drops %N without erroring. Without a fresh nonce the lint layer is # served from cache and this script exits 0 having linted nothing. epoch="$(date +%s%N)$$" docker build \ --build-arg CHECK_EPOCH="$epoch" \ -f Dockerfile.lint \ . } main "$@"Load-bearing properties:
-
CHECK_EPOCH, not--no-cache.docker build -f Dockerfile.lint .on an unchanged tree returns a sub-second cached success having linted nothing — the same false green theCHECK_EPOCHrule above exists to close, arriving through a new file. TheARGgoes after the dependency layer sogo mod download/yarn installstay cached and only the lint steps re-run. Blanket--no-cachealso busts the dependency layer, so every lint reaches the network instead of only the first one. -
Non-Go repos get the same pattern around their own linter —
eslint,ruff,prettier,shellcheck— because the ruling is every lint run, not every Go lint run. Only the base image and the lint commands change; theWORKDIR, dependency layer,ARG CHECK_EPOCH, guard and expanded-valueRUNare identical. A JS or docs repo bases on its pinned node image, runsscript/bootstrapas the dependency layer inside the image, and lints with the linter from that image'snode_modules. Note what this does not claim: a developer also runsscript/bootstrapon the host, so a copy of that linter exists there too. What the rule forbids is taking a verdict from it. -
Keep
golangci-lint config verify, and it costs no network. The two commands catch disjoint classes of defect, measured under the pinned v2.12.2 against a config carrying one planted defect at a time: a bogus top-level key and a bogus key nested underlinters.settings.lllboth passgolangci-lint runwith exit 0 and0 issueswhileconfig verifyexits 3 and names the key; an invalid value type fails both; an unknown linter name failsrunand passesconfig verify. Sorunalone silently ignores an unknown key, which is exactly the mode where a threshold reads as configured and is not applied. The earlier caution thatconfig verifyresolves its JSON schema over a live HTTPS fetch does not hold for this pinned version: every case above was re-run underdocker run --network noneand produced byte-identical diagnostics and exit statuses, in a container wheregetent hosts golangci-lint.runexits 2. The schema is embedded in the pinned binary. Re-run that control when bumping the pin rather than treating the result as permanent. -
No repo installs a linter on the host in order to lint — remove any such install from
script/bootstrap. A dedicated linter install (thego installof golangci-lint is the case that existed) is now dead weight whose only remaining effect is to reintroduce the version skew above. This does not forbid a host dependency install that happens to bring a linter along with everything else, which is unavoidable in a JS repo and is fine as long as no verdict is taken from it. -
A failed
script/lintthat names no finding is not a lint result. The exit status alone cannot tell you which happened:docker buildreturns 1 both when the lint step fails on findings and when the build never got that far — daemon unreachable, base image unpullable, disk full, dependency layer failing. The per-checkout wrapper this rule replaced drew that line explicitly, exiting 75 for a run that analysed nothing, and that machinery is deliberately not restored. Three reasons, and they are a position rather than an omission:- The dangerous direction is already closed. The wrapper's exit 75 existed because a lock collision could be read as a result on a run that analysed nothing. A build that cannot run fails closed: it can never read as clean. What is lost is diagnosability, not safety.
- The failure is self-describing, where the lock collision was not.
BuildKit prints the failing step verbatim —
ERROR: failed to solve: process "/bin/sh -c <the lint command>"for a genuine finding, against a named earlier step or a daemon error for anything else. The discriminator is already in the output and needs no code. The lock message, by contrast, went to stderr while findings went to stdout and was easy to lose. - It is not transient, so retrying is wrong. The lock collision cleared on a retry, which is what made an automatic retry worth building. A dead daemon or a full disk does not, and a retry loop over a failing dependency fetch hides a real reproducibility problem. Rebuilding the distinction in the script would also mean reintroducing per-invocation capture files and traps to get the exit status out from under a pipe, plus matching on BuildKit's message format, which is not a stable interface — and a mis-match in the "treat it as infrastructure" direction would be the false green this whole rule exists to prevent.
The substance of the VOID rule survives as a reading rule, and it is binding: do not record a lint verdict from a run that did not reach the lint step, and do not "fix" anything on the strength of one. Read which step failed, fix the environment, and re-run.
-
script/checkstill runstest,lintandfmt-check, so a developer and the pre-commit hook get all three. It therefore requires a docker daemon, and it must never be invoked from inside a build stage — see theDockerfilerule above. -
If the project uses
//go:embeddirectives referencing build artifacts (e.g. a web frontend compiled elsewhere),Dockerfile.lintmust create placeholder files so the directives resolve:RUN mkdir -p web/dist && touch web/dist/index.html. It must not depend on the real build output; it exists to fail fast. -
If linting requires CGO or system libraries (e.g.
vips-dev), install them inDockerfile.lint.
Scope: this rule is about linters, and a formatter is not one.
script/fmtwrites to your working tree, so it can only run on the host;script/fmt-checkis its read-only twin and runs on the host too, as well as inside the main image. Neither is a lint run and neither is in scope.The awkward case, stated rather than left for an adopter to trip over: in a repo whose formatter is its linter — prettier over a markdown or docs repo is the standard shape, and this repo is one — the command in
Dockerfile.lintand the command inscript/fmt-checkare the same string, so that exact command does still run on the host. That is a real gap in the absolute reading and it is accepted for two reasons: the mechanisms this rule exists to close do not reach it (prettier's version is pinned inpackage.jsonand installed into the repo's ownnode_modules, so there is no shared content-keyed cache, no host-global lock, and no version to skew against), and CI's verdict is containerised regardless, becausescript/fmt-checkalso runs inside the main image build. What is not acceptable is taking a lint verdict from the host copy:script/lintstays the only source of one. If a repo's linter and formatter ever diverge in version or configuration, this gap becomes a real defect and the check-mode invocation has to move into the container too.What a consuming repo does to adopt this, in order: add
Dockerfile.lint; replacescript/lintwith the build above; delete thelintstage from itsDockerfilealong with theCOPY --from=lint ... /dev/nullordering line; change thatDockerfile'sRUN make checktoscript/testandscript/fmt-checkwith the comment explaining why; addscript/lintas the first step ofscript/cibuild; delete any golangci-lint install fromscript/bootstrap; and delete the.lint-cache/entries from.gitignoreand.dockerignoretogether with the per-checkout cache/lock wrapper they served.The separate lint stage is superseded by this and must not survive alongside it. It ran
make lint, which is now a docker build, so keeping it is not a stylistic preference but a recursion. Its purpose — fail-fast feedback before the slow build — is served byscript/cibuildrunningscript/lintfirst, and itsCOPY --from=lint /src/go.sum /dev/nullordering trick, along with the warm-cache re-proof that trick required, is no longer needed because the ordering is now sequential in the shell. - A confirmed false green. An implementer reported
-
The canonical Go repo
Dockerfile, which builds and tests but does not lint:# Build stage # golang:1.x-alpine, YYYY-MM-DD FROM golang@sha256:... AS builder WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . ARG CHECK_EPOCH RUN [ -n "$CHECK_EPOCH" ] || exit 1 # The individual non-lint checks, NOT `make check`: script/lint is a # docker build (Dockerfile.lint), so `make check` here would nest a # build inside a build step, where there is no daemon. Lint is not # skipped — script/cibuild runs it first, in its own container. RUN echo "check epoch: ${CHECK_EPOCH}" && script/fmt-check RUN script/test # VERSION comes from the host via --build-arg; see the git-describe rule # below. Never run `git describe` here: .dockerignore excludes .git, so # it yields an empty version without failing the build. ARG VERSION=dev RUN CGO_ENABLED=0 go build -trimpath \ -ldflags="-s -w -X main.Version=${VERSION}" \ -o /app ./cmd/app/ # Runtime stage FROM alpine@sha256:... COPY --from=builder /app /usr/local/bin/app ENTRYPOINT ["app"]Key points:
- Tests run in the build stage because they may require compiled artifacts or heavier dependencies.
ARG CHECK_EPOCHmust be declared in every stage containing a check-runningRUN, becauseARGis stage-scoped: declaring it in one stage leaves the others frozen at their last cached result while the fix reviews as complete. In each such stage the guard sits immediately below theARG, and the value is expanded into the first checkRUNso the cache miss does not rely on BuildKit's unreferenced-ARGhandling. Both lines reference$CHECK_EPOCH, so each stage has two independent invalidation points. LaterRUNs in the same stage need no expansion of their own: their parent layer is already busted.ARG VERSION=devis declared in the build stage, and its value is supplied on the host byscript/dockerandscript/cibuildvia--build-arg VERSION=.... Thedevdefault is a placeholder for a local build, not a source of truth. No stage may callgit describe:.dockerignoreexcludes.git, so it yields an empty version without failing. See the git-describe rule further down.
-
Every repo should have a Gitea Actions workflow (
.gitea/workflows/) that runsscript/cibuildon push.script/cibuildruns two container builds:script/lint(Dockerfile.lint) first, thendocker build --build-arg CHECK_EPOCH="$epoch" --build-arg VERSION="$version" .for the main image, which runs the non-lint checks. A successfulscript/cibuildtherefore implies all checks pass; a successfuldocker build .on its own does not, because it never lints. That is the one claim to be careful with when reading these files: the guarantee belongs toscript/cibuild, not to any single Dockerfile. Both halves of it hold only because each build passes its ownCHECK_EPOCHnonce — without it an unchanged tree serves the layers from cache and the build reports a green it never earned. A baredocker build .ordocker build -f Dockerfile.lint .fails closed by design, on the[ -n "$CHECK_EPOCH" ]guard; always go throughscript/cibuild,script/dockerorscript/lint. Never accept a pass as evidence without confirming it ran: a sub-second wall time, orCACHEDon a check or lint layer, means nothing was executed. -
Use platform-standard formatters:
blackfor Python,prettierfor JS/CSS/Markdown/HTML,go fmtfor Go. Always use default configuration with two exceptions: four-space indents (except Go), andproseWrap: alwaysfor Markdown (hard-wrap at 80 columns). Documentation and writing repos (Markdown, HTML, CSS) should also have.prettierrcand.prettierignore. -
Pre-commit hook: runs
script/precommit, which callsscript/check. If local testing is not possible in the repo,script/precommitmay skipscript/testand run onlyscript/lintandscript/fmt-check. The hook is installed byscript/install-precommit; the Makefile must provide amake hookstarget that shims to it. -
All repos with software must have tests that run via the platform-standard test framework (
go test,pytest,jest/vitest, etc.). If no meaningful tests exist yet, add the most minimal test possible — e.g. importing the module under test to verify it compiles/parses. There is no excuse formake testto be a no-op. -
make testmust complete in under 20 seconds. Add a 30-second timeout in the Makefile. -
make testshould use the conditional verbose rerun pattern. Run tests without-v(verbose) first. If tests fail, automatically rerun with-vto show full output. This keeps CI logs anddocker buildoutput clean on success (just package/suite summaries) while providing full diagnostic detail on failure (every test case, every assertion). The general shell pattern:test: @<test-command> || \ { echo "--- Rerunning with -v for details ---"; \ <test-command-with-v>; exit 1; }Go example:
test: @go test -timeout 30s -race -cover ./... || \ { echo "--- Rerunning with -v for details ---"; \ go test -timeout 30s -race -v ./...; exit 1; }Python example:
test: @python -m pytest || \ { echo "--- Rerunning with -v for details ---"; \ python -m pytest -v; exit 1; }The
exit 1ensures the target always fails after a rerun — the first run already proved the tests are broken, so the build must not pass even if a flaky test happens to succeed on the second attempt. The rerun exists solely for diagnostic output. -
Docker builds must complete in under 5 minutes.
-
make checkmust not modify any files in the repo. Tests may use temporary directories. -
mainmust always passmake check, no exceptions. -
Never commit secrets.
.envfiles, credentials, API keys, and private keys must be in.gitignore. No exceptions. -
.gitignoreshould be comprehensive from the start: OS files (.DS_Store), editor files (.swp,*~), in-repo agent scratch directories (.claude/, which holds one worktree — an entire additional checkout of the repo — per in-flight agent), language build artifacts, andnode_modules/. Fetch the standard.gitignorefromhttps://git.eeqj.de/sneak/prompts/raw/branch/main/.gitignorewhen setting up a new repo. These patterns are written to.gitignore's own semantics, in which an unanchored pattern already matches at every depth. They are not a.dockerignoreand must not be transplanted into one unmodified — see the next rule. -
.dockerignoredoes not use.gitignoresemantics, and copying patterns across unmodified leaves secrets in the build context. Docker matches withmoby/patternmatcher: Gofilepath.Matchsemantics plus a**extension, compiled to a regexp — plainfilepath.Matchhas no**at all. So*does not cross/, and a pattern without a leading**/is anchored at the build-context root. A.dockerignorelisting.env,*.pemand*.keytherefore excludes only the copies at the repository root;config/.envandcerts/server.keystill reach the context and can land in an image layer. That file is more dangerous than a short one with no secret patterns at all, because it reads as solved and stops anyone looking. Give every depth-independent pattern the**/prefix —**/node_modules,**/.DS_Store, and the secret patterns in the canonical file, which are additionally case-folded per the rule below — and leave only genuinely root-anchored entries unprefixed:.git, the in-repo agent scratch directory.claude, and the repo's own host-built binary. The inverse move is equally wrong: never apply**/to.gitignore, where it is redundant and produces a file that is wrong in a way that looks careful. Each file is written to its own semantics; neither is derived from the other. Fetch the standard.dockerignorefromhttps://git.eeqj.de/sneak/prompts/raw/branch/main/.dockerignoreand extend it with the repo's own host-built artifacts — a hostmake buildthat leaves a compiled binary in the repo root puts that binary in the build context, where.gitignorehides it from every git-based check. Write that binary anchored,/myappand never**/myapp: the prefixed form also matchescmd/myapp/and deletes the package directory from the context. -
In-repo agent scratch belongs in both files, written to each file's own semantics.
.claude/holds one worktree per in-flight agent — an entire additional checkout of the repo — so withCOPY . .the build context inflates by a multiple of the repo, and another session's unreviewed, sometimes uncommitted work can be copied into an image layer. The directory is also created and destroyed constantly, so it invalidatesCOPY . .for reasons that have nothing to do with the repo's own content. In.gitignorethe entry is.claude/, unanchored, which already matches at every depth. In.dockerignoreit is.claude, anchored and with no**/prefix: the directory occurs exactly once where agents run at the repo root, and the prefixed form would also match any nested directory of that name and delete it from the build. It is not case-folded the way the secret patterns are, because tooling creates it in exactly one spelling, so a folded pattern would add no coverage.Known gap that comes with the anchored form. The directory is created in the agent's working directory, so the "exactly once, at the root" premise is a property of how agents are run and not of the tooling. Where agents run in subdirectories — a monorepo with a per-service agent is the ordinary case —
services/api/.claude/is not excluded by the canonical entry and still reaches the build context and the image, which is the exposure the entry exists to close. A repo in that shape adds its own anchored entries (/services/api/.claude), or**/.claudeonce it has confirmed no legitimately named nested directory would be caught. This is stated in the canonical.dockerignoreitself, since that file is what consuming repos receive. -
.dockerignorematching is case-sensitive, so cover capitalisation with character classes rather than by doubling patterns.**/*.keydoes not matchcerts/SERVER.KEY, which is reachable on the case-insensitive filesystems most laptops use. Adding an ALL-CAPS twin for each pattern is not the fix: it still missesServer.KeyandCa.Pemwhile reading as though case were handled — the same manufactured confidence as the root-anchored form. The matcher supports character ranges, so one line covers every spelling:**/*.[kK][eE][yY],**/*.[pP][eE][mM]. Apply this to every secret name, not only to extensions: the extensionless SSH keys and.envrcneed it for the same reason, since on the very filesystems that makeSERVER.KEYreachable, direnv reads.ENVRCand ssh readsID_RSA. Note that*matches the empty string, so**/*.[eE][nN][vV]already covers a bare.ENVand no separate literal.enventry is needed. -
A pattern that also catches something the build needs is re-included with a negation, not deleted. The canonical
**/*.[eE][nN][vV]excludes a committed env template such asexample.env; a repo whose build genuinely reads one adds!docs/example.envafter the pattern. Deleting the pattern instead reopens the exposure for every other file it covers. -
Verify
.dockerignoreby enumerating the image, not by reading the patterns. Plant files at the root and at least two directories deep, build a probe image that doesCOPY . ., and list what actually landed (docker run --rm --entrypoint find IMAGE /app). Reading the patterns and agreeing they look right is exactly what lets the root-only form through. Thetransferring contextsize is not a substitute: a nested secret is a few bytes, and BuildKit transfers only the delta from the previous build, so the reported size describes the transfer and not the contents of the image. -
Excluding
.gitmeansgit describecannot run inside any build stage, and it fails quietly there. TheGOLDFLAGSversion-embedding pattern assumes.gitis present; in a build stage there is no repository, sogit describewrites nothing to stdout and the-X main.Version=value comes out empty rather than erroring. The binary then reports no version at all and the build still exits 0. Compute the version on the host and thread it in as a build arg.script/dockerandscript/cibuilddo this, byte-identically across repos:# Assign on its own line: a failing command substitution inside an # argument does not trip `set -e`, so the inline form degrades to an # empty constant — the same silent-empty failure this rule is about. version="$(git describe --tags --always --dirty 2>/dev/null || true)" [ -n "$version" ] || version="unknown" docker build \ --build-arg CHECK_EPOCH="$epoch" \ --build-arg VERSION="$version" \ .--alwaysmakes an untagged repo yield the abbreviated commit hash instead of failing.|| truekeeps a failinggit describefrom trippingset -eand leaves the value empty, so the[ -n "$version" ]line is the single place the fallback is applied — and it is a live check, not defence in depth: it fires on a build from an export with no.git, and on a repository with no commits yet. Do not fold the fallback into the substitution as|| echo unknown; that makes the guard unreachable, and a guard that cannot fire is indistinguishable from one that works to everyone who copies it. The result is non-empty by construction either way, which is the point: an empty version reads as a successful one, whileunknownis visibly wrong. The Dockerfile's side isARG VERSION=devin the stage that compiles, declared there and not inherited, becauseARGis stage-scoped exactly asCHECK_EPOCHis. PassingVERSIONto a repo whose Dockerfile declares no suchARGis silently ignored by BuildKit and costs nothing, which is why the scripts stay byte-identical rather than growing a per-repo variant.One consequence for CI: the standard checkout action clones shallow and fetches no tags, so
git describe --tagsthere falls back to a bare commit hash. A repo that embeds a tag-derived version must setfetch-depth: 0on its checkout step; a repo that does not embed a version needs no change. -
No build artifacts in version control. Code-derived data (compiled bundles, minified output, generated assets) must never be committed to the repository if it can be avoided. The build process (e.g. Dockerfile, Makefile) should generate these at build time. Notable exception: Go protobuf generated files (
.pb.go) ARE committed because repos need to work withgo get, which downloads code but does not execute code generation. -
Never use
git add -Aorgit add .. Always stage files explicitly by name. -
Never force-push to
main. -
Make all changes on a feature branch. You can do whatever you want on a feature branch.
-
.golangci.ymlis standardized and must NEVER be modified by an agent, only manually by the user. Fetch fromhttps://git.eeqj.de/sneak/prompts/raw/branch/main/.golangci.yml. The canonical golangci-lint version is v2.12.2 (released 2026-05-06), pinned as the image digest inDockerfile.lint(golangci/golangci-lint@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240, which reportsgolangci-lint has version 2.12.2 built with go1.26.2 from c0d3ddc9). That digest is the only pin there is: the linter is not installed on the host, inscript/bootstrapor anywhere else. Bumping the version means changing that one digest, and it propagates to every consumer of the image with no host state able to disagree with it. -
script/bootstrapmust not install a linter at all. This supersedes the pinned-golangci-lint install that used to be canonical here. Nothing runs a linter on the host any more —script/lintis a container build — so a host install has no caller left, and its only remaining effect is to put a second, independently-versioned linter on the machine where somebody will eventually run it by hand and believe the result. The version-skew failures that install was written to close (a localmake checkgreen whilemake dockerrejected the same commit with sixgoconstfindings; a container linter surfacing thirteen findings the host run missed) are closed more completely by having exactly one linter, pinned by image digest, that no host state can shadow. Repos adopting the containerised lint delete the install block, its version and ref variables, and its call site fromscript/bootstrap.The version-enforcement principle it established still applies to any other tool a repo pins and installs on the host, and it is the part worth keeping, because each of its four properties guards a failure that otherwise reports success:
- Compare the installed version against the pin, never test presence. A
if missing <tool>; then install; figuard testsPATHpresence and never version, so on any already-provisioned machine the pin is inert and a version bump is a silent no-op. Compare the whole version token, exactly: a parser that stops at the first-reports2.12.2for a host running2.12.2-rc1and skips the install — the original defect, reintroduced through the comparison meant to fix it. - After installing, re-resolve the binary the way callers resolve it —
through
PATH, not the directory the installer wrote to — and assert the reported version is the pin. An installer that writes toGOBINwhile a different binary shadows it earlier inPATHgenuinely succeeds and changes nothing any caller sees, which is worse than no fix: it converts a known-stale tool into one everyone believes is pinned. Runhash -rfirst so the shell does not answer from its own lookup cache, and when the assertion fails, name the pathcommand -vfound, the version it reports, and the directory the install wrote to. Diagnose from the resolved path rather than asserting a cause: only a path outside the install directory is shadowing. - A mis-parse must fall through to reinstall, never to a false match. Absent binary, non-zero exit, empty output and unrecognised output should all yield an empty string, which compares unequal to the pin. The failure direction is always a redundant install, never a skipped one.
- Call it, and say so on success. A function defined and never called is a silent no-op indistinguishable from success: exit 0, nothing installed, no output. Both success branches must print a line naming the version.
Verifying such logic requires a negative control in an environment where a shadowing binary exists earlier in
PATHthan the install target — without it the control passes against the naive compare-then-install form too and proves nothing — plus a mis-parse control that feeds unparseable--versionoutput and confirms a reinstall. Run those controls against the block as a consuming repo would adopt it: pasted into ascript/bootstrap-shaped file that is then executed, never by sourcing it and invoking the function yourself. Driving the function directly tests something the artifact does not do, and it is exactly how a missing call site passes every control while the adopted snippet does nothing.Keep it POSIX sh: no bashisms, no arrays, no
[[, nogrep -P. - Compare the installed version against the pin, never test presence. A
-
SUPERSEDED, and deleted rather than kept: the per-checkout
GOLANGCI_LINT_CACHE/TMPDIRwrapper forscript/lint. Every line of it was about making a linter run on a shared host trustworthy — a private result cache so a byte-identical checkout could not serve its findings, a privateTMPDIRso the host-global lock could not collide, retry and VOID handling so a lock collision was never reported as findings. The containerised-lint rule above removes the host run itself, so there is nothing left for that wrapper to isolate, and a repo carrying both would carry two contradictory canonicalscript/lintforms. Its findings are not lost: they are the evidence for containerising, and they are recorded in that rule. Repos that adopted it delete the wrapper, the.lint-cache/entries from.gitignoreand.dockerignore, and the--allow-serial-runnersflag with them.Two of its conclusions are kept because they outlive it.
GOCACHEdoes not need isolating, measured rather than assumed: it is content-addressed, its entries are compiled artifacts rather than diagnostics carrying a foreign tree's paths, and it has no equivalent global lock — the whole fleet compiles concurrently against oneGOCACHEall day without a contention error. And verifying any change to lint plumbing requires paired controls: a control that passes against the broken form proves nothing, and it must be run against the artifact as a consuming repo would adopt it — the file executed, not the functions sourced and driven by hand. -
Interim rule for reading a lint result produced on the host, in a repo that has not yet adopted the containerised lint above. A lint run is VOID unless both hold:
- the output contains no
parallel golangci-lint is running, and - no reported file path begins with
../, and none is an absolute path outside the tree the run was launched from.
Do not record a verdict from a void run, and do not "fix" findings in files the change does not touch — chasing phantom findings across untouched files puts unrelated edits into a reviewed diff, which is more expensive than the wasted rework.
The
../clause is the one that actually bites, and it is why a filter keyed on/tmpor on absolute prefixes is not enough: golangci-lint reports paths relative to its own resolved root rather than yours, and three of the org's reported sightings had relative paths and would have passed such a filter. Both clauses are needed and neither alone is sufficient — one reproduction exited non-zero with the lock error and no foreign paths at all, and another reported 34 well-formed findings, every one of them against another checkout.State the limit of these tests rather than treating them as a guarantee. They catch contamination that names foreign files. They cannot catch contamination that suppresses findings through a poisoned entry for colliding content, which has no wall-clock tell either — no evidence of that mode has been observed, and nobody should go chasing it; the point is the reach of the tests, not a claim that the mode exists. They are a filter for the loud mode, not a proof of soundness — which is the whole argument for containerising the linter instead of documenting a discipline that depends on every agent remembering to apply it. Adopt the rule above and this one stops applying to the repo entirely.
- the output contains no
-
When pinning images or packages by hash, add a comment above the reference with the version and date (YYYY-MM-DD).
-
Use
yarn, notnpm. -
Write all dates as YYYY-MM-DD (ISO 8601).
-
Simple projects should be configured with environment variables.
-
Dockerized web services listen on port 8080 by default, overridable with
PORT. -
HTTP/web services must be hardened for production internet exposure before tagging 1.0. This means full compliance with security best practices including, without limitation, all of the following:
- Security headers on every response:
Strict-Transport-Security(HSTS) withmax-ageof at least one year andincludeSubDomains.Content-Security-Policy(CSP) with a restrictive default policy (default-src 'self'as a baseline, tightened per-resource as needed). Never useunsafe-inlineorunsafe-evalunless unavoidable, and document the reason.X-Frame-Options: DENY(orSAMEORIGINif framing is required). Prefer theframe-ancestorsCSP directive as the primary control.X-Content-Type-Options: nosniff.Referrer-Policy: strict-origin-when-cross-origin(or stricter).Permissions-Policyrestricting access to browser features the application does not use (camera, microphone, geolocation, etc.).
- Request and response limits:
- Maximum request body size enforced on all endpoints (e.g. Go
http.MaxBytesReader). Choose a sane default per-route; never accept unbounded input. - Maximum response body size where applicable (e.g. paginated APIs).
ReadTimeoutandReadHeaderTimeouton thehttp.Serverto defend against slowloris attacks.WriteTimeouton thehttp.Server.IdleTimeouton thehttp.Server.- Per-handler execution time limits via
context.WithTimeoutor chi/stdlibmiddleware.Timeout.
- Maximum request body size enforced on all endpoints (e.g. Go
- Authentication and session security:
- Rate limiting on password-based authentication endpoints. API keys are high-entropy and not susceptible to brute force, so they are exempt.
- CSRF tokens on all state-mutating HTML forms. API endpoints
authenticated via
Authorizationheader (Bearer token, API key) are exempt because the browser does not attach these automatically. - Passwords stored using bcrypt, scrypt, or argon2 — never plain-text, MD5, or SHA.
- Session cookies set with
HttpOnly,Secure, andSameSite=Lax(orStrict) attributes.
- Reverse proxy awareness:
- True client IP detection when behind a reverse proxy
(
X-Forwarded-For,X-Real-IP). The application must accept forwarded headers only from a configured set of trusted proxy addresses — never trustX-Forwarded-Forunconditionally.
- True client IP detection when behind a reverse proxy
(
- CORS:
- Authenticated endpoints must restrict
Access-Control-Allow-Originto an explicit allowlist of known origins. Wildcard (*) is acceptable only for public, unauthenticated read-only APIs.
- Authenticated endpoints must restrict
- Error handling:
- Internal errors must never leak stack traces, SQL queries, file paths,
or other implementation details to the client. Return generic error
messages in production; detailed errors only when
DEBUGis enabled.
- Internal errors must never leak stack traces, SQL queries, file paths,
or other implementation details to the client. Return generic error
messages in production; detailed errors only when
- TLS:
- Services never terminate TLS directly. They are always deployed behind
a TLS-terminating reverse proxy. The service itself listens on plain
HTTP. However, HSTS headers and
Securecookie flags must still be set by the application so that the browser enforces HTTPS end-to-end.
- Services never terminate TLS directly. They are always deployed behind
a TLS-terminating reverse proxy. The service itself listens on plain
HTTP. However, HSTS headers and
This list is non-exhaustive. Apply defense-in-depth: if a standard security hardening measure exists for HTTP services and is not listed here, it is still expected. When in doubt, harden.
- Security headers on every response:
-
README.mdis the primary documentation. Required sections:- Description: First line must include the project name, purpose, category (web server, SPA, CLI tool, etc.), license, and author. Example: "µPaaS is an MIT-licensed Go web application by @sneak that receives git-frontend webhooks and deploys applications via Docker in realtime."
- Getting Started: Copy-pasteable install/usage code block.
- Entrypoints: Opens by stating that the repo adheres to the
Scripts to Rule Them All
standard (with that link), then documents each provided
script/entrypoint and its purpose. - Rationale: Why does this exist?
- Design: How is the program structured?
- TODO: Update meticulously, even between commits. When planning, put the todo list in the README so a new agent can pick up where the last one left off.
- License: MIT, GPL, or WTFPL. Ask the user for new projects. Include a
LICENSEfile in the repo root and a License section in the README. - Author: @sneak.
-
First commit of a new repo should contain only
README.md. -
Go module root:
sneak.berlin/go/<name>. Always rungo mod tidybefore committing. -
Use SemVer.
-
Database migrations live in
internal/db/migrations/and must be embedded in the binary.000_migration.sql— contains ONLY the creation of the migrations tracking table itself. Nothing else.001_schema.sql— the full application schema.- Pre-1.0.0: never add additional migration files (002, 003, etc.).
There is no installed base to migrate. Edit
001_schema.sqldirectly. - Post-1.0.0: add new numbered migration files for each schema change. Never edit existing migrations after release.
-
All repos should have an
.editorconfigenforcing the project's indentation settings. -
Avoid putting files in the repo root unless necessary. Root should contain only project-level config files (
README.md,Makefile,Dockerfile,LICENSE,.gitignore,.editorconfig,REPO_POLICIES.md, and language-specific config). Everything else goes in a subdirectory. Canonical subdirectory names:bin/— executable scripts and toolscmd/— Go command entrypointsconfigs/— configuration templates and examplesdeploy/— deployment manifests (k8s, compose, terraform)docs/— documentation and markdown (README.md stays in root)internal/— Go internal packagesinternal/db/migrations/— database migrationspkg/— Go library packagesshare/— systemd units, data filesstatic/— static assets (images, fonts, etc.)web/— web frontend source
-
When setting up a new repo, files from the
promptsrepo may be used as templates. Fetch them fromhttps://git.eeqj.de/sneak/prompts/raw/branch/main/<path>. -
New repos must contain at minimum:
README.md,.git,.gitignore,.editorconfigLICENSE,REPO_POLICIES.md(copy from thepromptsrepo)Makefilescript/entrypoints (bootstrap,setup,projectname,test,lint,fmt,fmt-check,check,docker,cibuild,precommit,install-precommit)Dockerfile,.dockerignore.gitea/workflows/check.yml- Go:
go.mod,go.sum,.golangci.yml - JS:
package.json,yarn.lock,.prettierrc,.prettierignore - Python:
pyproject.toml