Compare commits
1 Commits
lint-polic
...
61448b0c4e
| Author | SHA1 | Date | |
|---|---|---|---|
| 61448b0c4e |
@@ -10,7 +10,7 @@
|
|||||||
# context root. A host-built binary is the usual case, and it must be
|
# context root. A host-built binary is the usual case, and it must be
|
||||||
# written anchored: `/myapp`, never `**/myapp`. The prefixed form also
|
# written anchored: `/myapp`, never `**/myapp`. The prefixed form also
|
||||||
# matches `cmd/myapp/`, which deletes the package directory from the
|
# matches `cmd/myapp/`, which deletes the package directory from the
|
||||||
# context.
|
# context. In-repo agent scratch is the other case, for the same reason.
|
||||||
#
|
#
|
||||||
# Matching is case-sensitive, so `**/*.key` does not match
|
# Matching is case-sensitive, so `**/*.key` does not match
|
||||||
# `certs/SERVER.KEY`, which is reachable on the case-insensitive
|
# `certs/SERVER.KEY`, which is reachable on the case-insensitive
|
||||||
@@ -31,9 +31,21 @@
|
|||||||
# belong here because a host build otherwise drops them into the
|
# belong here because a host build otherwise drops them into the
|
||||||
# context.
|
# context.
|
||||||
|
|
||||||
# Repository metadata: exactly one, at the context root.
|
# Repository metadata: exactly one, at the context root. Excluding it
|
||||||
|
# means `git describe` cannot run in any build stage, and it fails
|
||||||
|
# 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: one directory at the context root, holding a
|
||||||
|
# full additional checkout of the repo for each in-flight agent. Written
|
||||||
|
# anchored because it occurs exactly once — the `**/` form would also
|
||||||
|
# match a nested directory of that name. Not case-folded, unlike the
|
||||||
|
# secret patterns below: tooling creates this in exactly one spelling,
|
||||||
|
# and a miss costs build-context bloat rather than exposure.
|
||||||
|
.claude
|
||||||
|
|
||||||
# Environment files. `*.env` covers both the bare `.env` name (`*` matches
|
# Environment files. `*.env` covers both the bare `.env` name (`*` matches
|
||||||
# the empty string) and the `prod.env` convention.
|
# the empty string) and the `prod.env` convention.
|
||||||
**/*.[eE][nN][vV]
|
**/*.[eE][nN][vV]
|
||||||
|
|||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -11,6 +11,12 @@ Thumbs.db
|
|||||||
.vscode/
|
.vscode/
|
||||||
*.sublime-*
|
*.sublime-*
|
||||||
|
|
||||||
|
# Agent scratch (worktrees of this repo, created and destroyed by
|
||||||
|
# in-flight tooling). Unanchored: .gitignore patterns already match at
|
||||||
|
# every depth, so no prefix is wanted here. This is not a .dockerignore
|
||||||
|
# entry and must not be given a `**/` prefix on the way into one.
|
||||||
|
.claude/
|
||||||
|
|
||||||
# Node
|
# Node
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
|
|||||||
21
TODO.md
21
TODO.md
@@ -21,6 +21,27 @@ fmt-check, and commit.
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-08-09: Kept in-repo agent scratch out of the Docker build context and out
|
||||||
|
of version control. `.claude/` holds one worktree — an entire additional
|
||||||
|
checkout of the repo — per in-flight agent, and under `COPY . .` all of it was
|
||||||
|
reaching the image: another session's unreviewed, sometimes uncommitted work,
|
||||||
|
inflating the context by a multiple of the repo and invalidating `COPY` for
|
||||||
|
reasons unrelated to the repo's own content. The `.dockerignore` entry is
|
||||||
|
root-anchored, because the directory occurs exactly once and the `**/` form
|
||||||
|
additionally deletes any nested directory of that name; the `.gitignore` entry
|
||||||
|
is unanchored, because `.gitignore` patterns already match at every depth, and
|
||||||
|
each file is written to its own semantics rather than derived from the other.
|
||||||
|
Also closed the consequence that ships broken silently: excluding `.git` means
|
||||||
|
`git describe` cannot run in any build stage and yields an empty version
|
||||||
|
without erroring, so `script/docker` and `script/cibuild` now compute the
|
||||||
|
version on the host and pass `--build-arg VERSION`, and `REPO_POLICIES.md`
|
||||||
|
states where `VERSION` comes from instead of leaving the reader to fill the
|
||||||
|
gap with `git describe` inside the build. The two Go documents that carry the
|
||||||
|
`GOLDFLAGS` pattern were corrected in the same pass, from `:=` to `?=`, since
|
||||||
|
a `$(shell git describe)` evaluated inside a build stage is exactly the empty
|
||||||
|
version this closes. Verified by enumerating a probe image before, after, and
|
||||||
|
against the `**/`-prefixed form, with a positive control and the `CHECK_EPOCH`
|
||||||
|
cache verification re-run under the changed build context.
|
||||||
- 2026-08-09: Closed the secret exposure in the canonical `.dockerignore`: a
|
- 2026-08-09: Closed the secret exposure in the canonical `.dockerignore`: a
|
||||||
developer's local `.env`, `*.pem` or `*.key` was reaching the Docker build
|
developer's local `.env`, `*.pem` or `*.key` was reaching the Docker build
|
||||||
context under `COPY . .`, invisible to every git-based check because
|
context under `COPY . .`, invisible to every git-based check because
|
||||||
|
|||||||
@@ -49,7 +49,14 @@ last_modified: 2026-08-09
|
|||||||
```
|
```
|
||||||
|
|
||||||
```make
|
```make
|
||||||
VERSION := $(shell git describe --always --dirty)
|
# ?= so a Docker build can supply the value. `.dockerignore` excludes
|
||||||
|
# `.git`, so inside a build stage `$(shell git describe ...)` expands to
|
||||||
|
# the empty string without failing and the binary reports no version at
|
||||||
|
# all. The Dockerfile declares `ARG VERSION` in the stage that compiles,
|
||||||
|
# which puts it in the environment where this `?=` defers to it, and
|
||||||
|
# `script/docker` / `script/cibuild` compute it on the host. See the
|
||||||
|
# git-describe rule in REPO_POLICIES.md.
|
||||||
|
VERSION ?= $(shell git describe --always --dirty)
|
||||||
BUILDARCH := $(shell uname -m)
|
BUILDARCH := $(shell uname -m)
|
||||||
|
|
||||||
GOLDFLAGS += -X main.Version=$(VERSION)
|
GOLDFLAGS += -X main.Version=$(VERSION)
|
||||||
|
|||||||
@@ -24,9 +24,14 @@ with your task.
|
|||||||
- [ ] `LICENSE` file exists and matches the README
|
- [ ] `LICENSE` file exists and matches the README
|
||||||
- [ ] `REPO_POLICIES.md` exists and version date is current — fetch from
|
- [ ] `REPO_POLICIES.md` exists and version date is current — fetch from
|
||||||
`https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/REPO_POLICIES.md`
|
`https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/REPO_POLICIES.md`
|
||||||
- [ ] `.gitignore` is comprehensive (OS, editor, language artifacts, secrets) —
|
- [ ] `.gitignore` is comprehensive (OS, editor, agent scratch, language
|
||||||
fetch from `https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitignore`
|
artifacts, secrets) — fetch from
|
||||||
if missing
|
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitignore` if missing.
|
||||||
|
An existing repo usually has a hand-written one that is never re-fetched,
|
||||||
|
so check the entries rather than the file's presence: `.claude/` in
|
||||||
|
particular, unanchored, so agent worktrees cannot be committed by
|
||||||
|
accident. Do not give it a `**/` prefix — that is a `.dockerignore` form
|
||||||
|
and is wrong here.
|
||||||
- [ ] `.editorconfig` exists — fetch from
|
- [ ] `.editorconfig` exists — fetch from
|
||||||
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.editorconfig`
|
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.editorconfig`
|
||||||
- [ ] `Dockerfile` and `.dockerignore` exist (fetch `.dockerignore` from
|
- [ ] `Dockerfile` and `.dockerignore` exist (fetch `.dockerignore` from
|
||||||
@@ -42,6 +47,17 @@ with your task.
|
|||||||
`/myapp`, never `**/myapp`, which would also match `cmd/myapp/`. An
|
`/myapp`, never `**/myapp`, which would also match `cmd/myapp/`. An
|
||||||
existing repo is where such a binary is likeliest to already be sitting in
|
existing repo is where such a binary is likeliest to already be sitting in
|
||||||
the build context, invisible to git.
|
the build context, invisible to git.
|
||||||
|
- [ ] `.dockerignore` excludes `.claude`, root-anchored and with no `**/`
|
||||||
|
prefix. Agent worktrees are entire checkouts of the repo, so they inflate
|
||||||
|
the context by a multiple of it and can copy another session's unreviewed
|
||||||
|
work into an image layer. Confirm by enumerating the image, not by reading
|
||||||
|
the file — `.gitignore` hides these from `git status` too.
|
||||||
|
- [ ] If the repo embeds a version in a binary, that version is computed on the
|
||||||
|
host and passed with `--build-arg VERSION=...` by `script/docker` and
|
||||||
|
`script/cibuild`. No stage calls `git describe`: `.dockerignore` excludes
|
||||||
|
`.git`, so it yields an empty version without failing the build. A
|
||||||
|
tag-derived version additionally needs `fetch-depth: 0` on the CI checkout
|
||||||
|
step, which clones shallow and fetches no tags by default.
|
||||||
- [ ] Every depth-independent pattern in `.dockerignore` carries a `**/` prefix;
|
- [ ] Every depth-independent pattern in `.dockerignore` carries a `**/` prefix;
|
||||||
only genuinely root-anchored entries such as `.git` are unprefixed, and
|
only genuinely root-anchored entries such as `.git` are unprefixed, and
|
||||||
`.gitignore`'s patterns have not been transplanted unmodified.
|
`.gitignore`'s patterns have not been transplanted unmodified.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: Go HTTP Server Conventions
|
title: Go HTTP Server Conventions
|
||||||
last_modified: 2026-02-22
|
last_modified: 2026-08-09
|
||||||
---
|
---
|
||||||
|
|
||||||
This document defines the architectural patterns, design decisions, and
|
This document defines the architectural patterns, design decisions, and
|
||||||
@@ -991,7 +991,12 @@ func main() {
|
|||||||
Use ldflags to inject version information at build time:
|
Use ldflags to inject version information at build time:
|
||||||
|
|
||||||
```makefile
|
```makefile
|
||||||
VERSION := $(shell git describe --tags --always)
|
# ?= so a Docker build can supply the value. `.dockerignore` excludes `.git`,
|
||||||
|
# so inside a build stage `$(shell git describe ...)` expands to the empty
|
||||||
|
# string without failing and the binary reports no version. The Dockerfile
|
||||||
|
# declares `ARG VERSION` in the stage that compiles, and `script/docker` /
|
||||||
|
# `script/cibuild` compute it on the host — see REPO_POLICIES.md.
|
||||||
|
VERSION ?= $(shell git describe --tags --always)
|
||||||
BUILDARCH := $(shell go env GOARCH)
|
BUILDARCH := $(shell go env GOARCH)
|
||||||
|
|
||||||
build:
|
build:
|
||||||
|
|||||||
@@ -35,7 +35,11 @@ Template files can be fetched from:
|
|||||||
|
|
||||||
- [ ] `.gitignore` — fetch from
|
- [ ] `.gitignore` — fetch from
|
||||||
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitignore`, extend for
|
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitignore`, extend for
|
||||||
language-specific artifacts
|
language-specific artifacts. Extensions are written to `.gitignore`'s own
|
||||||
|
semantics, where an unanchored pattern already matches at every depth:
|
||||||
|
never add a `**/` prefix here, which is a `.dockerignore` form. The
|
||||||
|
canonical file already carries `.claude/` so agent worktrees cannot be
|
||||||
|
committed by accident.
|
||||||
- [ ] `.editorconfig` — fetch from
|
- [ ] `.editorconfig` — fetch from
|
||||||
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.editorconfig`
|
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.editorconfig`
|
||||||
- [ ] `Makefile` — fetch from
|
- [ ] `Makefile` — fetch from
|
||||||
@@ -59,7 +63,13 @@ Template files can be fetched from:
|
|||||||
`.gitignore`'s patterns: `.dockerignore` anchors an unprefixed pattern at
|
`.gitignore`'s patterns: `.dockerignore` anchors an unprefixed pattern at
|
||||||
the context root, so the copied form leaves `config/.env` in the build
|
the context root, so the copied form leaves `config/.env` in the build
|
||||||
context while reading as solved. See the `.dockerignore` rule in
|
context while reading as solved. See the `.dockerignore` rule in
|
||||||
`REPO_POLICIES.md`.
|
`REPO_POLICIES.md`. The canonical file's `.claude` entry is anchored for
|
||||||
|
the same reason as a repo-root binary; leave it that way.
|
||||||
|
- If the image embeds a version in a binary, the version is computed on the
|
||||||
|
host and passed with `--build-arg VERSION=...`. `ARG VERSION=dev` is
|
||||||
|
declared in the stage that compiles, and **no stage calls `git describe`**
|
||||||
|
— `.dockerignore` excludes `.git`, so it yields an empty version without
|
||||||
|
failing the build.
|
||||||
- All Dockerfiles must run `make check` as a build step, and every stage
|
- All Dockerfiles must run `make check` as a build step, and every stage
|
||||||
containing a check-running `RUN` must declare `ARG CHECK_EPOCH` with the
|
containing a check-running `RUN` must declare `ARG CHECK_EPOCH` with the
|
||||||
`RUN [ -n "$CHECK_EPOCH" ] || exit 1` guard immediately below it — see the
|
`RUN [ -n "$CHECK_EPOCH" ] || exit 1` guard immediately below it — see the
|
||||||
@@ -104,11 +114,17 @@ are thin shims calling them. Model scripts:
|
|||||||
for the image tag)
|
for the image tag)
|
||||||
- [ ] `script/docker` / `make docker` — builds Docker image, tagged via
|
- [ ] `script/docker` / `make docker` — builds Docker image, tagged via
|
||||||
`script/projectname` (byte-identical across repos); assigns
|
`script/projectname` (byte-identical across repos); assigns
|
||||||
`epoch="$(date +%s%N)$$"` on its own line and passes
|
`epoch="$(date +%s%N)$$"` and the `git describe` version on their own
|
||||||
`--build-arg CHECK_EPOCH="$epoch"`
|
lines, and passes `--build-arg CHECK_EPOCH="$epoch"` and
|
||||||
- [ ] `script/cibuild` — cd to repo root, assign `epoch="$(date +%s%N)$$"` on
|
`--build-arg VERSION="$version"`
|
||||||
its own line, then run `docker build --build-arg CHECK_EPOCH="$epoch" .`
|
- [ ] `script/cibuild` — cd to repo root, assign `epoch="$(date +%s%N)$$"` and
|
||||||
(what CI runs). The build arg is mandatory: see the `CHECK_EPOCH` rule in
|
`version="$(git describe --tags --always --dirty 2>/dev/null || echo unknown)"`
|
||||||
|
on their own lines, then run
|
||||||
|
`docker build --build-arg CHECK_EPOCH="$epoch" --build-arg VERSION="$version" .`
|
||||||
|
(what CI runs). Both build args are mandatory, and both assignments must
|
||||||
|
be on their own line: a failing command substitution inside an argument
|
||||||
|
does not trip `set -e`, so the inline form degrades silently to an empty
|
||||||
|
constant. See the `CHECK_EPOCH` and git-describe rules in
|
||||||
`REPO_POLICIES.md` for why each element is load-bearing. A bare
|
`REPO_POLICIES.md` for why each element is load-bearing. A bare
|
||||||
`docker build .` fails closed by design.
|
`docker build .` fails closed by design.
|
||||||
- [ ] `script/precommit` — called by the pre-commit hook; runs `script/check`
|
- [ ] `script/precommit` — called by the pre-commit hook; runs `script/check`
|
||||||
|
|||||||
@@ -60,19 +60,21 @@ style conventions are in separate documents:
|
|||||||
prerequisite since nvm requires bash. yarn is then pinned via
|
prerequisite since nvm requires bash. yarn is then pinned via
|
||||||
`corepack prepare yarn@<version> --activate`. Never install "latest" or "lts";
|
`corepack prepare yarn@<version> --activate`. Never install "latest" or "lts";
|
||||||
always exact versions. `script/cibuild` runs the CI build: it changes to the
|
always exact versions. `script/cibuild` runs the CI build: it changes to the
|
||||||
repo root and runs `docker build --build-arg CHECK_EPOCH="$epoch" .`, where
|
repo root and runs
|
||||||
`epoch` is a per-invocation nonce (see the `CHECK_EPOCH` rule below); the
|
`docker build --build-arg CHECK_EPOCH="$epoch" --build-arg VERSION="$version" .`,
|
||||||
Gitea workflow calls it. Four further scripts are our own extensions to the
|
where `epoch` is a per-invocation nonce (see the `CHECK_EPOCH` rule below) and
|
||||||
standard: `script/check` runs `script/test`, `script/lint`, and
|
`version` is computed on the host because `.git` is not in the build context
|
||||||
`script/fmt-check`; `script/precommit` is what the git pre-commit hook runs,
|
(see the git-describe rule below); the Gitea workflow calls it. Four further
|
||||||
and it calls `script/check`; `script/install-precommit` installs the git
|
scripts are our own extensions to the standard: `script/check` runs
|
||||||
pre-commit hook (the `make hooks` target shims to it); and
|
`script/test`, `script/lint`, and `script/fmt-check`; `script/precommit` is
|
||||||
`script/projectname` (literally that filename) simply outputs the project's
|
what the git pre-commit hook runs, and it calls `script/check`;
|
||||||
name. Scripts that need the name call `script/projectname` — e.g.
|
`script/install-precommit` installs the git pre-commit hook (the `make hooks`
|
||||||
`script/docker` assembles its image tag from it — so those scripts stay
|
target shims to it); and `script/projectname` (literally that filename) simply
|
||||||
byte-identical across all repos. Repo-type-specific pre-commit extras (e.g.
|
outputs the project's name. Scripts that need the name call
|
||||||
`go mod tidy` verification in Go repos) belong in `script/precommit`, not in
|
`script/projectname` — e.g. `script/docker` assembles its image tag from it —
|
||||||
the hook itself. Model scripts are at
|
so those scripts stay byte-identical across all repos. Repo-type-specific
|
||||||
|
pre-commit extras (e.g. `go mod tidy` verification in Go repos) belong in
|
||||||
|
`script/precommit`, not in the hook itself. Model scripts are at
|
||||||
`https://git.eeqj.de/sneak/prompts/raw/branch/main/script/<name>`. The README
|
`https://git.eeqj.de/sneak/prompts/raw/branch/main/script/<name>`. The README
|
||||||
must document the provided scripts in an **Entrypoints** section (see the
|
must document the provided scripts in an **Entrypoints** section (see the
|
||||||
README requirements below).
|
README requirements below).
|
||||||
@@ -122,11 +124,18 @@ style conventions are in separate documents:
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
epoch="$(date +%s%N)$$"
|
epoch="$(date +%s%N)$$"
|
||||||
docker build --build-arg CHECK_EPOCH="$epoch" .
|
version="$(git describe --tags --always --dirty 2>/dev/null || echo unknown)"
|
||||||
|
[ -n "$version" ] || version="unknown"
|
||||||
|
docker build \
|
||||||
|
--build-arg CHECK_EPOCH="$epoch" \
|
||||||
|
--build-arg VERSION="$version" \
|
||||||
|
.
|
||||||
```
|
```
|
||||||
|
|
||||||
All four elements are load-bearing; none is optional, and each guards a
|
The `VERSION` lines are there for a different reason, covered by the
|
||||||
failure mode that otherwise fails green:
|
git-describe rule below; they are shown here so the two rules do not each
|
||||||
|
document half a command. All four `CHECK_EPOCH` elements are load-bearing;
|
||||||
|
none is optional, and each guards a failure mode that otherwise fails green:
|
||||||
- `ARG` is stage-scoped, so a single declaration leaves the other check
|
- `ARG` is stage-scoped, so a single declaration leaves the other check
|
||||||
stages frozen while the fix reviews as complete. Declare it in every stage
|
stages frozen while the fix reviews as complete. Declare it in every stage
|
||||||
that runs checks, immediately above the first such `RUN`.
|
that runs checks, immediately above the first such `RUN`.
|
||||||
@@ -197,6 +206,9 @@ style conventions are in separate documents:
|
|||||||
RUN [ -n "$CHECK_EPOCH" ] || exit 1
|
RUN [ -n "$CHECK_EPOCH" ] || exit 1
|
||||||
RUN echo "check epoch: ${CHECK_EPOCH}" && make test
|
RUN echo "check epoch: ${CHECK_EPOCH}" && make 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
|
ARG VERSION=dev
|
||||||
RUN CGO_ENABLED=0 go build -trimpath \
|
RUN CGO_ENABLED=0 go build -trimpath \
|
||||||
-ldflags="-s -w -X main.Version=${VERSION}" \
|
-ldflags="-s -w -X main.Version=${VERSION}" \
|
||||||
@@ -247,18 +259,24 @@ style conventions are in separate documents:
|
|||||||
each stage is invalidated at two independent points. The later `RUN`s in
|
each stage is invalidated at two independent points. The later `RUN`s in
|
||||||
the same stage need no expansion of their own: they are already
|
the same stage need no expansion of their own: they are already
|
||||||
invalidated by their busted parent layer.
|
invalidated by their busted parent layer.
|
||||||
|
- `ARG VERSION=dev` is declared in the build stage, and its value is
|
||||||
|
supplied on the host by `script/docker` and `script/cibuild` via
|
||||||
|
`--build-arg VERSION=...`. The `dev` default is a placeholder for a local
|
||||||
|
build, not a source of truth. **No stage may call `git describe`**:
|
||||||
|
`.dockerignore` excludes `.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
|
- Every repo should have a Gitea Actions workflow (`.gitea/workflows/`) that
|
||||||
runs `script/cibuild` (which runs
|
runs `script/cibuild` (which runs
|
||||||
`docker build --build-arg CHECK_EPOCH="$epoch" .`) on push. The Dockerfile
|
`docker build --build-arg CHECK_EPOCH="$epoch" --build-arg VERSION="$version" .`)
|
||||||
runs `make check`, so a successful build implies all checks pass — but that
|
on push. The Dockerfile runs `make check`, so a successful build implies all
|
||||||
implication holds **only** because of the `CHECK_EPOCH` cache-bust described
|
checks pass — but that implication holds **only** because of the `CHECK_EPOCH`
|
||||||
above. Without it, an unchanged tree serves the check layer from cache and the
|
cache-bust described above. Without it, an unchanged tree serves the check
|
||||||
build reports a green it never earned. A bare `docker build .` fails closed by
|
layer from cache and the build reports a green it never earned. A bare
|
||||||
design, on the `[ -n "$CHECK_EPOCH" ]` guard; always go through
|
`docker build .` fails closed by design, on the `[ -n "$CHECK_EPOCH" ]` guard;
|
||||||
`script/cibuild` or `script/docker`. Never accept a `script/cibuild` pass as
|
always go through `script/cibuild` or `script/docker`. Never accept a
|
||||||
evidence without confirming it ran: a sub-second wall time, or `CACHED` on the
|
`script/cibuild` pass as evidence without confirming it ran: a sub-second wall
|
||||||
check layer, means nothing was executed.
|
time, or `CACHED` on the check layer, means nothing was executed.
|
||||||
|
|
||||||
- Use platform-standard formatters: `black` for Python, `prettier` for
|
- Use platform-standard formatters: `black` for Python, `prettier` for
|
||||||
JS/CSS/Markdown/HTML, `go fmt` for Go. Always use default configuration with
|
JS/CSS/Markdown/HTML, `go fmt` for Go. Always use default configuration with
|
||||||
@@ -328,8 +346,10 @@ style conventions are in separate documents:
|
|||||||
must be in `.gitignore`. No exceptions.
|
must be in `.gitignore`. No exceptions.
|
||||||
|
|
||||||
- `.gitignore` should be comprehensive from the start: OS files (`.DS_Store`),
|
- `.gitignore` should be comprehensive from the start: OS files (`.DS_Store`),
|
||||||
editor files (`.swp`, `*~`), language build artifacts, and `node_modules/`.
|
editor files (`.swp`, `*~`), in-repo agent scratch directories (`.claude/`,
|
||||||
Fetch the standard `.gitignore` from
|
which holds one worktree — an entire additional checkout of the repo — per
|
||||||
|
in-flight agent), language build artifacts, and `node_modules/`. Fetch the
|
||||||
|
standard `.gitignore` from
|
||||||
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitignore` when setting up
|
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitignore` when setting up
|
||||||
a new repo. These patterns are written to `.gitignore`'s own semantics, in
|
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
|
which an unanchored pattern already matches at every depth. They are not a
|
||||||
@@ -349,7 +369,8 @@ style conventions are in separate documents:
|
|||||||
depth-independent pattern the `**/` prefix — `**/node_modules`,
|
depth-independent pattern the `**/` prefix — `**/node_modules`,
|
||||||
`**/.DS_Store`, and the secret patterns in the canonical file, which are
|
`**/.DS_Store`, and the secret patterns in the canonical file, which are
|
||||||
additionally case-folded per the rule below — and leave only genuinely
|
additionally case-folded per the rule below — and leave only genuinely
|
||||||
root-anchored entries such as `.git` unprefixed. The inverse move is equally
|
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
|
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
|
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
|
own semantics; neither is derived from the other. Fetch the standard
|
||||||
@@ -361,6 +382,20 @@ style conventions are in separate documents:
|
|||||||
anchored, `/myapp` and never `**/myapp`: the prefixed form also matches
|
anchored, `/myapp` and never `**/myapp`: the prefixed form also matches
|
||||||
`cmd/myapp/` and deletes the package directory from the context.
|
`cmd/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 with `COPY . .` 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 invalidates `COPY . .` for
|
||||||
|
reasons that have nothing to do with the repo's own content. In `.gitignore`
|
||||||
|
the entry is `.claude/`, unanchored, which already matches at every depth. In
|
||||||
|
`.dockerignore` it is `.claude`, anchored and with **no** `**/` prefix: the
|
||||||
|
directory occurs exactly once, at the context root, and the prefixed form
|
||||||
|
would also match any nested directory of that name. It is not case-folded the
|
||||||
|
way the secret patterns are, because tooling creates it in exactly one
|
||||||
|
spelling and a miss costs context bloat rather than exposure.
|
||||||
|
|
||||||
- **`.dockerignore` matching is case-sensitive, so cover capitalisation with
|
- **`.dockerignore` matching is case-sensitive, so cover capitalisation with
|
||||||
character classes rather than by doubling patterns.** `**/*.key` does not
|
character classes rather than by doubling patterns.** `**/*.key` does not
|
||||||
match `certs/SERVER.KEY`, which is reachable on the case-insensitive
|
match `certs/SERVER.KEY`, which is reachable on the case-insensitive
|
||||||
@@ -390,6 +425,42 @@ style conventions are in separate documents:
|
|||||||
bytes, and BuildKit transfers only the delta from the previous build, so the
|
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.
|
reported size describes the transfer and not the contents of the image.
|
||||||
|
|
||||||
|
- **Excluding `.git` means `git describe` cannot run inside any build stage, and
|
||||||
|
it fails quietly there.** The `GOLDFLAGS` version-embedding pattern assumes
|
||||||
|
`.git` is present; in a build stage there is no repository, so `git describe`
|
||||||
|
writes 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/docker` and `script/cibuild` do this, byte-identically across
|
||||||
|
repos:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# 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 || echo unknown)"
|
||||||
|
[ -n "$version" ] || version="unknown"
|
||||||
|
docker build \
|
||||||
|
--build-arg CHECK_EPOCH="$epoch" \
|
||||||
|
--build-arg VERSION="$version" \
|
||||||
|
.
|
||||||
|
```
|
||||||
|
|
||||||
|
`--always` makes an untagged repo yield the abbreviated commit hash instead
|
||||||
|
of failing, and the `unknown` fallback covers a build from an export with no
|
||||||
|
`.git` at all. Both are non-empty by construction: an empty version reads as
|
||||||
|
a successful one, which is precisely the failure being closed. The
|
||||||
|
Dockerfile's side is `ARG VERSION=dev` in the stage that compiles, declared
|
||||||
|
there and not inherited, because `ARG` is stage-scoped exactly as
|
||||||
|
`CHECK_EPOCH` is. Passing `VERSION` to a repo whose Dockerfile declares no
|
||||||
|
such `ARG` is 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 --tags` there falls back to a bare commit
|
||||||
|
hash. A repo that embeds a tag-derived version must set `fetch-depth: 0` on
|
||||||
|
its checkout step; a repo that does not embed a version needs no change.
|
||||||
|
|
||||||
- **No build artifacts in version control.** Code-derived data (compiled
|
- **No build artifacts in version control.** Code-derived data (compiled
|
||||||
bundles, minified output, generated assets) must never be committed to the
|
bundles, minified output, generated assets) must never be committed to the
|
||||||
repository if it can be avoided. The build process (e.g. Dockerfile, Makefile)
|
repository if it can be avoided. The build process (e.g. Dockerfile, Makefile)
|
||||||
|
|||||||
@@ -14,7 +14,18 @@ main() {
|
|||||||
# nonce to an empty constant. `$$` is required because busybox `date`
|
# nonce to an empty constant. `$$` is required because busybox `date`
|
||||||
# drops %N without erroring.
|
# drops %N without erroring.
|
||||||
epoch="$(date +%s%N)$$"
|
epoch="$(date +%s%N)$$"
|
||||||
docker build --build-arg CHECK_EPOCH="$epoch" .
|
# 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, plus a non-empty fallback, so a repo built from an export
|
||||||
|
# with no .git reports `unknown` rather than an empty version that
|
||||||
|
# reads as a successful one.
|
||||||
|
version="$(git describe --tags --always --dirty 2>/dev/null || echo unknown)"
|
||||||
|
[ -n "$version" ] || version="unknown"
|
||||||
|
docker build \
|
||||||
|
--build-arg CHECK_EPOCH="$epoch" \
|
||||||
|
--build-arg VERSION="$version" \
|
||||||
|
.
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|||||||
@@ -16,7 +16,17 @@ main() {
|
|||||||
# nonce to an empty constant. `$$` is required because busybox `date`
|
# nonce to an empty constant. `$$` is required because busybox `date`
|
||||||
# drops %N without erroring.
|
# drops %N without erroring.
|
||||||
epoch="$(date +%s%N)$$"
|
epoch="$(date +%s%N)$$"
|
||||||
docker build --build-arg CHECK_EPOCH="$epoch" \
|
# 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, plus a non-empty fallback, so a repo built from an export
|
||||||
|
# with no .git reports `unknown` rather than an empty version that
|
||||||
|
# reads as a successful one.
|
||||||
|
version="$(git describe --tags --always --dirty 2>/dev/null || echo unknown)"
|
||||||
|
[ -n "$version" ] || version="unknown"
|
||||||
|
docker build \
|
||||||
|
--build-arg CHECK_EPOCH="$epoch" \
|
||||||
|
--build-arg VERSION="$version" \
|
||||||
-t "$("$SCRIPT_DIR/projectname")" .
|
-t "$("$SCRIPT_DIR/projectname")" .
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user