11name : Publish immutable release
22
33on :
4- workflow_call :
4+ push :
5+ branches :
6+ - main
7+ workflow_dispatch :
58 inputs :
69 release-tag :
10+ description : Exact immutable GitHub release tag
711 required : true
812 type : string
913 release-sha :
10- required : true
11- type : string
12- default-branch :
14+ description : Exact commit resolved by the release tag
1315 required : true
1416 type : string
1517
2426 UV_VERSION : 0.11.8
2527
2628jobs :
29+ release-please :
30+ name : Maintain the reviewed release PR and release
31+ if : >-
32+ github.run_attempt == 1 &&
33+ github.event_name == 'push' &&
34+ vars.RELEASE_PLEASE_ENABLED == 'true'
35+ runs-on : ubuntu-latest
36+ timeout-minutes : 15
37+ outputs :
38+ release-created : ${{ steps.release.outputs.release_created }}
39+ release-sha : ${{ steps.verify-release.outputs.release-sha }}
40+ release-tag : ${{ steps.verify-release.outputs.release-tag }}
41+ release-verified : ${{ steps.verify-release.outputs.release-verified }}
42+ permissions :
43+ contents : write
44+ pull-requests : write
45+ steps :
46+ - name : Open or update the release PR, or create its approved release
47+ id : release
48+ uses : googleapis/release-please-action@5c625bfb5d1ff62eadeeb3772007f7f66fdcf071 # v4.4.1
49+ with :
50+ config-file : release-please-config.json
51+ manifest-file : .release-please-manifest.json
52+ - name : Verify the immutable release created by Release Please
53+ id : verify-release
54+ if : steps.release.outputs.release_created == 'true'
55+ env :
56+ EXPECTED_SHA : ${{ steps.release.outputs.sha }}
57+ EXPECTED_TAG : ${{ steps.release.outputs.tag_name }}
58+ GH_TOKEN : ${{ github.token }}
59+ run : |
60+ test -n "$EXPECTED_TAG"
61+ test -n "$EXPECTED_SHA"
62+ release=""
63+ for attempt in $(seq 1 12); do
64+ release=$(gh api "repos/${{ github.repository }}/releases/tags/$EXPECTED_TAG") || true
65+ if test -n "$release" && test "$(jq -r .immutable <<<"$release")" = "true"; then
66+ break
67+ fi
68+ if test "$attempt" -ge 12; then
69+ echo "release did not become immutable" >&2
70+ exit 1
71+ fi
72+ sleep 5
73+ done
74+ test "$(jq -r .tag_name <<<"$release")" = "$EXPECTED_TAG"
75+ test "$(jq -r .draft <<<"$release")" = "false"
76+ test "$(jq -r .prerelease <<<"$release")" = "false"
77+ test "$(jq -r .immutable <<<"$release")" = "true"
78+ ref=$(gh api "repos/${{ github.repository }}/git/ref/tags/$EXPECTED_TAG")
79+ tag_type=$(jq -r .object.type <<<"$ref")
80+ tag_sha=$(jq -r .object.sha <<<"$ref")
81+ if test "$tag_type" = "tag"; then
82+ tag_sha=$(gh api "repos/${{ github.repository }}/git/tags/$tag_sha" --jq .object.sha)
83+ else
84+ test "$tag_type" = "commit"
85+ fi
86+ test "$tag_sha" = "$EXPECTED_SHA"
87+ {
88+ echo "release-tag=$EXPECTED_TAG"
89+ echo "release-sha=$EXPECTED_SHA"
90+ echo "release-verified=true"
91+ } >> "$GITHUB_OUTPUT"
92+
93+ verify-recovery :
94+ name : Verify the authorized immutable release recovery
95+ if : >-
96+ github.run_attempt == 1 &&
97+ github.event_name == 'workflow_dispatch' &&
98+ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) &&
99+ vars.RELEASE_RECOVERY_TAG == inputs.release-tag &&
100+ vars.RELEASE_RECOVERY_SHA == inputs.release-sha
101+ runs-on : ubuntu-latest
102+ timeout-minutes : 5
103+ outputs :
104+ release-sha : ${{ steps.verify-release.outputs.release-sha }}
105+ release-tag : ${{ steps.verify-release.outputs.release-tag }}
106+ permissions :
107+ contents : read
108+ steps :
109+ - name : Verify the immutable release selected for recovery
110+ id : verify-release
111+ env :
112+ EXPECTED_SHA : ${{ inputs.release-sha }}
113+ EXPECTED_TAG : ${{ inputs.release-tag }}
114+ GH_TOKEN : ${{ github.token }}
115+ run : |
116+ test -n "$EXPECTED_TAG"
117+ test -n "$EXPECTED_SHA"
118+ release=""
119+ for attempt in $(seq 1 12); do
120+ release=$(gh api "repos/${{ github.repository }}/releases/tags/$EXPECTED_TAG") || true
121+ if test -n "$release" && test "$(jq -r .immutable <<<"$release")" = "true"; then
122+ break
123+ fi
124+ if test "$attempt" -ge 12; then
125+ echo "release did not become immutable" >&2
126+ exit 1
127+ fi
128+ sleep 5
129+ done
130+ test "$(jq -r .tag_name <<<"$release")" = "$EXPECTED_TAG"
131+ test "$(jq -r .draft <<<"$release")" = "false"
132+ test "$(jq -r .prerelease <<<"$release")" = "false"
133+ test "$(jq -r .immutable <<<"$release")" = "true"
134+ ref=$(gh api "repos/${{ github.repository }}/git/ref/tags/$EXPECTED_TAG")
135+ tag_type=$(jq -r .object.type <<<"$ref")
136+ tag_sha=$(jq -r .object.sha <<<"$ref")
137+ if test "$tag_type" = "tag"; then
138+ tag_sha=$(gh api "repos/${{ github.repository }}/git/tags/$tag_sha" --jq .object.sha)
139+ else
140+ test "$tag_type" = "commit"
141+ fi
142+ test "$tag_sha" = "$EXPECTED_SHA"
143+ {
144+ echo "release-tag=$EXPECTED_TAG"
145+ echo "release-sha=$EXPECTED_SHA"
146+ echo "release-verified=true"
147+ } >> "$GITHUB_OUTPUT"
148+
149+ select-release :
150+ name : Select one independently verified release identity
151+ needs :
152+ - release-please
153+ - verify-recovery
154+ if : >-
155+ always() &&
156+ github.run_attempt == 1 &&
157+ (
158+ (
159+ github.event_name == 'push' &&
160+ needs.release-please.result == 'success' &&
161+ needs.release-please.outputs.release-created == 'true' &&
162+ needs.release-please.outputs.release-verified == 'true'
163+ ) ||
164+ (
165+ github.event_name == 'workflow_dispatch' &&
166+ needs.verify-recovery.result == 'success'
167+ )
168+ )
169+ runs-on : ubuntu-latest
170+ timeout-minutes : 5
171+ outputs :
172+ release-sha : ${{ steps.select.outputs.release-sha }}
173+ release-tag : ${{ steps.select.outputs.release-tag }}
174+ permissions :
175+ contents : read
176+ steps :
177+ - name : Select the independently verified release identity
178+ id : select
179+ env :
180+ EVENT_NAME : ${{ github.event_name }}
181+ RECOVERY_SHA : ${{ needs.verify-recovery.outputs.release-sha }}
182+ RECOVERY_TAG : ${{ needs.verify-recovery.outputs.release-tag }}
183+ RELEASE_PLEASE_SHA : ${{ needs.release-please.outputs.release-sha }}
184+ RELEASE_PLEASE_TAG : ${{ needs.release-please.outputs.release-tag }}
185+ run : |
186+ case "$EVENT_NAME" in
187+ push)
188+ release_sha=$RELEASE_PLEASE_SHA
189+ release_tag=$RELEASE_PLEASE_TAG
190+ ;;
191+ workflow_dispatch)
192+ release_sha=$RECOVERY_SHA
193+ release_tag=$RECOVERY_TAG
194+ ;;
195+ *)
196+ exit 1
197+ ;;
198+ esac
199+ test -n "$release_sha"
200+ test -n "$release_tag"
201+ {
202+ echo "release-sha=$release_sha"
203+ echo "release-tag=$release_tag"
204+ } >> "$GITHUB_OUTPUT"
205+
27206 build :
28207 name : Verify and build the immutable default-branch release
29208 if : github.run_attempt == 1
209+ needs :
210+ - select-release
30211 runs-on : ubuntu-latest
31212 timeout-minutes : 25
32213 outputs :
@@ -38,16 +219,16 @@ jobs:
38219 - name : Check out the published release tag
39220 uses : actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
40221 with :
41- ref : refs/tags/${{ inputs .release-tag }}
222+ ref : refs/tags/${{ needs.select-release.outputs .release-tag }}
42223 fetch-depth : 0
43224 persist-credentials : false
44225 - name : Reject an untrusted release target
45226 id : trust
46227 env :
47- DEFAULT_BRANCH : ${{ inputs.default-branch }}
48- EXPECTED_RELEASE_SHA : ${{ inputs .release-sha }}
228+ DEFAULT_BRANCH : ${{ github.event.repository.default_branch }}
229+ EXPECTED_RELEASE_SHA : ${{ needs.select-release.outputs .release-sha }}
49230 RELEASE_IMMUTABLE : " true"
50- RELEASE_TAG : ${{ inputs .release-tag }}
231+ RELEASE_TAG : ${{ needs.select-release.outputs .release-tag }}
51232 run : bash scripts/verify_release_trust.sh
52233 - name : Set up Python
53234 uses : actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
60241 - name : Verify project, manifest, changelog, release docs, and tag agreement
61242 id : version
62243 env :
63- RELEASE_TAG : ${{ inputs .release-tag }}
244+ RELEASE_TAG : ${{ needs.select-release.outputs .release-tag }}
64245 run : |
65246 version=$(uv run python scripts/check_version.py --tag "$RELEASE_TAG" --require-changelog --require-releasable-docs --print-version)
66247 echo "version=$version" >> "$GITHUB_OUTPUT"
72253 run : uv build
73254 - name : Verify artifact versions against the tag
74255 env :
75- RELEASE_TAG : ${{ inputs .release-tag }}
256+ RELEASE_TAG : ${{ needs.select-release.outputs .release-tag }}
76257 run : uv run python scripts/check_version.py --tag "$RELEASE_TAG" --require-changelog --require-releasable-docs dist/*
77258 - name : Check package metadata rendering
78259 run : uv run twine check dist/*
0 commit comments