mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
Qualify canonical relay images for staged delivery (#6781)
## Why Phase 1 of progressive delivery needs an explicit eligibility boundary before deployment automation can safely consume relay artifacts. Today the canonical Docker workflow can publish independently of the same-SHA CI result, the relay does not expose its source/build identity, and the Helm chart only renders mutable tags. The existing `ghcr.io/block/buzz-staging-dev` workflow in #6709 remains a manual pre-merge preview lane. It is deliberately not a canonical promotion input. ## What changed - Compile the full source SHA and GitHub Actions build identity into canonical relay binaries and expose it at `/_status`. - Gate creation of tagged multi-architecture manifests on the latest exact-SHA `CI` push run succeeding on `main` or `release`. - Preserve the signed SLSA provenance and add a signed, release-only deployment-eligibility predicate with source, build run, qualifying CI run, and exact compatible Buzz chart version. - Keep failed or pending builds untagged and without an eligibility predicate; preview-package artifacts are excluded by package and signer contract. - Add optional `image.digest` support to Buzz chart `0.1.8`, retaining the existing tag fallback. - Document verification and the intentional boundary between image/chart compatibility and backwards-compatible database migrations. ## Risk and rollout Canonical tag publication now waits for same-SHA CI and fails closed when the newest matching run fails. Per-architecture blobs may remain untagged after a failed qualification; they are not promotion inputs. Debug manifests are also CI-gated but do not receive the release eligibility predicate. There are no migration or schema-orchestration changes. The companion BuilderBot infrastructure PR will pin a separately verified canonical main digest and expose a read-only running-deployment inspection command. Signed eligibility is available only for artifacts published after this workflow lands. ## Verification - Blox: workflow contract fixtures, including stale-green rejection and predicate structure - Blox: `actionlint` for `ci.yml` and `docker.yml` - Blox: 46 Helm unit tests, chart lint, and production/quickstart render fixtures - Blox: relay status unit test, workspace Rust formatting, and workspace all-target clippy - Live evidence: exact-SHA CI selector chose successful run `32857345153` for source `9d1e4b257657f382d3111ce748f3da8d063b7671` - Existing canonical digest provenance verified with `gh attestation verify` ## References - #6709 — developer-preview precursor; intentionally separate from canonical eligibility - `docs/deployment-identity.md` *Generated with Codex* --------- Signed-off-by: Luke Tornquist <tornquist@squareup.com>
This commit is contained in:
151
.github/workflows/docker.yml
vendored
151
.github/workflows/docker.yml
vendored
@@ -49,6 +49,10 @@ on:
|
||||
- "Dockerfile.push-gateway"
|
||||
- ".dockerignore"
|
||||
- ".github/workflows/docker.yml"
|
||||
- "deploy/charts/buzz/Chart.yaml"
|
||||
- "scripts/create-deployment-eligibility-predicate.jq"
|
||||
- "scripts/select-qualified-ci-run.jq"
|
||||
- "scripts/test-relay-image-eligibility-workflow.sh"
|
||||
- "Cargo.toml"
|
||||
- "Cargo.lock"
|
||||
- "rust-toolchain.toml"
|
||||
@@ -172,6 +176,10 @@ jobs:
|
||||
target: runtime
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
BUZZ_SOURCE_SHA=${{ github.sha }}
|
||||
BUZZ_BUILD_ID=github-actions:${{ github.run_id }}:${{ github.run_attempt }}
|
||||
BUZZ_BUILD_URL=https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}
|
||||
# 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.
|
||||
@@ -190,6 +198,10 @@ jobs:
|
||||
target: runtime-debug
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
BUZZ_SOURCE_SHA=${{ github.sha }}
|
||||
BUZZ_BUILD_ID=github-actions:${{ github.run_id }}:${{ github.run_attempt }}
|
||||
BUZZ_BUILD_URL=https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}
|
||||
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 }}
|
||||
@@ -222,11 +234,86 @@ jobs:
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
qualify:
|
||||
name: Qualify relay image source
|
||||
if: github.event_name != 'pull_request'
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 70
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
outputs:
|
||||
source_sha: ${{ steps.qualify.outputs.source_sha }}
|
||||
ci_run_id: ${{ steps.qualify.outputs.ci_run_id }}
|
||||
ci_run_attempt: ${{ steps.qualify.outputs.ci_run_attempt }}
|
||||
ci_run_url: ${{ steps.qualify.outputs.ci_run_url }}
|
||||
chart_version: ${{ steps.qualify.outputs.chart_version }}
|
||||
steps:
|
||||
- name: Checkout source
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Require successful same-SHA CI run
|
||||
id: qualify
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
SOURCE_SHA: ${{ github.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [[ ! "$SOURCE_SHA" =~ ^[0-9a-f]{40}$ ]]; then
|
||||
echo "::error::Invalid source SHA: $SOURCE_SHA"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
deadline=$((SECONDS + 3900))
|
||||
while (( SECONDS < deadline )); do
|
||||
payload=$(gh api \
|
||||
"/repos/${GITHUB_REPOSITORY}/actions/workflows/ci.yml/runs?head_sha=${SOURCE_SHA}&event=push&per_page=100")
|
||||
successful_run=$(jq -c --arg source_sha "$SOURCE_SHA" \
|
||||
-f "$GITHUB_WORKSPACE/scripts/select-qualified-ci-run.jq" <<<"$payload")
|
||||
|
||||
if [[ -n "$successful_run" ]]; then
|
||||
ci_run_id=$(jq -r '.id' <<<"$successful_run")
|
||||
ci_run_attempt=$(jq -r '.run_attempt' <<<"$successful_run")
|
||||
ci_run_url=$(jq -r '.html_url' <<<"$successful_run")
|
||||
chart_version=$(awk '/^version:/ { print $2; exit }' deploy/charts/buzz/Chart.yaml)
|
||||
if [[ ! "$chart_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([+-][0-9A-Za-z.-]+)?$ ]]; then
|
||||
echo "::error::Invalid Buzz chart version: $chart_version"
|
||||
exit 1
|
||||
fi
|
||||
{
|
||||
echo "source_sha=$SOURCE_SHA"
|
||||
echo "ci_run_id=$ci_run_id"
|
||||
echo "ci_run_attempt=$ci_run_attempt"
|
||||
echo "ci_run_url=$ci_run_url"
|
||||
echo "chart_version=$chart_version"
|
||||
} >>"$GITHUB_OUTPUT"
|
||||
echo "Qualified source $SOURCE_SHA with CI run $ci_run_id (attempt $ci_run_attempt)."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
matching=$(jq --arg source_sha "$SOURCE_SHA" '[.workflow_runs[] | select(.head_sha == $source_sha) | select(.event == "push") | select(.head_branch == "main" or .head_branch == "release")] | length' <<<"$payload")
|
||||
pending=$(jq --arg source_sha "$SOURCE_SHA" '[.workflow_runs[] | select(.head_sha == $source_sha) | select(.event == "push") | select(.head_branch == "main" or .head_branch == "release") | select(.status != "completed")] | length' <<<"$payload")
|
||||
if (( matching > 0 && pending == 0 )); then
|
||||
echo "::error::No successful CI run exists for source $SOURCE_SHA"
|
||||
jq -r --arg source_sha "$SOURCE_SHA" '.workflow_runs[] | select(.head_sha == $source_sha) | "run=\(.id) attempt=\(.run_attempt) status=\(.status) conclusion=\(.conclusion)"' <<<"$payload"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Waiting for same-SHA CI qualification ($matching matching, $pending pending)..."
|
||||
sleep 30
|
||||
done
|
||||
|
||||
echo "::error::Timed out waiting for successful CI qualification of $SOURCE_SHA"
|
||||
exit 1
|
||||
|
||||
merge:
|
||||
name: Merge ${{ matrix.variant }} multi-arch manifest
|
||||
if: github.event_name != 'pull_request'
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build
|
||||
needs: [build, qualify]
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -286,6 +373,10 @@ jobs:
|
||||
env:
|
||||
IMAGE_NAME: ${{ env.IMAGE_NAME }}
|
||||
META_TAGS: ${{ steps.meta.outputs.tags }}
|
||||
SOURCE_SHA: ${{ needs.qualify.outputs.source_sha }}
|
||||
CI_RUN_ID: ${{ needs.qualify.outputs.ci_run_id }}
|
||||
CI_RUN_ATTEMPT: ${{ needs.qualify.outputs.ci_run_attempt }}
|
||||
CHART_VERSION: ${{ needs.qualify.outputs.chart_version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Build -t flags from the metadata-action output.
|
||||
@@ -300,7 +391,15 @@ jobs:
|
||||
digests+=("${IMAGE_NAME}@sha256:${digest}")
|
||||
done
|
||||
|
||||
docker buildx imagetools create "${tags[@]}" "${digests[@]}"
|
||||
annotations=(
|
||||
--annotation "index:org.opencontainers.image.revision=${SOURCE_SHA}"
|
||||
--annotation "index:xyz.block.buzz.build.id=github-actions:${GITHUB_RUN_ID}:${GITHUB_RUN_ATTEMPT}"
|
||||
--annotation "index:xyz.block.buzz.qualification.ci-run-id=${CI_RUN_ID}"
|
||||
--annotation "index:xyz.block.buzz.qualification.ci-run-attempt=${CI_RUN_ATTEMPT}"
|
||||
--annotation "index:xyz.block.buzz.qualification.ci-conclusion=success"
|
||||
--annotation "index:xyz.block.buzz.helm-chart.version=${CHART_VERSION}"
|
||||
)
|
||||
docker buildx imagetools create "${tags[@]}" "${annotations[@]}" "${digests[@]}"
|
||||
|
||||
# Capture the merged manifest digest for the attestation step.
|
||||
first_tag=$(echo "$META_TAGS" | head -n1)
|
||||
@@ -317,17 +416,58 @@ jobs:
|
||||
subject-digest: ${{ steps.manifest.outputs.digest }}
|
||||
push-to-registry: true
|
||||
|
||||
- name: Create deployment eligibility predicate
|
||||
if: matrix.variant == 'release'
|
||||
env:
|
||||
SOURCE_SHA: ${{ needs.qualify.outputs.source_sha }}
|
||||
CI_RUN_ID: ${{ needs.qualify.outputs.ci_run_id }}
|
||||
CI_RUN_ATTEMPT: ${{ needs.qualify.outputs.ci_run_attempt }}
|
||||
CI_RUN_URL: ${{ needs.qualify.outputs.ci_run_url }}
|
||||
CHART_VERSION: ${{ needs.qualify.outputs.chart_version }}
|
||||
run: |
|
||||
jq -n \
|
||||
--arg source_repository "$GITHUB_REPOSITORY" \
|
||||
--arg source_ref "$GITHUB_REF" \
|
||||
--arg source_sha "$SOURCE_SHA" \
|
||||
--arg build_workflow ".github/workflows/docker.yml" \
|
||||
--argjson build_run_id "$GITHUB_RUN_ID" \
|
||||
--argjson build_run_attempt "$GITHUB_RUN_ATTEMPT" \
|
||||
--arg build_run_url "https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/attempts/${GITHUB_RUN_ATTEMPT}" \
|
||||
--arg qualification_workflow ".github/workflows/ci.yml" \
|
||||
--argjson qualification_run_id "$CI_RUN_ID" \
|
||||
--argjson qualification_run_attempt "$CI_RUN_ATTEMPT" \
|
||||
--arg qualification_run_url "$CI_RUN_URL" \
|
||||
--arg chart_version "$CHART_VERSION" \
|
||||
-f "$GITHUB_WORKSPACE/scripts/create-deployment-eligibility-predicate.jq" \
|
||||
>/tmp/deployment-eligibility.json
|
||||
|
||||
- name: Attest deployment eligibility
|
||||
if: matrix.variant == 'release'
|
||||
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
|
||||
with:
|
||||
subject-name: ${{ env.IMAGE_NAME }}
|
||||
subject-digest: ${{ steps.manifest.outputs.digest }}
|
||||
predicate-type: https://buzz.block.xyz/attestations/deployment-eligibility/v1
|
||||
predicate-path: /tmp/deployment-eligibility.json
|
||||
push-to-registry: true
|
||||
|
||||
- name: Summary
|
||||
env:
|
||||
IMAGE_NAME: ${{ env.IMAGE_NAME }}
|
||||
VARIANT: ${{ matrix.variant }}
|
||||
MERGED_DIGEST: ${{ steps.manifest.outputs.digest }}
|
||||
META_TAGS: ${{ steps.meta.outputs.tags }}
|
||||
SOURCE_SHA: ${{ needs.qualify.outputs.source_sha }}
|
||||
CI_RUN_URL: ${{ needs.qualify.outputs.ci_run_url }}
|
||||
CHART_VERSION: ${{ needs.qualify.outputs.chart_version }}
|
||||
run: |
|
||||
{
|
||||
echo "### Published \`${IMAGE_NAME}\` (${VARIANT})"
|
||||
echo
|
||||
echo "**Digest:** \`${MERGED_DIGEST}\`"
|
||||
echo "**Source:** \`${SOURCE_SHA}\`"
|
||||
echo "**Compatible Buzz chart:** \`${CHART_VERSION}\`"
|
||||
echo "**Qualifying CI:** ${CI_RUN_URL} (success)"
|
||||
echo
|
||||
echo "**Tags:**"
|
||||
echo '```'
|
||||
@@ -338,6 +478,13 @@ jobs:
|
||||
echo '```'
|
||||
echo "gh attestation verify oci://${IMAGE_NAME}@${MERGED_DIGEST} --owner block"
|
||||
echo '```'
|
||||
if [[ "$VARIANT" == "release" ]]; then
|
||||
echo
|
||||
echo "Verify deployment eligibility:"
|
||||
echo '```'
|
||||
echo "gh attestation verify oci://${IMAGE_NAME}@${MERGED_DIGEST} --repo block/buzz --signer-workflow block/buzz/.github/workflows/docker.yml --predicate-type https://buzz.block.xyz/attestations/deployment-eligibility/v1 --source-digest ${SOURCE_SHA}"
|
||||
echo '```'
|
||||
fi
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
push-gateway-build:
|
||||
|
||||
Reference in New Issue
Block a user