@@ -2,11 +2,17 @@ name: Release
22
33on :
44 push :
5- # Ignore merge queue branches (matches primer/react configuration)
6- branches-ignore :
7- - ' gh-readonly-queue/**'
5+ # Release branches only.
6+ branches :
7+ - ' main'
8+ - ' next-minor'
9+ - ' changeset-release/main'
10+ - ' changeset-release/next-minor'
811 tags-ignore :
912 - ' **'
13+ pull_request :
14+ # Canary releases are opt-in via the "release canary" label.
15+ types : [labeled]
1016 workflow_dispatch :
1117
1218concurrency :
@@ -200,55 +206,149 @@ jobs:
200206 })
201207
202208 release-canary :
203- if : github.repository == 'primer/brand' && github.ref_name != 'main' && github.ref_name != 'next-minor' && github.ref_name != 'changeset-release/main' && github.ref_name != 'changeset-release/next-minor'
204-
209+ # Runs only when all of the following are true:
210+ # - Event is a pull request label event for "release canary"
211+ # - PR is currently open
212+ # - PR branch comes from this repository (not a fork)
213+ # - PR head branch is not one of the release branches
214+ if : >-
215+ github.event_name == 'pull_request' &&
216+ github.repository == 'primer/brand' &&
217+ github.event.action == 'labeled' &&
218+ github.event.label.name == 'release canary' &&
219+ github.event.pull_request.state == 'open' &&
220+ github.event.pull_request.head.repo.full_name == github.repository &&
221+ github.head_ref != 'main' &&
222+ github.head_ref != 'next-minor' &&
223+ github.head_ref != 'changeset-release/main' &&
224+ github.head_ref != 'changeset-release/next-minor'
205225 name : Canary
206226 runs-on : ubuntu-latest
227+ permissions :
228+ id-token : write
229+ contents : write
230+ issues : write
231+ pull-requests : write
232+ checks : write
233+ statuses : write
207234 steps :
235+ # Re-check the live PR state and labels to prevent stale event payloads
236+ # or workflow re-runs.
237+ - name : Verify canary publish is valid
238+ id : canary-gate
239+ uses : actions/github-script@v4.0.2
240+ with :
241+ script : |
242+ const labelName = 'release canary'
243+ const {owner, repo} = context.repo
244+ const pull_number = context.payload.pull_request.number
245+
246+ const {data: pullRequest} = await github.pulls.get({
247+ owner,
248+ repo,
249+ pull_number
250+ })
251+
252+ const hasLabel = pullRequest.labels.some((label) => label.name === labelName)
253+ const isOpen = pullRequest.state === 'open'
254+ const shouldPublish = hasLabel && isOpen
255+
256+ core.setOutput('should_publish', shouldPublish ? 'true' : 'false')
257+
258+ if (shouldPublish) {
259+ core.info(`Canary publish is authorized for PR #${pull_number}`)
260+ return
261+ }
262+
263+ core.info(`Skipping canary publish because PR #${pull_number} is ${pullRequest.state} or missing label "${labelName}"`)
264+
208265 - name : Checkout repository
266+ if : steps.canary-gate.outputs.should_publish == 'true'
209267 uses : actions/checkout@v4
210268 with :
211- # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
269+ # Pin to the labeled-event head SHA so publish and status refer to the same commit.
270+ repository : ${{ github.event.pull_request.head.repo.full_name }}
271+ ref : ${{ github.event.pull_request.head.sha }}
212272 fetch-depth : 0
213273 persist-credentials : false
214274
215275 - name : Set up Node
276+ if : steps.canary-gate.outputs.should_publish == 'true'
216277 uses : actions/setup-node@v6
217278 with :
218279 node-version : 24.14.1
219280
220281 - name : Install dependencies
282+ if : steps.canary-gate.outputs.should_publish == 'true'
221283 run : npm ci
222284
223285 - uses : actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42
286+ if : steps.canary-gate.outputs.should_publish == 'true'
224287 id : app-token
225288 with :
226289 app-id : ${{ vars.PRIMER_APP_ID_SHARED }}
227290 private-key : ${{ secrets.PRIMER_APP_PRIVATE_KEY_SHARED }}
228291
229292 - name : Build
293+ if : steps.canary-gate.outputs.should_publish == 'true'
230294 run : npm run build:lib
231295
232296 - name : Publish canary release
297+ id : publish-canary
298+ if : steps.canary-gate.outputs.should_publish == 'true'
233299 run : |
234300 npm exec --workspaces -- ../../packages/repo-configs/scripts/prepare-canary
235301 npx changeset publish --tag canary
236302 env :
237303 GITHUB_TOKEN : ${{ steps.app-token.outputs.token }}
238304
239305 - name : Output canary version
306+ if : steps.canary-gate.outputs.should_publish == 'true'
240307 uses : actions/github-script@v4.0.2
241308 with :
242309 script : |
310+ const sha = context.payload.pull_request.head.sha
243311 const paths = ['react', 'design-tokens', 'css'].forEach((path) => {
244312 const package = require(`${process.env.GITHUB_WORKSPACE}/packages/${path}/package.json`)
245313 github.repos.createCommitStatus({
246314 owner: context.repo.owner,
247315 repo: context.repo.repo,
248- sha: context.sha ,
316+ sha,
249317 state: 'success',
250318 context: `Published ${package.name}`,
251319 description: package.version,
252320 target_url: `https://unpkg.com/${package.name}@${package.version}/`
253321 })
254322 })
323+
324+ - name : Remove canary label on success
325+ # Treat the label as a one-time publish request. Maintainers can
326+ # re-add it later when they explicitly want a fresh canary.
327+ if : always() && steps.canary-gate.outputs.should_publish == 'true' && steps.publish-canary.outcome == 'success'
328+ uses : actions/github-script@v4.0.2
329+ with :
330+ script : |
331+ const labelName = 'release canary'
332+ const {owner, repo} = context.repo
333+ const issue_number = context.payload.pull_request.number
334+
335+ try {
336+ await github.issues.removeLabel({
337+ owner,
338+ repo,
339+ issue_number,
340+ name: labelName
341+ })
342+ core.info(`Removed label "${labelName}" from PR #${issue_number}`)
343+ } catch (error) {
344+ if (error.status === 404) {
345+ core.info(`Label "${labelName}" was already removed from PR #${issue_number}`)
346+ return
347+ }
348+
349+ throw error
350+ }
351+
352+ - name : Skip canary publish
353+ if : steps.canary-gate.outputs.should_publish != 'true'
354+ run : echo 'Skipping canary because the PR is closed or no longer has the "release canary" label.'
0 commit comments