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:
Luke Tornquist
2026-08-25 14:44:49 -04:00
committed by GitHub
parent 7ba1197aa6
commit f249710331
17 changed files with 467 additions and 10 deletions

View File

@@ -75,6 +75,8 @@ jobs:
- '.github/workflows/ci.yml'
- name: Release workflow source contract
run: scripts/test-release-ref-contract.sh
- name: Relay image eligibility contract
run: scripts/test-relay-image-eligibility-workflow.sh
- name: Desktop release candidate contract
run: scripts/test-desktop-release-candidate.sh
- name: OSS desktop promotion contract

View File

@@ -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:

View File

@@ -67,6 +67,14 @@ COPY --from=planner /build/recipe.json recipe.json
# scoping to -p buzz-relay misses transitive deps and re-builds them later.
RUN cargo chef cook --release --recipe-path recipe.json
COPY . .
# Compile immutable artifact identity into the relay. Defaults preserve local
# and third-party builds that do not run in provenance-aware CI.
ARG BUZZ_SOURCE_SHA=unknown
ARG BUZZ_BUILD_ID=local
ARG BUZZ_BUILD_URL=unknown
ENV BUZZ_SOURCE_SHA=${BUZZ_SOURCE_SHA} \
BUZZ_BUILD_ID=${BUZZ_BUILD_ID} \
BUZZ_BUILD_URL=${BUZZ_BUILD_URL}
RUN cargo build --release --locked -p buzz-relay --bin buzz-relay \
-p buzz-admin --bin buzz-admin \
-p buzz-pair-relay --bin buzz-pair-relay

View File

@@ -0,0 +1,16 @@
//! Build-time identity compiled into the relay binary.
/// Full source commit SHA, or `unknown` outside a provenance-aware build.
pub(crate) fn source_sha() -> &'static str {
option_env!("BUZZ_SOURCE_SHA").unwrap_or("unknown")
}
/// Stable build identifier, or `local` outside CI.
pub(crate) fn build_id() -> &'static str {
option_env!("BUZZ_BUILD_ID").unwrap_or("local")
}
/// Build details URL, or `unknown` outside CI.
pub(crate) fn build_url() -> &'static str {
option_env!("BUZZ_BUILD_URL").unwrap_or("unknown")
}

View File

@@ -3,6 +3,7 @@
//! NIP-01 WebSocket relay for Buzz private team communication.
mod admission;
mod build_info;
/// REST API route handlers.
pub mod api;

View File

@@ -413,14 +413,22 @@ async fn readiness_handler(State(state): State<Arc<AppState>>) -> impl IntoRespo
}
}
/// Status endpoint — service name, version, uptime.
async fn status_handler(State(state): State<Arc<AppState>>) -> impl IntoResponse {
let uptime_secs = state.started_at.elapsed().as_secs();
Json(json!({
fn status_payload(uptime_secs: u64) -> serde_json::Value {
json!({
"service": "buzz-relay",
"version": env!("CARGO_PKG_VERSION"),
"uptime_seconds": uptime_secs,
}))
"build": {
"source_sha": crate::build_info::source_sha(),
"id": crate::build_info::build_id(),
"url": crate::build_info::build_url(),
},
})
}
/// Status endpoint — service name, version, uptime, and intrinsic build identity.
async fn status_handler(State(state): State<Arc<AppState>>) -> impl IntoResponse {
Json(status_payload(state.started_at.elapsed().as_secs()))
}
/// `/_mesh` — live mesh status: peer table, connection/phi state, per-peer
@@ -506,6 +514,23 @@ mod tests {
assert!(!should_serve_spa("/arbitrary", true));
}
#[test]
fn status_payload_exposes_source_and_build_identity() {
let payload = status_payload(42);
assert_eq!(payload["service"], "buzz-relay");
assert_eq!(payload["version"], env!("CARGO_PKG_VERSION"));
assert_eq!(payload["uptime_seconds"], 42);
for field in ["source_sha", "id", "url"] {
assert!(
payload["build"][field]
.as_str()
.is_some_and(|value| !value.is_empty()),
"build.{field} must be a non-empty string"
);
}
}
#[tokio::test(flavor = "current_thread")]
async fn http_and_datastore_spans_are_exported_in_the_same_trace() {
let exporter = InMemorySpanExporter::default();

View File

@@ -7,7 +7,7 @@ description: |
PostgreSQL and Redis. Configurable for single-node evaluation
(subcharts on) and HA production (external services, existingSecret).
type: application
version: 0.1.7
version: 0.1.8
appVersion: "0.1.0"
home: https://github.com/block/buzz
sources:
@@ -24,7 +24,7 @@ maintainers:
annotations:
artifacthub.io/changes: |
- kind: added
description: Generic init-container, volume, volume-mount, command, and args extension points for the relay Pod.
description: Optional immutable relay image digest pinning with backwards-compatible tag fallback.
artifacthub.io/license: Apache-2.0
# Optional eval-only subcharts. Production deploys disable both and point

View File

@@ -12,7 +12,7 @@ This chart has two operating profiles selected by values:
## Quickstart (eval only)
```sh
helm install buzz oci://ghcr.io/block/buzz/charts/buzz --version 0.1.7 \
helm install buzz oci://ghcr.io/block/buzz/charts/buzz --version 0.1.8 \
--create-namespace --namespace buzz \
--set quickstart=true \
--set postgresql.enabled=true \
@@ -29,6 +29,15 @@ intent marker surfaced in NOTES.txt; the bundled services are opted in via the
four `*.enabled` flags above (see `ci/quickstart-values.yaml` for the exact set
CI installs). Eval-only: every bundled service is a single replica with no HA.
For immutable delivery, pin the OCI digest instead of a tag. `image.digest`
overrides `image.tag` when both are present:
```yaml
image:
repository: ghcr.io/block/buzz
digest: sha256:<64-lowercase-hex-characters>
```
## Production (GitOps)
The chart is designed for ArgoCD and Flux. Both render charts with `helm template`, in which mode Helm's `lookup` function returns empty — any chart-side `randAlphaNum` call would regenerate secrets on every sync. The chart-managed Secret path is **only** safe for `helm install` / `helm upgrade`.

View File

@@ -53,9 +53,13 @@ app.kubernetes.io/component: relay
{{- end -}}
{{- define "buzz.image" -}}
{{- if .Values.image.digest -}}
{{- printf "%s@%s" .Values.image.repository .Values.image.digest -}}
{{- else -}}
{{- $tag := default .Chart.AppVersion .Values.image.tag -}}
{{- printf "%s:%s" .Values.image.repository $tag -}}
{{- end -}}
{{- end -}}
{{/*
Name of the chart-managed Secret holding relay-identity material and any

View File

@@ -195,6 +195,24 @@ tests:
path: spec.template.spec.containers[0].args
template: templates/deployment.yaml
- it: renders an immutable digest instead of a configured tag
set:
relayUrl: wss://buzz.example.com
ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000"
externalPostgresql.url: postgres://u:p@h:5432/d
externalRedis.url: redis://h:6379
s3.endpoint: http://minio:9000
s3.accessKey: a
s3.secretKey: s
image.repository: ghcr.io/block/buzz
image.tag: sha-deadbee
image.digest: sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
asserts:
- equal:
path: spec.template.spec.containers[0].image
value: ghcr.io/block/buzz@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
template: templates/deployment.yaml
- it: appends generic Pod extensions and overrides the relay command
set:
relayUrl: wss://buzz.example.com

View File

@@ -2,6 +2,16 @@ suite: validation
templates:
- templates/deployment.yaml
tests:
- it: rejects a malformed image digest
set:
relayUrl: wss://buzz.example.com
ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000"
externalPostgresql.url: postgres://u:p@h:5432/d
image.digest: sha256:not-a-digest
asserts:
- failedTemplate:
errorPattern: "image.digest: Does not match pattern"
- it: fails when relayUrl is missing
set:
relayUrl: ""

View File

@@ -15,6 +15,11 @@
"properties": {
"repository": { "type": "string", "minLength": 1 },
"tag": { "type": "string" },
"digest": {
"type": "string",
"pattern": "^$|^sha256:[0-9a-f]{64}$",
"description": "Optional immutable OCI image digest. When set, the chart renders repository@digest and ignores tag."
},
"pullPolicy": { "type": "string", "enum": ["Always", "IfNotPresent", "Never"] },
"pullSecrets": {
"type": "array",

View File

@@ -25,6 +25,7 @@ quickstart: false
image:
repository: ghcr.io/block/buzz
tag: "" # empty → .Chart.AppVersion
digest: "" # optional sha256:...; when set, overrides tag
pullPolicy: IfNotPresent
pullSecrets: []

View File

@@ -0,0 +1,68 @@
# Relay deployment identity
Canonical relay images from `ghcr.io/block/buzz` carry two signed
attestations:
- SLSA build provenance maps the immutable image digest to the source commit
and Docker workflow run.
- The Buzz deployment-eligibility predicate records the successful same-SHA
CI run and the exact Buzz Helm chart version from that source commit.
The Docker workflow creates tagged multi-architecture manifests only after the
same full source SHA has a successful `CI` push run on `main` or `release`.
Architecture-specific build manifests may exist without tags while CI is
running or after it fails; they do not receive the deployment-eligibility
predicate and are not promotion inputs.
Verify a canonical eligible digest with:
```bash
gh attestation verify \
oci://ghcr.io/block/buzz@sha256:<digest> \
--repo block/buzz \
--signer-workflow block/buzz/.github/workflows/docker.yml \
--predicate-type https://buzz.block.xyz/attestations/deployment-eligibility/v1 \
--source-ref refs/heads/main
```
The predicate's `helm_chart.compatible_version` is image-to-chart metadata. It
does not describe database schema compatibility and does not relax Buzz's rule
that migrations remain backwards compatible.
The manual pre-merge workflow publishes only to
`ghcr.io/block/buzz-staging-dev`. Those preview images are intentionally
ineligible: they use a different package, may name non-main source, and do not
receive the canonical deployment-eligibility predicate.
## Runtime inspection
The relay health listener exposes intrinsic build identity at `/_status`:
```json
{
"service": "buzz-relay",
"version": "0.2.1",
"uptime_seconds": 123,
"build": {
"source_sha": "<40-character-source-sha>",
"id": "github-actions:<run-id>:<attempt>",
"url": "https://github.com/block/buzz/actions/runs/<run-id>/attempts/<attempt>"
}
}
```
Non-CI builds report stable `unknown` or `local` fallback values instead of
claiming provenance they do not have.
## Helm digest pinning
Buzz chart `0.1.8` and newer accept an immutable image digest:
```yaml
image:
repository: ghcr.io/block/buzz
digest: sha256:<64-lowercase-hex-characters>
```
When `image.digest` is set, the chart renders `repository@digest` and ignores
`image.tag`. Existing tag-only values remain backwards compatible.

View File

@@ -0,0 +1,26 @@
{
predicate_version: 1,
eligible: true,
source: {
repository: $source_repository,
ref: $source_ref,
sha: $source_sha
},
build: {
workflow: $build_workflow,
run_id: $build_run_id,
run_attempt: $build_run_attempt,
run_url: $build_run_url
},
qualification: {
workflow: $qualification_workflow,
run_id: $qualification_run_id,
run_attempt: $qualification_run_attempt,
run_url: $qualification_run_url,
conclusion: "success"
},
helm_chart: {
name: "buzz",
compatible_version: $chart_version
}
}

View File

@@ -0,0 +1,9 @@
[
.workflow_runs[]
| select(.head_sha == $source_sha)
| select(.event == "push")
| select(.head_branch == "main" or .head_branch == "release")
]
| sort_by([.id, .run_attempt])
| last // empty
| select(.conclusion == "success")

View File

@@ -0,0 +1,108 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
workflow="$repo_root/.github/workflows/docker.yml"
selector="$repo_root/scripts/select-qualified-ci-run.jq"
predicate_builder="$repo_root/scripts/create-deployment-eligibility-predicate.jq"
require_literal() {
local needle=$1
grep -Fq -- "$needle" "$workflow" || {
echo "relay image workflow is missing required delivery contract: $needle" >&2
exit 1
}
}
require_literal " qualify:"
require_literal "actions: read"
require_literal "actions/workflows/ci.yml/runs"
require_literal 'select-qualified-ci-run.jq'
require_literal "needs: [build, qualify]"
require_literal "https://buzz.block.xyz/attestations/deployment-eligibility/v1"
require_literal "actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6"
require_literal "if: matrix.variant == 'release'"
require_literal "BUZZ_SOURCE_SHA"
require_literal "BUZZ_BUILD_ID"
require_literal "BUZZ_BUILD_URL"
require_literal '- "deploy/charts/buzz/Chart.yaml"'
require_literal '- "scripts/create-deployment-eligibility-predicate.jq"'
require_literal '- "scripts/select-qualified-ci-run.jq"'
require_literal '- "scripts/test-relay-image-eligibility-workflow.sh"'
if grep -Fq "buzz-staging-dev" "$workflow"; then
echo "canonical relay image workflow references the preview-only image package" >&2
exit 1
fi
select_run() {
jq -r --arg source_sha aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -f "$selector" | jq -r '.id // empty'
}
selected=$(select_run <<'JSON'
{"workflow_runs":[
{"id":100,"run_attempt":1,"head_sha":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","head_branch":"main","event":"push","status":"completed","conclusion":"failure"},
{"id":101,"run_attempt":1,"head_sha":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","head_branch":"main","event":"push","status":"completed","conclusion":"success"}
]}
JSON
)
[[ "$selected" == "101" ]] || {
echo "latest successful same-SHA main run was not selected" >&2
exit 1
}
selected=$(select_run <<'JSON'
{"workflow_runs":[
{"id":100,"run_attempt":1,"head_sha":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","head_branch":"main","event":"push","status":"completed","conclusion":"success"},
{"id":101,"run_attempt":1,"head_sha":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","head_branch":"main","event":"push","status":"completed","conclusion":"failure"}
]}
JSON
)
[[ -z "$selected" ]] || {
echo "stale successful run remained eligible after a newer failure" >&2
exit 1
}
selected=$(select_run <<'JSON'
{"workflow_runs":[
{"id":102,"run_attempt":1,"head_sha":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","head_branch":"main","event":"push","status":"completed","conclusion":"success"},
{"id":103,"run_attempt":1,"head_sha":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","head_branch":"feature","event":"pull_request","status":"completed","conclusion":"success"}
]}
JSON
)
[[ -z "$selected" ]] || {
echo "wrong-SHA or pull-request CI run was accepted" >&2
exit 1
}
predicate=$(jq -n \
--arg source_repository "block/buzz" \
--arg source_ref "refs/heads/main" \
--arg source_sha "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" \
--arg build_workflow ".github/workflows/docker.yml" \
--argjson build_run_id 200 \
--argjson build_run_attempt 2 \
--arg build_run_url "https://github.com/block/buzz/actions/runs/200/attempts/2" \
--arg qualification_workflow ".github/workflows/ci.yml" \
--argjson qualification_run_id 201 \
--argjson qualification_run_attempt 1 \
--arg qualification_run_url "https://github.com/block/buzz/actions/runs/201" \
--arg chart_version "0.1.8" \
-f "$predicate_builder")
jq -e '
.predicate_version == 1 and
.eligible == true and
.source.sha == "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" and
.build.run_id == 200 and
.qualification.run_id == 201 and
.qualification.conclusion == "success" and
.helm_chart == {"name":"buzz","compatible_version":"0.1.8"} and
(has("schema") | not) and
(.helm_chart | has("schema_compatibility") | not)
' <<<"$predicate" >/dev/null || {
echo "deployment eligibility predicate has an invalid contract" >&2
exit 1
}
echo "relay image eligibility workflow contract passed"