From 6e2a7fc1b847ed7c474a682e2d45bb5851c1bff1 Mon Sep 17 00:00:00 2001 From: Neil Galvin Date: Fri, 18 Sep 2026 13:24:05 +0100 Subject: [PATCH] fix(auto-revert): handle workflow-file commits instead of failing on them auto-revert pushes with GITHUB_TOKEN, which GitHub categorically forbids from creating or updating anything under .github/workflows/. `workflows` is not among the grantable `permissions:` scopes, so this was never a misconfiguration anyone could correct -- the capability does not exist for that token. The failure mode was poor. The push was rejected only after the revert commit had been made, so the job died with refusing to allow a GitHub App to create or update workflow .github/workflows/ci.yml without `workflows` permission which reads like a setup error, and buried the CI failure it was reacting to under a second red check. Now the job detects the case before committing and opens an issue instead, so the regression is still surfaced loudly and the check stays green. Callers need `issues: write`; the example stub is updated. Two latent bugs surfaced while fixing it. The path check must diff against the first parent: `git diff-tree -r ` lists nothing for a merge commit, and the bad SHA is usually a merge, so the naive check would never have fired on the case that matters. And it follows that auto-revert has never once succeeded against a workflow-file regression in any consumer. For full automation, `revert-app-client-id` plus a `REVERT_APP_PRIVATE_KEY` secret mints a scoped installation token with permission-workflows: write and pushes with that. Both optional; unset keeps today behaviour plus the issue fallback. The docs call for a dedicated App rather than reusing deps-reader, which is deliberately contents:read only -- a credential that can rewrite workflow files can rewrite CI itself. Verified: actionlint clean; the run: block extracted and shellcheck -S warning clean; the heredoc rendered and its output inspected; diff-vs-first-parent checked against a real merge commit, where diff-tree -r returns 0 paths and diff ^1 correctly returns .github/workflows/ci.yml. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01EoicTZniGXry3W7YFZBsSM --- .../workflows/auto-revert-on-main-failure.yml | 89 ++++++++++++++++++- CHANGELOG.md | 29 ++++++ examples/README.md | 29 ++++++ 3 files changed, 145 insertions(+), 2 deletions(-) diff --git a/.github/workflows/auto-revert-on-main-failure.yml b/.github/workflows/auto-revert-on-main-failure.yml index c1708c6..96b1ba4 100644 --- a/.github/workflows/auto-revert-on-main-failure.yml +++ b/.github/workflows/auto-revert-on-main-failure.yml @@ -34,6 +34,33 @@ on: description: "Runner label(s) as a JSON array string (parsed with fromJSON). Pass '[\"self-hosted\", \"linux\", \"x64\"]' to use the self-hosted pool." type: string default: '["ubuntu-latest"]' + revert-app-client-id: + description: >- + Optional. Client ID of a GitHub App whose installation has + Workflows: write, used to push the revert branch. + + GITHUB_TOKEN — the default — is issued by GitHub's own Actions App and + is categorically forbidden from creating or updating any file under + .github/workflows/. There is no `permissions:` key that grants it; + pushing a revert of a commit that touched a workflow file fails with + "refusing to allow a GitHub App to create or update workflow ... + without `workflows` permission". So without this input a workflow-file + regression cannot be reverted automatically. The job opens an issue + instead of failing (see below). + + Set this, with the `REVERT_APP_PRIVATE_KEY` secret, to revert those + commits too. Use a dedicated App: a token that can rewrite workflow + files can rewrite CI itself, so it wants its own key and its own + installation rather than sharing one with deps-reader. + type: string + default: "" + secrets: + REVERT_APP_PRIVATE_KEY: + description: >- + Private key of the App named by `revert-app-client-id`. Required only + when that input is set; without it the job falls back to GITHUB_TOKEN + and opens an issue for workflow-file regressions instead of reverting. + required: false jobs: open-revert-pr: @@ -42,16 +69,32 @@ jobs: permissions: contents: write pull-requests: write + # issues: write is the fallback path — when the bad commit touched a + # workflow file and no App is configured, the job opens an issue rather + # than failing on a push GITHUB_TOKEN is never allowed to make. + issues: write steps: + - name: Mint revert token + id: revert-token + if: inputs.revert-app-client-id != '' + uses: nkg/github-actions/.github/actions/setup-token@v3 + with: + app-client-id: ${{ inputs.revert-app-client-id }} + app-private-key: ${{ secrets.REVERT_APP_PRIVATE_KEY }} + permission-contents: write + permission-pull-requests: write + permission-workflows: write + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 ref: ${{ inputs.default-branch }} - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ steps.revert-token.outputs.token || secrets.GITHUB_TOKEN }} - name: Open revert PR env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ steps.revert-token.outputs.token || secrets.GITHUB_TOKEN }} + HAVE_WORKFLOW_SCOPE: ${{ inputs.revert-app-client-id != '' }} BAD_SHA: ${{ inputs.bad-sha }} FAILED_RUN_URL: ${{ inputs.failed-run-url }} DEFAULT_BRANCH: ${{ inputs.default-branch }} @@ -75,6 +118,48 @@ jobs: short="${BAD_SHA:0:7}" branch="auto-revert/${short}" + # GITHUB_TOKEN cannot push a commit that touches .github/workflows/, + # ever — it is a platform rule, not a `permissions:` we forgot to + # grant. Detect it here rather than letting `git push` fail after the + # revert commit is already made: that produced a red check whose log + # said "refusing to allow a GitHub App to create or update workflow", + # which reads like a misconfiguration and buries the actual CI + # failure it was reacting to. + # + # Open an issue instead, so the regression is still surfaced loudly. + # Configure revert-app-client-id to revert these automatically. + # `diff-tree -r ` lists nothing for a merge commit, and the bad + # SHA is usually a merge — diffing against the first parent covers + # both shapes. + changed=$(git diff --name-only "${BAD_SHA}^1" "$BAD_SHA") + + if [[ "$HAVE_WORKFLOW_SCOPE" != "true" ]] \ + && grep -q '^\.github/workflows/' <<<"$changed"; then + echo "$short touches .github/workflows/ and no revert App is configured." + title="main is red at ${short} and cannot be auto-reverted" + if gh issue list --search "$title in:title" --state open --json number --jq 'length' | grep -q '^[1-9]'; then + echo "issue already open — skipping" + exit 0 + fi + cat >/tmp/revert-issue.md <` lists nothing for a merge + commit and the bad SHA usually *is* a merge; and auto-revert has therefore + never once succeeded against a workflow-file regression. + +### Added + +- **`auto-revert-on-main-failure.yml` gained `revert-app-client-id` and a + `REVERT_APP_PRIVATE_KEY` secret**, both optional. When supplied, the revert + branch is pushed with a scoped App installation token carrying + `permission-workflows: write`, so workflow-file commits are reverted + automatically rather than raising an issue. Use a dedicated App: a credential + that can rewrite workflow files can rewrite CI itself. + ## [3.5.0] - 2026-09-18 ### Added diff --git a/examples/README.md b/examples/README.md index a027114..b9436ee 100644 --- a/examples/README.md +++ b/examples/README.md @@ -722,12 +722,41 @@ jobs: permissions: contents: write pull-requests: write + issues: write uses: nkg/github-actions/.github/workflows/auto-revert-on-main-failure.yml@v3 with: bad-sha: ${{ github.event.workflow_run.head_sha }} failed-run-url: ${{ github.event.workflow_run.html_url }} ``` +**Commits that touch `.github/workflows/` cannot be auto-reverted by default.** +The job pushes with `GITHUB_TOKEN`, which GitHub categorically forbids from +creating or updating workflow files — there is no `permissions:` key that grants +it. Left alone, the push is rejected *after* the revert commit is made, and the +job fails with `refusing to allow a GitHub App to create or update workflow ... +without 'workflows' permission`, which reads like a misconfiguration and buries +the CI failure it was reacting to. + +Instead, the job detects that case up front and **opens an issue** rather than +failing, so the regression is still surfaced. That is why the stub grants +`issues: write`. + +To revert those commits automatically too, supply a GitHub App installation that +has **Workflows: read and write**: + +```yaml + with: + bad-sha: ${{ github.event.workflow_run.head_sha }} + failed-run-url: ${{ github.event.workflow_run.html_url }} + revert-app-client-id: Iv23li... # public, from the App's page + secrets: + REVERT_APP_PRIVATE_KEY: ${{ secrets.REVERT_APP_PRIVATE_KEY }} +``` + +Use a **dedicated** App for this, not one you already use for something else. +A token that may rewrite workflow files can rewrite CI itself, so it wants its +own key, its own installation, and only the repos that need it. + **Client-side complement:** `auto-revert` reacts *after* bad code lands. Pair it with a `pre-push` git hook that runs the same checks CI runs, so broken code never reaches `origin` in the first place. See