Files
buzz/scripts/test-release-ref-contract.sh
Luke Tornquist 99c5fc8bba ci(docker): publish qualified, sortable push gateway main tags (#8305)
## 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>
2026-10-14 14:36:00 -04:00

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"