script/cibuild can report a green it did not earn: docker layer cache can skip make check entirely #101
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Fleet-wide defect, confirmed present in pixa. Verified against
mainat61f42e6.script/cibuildis a baredocker build .with no cache control:and the
Dockerfileruns the checks after aCOPY . .:Dockerfile:14COPY . .→:17RUN make fmt-check→:18RUN make lint(lint stage)Dockerfile:43COPY . .→:46RUN make test(build stage)On an unchanged tree, Docker serves those
RUNlayers from cache: the suite never executes, the linter never executes, and the build still exits 0. CI reports success without having checked anything.Observed on
dnswatcher: SUCCESS in 0.262 seconds with every layerCACHED; the same tree forced uncached took 64.3 s and produced a real pass. Filed upstream as prompts #26; tracked in dnswatcher as #115.The comment at the top of
script/cibuild— "a successful build implies a green repo" — is precisely the assumption that does not hold.Why this matters more than it looks
Every gating decision in this repo cites a CI green. If a green can be free, the whole review gate rests on an assumption that is only true when the runner happens to have a cold cache.
Current pixa runs appear to have been genuine (but by luck, not design)
Checked the two open PRs' CI durations rather than assuming:
check / check (push)on4f43725: "Successful in 1m42s"bdae9cb: "Successful in 2m43s"A fully cache-served build is sub-second (dnswatcher's 0.262 s). Minutes of wall-clock means the layers really ran. So the greens those PRs were gated on were earned — the Gitea runners evidently do not carry a warm layer cache between runs today. That is circumstance, not a guarantee: the day a runner gains a persistent cache, every subsequent green becomes meaningless and nothing would announce the change.
Definition of done
ARG CHECK_EPOCHdeclared immediately above each checkRUN, withscript/cibuildpassing--build-arg CHECK_EPOCH="$(date +%s)". This busts the cache for the check layers only, keeping the expensive dependency layers (apk add,go mod download) cached — important here, where the CGO/libvips toolchain install dominates build time.make fmt-checkandmake lintin the lint stage, andmake testin the build stage. Fixing only one leaves the hole open.script/cibuildtwice in a row on an unchanged tree and show that the check layers execute both times (wall-clock in the tens of seconds, not sub-second), while the dependency layers still reportCACHED.script/cibuildcomment, which currently asserts the very thing that was untrue.Coordination
Touches
script/cibuildandDockerfile.Dockerfileis modified by PR #54 (lint-stage image pin) — do this after #54 merges. Related: #58 (script/lintdoes not pin the linter version) is the same class of defect, a check that can pass without having really checked; worth resolving in the same sweep.Important addendum to the fix, from the upstream prompts #26 discussion — the obvious implementation does not actually close the hole.
An unset
ARGis an empty string, and an empty string is a stable cache key. So a Dockerfile carryingARG CHECK_EPOCHabove the checkRUNsteps is still fully cacheable when built by anything that does not pass--build-arg— including a baredocker build .typed by hand, or any tooling other thanscript/cibuild. The green would be free again, and now with a fix in place that looks like it is working.The fix therefore needs a guard that fails closed:
so a build that omits the build-arg fails loudly instead of silently skipping the check.
Adding to this issue's definition of done:
ARG CHECK_EPOCHis paired with a[ -n "$CHECK_EPOCH" ] || exit 1guard, so an unset value is an error rather than a cache-friendly empty string.docker build .with no--build-argfails, rather than passing quickly. Without that test the fix cannot be distinguished from the bug.docker buildinvocations (script/docker, the Gitea workflow) and decide deliberately which of them must run the checks.script/dockerbuilds the runtime image and arguably should not be forced through the check layers; say which way it goes and why.Scope decision
Deliberately not folding this into PR #54, despite the overlap in
Dockerfile. #54 is already a 144-finding conformance job carrying two open design decisions; adding a change to what CI means would enlarge an already-large PR and reopen review surface on something orthogonal. This stays its own commit-sized unit, to be done after #54 merges.