feat(ci): mint an App token for the presentation-branch refresh - #44
Merged
Conversation
Refreshing `main` under GITHUB_TOKEN cannot work. GitHub rejects a push from a GitHub App identity that creates or updates any file under .github/workflows/ unless the installation holds the `workflows` permission; GITHUB_TOKEN is such an identity, and the workflow permissions: block exposes no `workflows` scope to grant it. The refresh resets `main` to the release tag, and that diff carries workflow files whenever `main` has fallen behind — seven of them at the time of writing. Observed in run 32065009956, dispatched right after #43 fixed the empty `from_branch`: ! [remote rejected] 57450b5 -> main (refusing to allow a GitHub App to create or update workflow `.github/workflows/release-cd-refresh-master.yml` without `workflows` permission) `main` carries no branch protection at all, so this is not a protection problem and no permissions: change can lift it. Forward vars.PORTFOLIO_APP_ID and secrets.PORTFOLIO_APP_PRIVATE_KEY so the reusable mints a short-lived installation token for the push. This reverses the reasoning removed in b16fc1d, which called the App wiring an audit-trail improvement deferred until gh-plumbing#330 Phase 2. That was wrong for this repository: the wiring is the only path that works today. PREREQUISITE, NOT YET VERIFIED: the installation behind PORTFOLIO_APP_ID must hold both `contents: write` and `workflows: write` on this repository. The reusable's mint step is continue-on-error, which only covers a failed mint — a successful mint with insufficient permissions has no fallback to secrets.token and fails the push. Confirm the installation permissions before merging; this PR stays draft until then. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A4KA8swD1ZzDqknDV5TUxx
This was referenced Aug 17, 2026
Review found the App wiring degrades silently. The reusable's mint step is `continue-on-error`, so if `PORTFOLIO_APP_ID` is set but `PORTFOLIO_APP_PRIVATE_KEY` is missing or stale — a key rotated in the App settings without updating the repo secret — the mint fails, the error is swallowed, `outputs.token` is empty, and the push falls back to GITHUB_TOKEN. It then dies with exactly the `refusing to allow a GitHub App to create or update workflow ...` rejection this PR wires the App token in to avoid, while the real cause appears only as an annotation on a step that didn't fail. Check both values in the existing validate_ref job, which already runs on both trigger paths. Both set or both empty pass; any mismatch fails with a message naming which half is missing. Verified by extracting the step's script and running the four combinations: true/true and false/false pass, true/false and false/true fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A4KA8swD1ZzDqknDV5TUxx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Refreshing
mainunderGITHUB_TOKENcannot work at all. GitHub rejects a pushfrom a GitHub App identity that creates or updates any file under
.github/workflows/unless the installation holds theworkflowspermission —and
GITHUB_TOKENis such an identity. Forward the portfolio App credentials sothe reusable mints a short-lived installation token for the push instead.
Changes
app-id: ${{ vars.PORTFOLIO_APP_ID }}andsecrets.app-private-key: ${{ secrets.PORTFOLIO_APP_PRIVATE_KEY }}toreusable-release-cd-refresh-master.yml.the permission set the installation must hold.
Linked issues
None
Testing
task --yes lint— all pre-commit hooks pass.python3 -c "yaml.safe_load(...)"— parses; the job now carriessecrets: [token, app-private-key]andwith: [target_branch, from_branch, app-id], matching the reusable's declared signature atd51e51e.https://github.com/nolte/taskfiles/actions/runs/32065009956 dispatched right
after fix(ci): make the refresh-master manual dispatch usable #43:
FROM_REF: v0.1.5resolved correctly and the push was attempted,then rejected with
refusing to allow a GitHub App to create or update workflow .github/workflows/release-cd-refresh-master.yml without 'workflows' permission.git diff --name-only origin/main 57450b5 -- .github/workflows/:seven workflow files differ between
mainand the release tag, so the resetnecessarily writes under
.github/workflows/.gh api repos/nolte/taskfiles/branches/main/protection→404 Branch not protected: the rejection is not a branch-protection effect.permissions: the settable scopelist carries no
workflowsentry, so nopermissions:change can grant it.PORTFOLIO_APP_ID(3784868) holdscontents: writeandworkflows: writeon this repository.
gh api /user/installationsreturns 403 without Appauthentication, so it can only be read from the App's settings page.
Risk / rollout notes
Medium, and gated. The change swaps the push identity on both the automatic
release: publishedpath and the manual dispatch path.time and
maincatches up.continue-on-errorfalls back tosecrets.tokenand behaviour is exactlytoday's — still broken, no worse.
the push fails. This is why the PR stays draft until the installation
permissions are confirmed.
Second effect, deliberate: App-token pushes do cascade downstream, which is the
point of the remediation.
build-static-tests.yamltriggers on an unfilteredon: push:, so each successful refresh ofmainwill additionally startpre-commit, trivy and chain-bench there, and
automerge.yamltriggers oncheck_suite. If that noise is unwanted, the fix is a branch filter inbuild-static-tests.yaml, not reverting this.The platform constraint behind this PR is recorded portfolio-wide in
nolte/claude-shared#550,
spec/project/workflow-health/§Known platformconstraints.
Originating source: workflow-health triage of the red
release-cd-refresh-master.ymlrun https://github.com/nolte/taskfiles/actions/runs/32065009956Dispatched specialist: no matching specialist existed — generalist handled