22#
33# Changelog generation is handled by release-plz + git-cliff.
44#
5- # Pipeline (release-pr and release both run on every push to main, independently):
6- # release-pr → creates/updates the single Release PR
7- # release → on a Release PR merge, publishes crates + tags and creates the
8- # app-authored zebrad GitHub Release; downstream workflows fire
9- # from that release event (exits early on non-release commits)
5+ # Pipeline:
6+ # push to main → creates/updates the single Release PR
7+ # merged, approved Release PR → publishes crates + tags and creates the
8+ # app-authored zebrad GitHub Release
109#
1110# Downstream triggers (independent workflows, not managed here):
1211# release:released → release-binaries.yml (Docker Hub)
@@ -16,6 +15,9 @@ name: Release
1615on :
1716 push :
1817 branches : [main]
18+ pull_request :
19+ branches : [main]
20+ types : [closed]
1921
2022# Deny all at workflow level — each job declares minimum permissions
2123permissions : {}
2830 # generates per-crate changelogs, and opens/updates a single Release PR.
2931 release-pr :
3032 name : Create or update Release PR
33+ if : github.event_name == 'push'
3134 runs-on : ubuntu-latest
3235 # Avoid racing updates to the single release-plz Release PR branch when
3336 # multiple commits land on main while a previous release-pr job is running.
6164
6265 # ── Stage 2: Publish crates and create the public zebrad release ────────
6366 #
64- # Runs on every push to main, but exits early for non-Release-PR commits
65- # (release_always = false in .release-plz.toml). When a Release PR merge
66- # is detected :
67+ # Runs only when an approved Release PR from the release-plz branch is
68+ # merged into main. The merged commit is the immutable release target.
69+ # The job :
6770 # 1. Publishes changed crates to crates.io in dependency order
6871 # 2. Uses trusted publishing (OIDC) — no static API tokens
6972 # 3. Creates per-crate git tags
@@ -73,17 +76,48 @@ jobs:
7376 # with a GitHub App installation token because release events created by the
7477 # repository GITHUB_TOKEN do not trigger downstream workflows.
7578 #
76- # No `needs: release-pr`: this job self-detects the merge and must publish even
77- # when release-pr's concurrency group cancels its superseded pending run.
7879 release :
7980 name : Publish crates and GitHub Release
81+ if : >-
82+ github.event_name == 'pull_request' &&
83+ github.event.pull_request.merged == true &&
84+ github.event.pull_request.head.repo.full_name == github.repository &&
85+ startsWith(github.event.pull_request.head.ref, 'release-plz-') &&
86+ contains(github.event.pull_request.labels.*.name, 'A-release')
8087 runs-on : ubuntu-latest
8188 permissions :
8289 contents : write
8390 id-token : write
8491 pull-requests : read
8592 environment : release
93+ concurrency :
94+ group : release-publish
95+ cancel-in-progress : false
8696 steps :
97+ - name : Verify approved Release PR
98+ env :
99+ GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
100+ PR_NUMBER : ${{ github.event.pull_request.number }}
101+ REPOSITORY : ${{ github.repository }}
102+ TARGET_SHA : ${{ github.event.pull_request.merge_commit_sha }}
103+ run : |
104+ set -euo pipefail
105+
106+ RELEASE_PR="$(gh pr view "${PR_NUMBER}" \
107+ --repo "${REPOSITORY}" \
108+ --json baseRefName,headRefName,labels,mergeCommit,reviewDecision,state)"
109+
110+ if ! jq -e --arg target_sha "${TARGET_SHA}" '
111+ .state == "MERGED" and
112+ .reviewDecision == "APPROVED" and
113+ .baseRefName == "main" and
114+ (.headRefName | startswith("release-plz-")) and
115+ .mergeCommit.oid == $target_sha and
116+ any(.labels[]; .name == "A-release")
117+ ' <<< "${RELEASE_PR}" >/dev/null; then
118+ echo "::error::PR #${PR_NUMBER} is not an approved A-release Release PR merged into main at ${TARGET_SHA}."
119+ exit 1
120+ fi
87121 - name : Generate release app token
88122 id : app-token
89123 if : vars.RELEASE_APP_ID != ''
@@ -97,27 +131,10 @@ jobs:
97131 with :
98132 fetch-depth : 0
99133 persist-credentials : false
134+ ref : ${{ github.event.pull_request.merge_commit_sha }}
100135 token : ${{ steps.app-token.outputs.token || secrets.GITHUB_TOKEN }}
101- - name : Detect Release PR merge
102- id : release-pr
103- env :
104- GH_TOKEN : ${{ steps.app-token.outputs.token || secrets.GITHUB_TOKEN }}
105- REPOSITORY : ${{ github.repository }}
106- TARGET_SHA : ${{ github.sha }}
107- run : |
108- set -euo pipefail
109-
110- ASSOCIATED_PRS="$(gh api \
111- -H "Accept: application/vnd.github+json" \
112- "/repos/${REPOSITORY}/commits/${TARGET_SHA}/pulls")"
113-
114- if jq -e 'map(select(.head.ref | startswith("release-plz-"))) | length > 0' <<< "${ASSOCIATED_PRS}" >/dev/null; then
115- echo "merged=true" >> "${GITHUB_OUTPUT}"
116- else
117- echo "merged=false" >> "${GITHUB_OUTPUT}"
118- fi
119136 - name : Validate release app token
120- if : steps.release-pr.outputs.merged == 'true' && steps. app-token.outputs.token == ''
137+ if : steps.app-token.outputs.token == ''
121138 run : |
122139 echo "::error::Configure RELEASE_APP_ID and RELEASE_APP_PRIVATE_KEY before merging a Release PR. The app token is required before crates publish so the zebrad GitHub Release can trigger Docker and GCP workflows."
123140 exit 1
@@ -158,6 +175,11 @@ jobs:
158175 echo "released=true"
159176 echo "version=${VERSION}"
160177 echo "tag=${TAG}"
178+ if [[ "${VERSION}" == *-* ]]; then
179+ echo "prerelease=true"
180+ else
181+ echo "prerelease=false"
182+ fi
161183 } >> "${GITHUB_OUTPUT}"
162184 - uses : actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 # v1.16.1
163185 if : steps.zebrad-release.outputs.released == 'true'
@@ -281,26 +303,32 @@ jobs:
281303 env :
282304 GH_TOKEN : ${{ steps.app-token.outputs.token }}
283305 REPOSITORY : ${{ github.repository }}
284- TARGET_SHA : ${{ github.sha }}
306+ TARGET_SHA : ${{ github.event.pull_request.merge_commit_sha }}
285307 TAG : ${{ steps.zebrad-release.outputs.tag }}
308+ PRERELEASE : ${{ steps.zebrad-release.outputs.prerelease }}
286309 run : |
287310 set -euo pipefail
288311
289312 notes_file="${RUNNER_TEMP}/release-notes.final.md"
313+ release_flags=(--latest --prerelease=false)
314+
315+ if [ "${PRERELEASE}" = "true" ]; then
316+ release_flags=(--latest=false --prerelease)
317+ fi
290318
291319 if gh release view "${TAG}" --repo "${REPOSITORY}" >/dev/null 2>&1; then
292320 gh release edit "${TAG}" \
293321 --repo "${REPOSITORY}" \
294322 --title "Zebra ${TAG}" \
295323 --notes-file "${notes_file}" \
296324 --target "${TARGET_SHA}" \
297- --latest
325+ "${release_flags[@]}"
298326 else
299327 gh release create "${TAG}" \
300328 --repo "${REPOSITORY}" \
301329 --title "Zebra ${TAG}" \
302330 --notes-file "${notes_file}" \
303331 --target "${TARGET_SHA}" \
304332 --verify-tag \
305- --latest
333+ "${release_flags[@]}"
306334 fi
0 commit comments