From a77eb1694ce8fc140b8f582ca66040d401f1a651 Mon Sep 17 00:00:00 2001 From: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc Date: Thu, 11 Jun 2026 15:40:39 -0400 Subject: [PATCH 1/3] ci(docker): publish public ghcr.io/block/buzz image (native multi-arch) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the CAKE-shaped private-ECR Dockerfile with a platform-agnostic public image and adds the GitHub Actions pipeline that publishes it as `ghcr.io/block/buzz`. This is the keystone for the Buzz one-click deploy work — Railway templates, Helm charts, and the compose bundle all reference this tag. Dockerfile - BuildKit 1.7, cargo-chef (planner/cook/build), workspace-wide recipe cook then `-p buzz-relay --locked` final compile + `strip`. - Independent Node/pnpm web stage so a CSS change doesn't bust the Rust cache and vice versa. - Runtime is debian:bookworm-slim with `git` (relay shells out for hydrate/receive-pack/upload-pack) and a non-root system user (uid/gid 1000). `BUZZ_WEB_DIR=/srv/buzz/web`, `EXPOSE 3000 8080 9102`, `ENTRYPOINT ["/usr/local/bin/buzz-relay"]`. - Drops every CAKE/Envoy/socat env, the start script, and the `--platform=linux/amd64` pins; multi-arch is the workflow's job. - OCI labels including `org.opencontainers.image.source` so GHCR can auto-link the package to the repo and inherit its visibility. .dockerignore - Aggressive trim of `target/`, host `node_modules/`, host `web/dist/`, `desktop/`, `mobile/`, `.git/`, `.github/`, `.scratch/`, secrets, docs, and stale compose/prometheus files. Keeps the build context small and prevents host build artifacts from invalidating layer cache. .github/workflows/docker.yml - Per-arch matrix on native runners — `ubuntu-24.04` for amd64, `ubuntu-24.04-arm` for arm64 — pushing by digest, then a final job stitches the digests into a multi-arch manifest with `docker buildx imagetools create`. No QEMU; arm64 Rust compiles at native speed. - Tags: `:main` + `:sha-<7>` on every main push; `:latest` + full semver family (`{version}`, `{major}.{minor}`, `{major}`) on `v*.*.*`. PRs build only (no push), `workflow_dispatch` for manual canaries. - Registry-mode buildx cache, keyed per-arch (`ghcr.io/block/buzz-buildcache:{amd64,arm64}`); the only cache mode that survives across matrix jobs on different runners. - `actions/attest-build-provenance` on the merged manifest digest produces a Sigstore-signed SLSA in-toto attestation, verifiable via `gh attestation verify oci://ghcr.io/block/buzz: --owner block`. No cosign keypair to manage. - `buildkitd max-parallelism=2` to avoid OOMing the 7 GB runner during Rust compiles. All third-party actions SHA-pinned. `permissions: {}` at workflow top with narrow per-job grants. Verified: - `docker buildx build --check .` clean (no buildkit lint warnings). - `actionlint .github/workflows/docker.yml` clean. - Ports / env names cross-checked against `crates/buzz-relay/src/config.rs` (BUZZ_BIND_ADDR :3000, BUZZ_HEALTH_PORT 8080, BUZZ_METRICS_PORT 9102, BUZZ_WEB_DIR). - Binary name `buzz-relay` matches `crates/buzz-relay/Cargo.toml`. Out of scope (follow-ups): - `buzz-cli` image — separate PR. - README "Deploy" wiring — Dawn's PR after the other lanes merge. - Manifest signing with cosign keyless — provenance attestation already covers the supply-chain story; cosign signature is additive if we decide we want it. Co-authored-by: Tyler Longwell Signed-off-by: Tyler Longwell --- .dockerignore | 49 ++++++- .github/workflows/docker.yml | 250 +++++++++++++++++++++++++++++++++++ Dockerfile | 109 ++++++++++----- 3 files changed, 377 insertions(+), 31 deletions(-) create mode 100644 .github/workflows/docker.yml diff --git a/.dockerignore b/.dockerignore index 8ccc08e55..4f654f780 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,7 +1,54 @@ +# Build outputs target/ +**/target/ +desktop/src-tauri/target/ + +# Native non-relay UI builds (not part of the public image) desktop/ +mobile/ + +# Node artifacts (built inside the image; host outputs would invalidate cache) +node_modules/ +**/node_modules/ +web/dist/ + +# VCS, IDE, scratch .git/ +.github/ +.gitignore +.gitattributes +.vscode/ +.idea/ .scratch/ + +# Local environment / secrets — never want these in the build context +.env +.env.* +!.env.example +*.pem +*.key +secrets/ + +# OS junk +.DS_Store +Thumbs.db + +# CI artifacts and tooling that the image doesn't need +.cache/ +coverage/ +dist/ +playwright-report/ +test-results/ +bin/ +sidecars/ + +# Markdown / docs / examples — image doesn't ship docs *.md !README.md -.env* +docs/ +examples/ + +# Old Dockerfile noise (this Dockerfile is THE source of truth now) +docker-compose.yml +docker-compose.*.yml +prometheus.yml diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 000000000..6e9069b95 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,250 @@ +name: Docker image + +# Builds and publishes the public Buzz relay image as ghcr.io/block/buzz. +# +# Strategy: each architecture builds on its native runner (ubuntu-24.04 for +# amd64, ubuntu-24.04-arm for arm64), pushes to GHCR by digest, then a final +# job stitches the per-arch digests into a single multi-arch manifest. +# This avoids QEMU emulation (~10× slower for Rust) at zero cost on free +# GitHub-hosted runners. +# +# Triggers: +# - push to main → :main + :sha-<7> +# - push tags v*.*.* → :latest + :{version} + :{major}.{minor} + :{major} +# - pull_request → build only (no push), cache stays warm +# - workflow_dispatch → manual canary + +on: + push: + branches: [main] + tags: ["v[0-9]*"] + pull_request: + paths: + - "Dockerfile" + - ".dockerignore" + - ".github/workflows/docker.yml" + - "Cargo.toml" + - "Cargo.lock" + - "rust-toolchain.toml" + - "crates/**" + - "web/**" + - "package.json" + - "pnpm-lock.yaml" + - "pnpm-workspace.yaml" + - "patches/**" + workflow_dispatch: + +# One image build per ref; cancel superseded PR builds, but never cancel +# tag/main builds (publishing must not be aborted mid-flight). +concurrency: + group: docker-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref_type == 'branch' && github.event_name == 'pull_request' }} + +permissions: {} + +env: + # Single source of truth for the image name. Set GHCR_IMAGE as a repo + # variable to override (e.g., for forks that want to push to their own + # namespace without forking this file). + IMAGE_NAME: ${{ vars.GHCR_IMAGE != '' && vars.GHCR_IMAGE || 'ghcr.io/block/buzz' }} + +jobs: + build: + name: Build (${{ matrix.platform }}) + runs-on: ${{ matrix.runner }} + timeout-minutes: 60 + permissions: + contents: read + packages: write # push to GHCR + id-token: write # OIDC for build provenance attestation + 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 + + outputs: + # Used downstream by `merge` to stitch the manifest. + version: ${{ steps.meta.outputs.version }} + + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + persist-credentials: false + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + with: + # Default parallelism of 4 OOMs the 7GB GitHub runner during Rust + # compiles (see moby/buildkit#3969). Vaultwarden hit this; we will + # too without the cap. + buildkitd-config-inline: | + [worker.oci] + max-parallelism = 2 + + - name: Log in to GHCR + # Skip on pull_request from forks — no GHCR creds, build-only. + 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 }} + # Tag matrix — every main commit gets sha-<7>, releases get full + # semver family. Pull requests get nothing (push: false below). + tags: | + type=ref,event=branch + type=sha,prefix=sha-,format=short + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=raw,value=latest,enable={{is_default_branch}} + labels: | + org.opencontainers.image.title=Buzz + org.opencontainers.image.description=WebSocket relay server for the Buzz communications platform + 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 + platforms: ${{ matrix.platform }} + labels: ${{ steps.meta.outputs.labels }} + # Push by digest, not by tag — the merge job assembles the tags + # into one multi-arch manifest. This is what makes the native-arm + # matrix possible. + 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: | + type=registry,ref=${{ env.IMAGE_NAME }}-buildcache:${{ matrix.arch }},mode=max,compression=zstd + + - name: Export digest + if: github.event_name != 'pull_request' + run: | + mkdir -p /tmp/digests + digest='${{ steps.build.outputs.digest }}' + touch "/tmp/digests/${digest#sha256:}" + + - name: Upload digest + if: github.event_name != 'pull_request' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: digests-${{ 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 # push the merged manifest + id-token: write # OIDC for provenance attestation on the manifest + attestations: write + + steps: + - name: Download all per-arch digests + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + path: /tmp/digests + pattern: digests-* + 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 }} + tags: | + type=ref,event=branch + type=sha,prefix=sha-,format=short + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=raw,value=latest,enable={{is_default_branch}} + + - name: Create and push manifest list + id: manifest + working-directory: /tmp/digests + run: | + set -euo pipefail + # Build -t flags from the metadata-action output. + tags=() + while IFS= read -r tag; do + [ -n "$tag" ] && tags+=("-t" "$tag") + done <<< '${{ steps.meta.outputs.tags }}' + + # Build the digest refs from the per-arch artifacts. + digests=() + for digest in *; do + digests+=("${IMAGE_NAME}@sha256:${digest}") + done + + docker buildx imagetools create "${tags[@]}" "${digests[@]}" + + # Capture the merged manifest digest for the attestation step. + first_tag=$(echo '${{ steps.meta.outputs.tags }}' | head -n1) + merged_digest=$(docker buildx imagetools inspect "$first_tag" \ + --format '{{json .Manifest}}' | jq -r '.digest') + echo "digest=${merged_digest}" >> "$GITHUB_OUTPUT" + env: + IMAGE_NAME: ${{ env.IMAGE_NAME }} + + - name: Attest provenance for the merged image + # Sigstore-signed in-toto attestation, verifiable with: + # gh attestation verify oci://ghcr.io/block/buzz: --owner block + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 + with: + subject-name: ${{ env.IMAGE_NAME }} + subject-digest: ${{ steps.manifest.outputs.digest }} + push-to-registry: true + + - name: Summary + run: | + { + echo "### Published \`${IMAGE_NAME}\`" + echo + echo "**Digest:** \`${{ steps.manifest.outputs.digest }}\`" + echo + echo "**Tags:**" + echo '```' + echo '${{ steps.meta.outputs.tags }}' + echo '```' + echo + echo "Verify provenance:" + echo '```' + echo "gh attestation verify oci://${IMAGE_NAME}@${{ steps.manifest.outputs.digest }} --owner block" + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + env: + IMAGE_NAME: ${{ env.IMAGE_NAME }} diff --git a/Dockerfile b/Dockerfile index 29b8fb737..aec27dea2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,46 +1,95 @@ -# ── Build stage (Rust) ────────────────────────────────────── -# Hard-code --platform to prevent exec format error on ARM Macs. -FROM --platform=linux/amd64 rust:1.95-bookworm AS builder +# syntax=docker/dockerfile:1.7 +# +# Public Buzz relay image — published as ghcr.io/block/buzz:. +# +# Builds the `buzz-relay` binary (Rust 1.95) and the `buzz-web` static bundle +# (pnpm + vite), then assembles them into a small debian-slim runtime with +# `git` available (the relay shells out to git for repo hydrate / receive-pack +# / upload-pack — see crates/buzz-relay/src/api/git). +# +# Multi-arch is handled by running this same Dockerfile on native amd64 and +# native arm64 runners (see .github/workflows/docker.yml). The Dockerfile +# itself is platform-agnostic; do not add --platform pins. + +ARG RUST_VERSION=1.95 +ARG NODE_VERSION=24 +ARG DEBIAN_VERSION=bookworm + +# ─── Stage 1: cargo-chef base ─────────────────────────────────────────────── +FROM rust:${RUST_VERSION}-${DEBIAN_VERSION} AS chef +RUN cargo install cargo-chef --locked --version 0.1.71 WORKDIR /build + +# ─── Stage 2: plan dependency graph ───────────────────────────────────────── +# Only the manifests are needed to compute the recipe; this layer rebuilds +# only when Cargo.{toml,lock} or crate manifests change, not on every source +# edit. +FROM chef AS planner +COPY . . +RUN cargo chef prepare --recipe-path recipe.json + +# ─── Stage 3: cook dependencies, then build the binary ────────────────────── +FROM chef AS builder +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + build-essential \ + pkg-config \ + libssl-dev \ + ca-certificates \ + git \ + && rm -rf /var/lib/apt/lists/* +COPY --from=planner /build/recipe.json recipe.json +# Cook the full workspace recipe — relay deps include workspace siblings, so +# scoping to -p buzz-relay misses transitive deps and re-builds them later. +RUN cargo chef cook --release --recipe-path recipe.json COPY . . -RUN cargo build --release -p buzz-relay \ +RUN cargo build --release --locked -p buzz-relay --bin buzz-relay \ && strip target/release/buzz-relay -# ── Web build stage (Node/pnpm) ──────────────────────────── -FROM --platform=linux/amd64 node:24-bookworm-slim AS web-builder +# ─── Stage 4: web bundle (pnpm + vite) ────────────────────────────────────── +# Independent of the Rust layers so a CSS change doesn't bust Rust cache and +# vice versa. +FROM node:${NODE_VERSION}-${DEBIAN_VERSION}-slim AS web-builder WORKDIR /build +RUN corepack enable COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ +COPY patches/ patches/ +COPY web/package.json web/ +RUN pnpm install --frozen-lockfile --filter buzz-web COPY web/ web/ -RUN corepack enable && pnpm install --frozen-lockfile --filter buzz-web RUN pnpm -C web build -# ── Runtime stage ─────────────────────────────────────────── -FROM --platform=linux/amd64 debian:bookworm-slim +# ─── Stage 5: runtime ─────────────────────────────────────────────────────── +FROM debian:${DEBIAN_VERSION}-slim AS runtime -# CAKE: non-root UID 1000 (numeric, not username) -RUN groupadd -g 1000 buzz && useradd -u 1000 -g buzz -m buzz +# OCI annotations: required for GHCR to auto-link the image to this repo and +# inherit its visibility. org.opencontainers.image.source is the load-bearing +# one — without it GHCR keeps the image private even when the repo is public. +LABEL org.opencontainers.image.title="Buzz" \ + org.opencontainers.image.description="WebSocket relay server for the Buzz communications platform" \ + org.opencontainers.image.source="https://github.com/block/buzz" \ + org.opencontainers.image.url="https://github.com/block/buzz" \ + org.opencontainers.image.documentation="https://github.com/block/buzz#readme" \ + org.opencontainers.image.licenses="Apache-2.0" -# CAKE: writable dirs -RUN mkdir -p /cache /tmp && chown buzz:buzz /cache /tmp +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + git \ + && rm -rf /var/lib/apt/lists/* \ + && groupadd --system --gid 1000 buzz \ + && useradd --system --uid 1000 --gid 1000 --home-dir /var/lib/buzz \ + --create-home --shell /usr/sbin/nologin buzz -# git: relay shells out to `git` for hydrate/receive-pack/upload-pack (S3-backed repos) -# socat: Istio abstract→file socket bridge -RUN apt-get update && apt-get install -y --no-install-recommends \ - ca-certificates git socat && rm -rf /var/lib/apt/lists/* +COPY --from=builder /build/target/release/buzz-relay /usr/local/bin/buzz-relay +COPY --from=web-builder /build/web/dist /srv/buzz/web -COPY --from=builder /build/target/release/buzz-relay /code/buzz-relay -COPY --from=web-builder /build/web/dist /code/web -COPY script/start /code/start -RUN chmod +x /code/start +ENV BUZZ_WEB_DIR=/srv/buzz/web -ENV BUZZ_WEB_DIR="/code/web" +# 3000: app (WS + REST + web UI) · 8080: /_liveness, /_readiness · 9102: /metrics +EXPOSE 3000 8080 9102 -# CAKE: required Envoy env vars (overridden at runtime by CAKE). -ENV ENVOY_ADMIN_SOCKET_PATH="@envoy-admin.sock" \ - ENVOY_INGRESS_PORT="20001" \ - ENVOY_HTTP_EGRESS_SOCKET_PATH="@egress.sock" \ - ENVOY_DATADOG_PORT="3030" \ - CASH_FRAMEWORK="rust" +USER buzz:buzz +WORKDIR /var/lib/buzz -USER 1000 -ENTRYPOINT ["/code/start"] +ENTRYPOINT ["/usr/local/bin/buzz-relay"] From b32350b9d745b06a7eb66648a2e5ff2519315950 Mon Sep 17 00:00:00 2001 From: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc Date: Thu, 11 Jun 2026 15:49:16 -0400 Subject: [PATCH 2/3] fix(docker): keep examples/ in build context (cargo workspace member) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First CI run failed in the planner stage with: error: failed to load manifest for workspace member `/build/examples/countdown-bot` referenced by workspace at `/build/Cargo.toml` The .dockerignore was excluding examples/ along with markdown and docs, but examples/countdown-bot is a Cargo workspace member declared in the root Cargo.toml. cargo-chef's planner runs `cargo metadata`, which fails when any workspace member's manifest is missing. Fix: drop the `examples/` and blanket `*.md` exclusions. The savings were negligible (small directory; markdown files inside crates are referenced by Cargo.toml `readme = ...` and warn when missing). Keep `docs/` excluded — no workspace members live there. Verified by building the `planner` target locally to completion: `docker build --target planner --platform linux/amd64 .` succeeds. Co-authored-by: Tyler Longwell Signed-off-by: Tyler Longwell --- .dockerignore | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.dockerignore b/.dockerignore index 4f654f780..8ce14320c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -42,11 +42,11 @@ test-results/ bin/ sidecars/ -# Markdown / docs / examples — image doesn't ship docs -*.md -!README.md +# Docs the image doesn't ship. examples/ stays — examples/countdown-bot is +# a Cargo workspace member, so cargo-chef needs its manifest. Markdown files +# inside crates (Cargo.toml `readme = ...`) likewise need to be present to +# avoid manifest warnings; the savings from excluding them are negligible. docs/ -examples/ # Old Dockerfile noise (this Dockerfile is THE source of truth now) docker-compose.yml From 0f302d8db325bcca2a3ec80cd6de4eb45d5c70bc Mon Sep 17 00:00:00 2001 From: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc Date: Thu, 11 Jun 2026 16:03:04 -0400 Subject: [PATCH 3/3] fix(docker): pass GHA step outputs through env: to satisfy Semgrep Semgrep OSS flagged three "Insecure GitHub Actions: Shell Injection via GitHub Context Variables" findings on the workflow. Even though the referenced contexts (`steps.build.outputs.digest`, `steps.meta.outputs.tags`, `steps.manifest.outputs.digest`) are not user-controllable, Semgrep's rule blocks the pattern uniformly. The defensive form is to pass each context into the step `env:` block and reference it as a regular shell variable inside `run:`, which gives the shell quoting semantics rather than YAML-time interpolation. No behavioral change. Same digests, same tags, same output. Diff is mechanical: lift `${{ steps.* }}` into per-step `env:` and reference as `"$VAR"` inside `run:`. Co-authored-by: Tyler Longwell Signed-off-by: Tyler Longwell --- .github/workflows/docker.yml | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 6e9069b95..948b86920 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -136,10 +136,11 @@ jobs: - name: Export digest if: github.event_name != 'pull_request' + env: + DIGEST: ${{ steps.build.outputs.digest }} run: | mkdir -p /tmp/digests - digest='${{ steps.build.outputs.digest }}' - touch "/tmp/digests/${digest#sha256:}" + touch "/tmp/digests/${DIGEST#sha256:}" - name: Upload digest if: github.event_name != 'pull_request' @@ -196,13 +197,16 @@ jobs: - 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 # Build -t flags from the metadata-action output. tags=() while IFS= read -r tag; do [ -n "$tag" ] && tags+=("-t" "$tag") - done <<< '${{ steps.meta.outputs.tags }}' + done <<< "$META_TAGS" # Build the digest refs from the per-arch artifacts. digests=() @@ -213,12 +217,10 @@ jobs: docker buildx imagetools create "${tags[@]}" "${digests[@]}" # Capture the merged manifest digest for the attestation step. - first_tag=$(echo '${{ steps.meta.outputs.tags }}' | head -n1) + 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" - env: - IMAGE_NAME: ${{ env.IMAGE_NAME }} - name: Attest provenance for the merged image # Sigstore-signed in-toto attestation, verifiable with: @@ -230,21 +232,23 @@ jobs: push-to-registry: true - name: Summary + env: + IMAGE_NAME: ${{ env.IMAGE_NAME }} + MERGED_DIGEST: ${{ steps.manifest.outputs.digest }} + META_TAGS: ${{ steps.meta.outputs.tags }} run: | { echo "### Published \`${IMAGE_NAME}\`" echo - echo "**Digest:** \`${{ steps.manifest.outputs.digest }}\`" + echo "**Digest:** \`${MERGED_DIGEST}\`" echo echo "**Tags:**" echo '```' - echo '${{ steps.meta.outputs.tags }}' + echo "${META_TAGS}" echo '```' echo echo "Verify provenance:" echo '```' - echo "gh attestation verify oci://${IMAGE_NAME}@${{ steps.manifest.outputs.digest }} --owner block" + echo "gh attestation verify oci://${IMAGE_NAME}@${MERGED_DIGEST} --owner block" echo '```' } >> "$GITHUB_STEP_SUMMARY" - env: - IMAGE_NAME: ${{ env.IMAGE_NAME }}