1212# 3. Creates a `release/vX.Y.Z` branch
1313# 4. Updates info.xml + CHANGELOG.md (promotes [Unreleased] block
1414# content into the new versioned entry; resets [Unreleased])
15- # 5. Commits + pushes the release branch (release/* is not a
16- # protected ref; main is)
15+ # 5. Commits + pushes the release branch using the RELEASE_TOKEN PAT
16+ # (release/* is not a protected ref; main is)
1717# 6. Opens a PR titled `chore(release): vX.Y.Z` with label `release`
18- # 7. Manually fires CI + integration workflows on the release branch
19- # so the PR's required-checks gate gets satisfied
2018#
21- # Why "manually fires" CI
22- # -----------------------
23- # Pushes and PRs created with the built-in GITHUB_TOKEN do NOT trigger
24- # downstream workflow runs (anti-loop protection in GitHub Actions).
25- # `workflow_dispatch` is the documented exception
26- # (https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication#using-the-github_token-in-a-workflow),
27- # so we add `workflow_dispatch:` to ci.yml + integration.yml and call
28- # them with `gh workflow run --ref release/vX.Y.Z`. This avoids the
29- # need for a long-lived PAT secret.
19+ # Why a PAT (RELEASE_TOKEN) is required
20+ # -------------------------------------
21+ # Pushes and PRs created with the built-in GITHUB_TOKEN are exempt
22+ # from triggering downstream workflow runs (anti-loop protection).
23+ # This means the resulting PR's required-checks rollup never gets
24+ # the CI/Integration check_runs, so branch protection blocks merge
25+ # even though every job actually passes when invoked manually.
26+ #
27+ # A user PAT (or fine-grained token) bypasses the anti-loop limit:
28+ # its `git push` and `gh pr create` calls fire the normal `push` /
29+ # `pull_request: opened` events, which trigger ci.yml + integration.yml
30+ # in the standard `pull_request` check_suite — exactly the suite that
31+ # branch protection consults.
32+ #
33+ # Setup (one-time)
34+ # ----------------
35+ # 1. Create a fine-grained PAT at
36+ # https://github.com/settings/personal-access-tokens/new
37+ # scoped to this repository, with these permissions:
38+ # Contents : Read and write
39+ # Pull requests : Read and write
40+ # Workflows : Read and write
41+ # Metadata : Read-only (auto-included)
42+ # 2. Add it as repo secret `RELEASE_TOKEN`
43+ # (Settings → Secrets and variables → Actions → New repository secret)
44+ # 3. Done — every Release — Prepare PR run will use it.
3045#
3146# Phase 2
3247# -------
3348# When the prep PR merges, release-publish.yml fires automatically,
3449# tags the merge commit, builds the App Store tarball, creates the
35- # GitHub Release, and uploads to the Nextcloud App Store.
50+ # GitHub Release, and uploads to the Nextcloud App Store. Phase 2
51+ # uses GITHUB_TOKEN; it does not need the PAT because it pushes a
52+ # tag (which is not covered by branch rulesets).
3653
3754name : Release — Prepare PR
3855
5875 - " true"
5976
6077permissions :
61- contents : write # push to release/* branch
62- pull-requests : write # gh pr create
63- actions : write # gh workflow run
78+ contents : write # push to release/* branch (only matters as a token-scope hint;
79+ # actual push uses RELEASE_TOKEN)
80+ pull-requests : write # gh pr create (likewise)
6481
6582jobs :
6683 prepare :
6784 name : Open release PR
6885 runs-on : ubuntu-latest
6986 steps :
87+ # -----------------------------------------------------------------------
88+ # 0. Guard — fail loudly if RELEASE_TOKEN is missing
89+ #
90+ # Failing in the first step is much friendlier than letting the workflow
91+ # bump the version, push the branch, and only THEN fail at PR creation
92+ # leaving a stale `release/vX.Y.Z` branch on origin.
93+ # -----------------------------------------------------------------------
94+ - name : Verify RELEASE_TOKEN secret is configured
95+ env :
96+ RELEASE_TOKEN : ${{ secrets.RELEASE_TOKEN }}
97+ run : |
98+ if [ -z "$RELEASE_TOKEN" ]; then
99+ cat <<'MSG' >&2
100+
101+ ::error::RELEASE_TOKEN secret is not set.
102+
103+ Phase 1 of the release flow needs a personal access token (PAT) to
104+ push and open the prep PR — pushes/PRs from the built-in GITHUB_TOKEN
105+ do not trigger CI in the pull_request check_suite, which leaves the
106+ PR's required-checks gate permanently unsatisfied.
107+
108+ One-time setup:
109+
110+ 1. Create a fine-grained PAT scoped to this repository at
111+ https://github.com/settings/personal-access-tokens/new
112+ with permissions:
113+ Contents : Read and write
114+ Pull requests : Read and write
115+ Workflows : Read and write
116+ 2. Add it as repo secret RELEASE_TOKEN
117+ (Settings → Secrets and variables → Actions)
118+
119+ See the header comment of .github/workflows/release-prepare.yml
120+ for the full rationale.
121+
122+ MSG
123+ exit 1
124+ fi
125+ echo "RELEASE_TOKEN is configured."
126+
70127 - name : Checkout main with full history
71128 uses : actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
72129 with :
73130 ref : main
74131 fetch-depth : 0
75- token : ${{ secrets.GITHUB_TOKEN }}
132+ # PAT, not GITHUB_TOKEN — see step 0's failure message for why.
133+ # actions/checkout configures git's credential helper from this
134+ # token, so subsequent `git push` calls in this job authenticate
135+ # as the PAT owner and trigger CI events normally.
136+ token : ${{ secrets.RELEASE_TOKEN }}
76137
77138 # -----------------------------------------------------------------------
78139 # 1. Read current version from appinfo/info.xml
@@ -206,6 +267,12 @@ jobs:
206267
207268 # -----------------------------------------------------------------------
208269 # 5. Commit on a new release/vX.Y.Z branch and push
270+ #
271+ # The push goes out as the RELEASE_TOKEN PAT owner (configured by
272+ # actions/checkout above), so GitHub fires `push` events normally
273+ # — and the matching `pull_request: opened` event in step 6 is
274+ # also unrestricted, which means CI fires automatically without
275+ # any explicit workflow_dispatch trick.
209276 # -----------------------------------------------------------------------
210277 - name : Create release branch and commit
211278 env :
@@ -230,6 +297,8 @@ jobs:
230297 # -----------------------------------------------------------------------
231298 - name : Ensure 'release' label exists
232299 env :
300+ # Using GITHUB_TOKEN is fine for label CRUD — labels are not
301+ # subject to the anti-loop rule, and gh CLI just needs auth.
233302 GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
234303 run : |
235304 # Idempotent — `--force` updates the label if it already exists.
@@ -242,7 +311,12 @@ jobs:
242311 - name : Open release PR
243312 id : pr
244313 env :
245- GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
314+ # PAT, NOT GITHUB_TOKEN — `gh pr create` authenticated as the
315+ # PAT owner fires the `pull_request: opened` event normally,
316+ # which triggers CI + Integration in the pull_request
317+ # check_suite. With GITHUB_TOKEN the event is suppressed and
318+ # the PR's required-checks gate stays unsatisfied.
319+ GH_TOKEN : ${{ secrets.RELEASE_TOKEN }}
246320 OLD : ${{ steps.current.outputs.current }}
247321 NEW : ${{ steps.new.outputs.version }}
248322 TAG : ${{ steps.new.outputs.tag }}
@@ -282,24 +356,6 @@ jobs:
282356 echo "Opened: $PR_URL"
283357 echo "url=$PR_URL" >> "$GITHUB_OUTPUT"
284358
285- # -----------------------------------------------------------------------
286- # 7. Trigger CI on the release branch via workflow_dispatch
287- #
288- # Pushes/PRs created with GITHUB_TOKEN do not trigger downstream
289- # workflows — workflow_dispatch IS the exception. ci.yml and
290- # integration.yml each declare a workflow_dispatch trigger; calling
291- # them here gives the PR's required-checks gate something to gate on
292- # without a PAT.
293- # -----------------------------------------------------------------------
294- - name : Trigger CI on the release branch
295- env :
296- GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
297- BRANCH : ${{ steps.new.outputs.branch }}
298- run : |
299- gh workflow run ci.yml --ref "$BRANCH"
300- gh workflow run integration.yml --ref "$BRANCH"
301- echo "Triggered ci.yml + integration.yml on $BRANCH"
302-
303359 - name : Print next steps
304360 env :
305361 PR_URL : ${{ steps.pr.outputs.url }}
0 commit comments