Skip to content

Commit 5ef9b8b

Browse files
schubydooclaude
andauthored
ci(release): mint the publish token with workflows:write (#1787)
## What The v1.3.3 publish failed with `403 Resource not accessible by integration` on knope's `POST /releases`. Renovate merged a `.github/workflows` change to `main` (PR 1785) between the release-PR merge and the publish job. knope sends the checked-out release commit as `target_commitish`. That commit's workflow files then differ from `main`, and GitHub refuses to create the tag without the Workflows permission. A manual `workflow_dispatch` on the `main` tip published v1.3.3 with the unchanged token, which confirms the cause. The maintainer granted the clauster-ci App "Workflows: Read and write". This PR adds `permission-workflows: write` to the `app-token` mint in the `release` job, moves that mint to the end of the job, and corrects the nearby comments. Risk: the token reaches only the verify step's `if:` and `knope release`. knope's `release` workflow has one `Release` step and no `Command` steps, and the job runs no `git push`. A compromised SHA-pinned action that runs after the mint could in theory use the wider scope to push a workflow. The token lives for one hour at most and is revoked at job end. To shrink that window, the second commit moves the mint and its empty-token check to just before "Create + publish the GitHub release", after the download, attest, Sigstore and knope-install steps. The cost: a broken secret now fails after signing instead of before it. Nothing publishes in either case. ## Checklist - [x] PR title follows Conventional Commits. - [x] `no-changelog` label applied (CI only). - [x] Tests: not applicable, workflow-only change. `uvx zizmor` reports no findings and `actionlint` with shellcheck passes. - [x] Docs: no user-facing behavior or config change. - [x] `just check` and the pre-commit hooks pass locally. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Schuby <12485317+schubydoo@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 0eb47aa commit 5ef9b8b

1 file changed

Lines changed: 32 additions & 22 deletions

File tree

‎.github/workflows/knope-release.yml‎

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -106,25 +106,6 @@ jobs:
106106
id-token: write # Sigstore keyless signing (OIDC)
107107
attestations: write # actions/attest records SLSA build provenance
108108
steps:
109-
- name: Mint GitHub App token
110-
id: app-token
111-
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
112-
with:
113-
client-id: ${{ secrets.CLAUSTER_CI_APP_CLIENT_ID }}
114-
private-key: ${{ secrets.CLAUSTER_CI_APP_PRIVATE_KEY }}
115-
permission-contents: write
116-
# Fail LOUD on an empty mint: knope uses this token to create the GitHub
117-
# Release and push the tag, and an App-identity tag is what lets the publish
118-
# trigger downstream automation. An empty token would degrade to GITHUB_TOKEN
119-
# (whose tag push does not re-trigger workflows) or fail confusingly. Surface
120-
# it here. (See issue #509.)
121-
- name: Verify App token was minted
122-
if: ${{ steps.app-token.outputs.token == '' }}
123-
run: |
124-
echo "::error::clauster-ci App token mint produced an empty token; \
125-
refusing to proceed. Check the CLAUSTER_CI_APP_CLIENT_ID / \
126-
CLAUSTER_CI_APP_PRIVATE_KEY secrets and the clauster-ci App installation."
127-
exit 1
128109
# Check out the squash commit that landed on main (merge_commit_sha), NOT the
129110
# orphaned PR-head — knope must tag a commit that is actually on main. Full
130111
# history + tags so knope can compare the bumped version against the last
@@ -257,9 +238,9 @@ jobs:
257238
echo "staged $(find dist/release-assets -type f | wc -l) assets for v${VERSION}"
258239
# A SEPARATE, read-only mint for the third-party download step. The binary
259240
# fetch needs public read only, so it must not carry the app-token's
260-
# contents:write scope (this job also holds id-token:write for Sigstore).
261-
# Same secrets as that mint, so the earlier "Verify App token was minted"
262-
# step already fails the job loud if they are broken.
241+
# contents:write and workflows:write scopes (this job also holds
242+
# id-token:write for Sigstore). It uses the same secrets as the app-token
243+
# mint below, and create-github-app-token fails the step if they are broken.
263244
- name: Mint download-scoped GitHub App token
264245
id: download-token
265246
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
@@ -277,6 +258,35 @@ jobs:
277258
# was exhausted by concurrent runs and 403'd the same release-asset fetch
278259
# in knope-prepare on 2026-09-03. The same limit can stop a publish here.
279260
github-token: ${{ steps.download-token.outputs.token }}
261+
# The write-scoped App token for knope. Minted LAST, after the third-party
262+
# download, attest, Sigstore and knope-install steps, so the contents:write +
263+
# workflows:write token exists only while knope runs.
264+
- name: Mint GitHub App token
265+
id: app-token
266+
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
267+
with:
268+
client-id: ${{ secrets.CLAUSTER_CI_APP_CLIENT_ID }}
269+
private-key: ${{ secrets.CLAUSTER_CI_APP_PRIVATE_KEY }}
270+
permission-contents: write
271+
# knope tags the checked-out release commit. If main moved on with a
272+
# .github/workflows change before this job ran, that commit's workflow
273+
# files differ from main. GitHub then refuses POST /releases, the call
274+
# that creates the tag (403 "Resource not accessible by integration"),
275+
# unless the token carries workflows:write.
276+
# Seen on v1.3.3, 2026-11-02.
277+
permission-workflows: write
278+
# Fail LOUD on an empty mint: knope uses this token to create the GitHub
279+
# Release and push the tag, and an App-identity tag is what lets the publish
280+
# trigger downstream automation. An empty token would degrade to GITHUB_TOKEN
281+
# (whose tag push does not re-trigger workflows) or fail confusingly. Surface
282+
# it here. (See issue #509.)
283+
- name: Verify App token was minted
284+
if: ${{ steps.app-token.outputs.token == '' }}
285+
run: |
286+
echo "::error::clauster-ci App token mint produced an empty token; \
287+
refusing to proceed. Check the CLAUSTER_CI_APP_CLIENT_ID / \
288+
CLAUSTER_CI_APP_PRIVATE_KEY secrets and the clauster-ci App installation."
289+
exit 1
280290
# knope creates the draft, uploads dist/release-assets/*, and publishes. The
281291
# App token (not GITHUB_TOKEN) is what lets the published tag trigger
282292
# release.yml. If the version already matches the latest release tag (e.g. a

0 commit comments

Comments
 (0)