name: Sprig image # Builds and publishes the public agent container image as # ghcr.io/block/buzz-sprig — the digest-pinned box the Kubernetes backend # deploys agents into (see Dockerfile.sprig and docs/remote-agents.md). # # Strategy mirrors docker.yml (the relay image): each architecture builds on # its native runner, pushes to GHCR by digest, then a merge job stitches the # per-arch digests into one multi-arch manifest and attests provenance. # No QEMU emulation. # # Triggers: # - push to main (paths-filtered) → :main + :sha-<7> # - tag sprig-v* → semver family (shared with sprig.yml's # binary release — one tag versions both) # - pull_request (paths-filtered) → build only, no push # - workflow_dispatch → manual publish at the current ref # # NOTE: the first push creates the GHCR package PRIVATE by default. An org # admin must flip ghcr.io/block/buzz-sprig to public once (Package settings → # Change visibility). Subsequent pushes keep the visibility. on: push: branches: [main] tags: ["sprig-v[0-9]*"] paths: - "Dockerfile.sprig" - "scripts/sprig-entrypoint.sh" - ".github/workflows/sprig-image.yml" - "Cargo.toml" - "Cargo.lock" - "rust-toolchain.toml" - "crates/**" pull_request: paths: - "Dockerfile.sprig" - "scripts/sprig-entrypoint.sh" - ".github/workflows/sprig-image.yml" workflow_dispatch: {} concurrency: group: sprig-image-${{ github.ref }} cancel-in-progress: ${{ github.ref_type == 'branch' && github.event_name == 'pull_request' }} permissions: {} env: # Single source of truth for the image name; override with the # GHCR_SPRIG_IMAGE repo variable (same pattern as docker.yml). IMAGE_NAME: ${{ vars.GHCR_SPRIG_IMAGE != '' && vars.GHCR_SPRIG_IMAGE || 'ghcr.io/block/buzz-sprig' }} jobs: build: name: Build (${{ matrix.platform }}) runs-on: ${{ matrix.runner }} timeout-minutes: 60 permissions: contents: read packages: write id-token: write attestations: write strategy: fail-fast: false matrix: include: - platform: linux/amd64 runner: ubuntu-24.04 arch: amd64 - platform: linux/arm64 runner: ubuntu-24.04-arm arch: arm64 steps: - name: Checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - name: Set up Docker Buildx uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 with: # Same OOM cap as docker.yml — Rust compiles blow the 7GB runner # at buildkit's default parallelism of 4. buildkitd-config-inline: | [worker.oci] max-parallelism = 2 - name: Log in to GHCR if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata id: meta uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 with: images: ${{ env.IMAGE_NAME }} # match=^sprig-v(.*)$ strips the tag prefix for the semver parser, # exactly as docker.yml does for relay-v. :latest comes from # flavor.latest=auto — stable semver only, never main pushes. tags: | type=ref,event=branch type=sha,prefix=sha-,format=short type=semver,pattern={{version}},match=^sprig-v(.*)$ type=semver,pattern={{major}}.{{minor}},match=^sprig-v(.*)$ labels: | org.opencontainers.image.title=Buzz Sprig org.opencontainers.image.description=Agent runtime image for Buzz remote agents (buzz-acp multicall + git + curl) org.opencontainers.image.licenses=Apache-2.0 - name: Build and push by digest id: build uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: . file: ./Dockerfile.sprig platforms: ${{ matrix.platform }} labels: ${{ steps.meta.outputs.labels }} outputs: type=image,name=${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }} cache-from: | type=registry,ref=${{ env.IMAGE_NAME }}-buildcache:${{ matrix.arch }} cache-to: | ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && format('type=registry,ref={0}-buildcache:{1},mode=max,compression=zstd', env.IMAGE_NAME, matrix.arch) || '' }} - name: Export digest if: github.event_name != 'pull_request' env: DIGEST: ${{ steps.build.outputs.digest }} run: | mkdir -p /tmp/digests touch "/tmp/digests/${DIGEST#sha256:}" - name: Upload digest if: github.event_name != 'pull_request' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: sprig-digest-${{ matrix.arch }} path: /tmp/digests/* if-no-files-found: error retention-days: 1 merge: name: Merge multi-arch manifest if: github.event_name != 'pull_request' runs-on: ubuntu-24.04 needs: build timeout-minutes: 15 permissions: contents: read packages: write id-token: write attestations: write steps: - name: Download per-arch digests uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: path: /tmp/digests pattern: sprig-digest-* merge-multiple: true - name: Set up Docker Buildx uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 - name: Log in to GHCR uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata id: meta uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 with: images: ${{ env.IMAGE_NAME }} # Must mirror the build job's tag matrix exactly (see docker.yml). flavor: | latest=auto tags: | type=ref,event=branch type=sha,prefix=sha-,format=short type=semver,pattern={{version}},match=^sprig-v(.*)$ type=semver,pattern={{major}}.{{minor}},match=^sprig-v(.*)$ - name: Create and push manifest list id: manifest working-directory: /tmp/digests env: IMAGE_NAME: ${{ env.IMAGE_NAME }} META_TAGS: ${{ steps.meta.outputs.tags }} run: | set -euo pipefail tags=() while IFS= read -r tag; do [ -n "$tag" ] && tags+=("-t" "$tag") done <<< "$META_TAGS" digests=() for digest in *; do digests+=("${IMAGE_NAME}@sha256:${digest}") done docker buildx imagetools create "${tags[@]}" "${digests[@]}" first_tag=$(echo "$META_TAGS" | head -n1) merged_digest=$(docker buildx imagetools inspect "$first_tag" \ --format '{{json .Manifest}}' | jq -r '.digest') echo "digest=${merged_digest}" >> "$GITHUB_OUTPUT" - name: Attest provenance for the merged image # Verify with: gh attestation verify oci://ghcr.io/block/buzz-sprig: --owner block uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1 with: subject-name: ${{ env.IMAGE_NAME }} subject-digest: ${{ steps.manifest.outputs.digest }} push-to-registry: true - name: Summary env: IMAGE_NAME: ${{ env.IMAGE_NAME }} DIGEST: ${{ steps.manifest.outputs.digest }} run: | { echo "### Sprig image published" echo '```' echo "${IMAGE_NAME}@${DIGEST}" echo '```' } >> "$GITHUB_STEP_SUMMARY"