Skip to content

Commit 04861bf

Browse files
author
CometAPI
committed
fix: preserve PyPI publisher workflow identity
1 parent 2e96574 commit 04861bf

13 files changed

Lines changed: 616 additions & 532 deletions

.github/workflows/publish.yml

Lines changed: 191 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
name: Publish immutable release
22

33
on:
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

@@ -24,9 +26,188 @@ env:
2426
UV_VERSION: 0.11.8
2527

2628
jobs:
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
@@ -60,7 +241,7 @@ jobs:
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"
@@ -72,7 +253,7 @@ jobs:
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/*

.github/workflows/release-please.yml

Lines changed: 0 additions & 91 deletions
This file was deleted.

0 commit comments

Comments
 (0)