Skip to content

Commit 2d0a86b

Browse files
fix(release): guard publishing and preserve release channels (#10977)
* fix(release): publish only approved release PRs * fix(release): preserve release channel metadata
1 parent c5f3ab6 commit 2d0a86b

1 file changed

Lines changed: 60 additions & 32 deletions

File tree

.github/workflows/release.yml

Lines changed: 60 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@
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
1615
on:
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
2123
permissions: {}
@@ -28,6 +30,7 @@ jobs:
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.
@@ -61,9 +64,9 @@ jobs:
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

Comments
 (0)