mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
## Summary This PR makes two changes to the push gateway image job (`push-gateway-merge` in `docker.yml`). Together they let staging auto-promote the gateway the same way it already auto-promotes the relay: 1. **Publish only after same-SHA CI qualification.** `push-gateway-merge` now needs `qualify` as well as `push-gateway-build`. This is the same gate the relay's `merge` job already uses: a successful `ci.yml` push run on the exact source SHA. Before this change, the gateway image was published even when main CI was red. That was safe while staging pinned the gateway by hand. It isn't safe once the newest main image deploys automatically. 2. **Publish the sortable `:main-<YYYYMMDDHHmmss>-<sha7>` tag on main pushes.** It's the same `type=raw` line the relay matrix gained in #8240. It uses the same `enable=` guard, so it applies only to `push` on `refs/heads/main`. A Lexical selector on `^main-\d{14}-[0-9a-f]{7}$` can then pick the newest gateway build from the tag list alone. `:sha-<7>` has no ordering. `:main`, `:sha-<7>`, and the release semver tags don't change. The gateway has no debug variant, so no `debug-main-*` tag is published. **Tradeoff:** a gateway image now appears only after `qualify` finishes, which takes as long as main CI. If main CI fails, no gateway image is published for that commit, and that includes `:main` and `:sha-<7>`. The relay already behaves this way, and it's the behavior we want for anything that auto-deploys. **What got simpler:** the relay and gateway images now share the same publication gate and the same tag set. Downstream tooling no longer needs a special case for the gateway's unqualified, unordered tags. **What stays different:** the relay's `merge` job also attests the `deployment-eligibility/v1` predicate (`docs/deployment-identity.md`); the gateway publishes no such attestation. That's intentional for now: the staging promotion guard selects the gateway by package and `main-*` tag and does not verify attestations, so the gateway warehouse must not be wired to require one. If promotion ever starts verifying eligibility, the gateway needs the same attestation first. ### Related issue N/A (staging rollout tooling). The downstream promotion-guard and warehouse changes land separately. The warehouse switches on only after this tag has been published. ### Testing - `scripts/test-relay-image-eligibility-workflow.sh` now pins both properties of the `push-gateway-merge` job (it runs in CI under "Relay image eligibility contract"). Both checks match whole active lines, so a commented-out copy can't satisfy them. - At `9b8c49600` it passes. - With the live `needs` line replaced by `needs: [push-gateway-build]` and a commented-out `# needs: [push-gateway-build, qualify]` left behind, it fails. - With the gateway `main-*` line commented out, it fails. - With `needs` reverted to `push-gateway-build`, it fails with `push gateway image must publish only after same-SHA CI qualification`. - With the `main-*` tag line removed, it fails with `push gateway image must publish the sortable main-<commit UTC>-<7> tag on main pushes`. - `scripts/test-release-ref-contract.sh` now counts the sortable `main-*` tag line on all three image legs (relay, relay-debug, gateway) instead of two. It passes at `9b8c49600`. - `bin/actionlint .github/workflows/docker.yml` is clean at `f6c03b6b5`. - PR runs don't push images. The first real `main-*` gateway tag appears on the first main push after merge, and I'll check the registry for it on that manifest. --------- Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
185 lines
9.0 KiB
Bash
Executable File
185 lines
9.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
verify="${repo_root}/scripts/verify-release-ref.sh"
|
|
tmp=$(mktemp -d)
|
|
trap 'rm -rf "$tmp"' EXIT
|
|
|
|
git -C "$tmp" init -q
|
|
git -C "$tmp" config user.name test
|
|
git -C "$tmp" config user.email test@example.com
|
|
echo first >"$tmp/file"
|
|
git -C "$tmp" add file
|
|
git -C "$tmp" commit -qm first
|
|
git -C "$tmp" tag -m "desktop release" desktop-v1.2.3
|
|
|
|
(
|
|
cd "$tmp"
|
|
GITHUB_REF=refs/tags/desktop-v1.2.3 "$verify" desktop-v 1.2.3
|
|
)
|
|
|
|
if (
|
|
cd "$tmp"
|
|
GITHUB_REF=refs/heads/main "$verify" desktop-v 1.2.3
|
|
); then
|
|
echo "branch-backed desktop release was accepted" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo second >>"$tmp/file"
|
|
git -C "$tmp" commit -qam second
|
|
if (
|
|
cd "$tmp"
|
|
GITHUB_REF=refs/tags/desktop-v1.2.3 "$verify" desktop-v 1.2.3
|
|
); then
|
|
echo "release accepted HEAD after the tag commit" >&2
|
|
exit 1
|
|
fi
|
|
|
|
git -C "$tmp" tag -m "relay release" relay-v2.0.0
|
|
(
|
|
cd "$tmp"
|
|
GITHUB_REF=refs/tags/relay-v2.0.0 "$verify" relay-v 2.0.0
|
|
)
|
|
|
|
if grep -q 'inputs\.ref' \
|
|
"$repo_root/.github/workflows/release.yml" \
|
|
"$repo_root/.github/workflows/docker.yml"; then
|
|
echo "publisher workflow still accepts a caller-selected source ref" >&2
|
|
exit 1
|
|
fi
|
|
|
|
grep -q 'verify-release-ref\.sh' "$repo_root/.github/workflows/release.yml"
|
|
grep -q 'verify-release-ref\.sh' "$repo_root/.github/workflows/docker.yml"
|
|
# Kargo's buzz-image and buzz-push-gateway-image warehouses select Lexical over
|
|
# ^main-[0-9]{14}-[0-9a-f]{7}$; a drifted format stops Freight discovery
|
|
# silently, so pin all three image legs (relay, relay-debug, push gateway).
|
|
sortable_main_tag=" type=raw,value=main-{{commit_date 'YYYYMMDDHHmmss'}}-{{sha}},enable=\${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}"
|
|
sortable_count="$(grep -Fxc "$sortable_main_tag" "$repo_root/.github/workflows/docker.yml" || true)"
|
|
if [ "$sortable_count" != 3 ]; then
|
|
echo "docker.yml must publish the sortable main-<UTC>-<sha7> tag on all three image legs (found $sortable_count)" >&2
|
|
exit 1
|
|
fi
|
|
grep -q 'test-release-ref-contract\.sh' "$repo_root/.github/workflows/ci.yml"
|
|
"$repo_root/scripts/test-signed-canary-contract.sh"
|
|
"$repo_root/scripts/test-desktop-release-cache-key.sh"
|
|
"$repo_root/scripts/test-desktop-release-cache-workflow.sh"
|
|
auto_tag="$repo_root/.github/workflows/auto-tag-on-release-pr-merge.yml"
|
|
grep -q 'actions/create-github-app-token@' "$auto_tag"
|
|
grep -q 'client-id:.*vars\.BUZZ_RELEASE_TAGGER_CLIENT_ID' "$auto_tag"
|
|
grep -q 'private-key:.*secrets\.BUZZ_RELEASE_TAGGER_PRIVATE_KEY' "$auto_tag"
|
|
grep -q 'permission-contents: write' "$auto_tag"
|
|
grep -q 'GH_TOKEN:.*steps\.release-tagger\.outputs\.token' "$auto_tag"
|
|
grep -Fq 'git/refs' "$auto_tag"
|
|
grep -Fq 'TAG_PREFIX="desktop-v"' "$auto_tag"
|
|
grep -Fq 'target_sha=${{ github.event.pull_request.head.sha }}' "$auto_tag"
|
|
grep -Fq 'scripts/verify-desktop-release-merge.sh' "$auto_tag"
|
|
candidate_workflow="$repo_root/.github/workflows/desktop-release-candidate.yml"
|
|
grep -Eq '^ pull-requests: read$' "$candidate_workflow" || {
|
|
echo "desktop candidate token cannot read pull requests for prior-release lookup" >&2
|
|
exit 1
|
|
}
|
|
grep -Fq 'GH_TOKEN: ${{ github.token }}' "$candidate_workflow" || {
|
|
echo "desktop candidate validation has no GitHub token for prior-release lookup" >&2
|
|
exit 1
|
|
}
|
|
grep -Fq 'reviewed candidate' "$repo_root/scripts/prepare-desktop-release.sh"
|
|
grep -Fq 'gh pr list --repo block/buzz' "$repo_root/scripts/prepare-desktop-release.sh"
|
|
grep -Fq 'gh pr edit --repo block/buzz' "$repo_root/scripts/prepare-desktop-release.sh"
|
|
grep -Fq 'gh pr create --repo block/buzz' "$repo_root/scripts/prepare-desktop-release.sh"
|
|
if grep -Fq 'current `main`' "$repo_root/scripts/prepare-desktop-release.sh"; then
|
|
echo "desktop release PR body contains executable command substitution" >&2
|
|
exit 1
|
|
fi
|
|
required_check_filter="$repo_root/scripts/required-check-succeeded.jq"
|
|
check_fixture() {
|
|
local expected="$1" conclusion="$2" app="${3:-15368}" completed="${4:-2026-01-01T00:00:00Z}"
|
|
local payload actual
|
|
# Production-shaped REST check run: notably, there is no created_at field.
|
|
payload=$(jq -n --arg conclusion "$conclusion" --argjson app "$app" --arg completed "$completed" \
|
|
'{check_runs: [{id: 100, check_suite: {id: 10}, name: "Web", app: {id: $app}, status: "completed", conclusion: $conclusion, started_at: "2026-01-01T00:00:00Z", completed_at: $completed}]}')
|
|
if jq -e --arg name Web --argjson integration_id 15368 \
|
|
--arg merged_at 2026-01-02T00:00:00Z \
|
|
-f "$required_check_filter" <<<"[$payload]" >/dev/null; then actual=pass; else actual=fail; fi
|
|
[[ "$actual" == "$expected" ]] || { echo "required-check fixture expected $expected, got $actual" >&2; exit 1; }
|
|
}
|
|
check_fixture pass success
|
|
check_fixture pass skipped
|
|
check_fixture pass neutral
|
|
check_fixture fail failure
|
|
check_fixture fail success 999
|
|
check_fixture fail success 15368 2026-01-03T00:00:00Z
|
|
|
|
# filter=latest may still return multiple same-name runs from distinct workflows.
|
|
# Highest immutable run ID is authoritative and must not reveal stale green.
|
|
jq -e --arg name Web --argjson integration_id 15368 --arg merged_at 2026-01-02T00:00:00Z \
|
|
-f "$required_check_filter" >/dev/null <<'JSON' && {
|
|
[{"check_runs":[
|
|
{"id":100,"check_suite":{"id":10},"name":"Web","app":{"id":15368},"status":"completed","conclusion":"success","started_at":"2026-01-01T00:00:00Z","completed_at":"2026-01-01T01:00:00Z"},
|
|
{"id":101,"check_suite":{"id":11},"name":"Web","app":{"id":15368},"status":"in_progress","conclusion":null,"started_at":"2026-01-01T23:59:00Z","completed_at":null}
|
|
]}]
|
|
JSON
|
|
echo "required-check filter hid the highest-ID pending attempt" >&2; exit 1;
|
|
}
|
|
# A post-merge rerun is indistinguishable from other latest attempts and fails closed.
|
|
jq -e --arg name Web --argjson integration_id 15368 --arg merged_at 2026-01-02T00:00:00Z \
|
|
-f "$required_check_filter" >/dev/null <<'JSON' && {
|
|
[{"check_runs":[
|
|
{"id":100,"check_suite":{"id":10},"name":"Web","app":{"id":15368},"status":"completed","conclusion":"success","started_at":"2026-01-01T00:00:00Z","completed_at":"2026-01-01T01:00:00Z"},
|
|
{"id":101,"check_suite":{"id":10},"name":"Web","app":{"id":15368},"status":"completed","conclusion":"failure","started_at":"2026-01-02T00:01:00Z","completed_at":"2026-01-02T00:10:00Z"}
|
|
]}]
|
|
JSON
|
|
echo "required-check filter accepted stale success after post-merge rerun" >&2; exit 1;
|
|
}
|
|
# DCO alone may complete just after merge, inside its explicit five-minute bound.
|
|
dco_fixture() {
|
|
local expected="$1" completed="$2" actual
|
|
if jq -e --arg name "DCO Check" --argjson integration_id 1455659 --arg merged_at 2026-01-02T00:00:00Z \
|
|
-f "$required_check_filter" >/dev/null <<JSON
|
|
[{"check_runs":[{"id":200,"check_suite":{"id":20},"name":"DCO Check","app":{"id":1455659},"status":"completed","conclusion":"success","started_at":"$completed","completed_at":"$completed"}]}]
|
|
JSON
|
|
then actual=pass; else actual=fail; fi
|
|
[[ "$actual" == "$expected" ]] || { echo "DCO completion $completed expected $expected" >&2; exit 1; }
|
|
}
|
|
dco_fixture pass 2026-01-02T00:04:59Z
|
|
dco_fixture fail 2026-01-02T00:05:01Z
|
|
|
|
# The verifier must request production endpoint semantics and pin helpers before checkout.
|
|
verify_merge="$repo_root/scripts/verify-desktop-release-merge.sh"
|
|
grep -Fq 'check-runs?filter=latest&per_page=100' "$verify_merge"
|
|
grep -Fq 'git fetch origin main --no-tags' "$verify_merge"
|
|
grep -Fq 'git merge-base --is-ancestor "$candidate_parents" origin/main' "$verify_merge"
|
|
grep -Fq 'git show "$candidate_parents:scripts/desktop_release.py"' "$verify_merge"
|
|
grep -Fq 'git show "$candidate_parents:scripts/required-check-succeeded.jq"' "$verify_merge"
|
|
grep -Fq 'DESKTOP_RELEASE_ROOT="$PWD" python3 "$verifier_dir/desktop_release.py"' "$verify_merge"
|
|
grep -Fq -- '-f "$verifier_dir/required-check-succeeded.jq"' "$verify_merge"
|
|
|
|
release_workflow="$repo_root/.github/workflows/release.yml"
|
|
[[ "$(grep -c 'contents: write' "$release_workflow")" -eq 1 ]] || {
|
|
echo "desktop release must have exactly one GitHub contents writer" >&2; exit 1;
|
|
}
|
|
grep -Fq "needs.release.result == 'success'" "$release_workflow"
|
|
grep -Fq "needs.release-macos-x64.result == 'success'" "$release_workflow"
|
|
grep -Fq "needs.release-linux.result == 'success'" "$release_workflow"
|
|
grep -Fq "needs.release-windows.result == 'success'" "$release_workflow"
|
|
grep -Fq "refs/tags/desktop-v{0}" "$release_workflow"
|
|
grep -Fq 'group: desktop-release-${{ github.ref }}' "$release_workflow"
|
|
grep -Fq 'cancel-in-progress: false' "$release_workflow"
|
|
grep -Fq 'release artifact basename collision' "$release_workflow"
|
|
[[ "$(grep -c 'gh release upload' "$release_workflow")" -eq 1 ]] || {
|
|
echo "desktop release must only upload versioned release assets" >&2; exit 1;
|
|
}
|
|
grep -Fq 'if: env.already_published' "$release_workflow"
|
|
grep -Fq 'if gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$TAG" --silent 2>/dev/null; then' "$auto_tag"
|
|
if grep -F 'git/ref/tags/$TAG' "$auto_tag" | grep -Fq '|| true'; then
|
|
echo "auto-tag ignores a failed tag lookup, so a 404 body can look like an existing tag" >&2
|
|
exit 1
|
|
fi
|
|
if grep -q 'gh workflow run' "$auto_tag"; then
|
|
echo "auto-tag still dispatches a publisher instead of using the tag push" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "release ref contract passed"
|