mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +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:
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -75,6 +75,8 @@ jobs:
|
|||||||
- '.github/workflows/ci.yml'
|
- '.github/workflows/ci.yml'
|
||||||
- name: Release workflow source contract
|
- name: Release workflow source contract
|
||||||
run: scripts/test-release-ref-contract.sh
|
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
|
- name: Desktop release candidate contract
|
||||||
run: scripts/test-desktop-release-candidate.sh
|
run: scripts/test-desktop-release-candidate.sh
|
||||||
- name: OSS desktop promotion contract
|
- name: OSS desktop promotion contract
|
||||||
|
|||||||
151
.github/workflows/docker.yml
vendored
151
.github/workflows/docker.yml
vendored
@@ -49,6 +49,10 @@ on:
|
|||||||
- "Dockerfile.push-gateway"
|
- "Dockerfile.push-gateway"
|
||||||
- ".dockerignore"
|
- ".dockerignore"
|
||||||
- ".github/workflows/docker.yml"
|
- ".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.toml"
|
||||||
- "Cargo.lock"
|
- "Cargo.lock"
|
||||||
- "rust-toolchain.toml"
|
- "rust-toolchain.toml"
|
||||||
@@ -172,6 +176,10 @@ jobs:
|
|||||||
target: runtime
|
target: runtime
|
||||||
platforms: ${{ matrix.platform }}
|
platforms: ${{ matrix.platform }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
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
|
# Push by digest, not by tag — the merge job assembles the tags
|
||||||
# into one multi-arch manifest. This is what makes the native-arm
|
# into one multi-arch manifest. This is what makes the native-arm
|
||||||
# matrix possible.
|
# matrix possible.
|
||||||
@@ -190,6 +198,10 @@ jobs:
|
|||||||
target: runtime-debug
|
target: runtime-debug
|
||||||
platforms: ${{ matrix.platform }}
|
platforms: ${{ matrix.platform }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
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' }}
|
outputs: type=image,name=${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }}
|
||||||
cache-from: |
|
cache-from: |
|
||||||
type=registry,ref=${{ env.IMAGE_NAME }}-buildcache:${{ matrix.arch }}
|
type=registry,ref=${{ env.IMAGE_NAME }}-buildcache:${{ matrix.arch }}
|
||||||
@@ -222,11 +234,86 @@ jobs:
|
|||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 1
|
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:
|
merge:
|
||||||
name: Merge ${{ matrix.variant }} multi-arch manifest
|
name: Merge ${{ matrix.variant }} multi-arch manifest
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
needs: build
|
needs: [build, qualify]
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -286,6 +373,10 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
IMAGE_NAME: ${{ env.IMAGE_NAME }}
|
IMAGE_NAME: ${{ env.IMAGE_NAME }}
|
||||||
META_TAGS: ${{ steps.meta.outputs.tags }}
|
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: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
# Build -t flags from the metadata-action output.
|
# Build -t flags from the metadata-action output.
|
||||||
@@ -300,7 +391,15 @@ jobs:
|
|||||||
digests+=("${IMAGE_NAME}@sha256:${digest}")
|
digests+=("${IMAGE_NAME}@sha256:${digest}")
|
||||||
done
|
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.
|
# Capture the merged manifest digest for the attestation step.
|
||||||
first_tag=$(echo "$META_TAGS" | head -n1)
|
first_tag=$(echo "$META_TAGS" | head -n1)
|
||||||
@@ -317,17 +416,58 @@ jobs:
|
|||||||
subject-digest: ${{ steps.manifest.outputs.digest }}
|
subject-digest: ${{ steps.manifest.outputs.digest }}
|
||||||
push-to-registry: true
|
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
|
- name: Summary
|
||||||
env:
|
env:
|
||||||
IMAGE_NAME: ${{ env.IMAGE_NAME }}
|
IMAGE_NAME: ${{ env.IMAGE_NAME }}
|
||||||
VARIANT: ${{ matrix.variant }}
|
VARIANT: ${{ matrix.variant }}
|
||||||
MERGED_DIGEST: ${{ steps.manifest.outputs.digest }}
|
MERGED_DIGEST: ${{ steps.manifest.outputs.digest }}
|
||||||
META_TAGS: ${{ steps.meta.outputs.tags }}
|
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: |
|
run: |
|
||||||
{
|
{
|
||||||
echo "### Published \`${IMAGE_NAME}\` (${VARIANT})"
|
echo "### Published \`${IMAGE_NAME}\` (${VARIANT})"
|
||||||
echo
|
echo
|
||||||
echo "**Digest:** \`${MERGED_DIGEST}\`"
|
echo "**Digest:** \`${MERGED_DIGEST}\`"
|
||||||
|
echo "**Source:** \`${SOURCE_SHA}\`"
|
||||||
|
echo "**Compatible Buzz chart:** \`${CHART_VERSION}\`"
|
||||||
|
echo "**Qualifying CI:** ${CI_RUN_URL} (success)"
|
||||||
echo
|
echo
|
||||||
echo "**Tags:**"
|
echo "**Tags:**"
|
||||||
echo '```'
|
echo '```'
|
||||||
@@ -338,6 +478,13 @@ jobs:
|
|||||||
echo '```'
|
echo '```'
|
||||||
echo "gh attestation verify oci://${IMAGE_NAME}@${MERGED_DIGEST} --owner block"
|
echo "gh attestation verify oci://${IMAGE_NAME}@${MERGED_DIGEST} --owner block"
|
||||||
echo '```'
|
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"
|
} >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
|
||||||
push-gateway-build:
|
push-gateway-build:
|
||||||
|
|||||||
@@ -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.
|
# scoping to -p buzz-relay misses transitive deps and re-builds them later.
|
||||||
RUN cargo chef cook --release --recipe-path recipe.json
|
RUN cargo chef cook --release --recipe-path recipe.json
|
||||||
COPY . .
|
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 \
|
RUN cargo build --release --locked -p buzz-relay --bin buzz-relay \
|
||||||
-p buzz-admin --bin buzz-admin \
|
-p buzz-admin --bin buzz-admin \
|
||||||
-p buzz-pair-relay --bin buzz-pair-relay
|
-p buzz-pair-relay --bin buzz-pair-relay
|
||||||
|
|||||||
16
crates/buzz-relay/src/build_info.rs
Normal file
16
crates/buzz-relay/src/build_info.rs
Normal 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")
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
//! NIP-01 WebSocket relay for Buzz private team communication.
|
//! NIP-01 WebSocket relay for Buzz private team communication.
|
||||||
|
|
||||||
mod admission;
|
mod admission;
|
||||||
|
mod build_info;
|
||||||
|
|
||||||
/// REST API route handlers.
|
/// REST API route handlers.
|
||||||
pub mod api;
|
pub mod api;
|
||||||
|
|||||||
@@ -413,14 +413,22 @@ async fn readiness_handler(State(state): State<Arc<AppState>>) -> impl IntoRespo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Status endpoint — service name, version, uptime.
|
fn status_payload(uptime_secs: u64) -> serde_json::Value {
|
||||||
async fn status_handler(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
json!({
|
||||||
let uptime_secs = state.started_at.elapsed().as_secs();
|
|
||||||
Json(json!({
|
|
||||||
"service": "buzz-relay",
|
"service": "buzz-relay",
|
||||||
"version": env!("CARGO_PKG_VERSION"),
|
"version": env!("CARGO_PKG_VERSION"),
|
||||||
"uptime_seconds": uptime_secs,
|
"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
|
/// `/_mesh` — live mesh status: peer table, connection/phi state, per-peer
|
||||||
@@ -506,6 +514,23 @@ mod tests {
|
|||||||
assert!(!should_serve_spa("/arbitrary", true));
|
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")]
|
#[tokio::test(flavor = "current_thread")]
|
||||||
async fn http_and_datastore_spans_are_exported_in_the_same_trace() {
|
async fn http_and_datastore_spans_are_exported_in_the_same_trace() {
|
||||||
let exporter = InMemorySpanExporter::default();
|
let exporter = InMemorySpanExporter::default();
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ description: |
|
|||||||
PostgreSQL and Redis. Configurable for single-node evaluation
|
PostgreSQL and Redis. Configurable for single-node evaluation
|
||||||
(subcharts on) and HA production (external services, existingSecret).
|
(subcharts on) and HA production (external services, existingSecret).
|
||||||
type: application
|
type: application
|
||||||
version: 0.1.7
|
version: 0.1.8
|
||||||
appVersion: "0.1.0"
|
appVersion: "0.1.0"
|
||||||
home: https://github.com/block/buzz
|
home: https://github.com/block/buzz
|
||||||
sources:
|
sources:
|
||||||
@@ -24,7 +24,7 @@ maintainers:
|
|||||||
annotations:
|
annotations:
|
||||||
artifacthub.io/changes: |
|
artifacthub.io/changes: |
|
||||||
- kind: added
|
- 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
|
artifacthub.io/license: Apache-2.0
|
||||||
|
|
||||||
# Optional eval-only subcharts. Production deploys disable both and point
|
# Optional eval-only subcharts. Production deploys disable both and point
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ This chart has two operating profiles selected by values:
|
|||||||
## Quickstart (eval only)
|
## Quickstart (eval only)
|
||||||
|
|
||||||
```sh
|
```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 \
|
--create-namespace --namespace buzz \
|
||||||
--set quickstart=true \
|
--set quickstart=true \
|
||||||
--set postgresql.enabled=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
|
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.
|
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)
|
## 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`.
|
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`.
|
||||||
|
|||||||
@@ -53,9 +53,13 @@ app.kubernetes.io/component: relay
|
|||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|
||||||
{{- define "buzz.image" -}}
|
{{- define "buzz.image" -}}
|
||||||
|
{{- if .Values.image.digest -}}
|
||||||
|
{{- printf "%s@%s" .Values.image.repository .Values.image.digest -}}
|
||||||
|
{{- else -}}
|
||||||
{{- $tag := default .Chart.AppVersion .Values.image.tag -}}
|
{{- $tag := default .Chart.AppVersion .Values.image.tag -}}
|
||||||
{{- printf "%s:%s" .Values.image.repository $tag -}}
|
{{- printf "%s:%s" .Values.image.repository $tag -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
{{/*
|
{{/*
|
||||||
Name of the chart-managed Secret holding relay-identity material and any
|
Name of the chart-managed Secret holding relay-identity material and any
|
||||||
|
|||||||
@@ -195,6 +195,24 @@ tests:
|
|||||||
path: spec.template.spec.containers[0].args
|
path: spec.template.spec.containers[0].args
|
||||||
template: templates/deployment.yaml
|
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
|
- it: appends generic Pod extensions and overrides the relay command
|
||||||
set:
|
set:
|
||||||
relayUrl: wss://buzz.example.com
|
relayUrl: wss://buzz.example.com
|
||||||
|
|||||||
@@ -2,6 +2,16 @@ suite: validation
|
|||||||
templates:
|
templates:
|
||||||
- templates/deployment.yaml
|
- templates/deployment.yaml
|
||||||
tests:
|
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
|
- it: fails when relayUrl is missing
|
||||||
set:
|
set:
|
||||||
relayUrl: ""
|
relayUrl: ""
|
||||||
|
|||||||
@@ -15,6 +15,11 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"repository": { "type": "string", "minLength": 1 },
|
"repository": { "type": "string", "minLength": 1 },
|
||||||
"tag": { "type": "string" },
|
"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"] },
|
"pullPolicy": { "type": "string", "enum": ["Always", "IfNotPresent", "Never"] },
|
||||||
"pullSecrets": {
|
"pullSecrets": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ quickstart: false
|
|||||||
image:
|
image:
|
||||||
repository: ghcr.io/block/buzz
|
repository: ghcr.io/block/buzz
|
||||||
tag: "" # empty → .Chart.AppVersion
|
tag: "" # empty → .Chart.AppVersion
|
||||||
|
digest: "" # optional sha256:...; when set, overrides tag
|
||||||
pullPolicy: IfNotPresent
|
pullPolicy: IfNotPresent
|
||||||
pullSecrets: []
|
pullSecrets: []
|
||||||
|
|
||||||
|
|||||||
68
docs/deployment-identity.md
Normal file
68
docs/deployment-identity.md
Normal 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.
|
||||||
26
scripts/create-deployment-eligibility-predicate.jq
Normal file
26
scripts/create-deployment-eligibility-predicate.jq
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
9
scripts/select-qualified-ci-run.jq
Normal file
9
scripts/select-qualified-ci-run.jq
Normal 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")
|
||||||
108
scripts/test-relay-image-eligibility-workflow.sh
Executable file
108
scripts/test-relay-image-eligibility-workflow.sh
Executable 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"
|
||||||
Reference in New Issue
Block a user