Compare commits
1 Commits
24f6e2f9ef
...
09dbe6f4c9
| Author | SHA1 | Date | |
|---|---|---|---|
| 09dbe6f4c9 |
40
Dockerfile
40
Dockerfile
@@ -21,14 +21,31 @@ COPY . .
|
|||||||
|
|
||||||
# Run formatting check and linter.
|
# Run formatting check and linter.
|
||||||
#
|
#
|
||||||
# CHECK_EPOCH must stay immediately above these RUNs. script/cibuild
|
# CHECK_EPOCH must stay immediately above these RUNs. These layers are
|
||||||
# passes a fresh value on every build so the check layers can never be
|
# keyed on its value, so they are cache-eligible only for a value
|
||||||
# served from the layer cache: without it an unchanged tree replays
|
# already built against this same tree. script/cibuild passes a fresh
|
||||||
# cached layers, the checks never execute, and the build still exits 0:
|
# value on every invocation, which is what makes its green mean the
|
||||||
# a green nothing earned. ARG scope is per-stage, so the builder
|
# checks really executed.
|
||||||
# stage declares its own. Everything above this line (apk, go.mod,
|
#
|
||||||
# `go mod download`) is deliberately outside the busted range and keeps
|
# The guarantee is conditional on that fresh value, not absolute. A
|
||||||
# caching.
|
# build that omits --build-arg -- a bare `docker build .` -- gets an
|
||||||
|
# empty CHECK_EPOCH, and an empty string is a constant: the first such
|
||||||
|
# build runs the checks, and every one after it on an unchanged tree
|
||||||
|
# replays these layers from cache, never executing a check and still
|
||||||
|
# exiting 0, a green nothing earned. Gate through script/cibuild.
|
||||||
|
# Making the missing-arg case fail loudly instead is tracked in #91.
|
||||||
|
#
|
||||||
|
# CHECK_EPOCH is deliberately not referenced by the commands below: a
|
||||||
|
# declared-but-unreferenced ARG does enter BuildKit's cache key, which
|
||||||
|
# is measured on this host rather than assumed (PR #89). Upstream
|
||||||
|
# sneak/prompts #26 prefers expanding the value into the command so
|
||||||
|
# that the miss is contractual rather than dependent on that behavior
|
||||||
|
# staying as it is; adopting that here is tracked in #91. Do not delete
|
||||||
|
# this ARG as dead code -- the gate depends on it.
|
||||||
|
#
|
||||||
|
# ARG scope is per-stage, so the builder stage declares its own.
|
||||||
|
# Everything above this line (apk, go.mod, `go mod download`) is
|
||||||
|
# deliberately outside the busted range and keeps caching.
|
||||||
ARG CHECK_EPOCH
|
ARG CHECK_EPOCH
|
||||||
RUN make fmt-check
|
RUN make fmt-check
|
||||||
RUN make lint
|
RUN make lint
|
||||||
@@ -54,9 +71,10 @@ RUN go mod download
|
|||||||
# Copy source code
|
# Copy source code
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Run tests. See the CHECK_EPOCH comment in the lint stage; ARG scope
|
# Run tests. See the CHECK_EPOCH comment in the lint stage, including
|
||||||
# is per-stage, so this stage needs its own declaration, and it must
|
# the conditions the guarantee depends on; ARG scope is per-stage, so
|
||||||
# stay immediately above the check RUN.
|
# this stage needs its own declaration, and it must stay immediately
|
||||||
|
# above the check RUN.
|
||||||
ARG CHECK_EPOCH
|
ARG CHECK_EPOCH
|
||||||
RUN make test
|
RUN make test
|
||||||
|
|
||||||
|
|||||||
24
README.md
24
README.md
@@ -617,16 +617,24 @@ them. We provide:
|
|||||||
lint findings.
|
lint findings.
|
||||||
* `script/docker` — build the Docker image tagged via
|
* `script/docker` — build the Docker image tagged via
|
||||||
`script/projectname`
|
`script/projectname`
|
||||||
* `script/cibuild` — CI entrypoint: `docker build` (the Dockerfile runs
|
* `script/cibuild` — CI entrypoint: `docker build` (the `Dockerfile`
|
||||||
the checks). This is the full CI-equivalent gate — it runs the checks
|
runs `make fmt-check` and `make lint` in its lint stage and `make
|
||||||
in the same containers CI does, from a clean copy of the tree, so it
|
test` in its builder stage). This is the full CI-equivalent gate — it
|
||||||
also catches anything that depends on host state. It passes a fresh
|
runs the checks in the same containers CI does, from a clean copy of
|
||||||
`--build-arg CHECK_EPOCH`, which the `Dockerfile` declares
|
the tree, so it also catches anything that depends on host state. It
|
||||||
immediately above the check `RUN`s in both the lint and builder
|
passes a fresh `--build-arg CHECK_EPOCH`, which the `Dockerfile`
|
||||||
stages, so those layers can never be served from the Docker layer
|
declares immediately above the check `RUN`s in both stages. Those
|
||||||
cache: a green from this script always means the checks actually
|
layers are keyed on that value, so a new value re-runs them even on a
|
||||||
|
byte-identical tree, and a green from this script means the checks
|
||||||
executed. Dependency and module layers sit above the `ARG` and still
|
executed. Dependency and module layers sit above the `ARG` and still
|
||||||
cache, so a build is not cold.
|
cache, so a build is not cold.
|
||||||
|
|
||||||
|
That guarantee is conditional on the fresh value, so **run the gate
|
||||||
|
through `script/cibuild`, not by invoking `docker build` yourself**. A
|
||||||
|
bare `docker build .` supplies no `CHECK_EPOCH`; the empty default is
|
||||||
|
a constant, so the second and every later build on an unchanged tree
|
||||||
|
serves all three check layers from cache, executes nothing, and still
|
||||||
|
exits 0. Issue #91 tracks making that case fail loudly instead.
|
||||||
* `script/precommit` — pre-commit gate: `go mod tidy` + `go fmt` (must
|
* `script/precommit` — pre-commit gate: `go mod tidy` + `go fmt` (must
|
||||||
not change files), then `script/check`
|
not change files), then `script/check`
|
||||||
* `script/install-precommit` — install the git pre-commit hook that
|
* `script/install-precommit` — install the git pre-commit hook that
|
||||||
|
|||||||
40
TODO.md
40
TODO.md
@@ -22,25 +22,27 @@ or delete the branch.
|
|||||||
- 2026-08-09: Stopped `script/cibuild` from reporting a green it did
|
- 2026-08-09: Stopped `script/cibuild` from reporting a green it did
|
||||||
not earn (issue #85). A bare `docker build .` let Docker serve the
|
not earn (issue #85). A bare `docker build .` let Docker serve the
|
||||||
check layers from the layer cache whenever the tree had not changed:
|
check layers from the layer cache whenever the tree had not changed:
|
||||||
the checks never executed and the build still exited 0. Reproduced on
|
the checks never executed and the build still exited 0. The fix is an
|
||||||
this branch's base — a genuine changed-tree run took 162s with 14
|
`ARG CHECK_EPOCH` declared immediately above the check `RUN`s in both
|
||||||
`ok` lines, and the immediately following unchanged-tree run took
|
the lint stage and the builder stage (`ARG` scope is per-stage, so
|
||||||
221ms with 0 `ok` lines, 19 cached layers, and the same exit 0, with
|
each declares its own), with `script/cibuild` assigning
|
||||||
`RUN make fmt-check`, `RUN make lint`, and `RUN make test` all
|
`epoch="$(date +%s)"` and passing `--build-arg CHECK_EPOCH="$epoch"`.
|
||||||
reported `CACHED`. The fix matches the upstream one in
|
The assignment is separate on purpose: under `set -eu` a command
|
||||||
`sneak/prompts` #26: an `ARG CHECK_EPOCH` declared immediately above
|
substitution that fails inside an argument does not abort the script,
|
||||||
the check `RUN`s in both the lint stage and the builder stage (`ARG`
|
which would leave an empty constant `CHECK_EPOCH` and restore the
|
||||||
scope is per-stage, so each declares its own), with `script/cibuild`
|
very false green being fixed. Placement is the rest of the point —
|
||||||
passing `--build-arg CHECK_EPOCH="$(date +%s)"`. Placement is the
|
the `ARG` sits below the `apk add`, `COPY go.mod go.sum`, and `go mod
|
||||||
whole point — the `ARG` sits below the `apk add`, `COPY go.mod
|
download` layers, so only the checks are invalidated and the
|
||||||
go.sum`, and `go mod download` layers, so only the checks are
|
dependency layers still cache. The guarantee is conditional on a
|
||||||
invalidated and the dependency layers still cache. Verified by
|
fresh value rather than absolute: a bare `docker build .` gets an
|
||||||
re-running the reproduction: two back-to-back runs on an unchanged
|
empty `CHECK_EPOCH` and can still serve the check layers from cache,
|
||||||
tree took 167s and 174s, each with 14 `ok` lines and `0 issues.`, no
|
which `README.md` and the `Dockerfile` now say plainly, with issue
|
||||||
`CACHED` on any of the three check layers, while every `apk add` and
|
#91 tracking the upstream hardening (expanded `ARG` form, unset
|
||||||
`go mod download` layer stayed `CACHED` in both. A changed-tree build
|
guard, per-invocation epoch, `script/docker`) that would close it.
|
||||||
went from 162s to 176s, so this is not a cold build. `.golangci.yml`,
|
Verified by re-running the reproduction plus the withheld-`--build-arg`
|
||||||
the lint-stage `FROM` line and its digest, `script/lint`, and
|
counterfactual; the measurements are recorded once, in the PR #89
|
||||||
|
verification comment, rather than restated here. `.golangci.yml`, the
|
||||||
|
lint-stage `FROM` line and its digest, `script/lint`, and
|
||||||
`.gitea/workflows/check.yml` are all untouched.
|
`.gitea/workflows/check.yml` are all untouched.
|
||||||
- 2026-08-09: Corrected the `Vaultik.UI` doc comment (issue #84). It
|
- 2026-08-09: Corrected the `Vaultik.UI` doc comment (issue #84). It
|
||||||
claimed the cli layer replaces the writer with a discarding one in
|
claimed the cli layer replaces the writer with a discarding one in
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# script/cibuild: run the CI build. The Dockerfile runs script/check
|
# script/cibuild: run the CI build. The Dockerfile does not run
|
||||||
# (via make check), so a successful build implies all checks pass.
|
# script/check; it runs `make fmt-check` and `make lint` in its lint
|
||||||
|
# stage and `make test` in its builder stage. A successful build
|
||||||
|
# implies those three passed, provided they actually ran -- which is
|
||||||
|
# what the CHECK_EPOCH below is for.
|
||||||
# Generic: needs no adaptation. The Gitea workflow runs this on push.
|
# Generic: needs no adaptation. The Gitea workflow runs this on push.
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
@@ -8,12 +11,22 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
|||||||
|
|
||||||
main() {
|
main() {
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
# CHECK_EPOCH changes on every invocation, which invalidates the
|
# The Dockerfile's check layers are keyed on CHECK_EPOCH, so a
|
||||||
# Dockerfile layers that run the checks. Without it an unchanged
|
# fresh value here is what forces them to re-run: without it an
|
||||||
# tree replays those layers from cache, the checks never execute,
|
# unchanged tree replays them from cache, the checks never execute,
|
||||||
# and the build still exits 0. The ARG sits immediately above the
|
# and the build still exits 0. The ARG sits immediately above the
|
||||||
# check RUNs, so dependency and module layers still cache.
|
# check RUNs, so dependency and module layers still cache.
|
||||||
docker build --build-arg CHECK_EPOCH="$(date +%s)" .
|
#
|
||||||
|
# Assign the epoch on its own line rather than inline in the
|
||||||
|
# argument. Under `set -eu` a command substitution that fails
|
||||||
|
# inside an argument does NOT abort the script: CHECK_EPOCH would
|
||||||
|
# become an empty string, an empty string is a constant, and a
|
||||||
|
# constant CHECK_EPOCH is exactly the cached-check false green this
|
||||||
|
# script exists to prevent -- so the guard would disarm itself and
|
||||||
|
# still exit 0. As a bare assignment, `set -e` catches a failing
|
||||||
|
# `date` and no build starts.
|
||||||
|
epoch="$(date +%s)"
|
||||||
|
docker build --build-arg CHECK_EPOCH="$epoch" .
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|||||||
Reference in New Issue
Block a user