From 48f15a3bca9b653cb9c6eb61ae0551b6caa89bb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20Magyar?= Date: Thu, 7 May 2026 12:17:12 +0100 Subject: [PATCH] ci(release): use fine-grained PAT for rc tag push (#1407) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The default GITHUB_TOKEN cannot be granted `workflows: write`, so `git push --atomic` of the rc v-tag fails when its commit chain reaches any commit that modified `.github/workflows/**`. Symptom on the most recent run: ! [remote rejected] v1.6.4-rc.82 -> v1.6.4-rc.82 (refusing to allow a GitHub App to create or update workflow `.github/workflows/trivy.yml` without `workflows` permission) GitHub's rule: any ref-update that makes a workflow-modifying commit reachable through the new ref requires `workflows: write` on the identity performing the push, regardless of whether that commit is already on another remote ref. The default GITHUB_TOKEN cannot hold that permission. Pass a fine-grained PAT (RELEASE_PUSH_TOKEN, scoped to this repo with Contents: write + Workflows: write) into actions/checkout's `token` input so origin is preauthed for the subsequent `git push`. The job-level GITHUB_TOKEN keeps its scoped permissions for npm provenance and other steps. Required one-time setup: 1. Generate a fine-grained PAT - Resource owner: account that owns this repo - Repository access: Only select repositories → GitNexus - Permissions: Contents: write, Workflows: write, Metadata: read 2. Add as repo secret named RELEASE_PUSH_TOKEN 3. Re-run the failed Release Candidate workflow with force=true Considered and skipped: GitHub App approach (org-owned, bot identity, short-lived tokens). Better long-term, but a fine-grained PAT is acceptable at one-maintainer scale. Migration is mechanical if the project later wants to switch. Co-authored-by: Claude Opus 4.7 (1M context) --- .github/workflows/release-candidate.yml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index 5b598ee14..9ab00c0bd 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -123,7 +123,16 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 20 permissions: - contents: write # push rc tag + marker + # The default GITHUB_TOKEN cannot be granted `workflows: write`, so + # tag pushes that reach a commit which modified `.github/workflows/**` + # are rejected with: "refusing to allow a GitHub App to create or + # update workflow ... without `workflows` permission". We pass a + # fine-grained PAT (RELEASE_PUSH_TOKEN, scoped to this repo with + # Contents: write + Workflows: write) to `actions/checkout` so that + # the subsequent `git push --atomic` of the v-tag and rc marker + # carries the PAT's identity. Job-level GITHUB_TOKEN keeps its + # scoped permissions for everything else (npm provenance, etc.). + contents: write # push rc tag + marker (via PAT) id-token: write # npm provenance outputs: vtag: ${{ steps.reltag.outputs.vtag }} @@ -132,6 +141,11 @@ jobs: with: fetch-depth: 0 fetch-tags: true + # Use the PAT so `origin` is preauthed for `git push`. Without + # this the default GITHUB_TOKEN is wired into the remote, and a + # workflows-touching tag push is rejected — see the permissions + # block above. + token: ${{ secrets.RELEASE_PUSH_TOKEN }} - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: