Skip to content

ci(build): native amd64+arm64 runners + merge-job pattern#27

Draft
CybotTM wants to merge 1 commit into
mainfrom
feat/arm64-native-runners
Draft

ci(build): native amd64+arm64 runners + merge-job pattern#27
CybotTM wants to merge 1 commit into
mainfrom
feat/arm64-native-runners

Conversation

@CybotTM
Copy link
Copy Markdown
Member

@CybotTM CybotTM commented May 23, 2026

Draft — needs a CI run before non-draft; touches the security-sensitive attestation + cosign signing chain.

What

Splits the multi-arch build from one QEMU-emulated job per (track, composer) into two native-arch parallel jobs that build by digest, plus a merge job per cell that assembles the manifest list, attests, and signs it.

Before / After

Before After
Per-cell wall-time ~40 min (QEMU arm64 on amd64) ~12 min (two native parallel)
Daily build total ~40 min ~15 min
Per-cell build jobs 1 2 (one per arch)
Merge step implicit in build-push-action dedicated merge job
Where SLSA + cosign run per-cell, on the (only) per-build digest once per (track, composer), on the multi-arch manifest-list digest

Implementation

Build job (now per-arch)

  • runs-on: ${{ matrix.platform.runner }}ubuntu-latest / ubuntu-24.04-arm
  • QEMU step removed (native runners don't need it)
  • docker/build-push-action outputs type=image,push-by-digest=true,name-canonical=true — no tags pushed yet
  • Per-arch digest exported and uploaded as a workflow artifact named digests-<track>-<composer>-<arch>

Merge job (new)

  • needs: build, per (track, composer) matrix matching the build matrix
  • Downloads the per-arch digest artifacts (pattern: digests-<track>-<composer>-*)
  • Re-resolves the ref and re-computes the tag list (same logic as the build job — inlined; could be factored later if it duplicates more)
  • docker buildx imagetools create -t <tag1> -t <tag2> ... <image>@sha256:<amd64-digest> <image>@sha256:<arm64-digest>
  • Captures the manifest-list digest via docker buildx imagetools inspect <first-tag> --format '{{json .Manifest}}' | jq -r '.digest'
  • Runs actions/attest-build-provenance and cosign sign on the manifest-list digest

Sanity guard

Merge job aborts if fewer than 2 per-arch digests arrived (catches silent per-arch build failures that would otherwise publish a degenerate single-arch manifest under the multi-arch tag).

What stays identical for consumers

  • All published tags (8.5.0, 8.5.0-rolling, 8.5.0-YYYYMMDD, 8.5, 8, latest, rolling, sha-pinned-<sha>, sha-rolling-<sha>, branch tags) — same names, same multi-arch manifest behind them
  • SLSA attestation + cosign signature on the manifest-list digest — same verification commands (gh attestation verify, cosign verify)
  • The tag/rolling matrix exclude (symfony/dom-crawler audit advisory)
  • continue-on-error: rolling semantics
  • The PR-event gate (tag-track-only)

Verification

Once CI runs on this PR:

  • 2 per-arch build jobs for tag/pinned (PR events skip non-tag tracks)
  • Each by-digest build succeeds (push: false on PR; push-by-digest=true,push=true on main/schedule)
  • PR builds skip the merge job (if: github.event_name != 'pull_request')

After merge:

  • The next scheduled daily build runs the merge job on main, publishing the multi-arch manifest list with SLSA + cosign signatures on the manifest digest
  • Wall-time should drop from ~40 min to ~15 min for the daily build

Why draft

  • Touches the security-sensitive attestation + cosign chain — subject-digest now points at the manifest-list digest instead of the per-platform digest. The new behaviour matches docker's recommended multi-platform pattern (https://docs.docker.com/build/ci/github-actions/multi-platform/) but it's a real change in the verification surface for consumers
  • The merge job's tag computation duplicates the build job's logic; if we want to factor this out, that's a follow-up
  • arm64 matrix cell on PR events doubles the PR-event runner footprint; if that becomes a cost concern we can gate arm64 to non-PR events in the matrix-exclude

Resolves the "arm64 native runners" roadmap item that was in README before PR #22 removed it as in-progress work.

Splits the multi-arch build from one QEMU-emulated job per
(track, composer) into two native-arch parallel jobs that build by
digest, plus a merge job per cell that assembles the final manifest
list, attests, and signs it.

Before: ubuntu-latest with `setup-qemu-action` and
`platforms: linux/amd64,linux/arm64` → arm64 emulated → ~40 min wall
per cell. With 5 active matrix cells (3 tracks × 2 composer modes
minus tag/rolling exclude), the full daily build took ~40 min.

After: each (track, composer, platform) builds on its native runner —
amd64 on `ubuntu-latest`, arm64 on `ubuntu-24.04-arm` — both ~12 min
in parallel. Then a per-(track, composer) merge job joins the two
digests with `docker buildx imagetools create` and pushes the
manifest list under the canonical tags. Daily build now ~15 min wall.

Implementation:

- Build job (now per-arch):
  - `runs-on: ${{ matrix.platform.runner }}` — `ubuntu-latest` / `ubuntu-24.04-arm`
  - QEMU step removed (native)
  - `docker/build-push-action` outputs `type=image,push-by-digest=true,
    name-canonical=true` (no tags pushed yet)
  - Per-arch digest exported and uploaded as a workflow artifact
- Merge job (new):
  - `needs: build`, runs after both per-arch builds complete
  - Per (track, composer) matrix matching the build matrix
  - Downloads the per-arch digest artifacts
  - Re-resolves the ref and re-computes the tag list (same logic as
    the build job)
  - `docker buildx imagetools create -t <tag1> -t <tag2> ...
    <image>@sha256:<amd64> <image>@sha256:<arm64>`
  - Captures the manifest-list digest, then runs
    `attest-build-provenance` and `cosign sign` on THAT digest
  - Sanity check: aborts if fewer than 2 per-arch digests arrived
    (catches silent build-job arch failures that would otherwise
    publish a degenerate single-arch manifest)

What stays the same:
- The `tag/rolling` matrix exclude (symfony/dom-crawler audit advisory)
- `continue-on-error` for the rolling variants
- The PR-event gate that runs only the `tag` track for PR builds
  (PR builds now produce 2 parallel per-arch builds — both push-by-
  digest=false, so no registry writes happen on PR)
- All attestation + cosign outputs land under the manifest-list digest,
  identical observable behaviour for consumers

Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
@gemini-code-assist
Copy link
Copy Markdown

Note

Gemini is unable to generate a review for this pull request due to the file types involved not being currently supported.

@sonarqubecloud
Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant