@@ -3,13 +3,123 @@ name: Publish
33on :
44 push :
55 tags :
6- - ' **'
6+ - ' [0-9]+.[0-9]+.[0-9]+'
7+ - ' [0-9]+.[0-9]+.[0-9]+rc[0-9]+'
8+ - ' [0-9]+.[0-9]+.[0-9]+\+backport-[0-9]+'
9+ workflow_dispatch :
10+ inputs :
11+ skip_test_gate :
12+ description : " Skip the test-gate check for an emergency release. Run only with --ref <tag>."
13+ required : false
14+ default : false
15+ type : boolean
716
817env :
918 dists-artifact-name : python-package-distributions
1019
1120jobs :
21+ verify :
22+ name : Verify release gate
23+ runs-on : ubuntu-latest
24+ permissions :
25+ actions : read
26+ checks : read
27+ contents : read
28+ steps :
29+ - name : Validate release tag
30+ env :
31+ RELEASE_REF : ${{ github.ref }}
32+ RELEASE_TAG : ${{ github.ref_name }}
33+ run : |
34+ set -euo pipefail
35+ if [ "$RELEASE_REF" != "refs/tags/$RELEASE_TAG" ]; then
36+ echo "Publish must run from a release tag ref. Use: gh workflow run publish.yaml --ref <tag>" >&2
37+ exit 1
38+ fi
39+ python3 - <<'PY'
40+ import os
41+ import re
42+ import sys
43+
44+ tag = os.environ["RELEASE_TAG"]
45+ if re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+(?:rc[0-9]+|\+backport-[0-9]+)?", tag):
46+ sys.exit(0)
47+ print(f"Release tag {tag!r} must look like 0.66.3, 0.66.3rc1, or 0.2.11+backport-1.", file=sys.stderr)
48+ sys.exit(1)
49+ PY
50+ - name : Verify test-gate passed
51+ if : ${{ github.event_name != 'workflow_dispatch' || inputs.skip_test_gate == false }}
52+ env :
53+ GH_TOKEN : ${{ github.token }}
54+ RELEASE_TAG : ${{ github.ref_name }}
55+ REPOSITORY : ${{ github.repository }}
56+ run : |
57+ set -euo pipefail
58+ response="$(
59+ gh api "repos/${REPOSITORY}/commits/refs/tags/${RELEASE_TAG}/check-runs?check_name=test-gate&filter=latest"
60+ )"
61+ runs="$(
62+ jq -c '
63+ [
64+ .check_runs[]?
65+ | select(.name == "test-gate")
66+ | {
67+ status,
68+ conclusion,
69+ completed_at,
70+ started_at,
71+ head_sha,
72+ html_url,
73+ run_id: (try ((.html_url // "") | capture("/actions/runs/(?<id>[0-9]+)/job/").id) catch "")
74+ }
75+ ]
76+ | sort_by(.completed_at // .started_at // "")
77+ | reverse
78+ | .[]
79+ ' <<< "$response"
80+ )"
81+ if [ -z "$runs" ]; then
82+ echo "No test-gate check run was found for tag ${RELEASE_TAG}." >&2
83+ echo "Wait for Test on main to finish, then re-run Publish. For emergency releases, dispatch with skip_test_gate=true." >&2
84+ exit 1
85+ fi
86+
87+ while IFS= read -r run; do
88+ run_id="$(jq -r '.run_id' <<< "$run")"
89+ head_sha="$(jq -r '.head_sha' <<< "$run")"
90+ if [ -z "$run_id" ]; then
91+ continue
92+ fi
93+ metadata="$(gh api "repos/${REPOSITORY}/actions/runs/${run_id}")"
94+ if ! jq -e --arg head_sha "$head_sha" \
95+ '.name == "Test" and .event == "push" and .head_branch == "main" and .head_sha == $head_sha' \
96+ <<< "$metadata" > /dev/null; then
97+ continue
98+ fi
99+ status="$(jq -r '.status' <<< "$run")"
100+ conclusion="$(jq -r '.conclusion' <<< "$run")"
101+ html_url="$(jq -r '.html_url' <<< "$run")"
102+ if [ "$status" != "completed" ]; then
103+ echo "test-gate for tag ${RELEASE_TAG} is ${status}; re-run Publish after it completes." >&2
104+ exit 1
105+ fi
106+ if [ "$conclusion" != "success" ]; then
107+ echo "test-gate for tag ${RELEASE_TAG} concluded ${conclusion}: ${html_url}" >&2
108+ exit 1
109+ fi
110+ exit 0
111+ done <<< "$runs"
112+
113+ echo "Found test-gate check runs for tag ${RELEASE_TAG}, but none came from the Test workflow push on main." >&2
114+ echo "Wait for Test on main to finish, then re-run Publish. For emergency releases, dispatch with skip_test_gate=true." >&2
115+ exit 1
116+ - name : Report skipped test gate
117+ if : ${{ github.event_name == 'workflow_dispatch' && inputs.skip_test_gate }}
118+ run : echo "::warning::test-gate verification was skipped for this manual release."
119+
12120 build :
121+ needs :
122+ - verify
13123 runs-on : ubuntu-latest
14124 permissions :
15125 contents : read
99209 build-args : |
100210 VERSION=${{ github.ref_name }}
101211 platforms : linux/amd64,linux/arm64
212+ provenance : mode=max
213+ sbom : true
0 commit comments