From 7a170c261fef80785bf97e5014ab494558811ff0 Mon Sep 17 00:00:00 2001 From: Zohar Zilberman <28088+popen2@users.noreply.github.com> Date: Mon, 25 May 2026 19:14:45 +0300 Subject: [PATCH 1/2] ci: run image builds in parallel with tests, gate release on tests The image matrix was gated on the `test` job (clippy + cargo test), so the two native-arch builds couldn't start until linting and tests finished. Since the test job shares no compiled artifacts with the image builds (host gnu target vs. musl targets), that serialization adds the whole clippy+test duration to the critical path for no reuse benefit. Drop `needs: test` from the image job so tests and the per-arch builds run concurrently. To keep release correctness, the `merge` job (which creates the published multi-arch tag) now also depends on `test`, so a tagged image is never published when linting or tests fail. On a tag push the per-arch jobs may push untagged digests before tests finish, but those stay dangling and unreferenced unless `merge` runs. --- .github/workflows/release.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5d77a93..e10fe91 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,10 +49,14 @@ jobs: # One image build per architecture, on a native runner. We push by digest; # a final `merge` job stitches the two digests into a multi-arch manifest list. # Native arm64 runners avoid QEMU emulation, which dominated build time. + # + # This job does NOT depend on `test`: the test job compiles for the host gnu + # target while these build for musl, so they share no artifacts and gating + # would only serialize clippy+test ahead of the build for no reuse. Tests run + # concurrently instead; the `merge` job below re-gates on `test` so a tagged + # multi-arch image is never published when linting or tests fail. image: name: 🐳 Build (${{ matrix.platform }}) - needs: - - test runs-on: ${{ matrix.runner }} strategy: fail-fast: false @@ -132,6 +136,7 @@ jobs: if: github.event_name == 'push' needs: - image + - test runs-on: ubuntu-latest outputs: tag: ${{ steps.tag.outputs.tag }} From c4d30bf57df29e8e658238f491c0462e05dc5276 Mon Sep 17 00:00:00 2001 From: Zohar Zilberman <28088+popen2@users.noreply.github.com> Date: Mon, 25 May 2026 19:15:21 +0300 Subject: [PATCH 2/2] build: let cargo-chef deps land in the layer cache, not an ephemeral mount The builder stage mounted /build/target (plus the cargo git/registry dirs) as BuildKit `type=cache` mounts. BuildKit cache mounts are NOT part of the image layer and are not exported by `cache-to: type=gha`; setup-buildx-action spins up a fresh builder each CI run, so those mounts start empty every time. cargo-chef relies on the `cook` step's compiled deps being captured so a later source-only build can reuse them. Because the deps were written into a cache mount, they vanished between runs, and on the common case (source changed, deps unchanged) the cook layer was a layer-cache hit and got skipped without repopulating the empty mount -- so the final `cargo build` recompiled every dependency from scratch on each arch, every run. cargo-chef was effectively a no-op in CI. Drop the cache mounts so the cook step's output (compiled deps + downloaded crates) lands in the layer filesystem, where Docker's layer cache captures it and `cache-to: type=gha,mode=max` (scoped per arch) persists it across runs. Now a source-only change restores the cook layer from cache and only rebuilds the workspace crates. --- Dockerfile | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/Dockerfile b/Dockerfile index 17f2a19..dae7f46 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,8 @@ -# Multi-stage build using cargo-chef for dep-only layer caching and per-arch -# cache mounts so amd64 and arm64 don't fight over the same target dir. +# Multi-stage build using cargo-chef so dependency compilation lands in a +# dedicated layer that is only invalidated when the dependency graph +# (recipe.json) changes. That layer is what CI restores from the GHA layer +# cache (see `cache-to: type=gha,mode=max`, scoped per arch in the workflow), +# so source-only changes skip recompiling dependencies. # # Drives the workspace into a static musl binary so the runtime image (alpine- # based platzio/base) doesn't need a libc. Architecture is selected via Docker @@ -31,9 +34,14 @@ COPY . . RUN cargo chef prepare --recipe-path recipe.json # --------------------------------------------------------------------------- -# 3. builder — cook deps from the recipe, then build the workspace. The cooked -# deps live in a buildkit cache mount keyed by TARGETARCH, so each architecture -# keeps its own warm target dir across CI runs. +# 3. builder — cook deps from the recipe, then build the workspace. The cook +# step writes compiled deps (and downloaded crates) into the layer filesystem +# — deliberately NOT a buildkit cache mount, which would be excluded from the +# image layer and dropped between CI runs on fresh builders. Keeping them in +# the layer lets Docker's layer cache capture the cook step; the workflow +# exports it via `cache-to: type=gha,mode=max` scoped per arch. recipe.json +# only changes when the dep graph changes, so source-only edits restore the +# cook layer from cache and re-run just the final `cargo build`. # --------------------------------------------------------------------------- FROM chef AS builder ARG RELEASE_BUILD=1 @@ -49,10 +57,7 @@ RUN set -eux; \ rustup target add "${target}" COPY --from=planner /build/recipe.json recipe.json -RUN --mount=type=cache,id=platz-cargo-target-${TARGETARCH},target=/build/target,sharing=locked \ - --mount=type=cache,id=platz-cargo-git,target=/usr/local/cargo/git,sharing=locked \ - --mount=type=cache,id=platz-cargo-registry,target=/usr/local/cargo/registry,sharing=locked \ - set -eux; \ +RUN set -eux; \ target="$(cat /target.txt)"; \ if [ "${RELEASE_BUILD}" = "1" ]; then \ cargo chef cook --release --target "${target}" --recipe-path recipe.json; \ @@ -61,10 +66,7 @@ RUN --mount=type=cache,id=platz-cargo-target-${TARGETARCH},target=/build/target, fi COPY . . -RUN --mount=type=cache,id=platz-cargo-target-${TARGETARCH},target=/build/target,sharing=locked \ - --mount=type=cache,id=platz-cargo-git,target=/usr/local/cargo/git,sharing=locked \ - --mount=type=cache,id=platz-cargo-registry,target=/usr/local/cargo/registry,sharing=locked \ - set -eux; \ +RUN set -eux; \ target="$(cat /target.txt)"; \ if [ "${RELEASE_BUILD}" = "1" ]; then \ cargo build --release --target "${target}"; \