| 1 | #!/usr/bin/env bash |
| 2 | # Refuse a release unless a green release-candidate run validated the exact |
| 3 | # commit, including its Parity job. |
| 4 | # |
| 5 | # Usage: require-rc-receipt.sh <owner/repo> <40-char sha> |
| 6 | # |
| 7 | # The receipt is a completed, successful `release-candidate.yml` run for |
| 8 | # <sha>, dispatched by hand, whose Parity job (the shared |
| 9 | # release-parity.yml gate) concluded success. A green run whose Parity job |
| 10 | # was skipped is not a receipt. Requires `gh` authenticated with actions:read. |
| 11 | # |
| 12 | # RC_RECEIPT_GH overrides the gh executable (tests only). |
| 13 | set -euo pipefail |
| 14 | |
| 15 | repo="${1:-}" |
| 16 | sha="${2:-}" |
| 17 | gh_bin="${RC_RECEIPT_GH:-gh}" |
| 18 | |
| 19 | if [[ -z "${repo}" || -z "${sha}" ]]; then |
| 20 | echo "usage: $0 <owner/repo> <sha>" >&2 |
| 21 | exit 2 |
| 22 | fi |
| 23 | if ! [[ "${repo}" =~ ^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$ ]]; then |
| 24 | echo "::error::Repository '${repo}' must be owner/name." >&2 |
| 25 | exit 2 |
| 26 | fi |
| 27 | if [[ "${#sha}" -ne 40 || "${sha}" =~ [^0-9a-f] ]]; then |
| 28 | echo "::error::Release SHA must be a full 40-character lowercase commit SHA, got '${sha}'." >&2 |
| 29 | exit 2 |
| 30 | fi |
| 31 | |
| 32 | # sha is validated hex, so it is safe to place inside the jq program. |
| 33 | runs="$("${gh_bin}" api \ |
| 34 | "repos/${repo}/actions/workflows/release-candidate.yml/runs?head_sha=${sha}&status=success&per_page=100" \ |
| 35 | --jq ".workflow_runs[] | select(.head_sha == \"${sha}\" and .conclusion == \"success\" and .event == \"workflow_dispatch\") | [.id, .html_url] | @tsv")" |
| 36 | |
| 37 | while IFS=$'\t' read -r run_id run_url; do |
| 38 | [[ -n "${run_id}" ]] || continue |
| 39 | if ! [[ "${run_id}" =~ ^[0-9]+$ ]]; then |
| 40 | echo "::error::Unexpected run id '${run_id}' from the Actions API." >&2 |
| 41 | exit 1 |
| 42 | fi |
| 43 | # Jobs from a reusable workflow are named "<caller name> / <job name>". |
| 44 | parity_green="$("${gh_bin}" api \ |
| 45 | "repos/${repo}/actions/runs/${run_id}/jobs?filter=latest&per_page=100" \ |
| 46 | --jq '[.jobs[] | select((.name == "Parity" or (.name | startswith("Parity / "))) and .conclusion == "success")] | length')" |
| 47 | if [[ "${parity_green}" =~ ^[0-9]+$ && "${parity_green}" -gt 0 ]]; then |
| 48 | echo "Release-candidate receipt for ${sha}: ${run_url} (Parity green)" |
| 49 | exit 0 |
| 50 | fi |
| 51 | echo "::warning::Release-candidate run ${run_url} is green but has no successful Parity job; it is not a receipt." >&2 |
| 52 | done <<< "${runs}" |
| 53 | |
| 54 | echo "::error::No green release-candidate run with a passing Parity job for ${sha}. Validate that exact commit first: gh workflow run release-candidate.yml --ref main -f expected_sha=${sha} (use the release tag as --ref if main has moved past it), wait for it to go green, then re-run this Release. Never move a tag to a different SHA to get past this." >&2 |
| 55 | exit 1 |
| 56 |