From 0ecba6a2d5fb56314df79408aa91de61a696b6f6 Mon Sep 17 00:00:00 2001 From: JJ Fullmer Date: Mon, 17 Aug 2026 22:39:12 -0600 Subject: [PATCH] Ask for the token permission this workflow actually needs fogproject's merge stub calls this workflow and grants `permissions: contents: read`, which is correct -- nothing on that path needs a writable GITHUB_TOKEN. The call was refused before a single job started: The workflow is requesting 'contents: write', but is only allowed 'contents: read'. A reusable workflow's `permissions:` is a REQUEST, not a ceiling, and GitHub rejects the call outright when the caller grants less rather than quietly capping it. Both of the stub's first two real merges ended in startup_failure -- no jobs, no logs, just "this run likely failed because of a workflow file issue", which is a hard failure to read backwards. The `contents: write` this asked for has been vestigial since #17. The checkout, the commit, the push and the badge write all run on GitHub App tokens now; `grep github.token` over this file returns nothing but the comment explaining why the badge stopped using it. So the request was for a token nothing here holds or uses, and its only real effect was to force every caller to grant write too. Lowering it to `read` fixes the caller without widening anyone's permissions, which is the right direction: the alternative is making fogproject grant write for a token it never exercises. The comment says not to raise it back, and why -- "be safe, ask for write" is the natural instinct and it is the exact change that breaks every caller at startup. If a step here ever does need the ambient token, it should get its own scoped App token, the way the badge step already does. Adds the same point to CLAUDE.md, next to the two trigger rules. Three distinct gotchas have now cost a round trip each on this one integration: whether the bot can raise its own trigger, which ref a trigger is read from, and this one. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01PqufBbuckux8kitJeW3uAK --- .../update-lang-fix-psr-and-sync-version.yml | 23 ++++++++++++++++++- CLAUDE.md | 6 +++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/.github/workflows/update-lang-fix-psr-and-sync-version.yml b/.github/workflows/update-lang-fix-psr-and-sync-version.yml index 5cf3c8f..279ada9 100644 --- a/.github/workflows/update-lang-fix-psr-and-sync-version.yml +++ b/.github/workflows/update-lang-fix-psr-and-sync-version.yml @@ -114,7 +114,28 @@ concurrency: cancel-in-progress: false permissions: - contents: write + # `read`, and deliberately not `write`, because nothing here uses the ambient + # GITHUB_TOKEN any more: the checkout, the commit/push and the version badge + # all run on GitHub App tokens. What this line really controls now is who can + # CALL this workflow. + # + # A reusable workflow's `permissions:` is a REQUEST, and GitHub refuses the + # call outright when a caller grants less -- it is a hard startup error, not a + # silent capping: + # + # The workflow is requesting 'contents: write', but is only allowed + # 'contents: read'. + # + # No jobs run, no logs are produced, and the run just shows "a workflow file + # issue" -- which is how fogproject's merge stub failed on its first two real + # merges while asking for the correct, least-privilege `contents: read`. + # + # So do not raise this back to `write` to be safe. Raising it forces every + # caller to grant write for a token none of them use, and breaks at startup + # any caller that does not. If a step here ever genuinely needs the ambient + # token, give that step its own App token instead, the way the badge step + # does. + contents: read env: # Pinned, not "latest". An unpinned formatter is a second source of drift: diff --git a/CLAUDE.md b/CLAUDE.md index df9db1e..b6d0e60 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -179,6 +179,12 @@ don't "simplify" those back to the default `GITHUB_TOKEN` just because the call `workflow_call`/`workflow_dispatch`, and `pull_request: types: [closed]` (a merge — which the bot's direct push is not) are all safe for the same reason, and all three are in use. The test to apply to a new trigger is that one question, not whether it happens to be a cron. +- A reusable workflow's `permissions:` block is a **request**, and a caller granting less is a + hard startup error — `The workflow is requesting 'contents: write', but is only allowed + 'contents: read'` — not a silent capping. No jobs run and no logs are written, so it surfaces + only as "a workflow file issue". Keep a reusable workflow's request as low as it genuinely + needs (these workflows do their writing with App tokens, so `contents: read` is usually + right); raising it forces every caller to grant the same, and breaks the ones that don't. - Separately from that safety question, check **which ref a trigger is read from** before relying on it. Most events — including `pull_request_target`, `schedule` and `workflow_dispatch` — are read only from the repository's default branch, so a workflow file