From 0116419121c78ef413a0abe1a925977ccf5821fc Mon Sep 17 00:00:00 2001 From: Scott Robinson Date: Tue, 3 Nov 2026 05:46:14 +0000 Subject: [PATCH] ci(release-image): build the release image without the gha cache The release image build read and wrote the GitHub Actions build cache (cache-from/cache-to type=gha). The image that step produces is the one the workflow scans, pushes and signs, so any layer restored from the cache would ship in a signed release. 8a792cf1 removed the same cache from release-dev-image.yml, citing CodeQL's actions/cache-poisoning/poisonable-step query; release-image.yml kept it. The cache did little for release builds. The cargo build layer's inputs change with every release, so it was restored only when the same release was built again: the re-dispatches of v0.1.6 (run 32170242786) and v0.1.8 (run 32519595682), the re-run of v0.1.11 (run 34395238814, attempt 2, arm64) and, judging by its 76-85 s build times, the re-run of v0.1.6 (run 32124353804, attempt 2, whose logs are gone). In one other build (mongodb v0.1.13, amd64) the WORKDIR, apt, both COPY and first two runtime layers came from the postgres build of the same tag, because the cache scope was per architecture only. Exporting the cache took 52 s to 150 s per uncached build. The step now has no cache-from or cache-to, with a comment saying why. This removes one way to recover a failed release. With the cache, a new dispatch of the same tag rebuilt the same image IDs (the v0.1.8 re-dispatch did), so the publish-candidate check would accept a candidate an earlier run had already pushed. Without it, a new dispatch builds different IDs and that check refuses to overwrite the candidate. The comment on the check said a failed run could be finished by re-dispatch; it now says to use "Re-run failed jobs" on the failed run while its image artifacts are kept (one day), and points to sign-image.yml once they have expired. The comment on the commit-derived build date no longer says a re-run reproduces the same image config. Signed-off-by: Scott Robinson --- .github/workflows/release-image.yml | 34 +++++++++++++++++++---------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/.github/workflows/release-image.yml b/.github/workflows/release-image.yml index 131c48d46..40e18dee9 100644 --- a/.github/workflows/release-image.yml +++ b/.github/workflows/release-image.yml @@ -175,8 +175,8 @@ jobs: fi # 5. Deterministic metadata: the build date is the tagged commit's - # own timestamp, so re-running the same release reproduces the - # same image config instead of minting a new digest per run. + # own timestamp, so the image label and the binary's build time + # are the same in every run of the release. BUILD_DATE=$(TZ=UTC git show -s --date=format-local:'%Y-%m-%dT%H:%M:%SZ' --format=%cd "$SHA") { @@ -287,8 +287,14 @@ jobs: VERSION=${{ needs.gate.outputs.version }} VCS_REF=${{ needs.gate.outputs.sha }} BUILD_DATE=${{ needs.gate.outputs.build_date }} - cache-from: type=gha,scope=${{ matrix.arch }} - cache-to: type=gha,mode=max,scope=${{ matrix.arch }} + # No gha build cache, as in release-dev-image.yml. The image built + # here is the one that is scanned, pushed and signed, so a layer + # restored from the Actions cache would ship in a signed release + # (actions/cache-poisoning/poisonable-step). The cache did little + # for release builds: the cargo build layer's inputs change with + # every release, so it was restored only when the same release was + # built a second time, and exporting the cache took up to two and + # a half minutes per build. - name: Confirm the image really is ${{ matrix.arch }} # Cheap guard against a silent platform fallback: a mislabelled image @@ -415,13 +421,19 @@ jobs: CANDIDATE="sha-${SHA}" # Idempotent no-overwrite: if the candidate tag exists and holds - # EXACTLY what this run built, fall through to sign and propagate - # (a failed earlier run must be finishable by re-dispatch, or the - # failure-prone tail strands a published-but-unsigned candidate no - # re-dispatch can fix). Identity is compared by per-arch image - # config digest — the image ID, which docker save/load preserves - # and the smoke test certified — with NO registry mutation: a - # different artifact errors before anything is pushed. + # EXACTLY what this run built, fall through to sign and propagate, + # so a run that failed after pushing can still be finished. + # Identity is compared by per-arch image config digest (the image + # ID, which docker save/load preserves and the smoke test + # certified) with NO registry mutation: a different artifact + # errors before anything is pushed. The build has no layer cache, + # so a new dispatch builds images with different IDs and, if the + # earlier run pushed the candidate, stops here. To finish a failed + # run, use "Re-run failed jobs" on that run while its image + # artifacts are kept (retention-days: 1 above); this job then + # reloads the images it pushed and they match. Once the artifacts + # have expired the run cannot be finished this way; sign-image.yml + # covers signing a candidate that was pushed but not signed. if EXISTING=$(docker buildx imagetools inspect "${REPO}:${CANDIDATE}" --format '{{.Manifest.Digest}}' 2>/dev/null); then RAW=$(skopeo inspect --no-creds --raw "docker://${REPO}@${EXISTING}") MATCH=true