Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdb566a209 | ||
|
|
d709d88e79 | ||
|
|
f77d785bed | ||
|
|
3f8201d532 | ||
|
|
a8686891ba | ||
|
|
cc6a5a00e7 |
+24
-63
@@ -1,75 +1,37 @@
|
|||||||
# Docker matches this file with moby/patternmatcher: Go filepath.Match
|
# .dockerignore does NOT use .gitignore semantics. Docker matches with
|
||||||
# semantics plus a `**` extension, compiled to a regexp. Plain
|
# moby/patternmatcher: filepath.Match plus `**`, so `*` does not cross
|
||||||
# filepath.Match has no `**` at all. What follows from that: `*` does not
|
# `/` and an unprefixed pattern is anchored at the context root. Every
|
||||||
# cross `/`, and a pattern without a leading `**/` is anchored at the
|
# depth-independent pattern therefore needs `**/`, or `config/.env` and
|
||||||
# build-context root. Every depth-independent pattern therefore needs the
|
# `certs/server.key` still ship while the file reads as solved. Only
|
||||||
# `**/` prefix — without it `config/.env` and `certs/server.key` still
|
# genuinely root-anchored entries go unprefixed. Never transplant these
|
||||||
# ship while the file reads as solved.
|
# into .gitignore, where `**/` is wrong.
|
||||||
#
|
#
|
||||||
# Root-anchored entries are for paths that occur exactly once, at the
|
# Matching is case-sensitive, so secrets use character ranges rather
|
||||||
# context root. A host-built binary is the usual case, and it must be
|
# than an ALL-CAPS twin, which would still miss `Server.Key`.
|
||||||
# written anchored: `/myapp`, never `**/myapp`. The prefixed form also
|
|
||||||
# matches `cmd/myapp/`, which deletes the package directory from the
|
|
||||||
# context. In-repo agent scratch is the other case, for the same reason
|
|
||||||
# — with the caveat recorded at that entry: anchoring is exact only
|
|
||||||
# where agents run at the repo root, and a repo where they do not must
|
|
||||||
# add its own entries.
|
|
||||||
#
|
#
|
||||||
# Matching is case-sensitive, so `**/*.key` does not match
|
# Extend with this repo's own host-built artifacts, written anchored:
|
||||||
# `certs/SERVER.KEY`, which is reachable on the case-insensitive
|
# `/myapp`, never `**/myapp`, which also matches `cmd/myapp/` and
|
||||||
# filesystems most laptops use. Adding an ALL-CAPS twin per pattern is
|
# deletes the package directory from the context.
|
||||||
# not the fix: it still misses `Server.Key` while reading as though case
|
|
||||||
# were handled. Character ranges cover every spelling in one line, so
|
|
||||||
# every secret name below is written that way — including the
|
|
||||||
# extensionless SSH keys and `.envrc`, because on those same
|
|
||||||
# case-insensitive filesystems direnv reads `.ENVRC` and ssh reads
|
|
||||||
# `ID_RSA`.
|
|
||||||
#
|
|
||||||
# `**/*.[eE][nN][vV]` also excludes a committed env template such as
|
|
||||||
# `example.env`. If the build genuinely needs one, re-include it with a
|
|
||||||
# negation after the pattern: `!docs/example.env`.
|
|
||||||
#
|
|
||||||
# Extend this file with the repo's own host-built artifacts (compiled
|
|
||||||
# binaries, test binaries, coverage output); those are per-repo and
|
|
||||||
# belong here because a host build otherwise drops them into the
|
|
||||||
# context.
|
|
||||||
|
|
||||||
# Repository metadata: exactly one, at the context root. Excluding it
|
# Excluding .git means `git describe` cannot run in any build stage and
|
||||||
# means `git describe` cannot run in any build stage, and it fails
|
# fails quietly there; pass the version in with --build-arg VERSION.
|
||||||
# quietly there rather than erroring, so a version embedded that way
|
|
||||||
# comes out empty. Compute the version on the host and pass it in with
|
|
||||||
# `--build-arg VERSION=...`; see the version rule in REPO_POLICIES.md.
|
|
||||||
.git
|
.git
|
||||||
|
|
||||||
# In-repo agent scratch: a directory holding a full additional checkout
|
# Agent scratch: one full checkout of the repo per in-flight agent.
|
||||||
# of the repo for each in-flight agent. Anchored because it occurs
|
# Anchored because it occurs once where agents run at the repo root.
|
||||||
# exactly once *where agents run at the repo root*, which is the
|
# KNOWN GAP: a repo running agents in subdirectories still ships
|
||||||
# convention this file assumes; the `**/` form would also match any
|
# `services/api/.claude/` and must add its own anchored entry.
|
||||||
# nested directory of that name and delete it from the build.
|
|
||||||
#
|
|
||||||
# KNOWN GAP, and it is not hypothetical: the directory is created in the
|
|
||||||
# agent's working directory. If agents in this repo run in
|
|
||||||
# subdirectories — a monorepo with a per-service agent, say — then
|
|
||||||
# `services/api/.claude/` is NOT excluded by the line below and still
|
|
||||||
# reaches the build context and the image, which is the exposure this
|
|
||||||
# entry exists to close. A repo in that shape adds its own anchored
|
|
||||||
# entries (`/services/api/.claude`), or `**/.claude` after confirming no
|
|
||||||
# legitimately named nested directory would be caught.
|
|
||||||
#
|
|
||||||
# Not case-folded, unlike the secret patterns below: tooling creates
|
|
||||||
# this directory in exactly one spelling, so a folded pattern would add
|
|
||||||
# no coverage.
|
|
||||||
.claude
|
.claude
|
||||||
|
|
||||||
# Environment files. `*.env` covers both the bare `.env` name (`*` matches
|
# Environment files. `*.env` covers bare `.env` and the `prod.env`
|
||||||
# the empty string) and the `prod.env` convention.
|
# convention. Re-include a committed template with a negation if the
|
||||||
|
# build needs one: `!docs/example.env`.
|
||||||
**/*.[eE][nN][vV]
|
**/*.[eE][nN][vV]
|
||||||
**/.[eE][nN][vV].*
|
**/.[eE][nN][vV].*
|
||||||
**/.[eE][nN][vV][rR][cC]
|
**/.[eE][nN][vV][rR][cC]
|
||||||
|
|
||||||
# Private keys and the bundles that carry them. Public certificates
|
# Private keys and the bundles carrying them. Public certificates
|
||||||
# (*.crt, *.cer) are deliberately absent: they are not secrets and are
|
# (*.crt, *.cer) are deliberately absent: they are legitimate inputs.
|
||||||
# sometimes a legitimate build input.
|
|
||||||
**/*.[pP][eE][mM]
|
**/*.[pP][eE][mM]
|
||||||
**/*.[kK][eE][yY]
|
**/*.[kK][eE][yY]
|
||||||
**/*.[pP]12
|
**/*.[pP]12
|
||||||
@@ -86,8 +48,7 @@
|
|||||||
**/.DS_Store
|
**/.DS_Store
|
||||||
**/Thumbs.db
|
**/Thumbs.db
|
||||||
|
|
||||||
# Editor state. Never a build input, and it churns under a developer's
|
# Editor state: never a build input, and it churns COPY.
|
||||||
# hands, so it invalidates COPY for reasons unrelated to the source.
|
|
||||||
**/*.swp
|
**/*.swp
|
||||||
**/*.swo
|
**/*.swo
|
||||||
**/*~
|
**/*~
|
||||||
|
|||||||
+13
-13
@@ -3,26 +3,26 @@ FROM node@sha256:e4bf2a82ad0a4037d28035ae71529873c069b13eb0455466ae0bc13363826e3
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# script/bootstrap installs all prerequisites (make via apk here; node
|
# Makes script/lint run the linter directly rather than building
|
||||||
# and yarn are already in the base image, so those steps are skipped).
|
# Dockerfile.lint, which would need a docker daemon here.
|
||||||
# Dependency manifests are copied first so the bootstrap layer is
|
ENV LINT_IN_CONTAINER=1
|
||||||
# cached until they change.
|
|
||||||
|
# script/bootstrap installs all prerequisites. Manifests are copied
|
||||||
|
# first so that layer stays cached until dependencies change.
|
||||||
COPY script/ script/
|
COPY script/ script/
|
||||||
COPY package.json yarn.lock ./
|
COPY package.json yarn.lock ./
|
||||||
RUN script/bootstrap
|
RUN script/bootstrap
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# CHECK_EPOCH is a per-invocation nonce supplied by script/cibuild and
|
# CHECK_EPOCH is a per-invocation nonce from script/cibuild and
|
||||||
# script/docker. Without it an unchanged tree serves this layer from
|
# script/docker; without it an unchanged tree serves this layer from
|
||||||
# cache and the build reports a green it never ran. ARG is stage-scoped,
|
# cache and the build reports a green it never ran. ARG is stage-scoped,
|
||||||
# so it must be redeclared in every stage that runs checks. The guard
|
# so declare it in every stage that runs checks. The guard fails a bare
|
||||||
# makes a bare `docker build .` fail loudly instead of silently reusing
|
# `docker build .`, which would otherwise reuse the empty (and therefore
|
||||||
# the empty (and therefore stable) cache key. Expand the value into the
|
# stable) cache key. The value is also expanded into the check command,
|
||||||
# command so the cache miss does not depend on BuildKit's handling of an
|
# so the cache miss does not depend on BuildKit's handling of an
|
||||||
# unreferenced ARG. Both the guard and the check RUN reference the value,
|
# unreferenced ARG; keep both references.
|
||||||
# so both are value-keyed: there are two independent invalidation points
|
|
||||||
# here, not one. Keep both.
|
|
||||||
ARG CHECK_EPOCH
|
ARG CHECK_EPOCH
|
||||||
RUN [ -n "$CHECK_EPOCH" ] || exit 1
|
RUN [ -n "$CHECK_EPOCH" ] || exit 1
|
||||||
RUN echo "check epoch: ${CHECK_EPOCH}" && make check
|
RUN echo "check epoch: ${CHECK_EPOCH}" && make check
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Lint-only image, built by script/lint when it is not already inside a
|
||||||
|
# container. Linting is a build step, so a successful build is a clean
|
||||||
|
# lint, and nothing is bind-mounted, which matters when the daemon is
|
||||||
|
# remote.
|
||||||
|
#
|
||||||
|
# node 22-alpine, 2026-02-22
|
||||||
|
FROM node@sha256:e4bf2a82ad0a4037d28035ae71529873c069b13eb0455466ae0bc13363826e34
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Makes script/lint run the linter directly instead of recursing into
|
||||||
|
# another docker build, which has no daemon here.
|
||||||
|
ENV LINT_IN_CONTAINER=1
|
||||||
|
|
||||||
|
COPY script/ script/
|
||||||
|
COPY package.json yarn.lock ./
|
||||||
|
RUN script/bootstrap
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# ARG sits after the dependency layer so that layer stays cached and
|
||||||
|
# only the lint re-runs. The guard fails a bare `docker build
|
||||||
|
# -f Dockerfile.lint .`, which would otherwise reuse the empty (stable)
|
||||||
|
# cache key and report a lint it never ran.
|
||||||
|
ARG CHECK_EPOCH
|
||||||
|
RUN [ -n "$CHECK_EPOCH" ] || exit 1
|
||||||
|
RUN echo "lint epoch: ${CHECK_EPOCH}" && make lint
|
||||||
@@ -117,7 +117,9 @@ alpine. We provide:
|
|||||||
- `script/projectname` — output the project name (our own extension); used by
|
- `script/projectname` — output the project name (our own extension); used by
|
||||||
`script/docker` for the image tag
|
`script/docker` for the image tag
|
||||||
- `script/test` — run the test suite (no tests defined here)
|
- `script/test` — run the test suite (no tests defined here)
|
||||||
- `script/lint` — lint the markdown files with prettier
|
- `script/lint` — lint the markdown files with prettier. Inside a container
|
||||||
|
(`LINT_IN_CONTAINER=1`, set by both Dockerfiles) it runs prettier directly; on
|
||||||
|
a host it builds `Dockerfile.lint` so the linter still runs in a container
|
||||||
- `script/fmt` — format all markdown files with prettier (writes)
|
- `script/fmt` — format all markdown files with prettier (writes)
|
||||||
- `script/fmt-check` — check formatting (read-only)
|
- `script/fmt-check` — check formatting (read-only)
|
||||||
- `script/check` — run all checks: `test`, `lint`, `fmt-check` (our own
|
- `script/check` — run all checks: `test`, `lint`, `fmt-check` (our own
|
||||||
|
|||||||
@@ -21,6 +21,27 @@ fmt-check, and commit.
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-08-10: Moved every lint run into a container. `script/lint` now runs the
|
||||||
|
linter directly when `LINT_IN_CONTAINER=1` and otherwise builds
|
||||||
|
`Dockerfile.lint`, so the linter never runs on a developer host — closing the
|
||||||
|
content-keyed result cache that produced a confirmed false green, the
|
||||||
|
host-global `$TMPDIR/golangci-lint.lock`, and host/container version skew.
|
||||||
|
Detection is on that marker alone: a false negative inside a container fails
|
||||||
|
loudly on the missing daemon, while a false positive on a host would silently
|
||||||
|
restore host linting, so `/.dockerenv` is rejected outright — measured absent
|
||||||
|
inside BuildKit `RUN` steps and present on hosts that are themselves
|
||||||
|
containers. Everything else keeps its existing shape: `make check` still runs
|
||||||
|
in the image, `script/cibuild` is still one build, and the Go multistage lint
|
||||||
|
stage survives with `ENV LINT_IN_CONTAINER=1`. `Dockerfile.lint` carries the
|
||||||
|
same `CHECK_EPOCH` guard, with the `ARG` below the dependency layer so only
|
||||||
|
the lint re-runs. The `script/bootstrap` golangci-lint install and the
|
||||||
|
per-checkout cache/lock/`.lint-cache` wrapper are deleted as superseded; a JS
|
||||||
|
repo's `yarn install` stays, since the rule is about where a verdict comes
|
||||||
|
from, not about which binaries exist. `golangci-lint config verify` was kept
|
||||||
|
on measurement: a bogus config key passes `golangci-lint run` with `0 issues`
|
||||||
|
and fails `config verify`, and every case reproduced byte-identically under
|
||||||
|
`--network none`, so the schema is embedded and the line costs no network.
|
||||||
|
Comment blocks across the touched files were cut hard in the same pass.
|
||||||
- 2026-08-09: Made a golangci-lint result belong to the tree that asked for it.
|
- 2026-08-09: Made a golangci-lint result belong to the tree that asked for it.
|
||||||
REPO_POLICIES.md now carries the canonical Go `script/lint`, which gives the
|
REPO_POLICIES.md now carries the canonical Go `script/lint`, which gives the
|
||||||
linter per-checkout `GOLANGCI_LINT_CACHE` and per-checkout `TMPDIR`. The two
|
linter per-checkout `GOLANGCI_LINT_CACHE` and per-checkout `TMPDIR`. The two
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: Code Styleguide — Go
|
title: Code Styleguide — Go
|
||||||
last_modified: 2026-08-09
|
last_modified: 2026-08-10
|
||||||
---
|
---
|
||||||
|
|
||||||
1. Try to hard wrap long lines at 77 characters or less.
|
1. Try to hard wrap long lines at 77 characters or less.
|
||||||
@@ -111,7 +111,11 @@ last_modified: 2026-08-09
|
|||||||
|
|
||||||
1. For anything beyond a simple script or tool, or anything that is going to
|
1. For anything beyond a simple script or tool, or anything that is going to
|
||||||
run in any sort of "production" anywhere, make sure it passes
|
run in any sort of "production" anywhere, make sure it passes
|
||||||
`golangci-lint`.
|
`golangci-lint`. Run it with `make lint`, never by invoking the binary: the
|
||||||
|
linter always runs in a container, and `golangci-lint` is not installed on
|
||||||
|
the host by any repo. Invoked directly on a shared host it reads a result
|
||||||
|
cache keyed on file content rather than location, and a host-global lock, so
|
||||||
|
its answer may belong to another checkout entirely.
|
||||||
|
|
||||||
1. Write a `Dockerfile` for every repo, even if it only runs the tests and
|
1. Write a `Dockerfile` for every repo, even if it only runs the tests and
|
||||||
linting. `script/cibuild` and `script/docker` should always make sure that
|
linting. `script/cibuild` and `script/docker` should always make sure that
|
||||||
@@ -144,10 +148,15 @@ last_modified: 2026-08-09
|
|||||||
|
|
||||||
1. Keep the `main()` function as small as possible.
|
1. Keep the `main()` function as small as possible.
|
||||||
|
|
||||||
1. Keep the `main` package as small as possible. Move as much code as is
|
1. Keep the `main` package as small as possible. Each `cmd/<name>/` directory
|
||||||
feasible to a library package, even if it's an internal one. `main` is just
|
contains a single `main.go` whose body is one call into library code (for
|
||||||
an entrypoint to your code, not a place for implementations. Exception:
|
example `os.Exit(cli.Main())` calling `internal/cli`). All CLI logic — flag
|
||||||
single-file scripts.
|
parsing, subcommand dispatch, argument handling, output formatting — lives
|
||||||
|
in `internal/` or `pkg/`, not in `cmd/`. `main` is just an entrypoint to
|
||||||
|
your code, not a place for implementations. Exception: single-file scripts.
|
||||||
|
|
||||||
|
1. No project logic outside `internal/` or `pkg/`. Anything in `cmd/` is a thin
|
||||||
|
entrypoint only.
|
||||||
|
|
||||||
1. HTTP HandleFuncs should be returned from methods or functions that need to
|
1. HTTP HandleFuncs should be returned from methods or functions that need to
|
||||||
handle HTTP requests. Don't use methods or your top level functions as
|
handle HTTP requests. Don't use methods or your top level functions as
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: Existing Repo Checklist
|
title: Existing Repo Checklist
|
||||||
last_modified: 2026-08-09
|
last_modified: 2026-08-10
|
||||||
---
|
---
|
||||||
|
|
||||||
Use this checklist when beginning work in a repo that may not yet conform to our
|
Use this checklist when beginning work in a repo that may not yet conform to our
|
||||||
@@ -42,6 +42,15 @@ with your task.
|
|||||||
`CHECK_EPOCH` rule in `REPO_POLICIES.md`. Without them the check layer is
|
`CHECK_EPOCH` rule in `REPO_POLICIES.md`. Without them the check layer is
|
||||||
served from cache on an unchanged tree and the build reports a green it
|
served from cache on an unchanged tree and the build reports a green it
|
||||||
never ran.
|
never ran.
|
||||||
|
- [ ] **Every stage that runs checks sets `ENV LINT_IN_CONTAINER=1`** — the lint
|
||||||
|
stage and the build stage both. This is the item an existing repo most
|
||||||
|
often fails after adopting the containerised lint: without it
|
||||||
|
`script/lint` tries to build `Dockerfile.lint` from inside a build step,
|
||||||
|
where there is no daemon.
|
||||||
|
- [ ] `Dockerfile.lint` exists and `script/lint` builds it when not already in a
|
||||||
|
container — see the containerised-lint rule in `REPO_POLICIES.md`. Base
|
||||||
|
image pinned by sha256 with a version/date comment, `ARG CHECK_EPOCH`
|
||||||
|
**after** the dependency layer with the guard below it.
|
||||||
- [ ] `.dockerignore` excludes the repo's own host-built artifacts (compiled
|
- [ ] `.dockerignore` excludes the repo's own host-built artifacts (compiled
|
||||||
binaries, test binaries, coverage output), written root-anchored —
|
binaries, test binaries, coverage output), written root-anchored —
|
||||||
`/myapp`, never `**/myapp`, which would also match `cmd/myapp/`. An
|
`/myapp`, never `**/myapp`, which would also match `cmd/myapp/`. An
|
||||||
@@ -100,15 +109,28 @@ with your task.
|
|||||||
`script/install-precommit`, shimmed by `make hooks`) runs it
|
`script/install-precommit`, shimmed by `make hooks`) runs it
|
||||||
- [ ] README has an **Entrypoints** section documenting the `script/`
|
- [ ] README has an **Entrypoints** section documenting the `script/`
|
||||||
entrypoints and linking the standard
|
entrypoints and linking the standard
|
||||||
- [ ] Go: `script/lint` isolates golangci-lint per checkout —
|
- [ ] `script/lint` is the canonical detect-and-branch form, and no host
|
||||||
`GOLANGCI_LINT_CACHE` and `TMPDIR` both exported into `.lint-cache/`
|
invocation anywhere in the repo can produce a lint **verdict** — grep for
|
||||||
(which is in `.gitignore` and `.dockerignore`), `--allow-serial-runners`
|
the linter's own name across `script/`, the `Makefile` and CI config, not
|
||||||
passed, and the lock error retried rather than reported as findings. Copy
|
just `script/lint`. A second path is likeliest here: a `make lint-fast`,
|
||||||
the canonical block from `REPO_POLICIES.md`. Setting only the cache is the
|
an older container-versus-host branch, or a CI step calling the binary
|
||||||
common half-fix and leaves `parallel golangci-lint is running` failing
|
directly. **Expected hits that are not the defect**: `script/fmt`, and in
|
||||||
runs red.
|
a repo whose formatter is also its linter, `script/fmt-check`. Everything
|
||||||
|
else the grep finds is a real second path and goes.
|
||||||
|
- [ ] Detection is on `LINT_IN_CONTAINER` alone. Reject any `/.dockerenv` or
|
||||||
|
cgroup heuristic: absent in BuildKit `RUN` steps, present on hosts that
|
||||||
|
are themselves containers, and a false positive lints on the host.
|
||||||
|
- [ ] `script/bootstrap` installs no golangci-lint. Delete the block, its
|
||||||
|
version and ref variables, and its call site. A JS repo's `yarn install`
|
||||||
|
stays — it brings a linter along with every other dependency, which is
|
||||||
|
fine as long as no verdict is taken from it.
|
||||||
|
- [ ] The per-checkout lint state is gone: no `GOLANGCI_LINT_CACHE` or `TMPDIR`
|
||||||
|
exports, no `--allow-serial-runners`, and `.lint-cache/` removed from
|
||||||
|
`.gitignore` and `.dockerignore`.
|
||||||
- [ ] `make check` does not modify any files in the repo
|
- [ ] `make check` does not modify any files in the repo
|
||||||
- [ ] `make test` has a 30-second timeout
|
- [ ] `make test` has a 90-second timeout and completes within the 60-second
|
||||||
|
hard cap (over 20 seconds is green but must be filed as an improvement
|
||||||
|
bug)
|
||||||
- [ ] `make test` runs real tests, not a no-op (at minimum, import/compile
|
- [ ] `make test` runs real tests, not a no-op (at minimum, import/compile
|
||||||
check)
|
check)
|
||||||
- [ ] `make check` passes on current branch
|
- [ ] `make check` passes on current branch
|
||||||
@@ -153,6 +175,9 @@ with your task.
|
|||||||
# Final
|
# Final
|
||||||
|
|
||||||
- [ ] `make check` passes
|
- [ ] `make check` passes
|
||||||
- [ ] `script/cibuild` succeeds (a bare `docker build .` fails closed by design,
|
- [ ] `make lint` runs twice on an unchanged tree with the lint layer `DONE`
|
||||||
on the `CHECK_EPOCH` guard)
|
both times, never `CACHED` and never sub-second
|
||||||
|
- [ ] `script/cibuild` succeeds (a bare `docker build .` or
|
||||||
|
`docker build -f Dockerfile.lint .` fails closed by design, on the
|
||||||
|
`CHECK_EPOCH` guard)
|
||||||
- [ ] Commit and merge fixes before starting your actual task
|
- [ ] Commit and merge fixes before starting your actual task
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: New Repo Checklist
|
title: New Repo Checklist
|
||||||
last_modified: 2026-08-09
|
last_modified: 2026-08-10
|
||||||
---
|
---
|
||||||
|
|
||||||
Use this checklist when creating a new repository from scratch. Follow the steps
|
Use this checklist when creating a new repository from scratch. Follow the steps
|
||||||
@@ -79,9 +79,21 @@ Template files can be fetched from:
|
|||||||
`CHECK_EPOCH` rule in `REPO_POLICIES.md`. Without them the check layer is
|
`CHECK_EPOCH` rule in `REPO_POLICIES.md`. Without them the check layer is
|
||||||
served from cache on an unchanged tree and the build reports a green it
|
served from cache on an unchanged tree and the build reports a green it
|
||||||
never ran.
|
never ran.
|
||||||
|
- Every stage that runs checks sets `ENV LINT_IN_CONTAINER=1`, so
|
||||||
|
`script/lint` runs the linter natively instead of trying to build
|
||||||
|
`Dockerfile.lint` where there is no daemon.
|
||||||
|
- Go repos: separate `lint` stage on the `golangci/golangci-lint` image,
|
||||||
|
with `COPY --from=lint /src/go.sum /dev/null` in the build stage to force
|
||||||
|
the ordering. Re-prove that ordering warm after adopting `CHECK_EPOCH`.
|
||||||
- Server: also builds and runs the application
|
- Server: also builds and runs the application
|
||||||
- Non-server: brings up dev environment and runs `make check`
|
- Non-server: brings up dev environment and runs `make check`
|
||||||
- Image pinned by sha256 hash with version/date comment
|
- Image pinned by sha256 hash with version/date comment
|
||||||
|
- [ ] `Dockerfile.lint` — the lint-only image `script/lint` builds when it is
|
||||||
|
not already inside a container. Sets `ENV LINT_IN_CONTAINER=1`; same
|
||||||
|
`ARG CHECK_EPOCH` + guard + expanded-value discipline as above, with the
|
||||||
|
`ARG` **after** the dependency layer so only the lint re-runs. Base image
|
||||||
|
pinned by sha256 with a version/date comment. Copy from
|
||||||
|
`REPO_POLICIES.md`.
|
||||||
- [ ] Gitea Actions workflow at `.gitea/workflows/check.yml` that runs
|
- [ ] Gitea Actions workflow at `.gitea/workflows/check.yml` that runs
|
||||||
`script/cibuild` on push — reference
|
`script/cibuild` on push — reference
|
||||||
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitea/workflows/check.yml`
|
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitea/workflows/check.yml`
|
||||||
@@ -106,20 +118,17 @@ are thin shims calling them. Model scripts:
|
|||||||
installs
|
installs
|
||||||
- [ ] `script/setup` / `make setup` — readies a fresh clone: runs `bootstrap`,
|
- [ ] `script/setup` / `make setup` — readies a fresh clone: runs `bootstrap`,
|
||||||
then `install-precommit`, plus repo-specific init
|
then `install-precommit`, plus repo-specific init
|
||||||
- [ ] `script/test` / `make test` — runs real tests, not a no-op (30-second
|
- [ ] `script/test` / `make test` — runs real tests, not a no-op (90-second
|
||||||
timeout)
|
timeout, 60-second hard cap on wall time)
|
||||||
- [ ] `script/lint` / `make lint` — runs linter
|
- [ ] `script/lint` / `make lint` — runs the linter directly when
|
||||||
- [ ] Go: exports `GOLANGCI_LINT_CACHE` **and** `TMPDIR` into a
|
`LINT_IN_CONTAINER=1`, otherwise `epoch="$(date +%s%N)$$"` on its own line
|
||||||
`.lint-cache/` directory inside the checkout, above any
|
then `docker build --build-arg CHECK_EPOCH="$epoch" -f Dockerfile.lint .`.
|
||||||
container-versus-host branch so every path that reaches the linter
|
No lint verdict may come from a host invocation. Copy from
|
||||||
gets them; passes `--allow-serial-runners` (never
|
`REPO_POLICIES.md`. Detect on `LINT_IN_CONTAINER` only — never
|
||||||
`--allow-parallel-runners`); retries on
|
`/.dockerenv`, which is absent in BuildKit `RUN` steps and present on
|
||||||
`parallel golangci-lint is running` detected on **stderr** and exits
|
hosts that are themselves containers. Without the nonce this exits 0 on an
|
||||||
75 with a VOID message on exhaustion. Copy the canonical block from
|
unchanged tree having linted nothing; without `-f Dockerfile.lint` it
|
||||||
`REPO_POLICIES.md` rather than writing your own: a version that sets
|
builds the main image and lints nothing at all.
|
||||||
only the cache leaves the false-red half live, and one that detects
|
|
||||||
the collision by exit status can retry a real finding away.
|
|
||||||
- [ ] Go: `.lint-cache/` is in both `.gitignore` and `.dockerignore`
|
|
||||||
- [ ] `script/fmt` / `make fmt` — formats code (writes)
|
- [ ] `script/fmt` / `make fmt` — formats code (writes)
|
||||||
- [ ] `script/fmt-check` / `make fmt-check` — checks formatting (read-only)
|
- [ ] `script/fmt-check` / `make fmt-check` — checks formatting (read-only)
|
||||||
- [ ] `script/check` / `make check` — runs `test`, `lint`, `fmt-check`; must not
|
- [ ] `script/check` / `make check` — runs `test`, `lint`, `fmt-check`; must not
|
||||||
@@ -149,7 +158,10 @@ are thin shims calling them. Model scripts:
|
|||||||
export with no `.git` and on a repo with no commits — keep it, and do not
|
export with no `.git` and on a repo with no commits — keep it, and do not
|
||||||
collapse it into `|| echo unknown`, which makes it unreachable. See the
|
collapse it into `|| echo unknown`, which makes it unreachable. See the
|
||||||
`CHECK_EPOCH` and git-describe rules in `REPO_POLICIES.md` for why each
|
`CHECK_EPOCH` and git-describe rules in `REPO_POLICIES.md` for why each
|
||||||
element is load-bearing. A bare `docker build .` fails closed by design.
|
element is load-bearing. A bare `docker build .` fails closed by design, and
|
||||||
|
so does a bare `docker build -f Dockerfile.lint .`. The image runs
|
||||||
|
`make check`, which includes lint, so `script/cibuild` needs no separate
|
||||||
|
lint step.
|
||||||
|
|
||||||
- [ ] `script/precommit` — called by the pre-commit hook; runs `script/check`
|
- [ ] `script/precommit` — called by the pre-commit hook; runs `script/check`
|
||||||
- [ ] `script/install-precommit` — installs the pre-commit hook that runs
|
- [ ] `script/install-precommit` — installs the pre-commit hook that runs
|
||||||
@@ -161,7 +173,11 @@ are thin shims calling them. Model scripts:
|
|||||||
# 4. Verify
|
# 4. Verify
|
||||||
|
|
||||||
- [ ] `make check` passes
|
- [ ] `make check` passes
|
||||||
|
- [ ] `make lint` demonstrably runs the linter rather than returning a cached
|
||||||
|
build: run it twice on an unchanged tree and confirm the lint layer says
|
||||||
|
`DONE`, never `CACHED`, both times
|
||||||
- [ ] `make docker` succeeds
|
- [ ] `make docker` succeeds
|
||||||
|
- [ ] `script/cibuild` succeeds and demonstrably executes
|
||||||
- [ ] No secrets in repo
|
- [ ] No secrets in repo
|
||||||
- [ ] No mutable image/package references
|
- [ ] No mutable image/package references
|
||||||
- [ ] No unnecessary files in repo root
|
- [ ] No unnecessary files in repo root
|
||||||
|
|||||||
+331
-582
File diff suppressed because it is too large
Load Diff
+9
-14
@@ -1,27 +1,22 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# script/cibuild: run the CI build. The Dockerfile runs script/check, but
|
# script/cibuild: run the CI build. The Dockerfile runs script/check, but
|
||||||
# that only proves anything because CHECK_EPOCH is a fresh nonce on every
|
# that only proves anything because CHECK_EPOCH is a fresh nonce on every
|
||||||
# invocation: without it Docker serves the check layer from cache on an
|
# invocation: without it Docker serves the check layer from cache and the
|
||||||
# unchanged tree and the build exits 0 without running the suite.
|
# build exits 0 without running the suite.
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
# Assign on its own line: a failing command substitution inside an
|
# Both assignments on their own line: a failing command substitution
|
||||||
# argument does not trip `set -e`, which would silently degrade the
|
# inside an argument does not trip `set -e`, so the inline form
|
||||||
# nonce to an empty constant. `$$` is required because busybox `date`
|
# degrades silently to an empty constant. `$$` because busybox `date`
|
||||||
# drops %N without erroring.
|
# drops %N without erroring. VERSION is computed here because
|
||||||
|
# .dockerignore excludes .git, so `git describe` in a build stage
|
||||||
|
# yields an empty version without failing; the guard below is the
|
||||||
|
# single place the fallback is applied.
|
||||||
epoch="$(date +%s%N)$$"
|
epoch="$(date +%s%N)$$"
|
||||||
# VERSION must be computed here, on the host: .dockerignore excludes
|
|
||||||
# .git, so `git describe` cannot run in any build stage and fails
|
|
||||||
# quietly there rather than erroring. Same own-line discipline as the
|
|
||||||
# epoch. `|| true` keeps a failing describe from tripping `set -e`
|
|
||||||
# and leaves the value empty; the guard below is then the single
|
|
||||||
# place the fallback is applied, and it does fire — on an export with
|
|
||||||
# no .git, or a repo with no commits yet. `unknown` is visibly wrong
|
|
||||||
# in a binary in a way that an empty version is not.
|
|
||||||
version="$(git describe --tags --always --dirty 2>/dev/null || true)"
|
version="$(git describe --tags --always --dirty 2>/dev/null || true)"
|
||||||
[ -n "$version" ] || version="unknown"
|
[ -n "$version" ] || version="unknown"
|
||||||
docker build \
|
docker build \
|
||||||
|
|||||||
+6
-12
@@ -11,19 +11,13 @@ ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
|||||||
|
|
||||||
main() {
|
main() {
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
# Assign on its own line: a failing command substitution inside an
|
# Both assignments on their own line: a failing command substitution
|
||||||
# argument does not trip `set -e`, which would silently degrade the
|
# inside an argument does not trip `set -e`, so the inline form
|
||||||
# nonce to an empty constant. `$$` is required because busybox `date`
|
# degrades silently to an empty constant. `$$` because busybox `date`
|
||||||
# drops %N without erroring.
|
# drops %N without erroring. VERSION is computed here because
|
||||||
|
# .dockerignore excludes .git, so `git describe` in a build stage
|
||||||
|
# yields an empty version without failing.
|
||||||
epoch="$(date +%s%N)$$"
|
epoch="$(date +%s%N)$$"
|
||||||
# VERSION must be computed here, on the host: .dockerignore excludes
|
|
||||||
# .git, so `git describe` cannot run in any build stage and fails
|
|
||||||
# quietly there rather than erroring. Same own-line discipline as the
|
|
||||||
# epoch. `|| true` keeps a failing describe from tripping `set -e`
|
|
||||||
# and leaves the value empty; the guard below is then the single
|
|
||||||
# place the fallback is applied, and it does fire — on an export with
|
|
||||||
# no .git, or a repo with no commits yet. `unknown` is visibly wrong
|
|
||||||
# in a binary in a way that an empty version is not.
|
|
||||||
version="$(git describe --tags --always --dirty 2>/dev/null || true)"
|
version="$(git describe --tags --always --dirty 2>/dev/null || true)"
|
||||||
[ -n "$version" ] || version="unknown"
|
[ -n "$version" ] || version="unknown"
|
||||||
docker build \
|
docker build \
|
||||||
|
|||||||
+24
-3
@@ -1,13 +1,34 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# script/lint: run the linter.
|
# script/lint: run the linter. Inside a container, run it directly;
|
||||||
|
# on a host, build Dockerfile.lint so it runs in one anyway. The linter
|
||||||
|
# is never run on a developer host, where a shared result cache, a
|
||||||
|
# host-global lock and a stale toolchain make its answer untrustworthy.
|
||||||
|
#
|
||||||
|
# LINT_IN_CONTAINER is set by this repo's Dockerfiles and is the ONLY
|
||||||
|
# accepted signal. Do not add a /.dockerenv fallback: it is absent
|
||||||
|
# inside BuildKit RUN steps and present on hosts that are themselves
|
||||||
|
# containers, so it both misses and false-positives — and a false
|
||||||
|
# positive silently restores host linting.
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
echo "Linting markdown files..."
|
|
||||||
yarn run prettier --check '**/*.md' --tab-width 4 --prose-wrap always
|
if [ "${LINT_IN_CONTAINER:-}" = "1" ]; then
|
||||||
|
exec yarn run prettier --check '**/*.md' \
|
||||||
|
--tab-width 4 --prose-wrap always
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Own line, and `$$` because busybox `date` drops %N silently.
|
||||||
|
# Without a fresh nonce the lint layer is cached and this exits 0
|
||||||
|
# having linted nothing.
|
||||||
|
epoch="$(date +%s%N)$$"
|
||||||
|
docker build \
|
||||||
|
--build-arg CHECK_EPOCH="$epoch" \
|
||||||
|
-f Dockerfile.lint \
|
||||||
|
.
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|||||||
Reference in New Issue
Block a user