All checks were successful
check / check (push) Successful in 43s
`script/cibuild` was plain `docker build .`. The Dockerfile does
`COPY . .` and then `RUN make check`, and Docker invalidates `COPY . .`
only on a content change, so on a byte-identical tree the check layer
was reused and the suite never ran. The script's header comment claimed
that a successful build implies all checks pass, which was false
whenever the cache was warm. Reproduced on this branch's parent: a
second consecutive run returned success in 283 ms with
`#13 [builder 9/10] RUN make check` reported `CACHED`.
That matters more here than in a typical repo. DNS is never mocked in
this repository, so the suite queries live DNS and its outcome varies
with real-world conditions; caching the verdict of a non-deterministic
check replays a stale result in exactly the case where re-running is
most valuable. It is also the gate every PR is verified through.
Fix: declare `ARG CHECK_EPOCH` immediately above the check step and
expand it into the command, with `script/cibuild` passing a fresh
`$(date +%s%N)` per invocation. A build argument's value participates in
the cache key of later instructions in the stage even when they do not
reference it, so a fresh value busts this layer either way; the value is
expanded into the command deliberately, which makes the invalidation a
property of the command string itself rather than of how a given builder
treats unreferenced args, and surfaces the epoch in the build log as a
diagnostic. Placing the ARG here and no earlier keeps the pinned
toolchain installs and `go mod download` above the invalidation line, so
only the check and the steps after it re-run. The epoch is nanosecond
granular so that two concurrent invocations starting in the same second
cannot share a value.
A plain `docker build` without the argument caches as before; nothing
outside the CI entrypoint changes behaviour.
Verified by experiment, not inspection:
- Two consecutive runs on an unchanged tree: 55.2 s and 42.2 s, both
exit 0, with distinct epochs. The second run shows
`RUN echo "check epoch: ..." && make check` executing for 36.0 s and
216 passing tests across all eight packages, while `apk add`, both
pinned `go install` steps, `go mod download`, `COPY go.mod go.sum` and
`COPY . .` all report `CACHED`.
- Negative control: planted `internal/config/zz_negative_control_test.go`
calling `t.Fatal("NEGATIVE-CONTROL-115: planted failure, cache did not
serve this layer")`. The build failed in 24.7 s with exit 1, printing
that exact message and `--- FAIL: TestNegativeControlIssue115`, and the
check step exited with code 2. A cached layer cannot produce a failure
predicted in advance, so this establishes the suite ran. The file was
then removed, `git status` confirmed clean, and the tree built green
again in 48.1 s.
- Total build time 42-55 s against the policy's 5-minute ceiling.
- `make check` green. No pin touched: the `golang` and `alpine` sha256
digests, golangci-lint `c0d3ddc9`, and goimports `009367f5` are
unchanged, and `.golangci.yml` still hashes to `021cc83f4e6f...`.
59 lines
2.0 KiB
Docker
59 lines
2.0 KiB
Docker
# Build stage
|
|
# golang 1.25-alpine, 2026-02-28
|
|
FROM golang@sha256:f6751d823c26342f9506c03797d2527668d095b0a15f1862cddb4d927a7a4ced AS builder
|
|
|
|
RUN apk add --no-cache git make gcc musl-dev binutils-gold
|
|
|
|
# golangci-lint v2.12.2, 2026-08-07
|
|
RUN go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@c0d3ddc9cf3faa61a4e378e879ece580256d76e5
|
|
# goimports v0.42.0
|
|
RUN go install golang.org/x/tools/cmd/goimports@009367f5c17a8d4c45a961a3a509277190a9a6f0
|
|
|
|
WORKDIR /src
|
|
COPY go.mod go.sum ./
|
|
RUN go mod download
|
|
|
|
COPY . .
|
|
|
|
# Run all checks - build fails if any check fails.
|
|
#
|
|
# CHECK_EPOCH is a cache-busting build argument. Without it, an
|
|
# unchanged tree leaves this layer's cache key identical and Docker
|
|
# serves the previous verdict instead of re-running the suite, so the
|
|
# build reports a green it did not earn. A build argument's value
|
|
# participates in the cache key of later instructions in the stage even
|
|
# when they do not reference it, so a fresh value busts this layer
|
|
# either way. It is expanded into the command deliberately: that makes
|
|
# the invalidation a property of the command string itself rather than
|
|
# of how a given builder treats unreferenced args, and it surfaces the
|
|
# epoch in the build log as a diagnostic.
|
|
#
|
|
# Placing the ARG here and nowhere earlier keeps everything above it
|
|
# (toolchain install, go mod download) cached, so only the check and the
|
|
# steps after it re-run. script/cibuild passes a fresh value per run; a
|
|
# plain `docker build` without it caches as before.
|
|
ARG CHECK_EPOCH
|
|
RUN echo "check epoch: ${CHECK_EPOCH}" && make check
|
|
|
|
# Build the binary
|
|
RUN make build
|
|
|
|
# Runtime stage
|
|
# alpine 3.21, 2026-02-28
|
|
FROM alpine@sha256:c3f8e73fdb79deaebaa2037150150191b9dcbfba68b4a46d70103204c53f4709
|
|
|
|
RUN apk add --no-cache ca-certificates tzdata
|
|
|
|
WORKDIR /app
|
|
|
|
COPY --from=builder /src/bin/dnswatcher /app/dnswatcher
|
|
|
|
# Create data directory
|
|
RUN mkdir -p /var/lib/dnswatcher
|
|
|
|
ENV DNSWATCHER_DATA_DIR=/var/lib/dnswatcher
|
|
|
|
EXPOSE 8080
|
|
|
|
ENTRYPOINT ["/app/dnswatcher"]
|