Skip to content

ci: share one build between the image check and the publish - #8

Merged
sajonaro merged 1 commit into
mainfrom
image-workflow-consolidate
Aug 24, 2026
Merged

ci: share one build between the image check and the publish#8
sajonaro merged 1 commit into
mainfrom
image-workflow-consolidate

Conversation

@sajonaro

Copy link
Copy Markdown
Contributor

Answers "why are there three workflows?" — by making the answer defensible.

The problem

image.yml and image-check.yml both built this image, and each held its own byte-identical copy of the architecture matrix:

- platform: linux/amd64
  runner: ubuntu-latest
  arch: amd64
- platform: linux/arm64
  runner: ubuntu-24.04-arm
  arch: arm64

That is the same shape as the COPY list in the Dockerfile that shipped a broken image in #5: a hand-kept list, in two places, that nothing forces to agree. Add an architecture to one and forget the other, and the check stops shadowing the publish — which is the one property that makes it a check.

The change

image-build.yml holds the matrix and the buildx invocation, and has no trigger of its own — workflow_call only, so it cannot run by itself. Its single input is push, which is the entire difference between the callers:

push output effect
false type=cacheonly every RUN executes, including the stage-2 smoke checks; nothing is exported
true type=image,push-by-digest=true each architecture pushed under no tag, digest recorded for the merge job

image.ymlimage-publish.yml. image and image-check side by side read as "the image one" and "some extra check", which hid that the real difference is publish versus verify.

The permission boundary got stronger, not weaker

I originally justified two files on the token — a PR-triggered job must never hold packages: write — and that was weaker than it sounded, since job-level permissions: can express the same thing in one file. Two things now make it structural:

  • A called workflow is capped by its caller's grant. image-check.yml calls with contents: read, so the shared build cannot push from a pull request no matter what that file asks for.
  • packages: write moved off the top of the publish workflow onto the two jobs that need it, so a job added later doesn't silently inherit the ability to publish.

Verification

check result
matrix defined in exactly one file image-build.yml:1, others 0
packages: write appears only in image-publish.yml ✅ (hits in image-check.yml are comments)
both callers reference the reusable workflow
stale workflows/image.yml references one, in the Dockerfile header — updated
type=cacheonly is a real exporter confirmed via Docker's exporters overview and docker/buildx#2583; the CLI reference page just doesn't enumerate it

The image-check run on this PR is the real test — it exercises the reusable workflow on both architectures, which is the changed machinery.

Note

Manual dispatch is now gh workflow run image-publish.yml --ref main. The old name will not resolve. Renaming also starts a fresh run history for the workflow; the old image entry keeps its past runs.

🤖 Generated with Claude Code

Two workflows built this image and held byte-identical copies of the
architecture matrix to do it. That is the same shape as the COPY list in
the Dockerfile that shipped a broken image: a hand-kept list, in two
places, that nothing forces to agree. Add an architecture to one and
forget the other and the check stops shadowing the publish, which is the
one property that makes it a check at all.

image-build.yml holds the matrix and the buildx invocation now, and has
no trigger of its own — `workflow_call` only. Its single input is `push`,
which is the entire difference between the two callers: false builds to
`type=cacheonly`, running every RUN in the Dockerfile including the
stage-2 smoke checks and exporting nothing; true pushes each architecture
by digest and records it for the merge job.

The permission boundary survives the sharing, and is stronger than
before. A called workflow is capped by its caller's grant, so
image-check.yml calling with `contents: read` cannot push whatever the
shared file asks for. `packages: write` moves off the top of the publish
workflow and onto the two jobs that need it, so a job added later does
not inherit the ability to publish by default.

image.yml becomes image-publish.yml. `image` and `image-check` side by
side read as "the image one" and "some extra check", which hid that the
real difference is publish versus verify. The Dockerfile's header
referenced the old path and now references the new one.

Dispatching by hand is `gh workflow run image-publish.yml --ref main`;
the old name will not resolve.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sajonaro
sajonaro merged commit b054efa into main Aug 24, 2026
3 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Aug 24, 2026
@sajonaro
sajonaro deleted the image-workflow-consolidate branch August 24, 2026 10:52
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant