All checks were successful
check / check (push) Successful in 11s
The canonical .dockerignore and .gitignore both omitted the in-repo agent scratch directory. On this fleet that directory holds one worktree per in-flight agent -- an entire additional checkout of the repo each -- so under `COPY . .` all of it reached the build context and the image. Measured on this repo before the change: five planted scratch files, at every depth beneath the directory, all present inside a probe image built from the real context. Three consequences, only the first of which is about size. The context inflates by a multiple of the repo. Another session's unreviewed and sometimes uncommitted work is copied into a build artifact. And the directory is created and destroyed constantly by tooling, so it invalidates `COPY . .` for reasons that have nothing to do with this repo's content -- which is the accidental cache protection described at length in the issue thread, and the reason this change was sequenced behind the CHECK_EPOCH bust rather than landed alongside the rest of the .dockerignore work. The two entries are deliberately different shapes, because the two files have different semantics and neither is derived from the other. In .dockerignore the entry is anchored, `.claude`, with no `**/` prefix: the directory occurs exactly once, at the context root, and the prefixed form additionally matches any nested directory of that name. Measured rather than argued -- the `**/`-prefixed control was built and enumerated too, and it removes prompts/.claude/ from the context as well, which in a repo with a legitimately named nested directory would silently delete it from the build. In .gitignore the entry is unanchored, `.claude/`, because a .gitignore pattern already matches at every depth; `git check-ignore -v` confirms it covering both .claude/ and prompts/.claude/, so a `**/` prefix there would be redundant at best, and on an anchored pattern it would be actively wrong. It is not case-folded the way the neighbouring secret patterns are. Tooling creates the directory in exactly one spelling, and a miss costs context bloat rather than exposure, so the character-class treatment that the secret names require would be noise here. The file says so, since the header comment is the only part of this guidance a consuming repo actually receives. The second half of this change is the consequence that ships broken silently. Excluding .git means `git describe` cannot run in any build stage, and it fails quietly there rather than erroring: `-X main.Version=` comes out empty, the binary reports no version, and the build still exits 0. The Go template in REPO_POLICIES.md had `ARG VERSION=dev` and never said where VERSION came from, which is precisely the gap a reader fills in with `git describe` inside the build. It now says: computed on the host, threaded in with `--build-arg VERSION=...`, shown as a complete command rather than as two rules each documenting half of one. script/docker and script/cibuild do it, with the same discipline the epoch already has -- assignment on its own line, because a failing command substitution inside an argument does not trip `set -e`, plus a non-empty fallback so a build from an export with no .git reports `unknown` rather than an empty string that reads as a successful version. The scripts pass VERSION unconditionally rather than growing a per-repo variant. This repo's Dockerfile declares no `ARG VERSION`, and BuildKit was measured accepting the unconsumed arg silently -- no warning, no cache effect, confirmed by the paired runs below in which the bootstrap layer still caches. The alternative, leaving it to each repo, reintroduces the trap: a repo that needs a version and finds no VERSION in its scripts writes `git describe` into the Dockerfile, which is the failure being closed. The same correction reaches the two Go documents that carry the GOLDFLAGS pattern, since a `$(shell git describe)` evaluated inside a build stage is exactly this empty version. Both are now `?=`, so an `ARG VERSION` in the compiling stage arrives through the environment and wins. Leaving them as `:=` would have left the corpus telling a reader one thing in the policy and the opposite in the styleguide. Both repo checklists gain the entries too. They are what an agent reads while writing these files, so they are where the wrong shape actually gets written: the .gitignore item is the one an existing repo never re-fetches, and it now names `.claude/` explicitly along with the warning not to prefix it. Verification, by enumerating a probe image rather than by reading the patterns. Standalone minimal Dockerfile held outside the context, `--no-cache` scoped to that one image, no prune of any kind. Before: all five planted scratch files in the image, 42 files total. After: zero, 37 files total, with README.md, script/check, prompts/NEW_REPO_CHECKLIST.md and a planted probe_src/app.md all still present as positive controls, so the exclusion is a real exclusion and not a COPY that stopped copying. Transferred context fell from 161.86kB to 68.82kB, recorded as corroboration only: BuildKit reports a delta, not a total, and an earlier run in this repo transferred 2.18kB while shipping 43 files. The CHECK_EPOCH verification was re-run under the changed context, because the context moved underneath the earlier measurement. Two consecutive script/cibuild runs on an unchanged tree: run 1 in 17.07s, run 2 in 5.73s, both executing the check layer with a distinct epoch and real prettier output from both lint and fmt-check. The `RUN script/bootstrap` layer is CACHED in run 2, which is the validity control -- it proves no concurrent prune landed between the runs and that no --no-cache path was taken, so the check layer executing is the bust working rather than a cold cache. Planted files were removed afterwards and their absence confirmed against the filesystem with `find`, not against `git status`, which cannot see them once .gitignore covers the directory -- the same blind spot that made the earlier secret exposure invisible.
34 lines
1.4 KiB
Bash
Executable File
34 lines
1.4 KiB
Bash
Executable File
#!/bin/sh
|
|
# script/docker: build the Docker image tagged with the project name.
|
|
# Identical in all repos; the tag comes from script/projectname. The
|
|
# Dockerfile's checks only actually run because CHECK_EPOCH is a fresh
|
|
# nonce on every invocation; without it a warm cache turns this into a
|
|
# green that proves nothing.
|
|
set -eu
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
|
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
|
|
|
main() {
|
|
cd "$ROOT"
|
|
# Assign on its own line: a failing command substitution inside an
|
|
# argument does not trip `set -e`, which would silently degrade the
|
|
# nonce to an empty constant. `$$` is required because busybox `date`
|
|
# drops %N without erroring.
|
|
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, 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")" .
|
|
}
|
|
|
|
main "$@"
|