Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 87 additions & 2 deletions .github/workflows/auto-revert-on-main-failure.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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 }}
Expand All @@ -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 <sha>` 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 <<EOF
CI failed on \`${DEFAULT_BRANCH}\` at \`${short}\`.

[Failed run](${FAILED_RUN_URL})

That commit changes files under \`.github/workflows/\`, and this job
pushes with \`GITHUB_TOKEN\`, which GitHub forbids from creating or
updating workflow files. No \`permissions:\` grant lifts that, so the
revert branch could not be pushed and **no revert PR was opened** --
revert or fix forward by hand.

To automate this case, pass \`revert-app-client-id\` (plus a
\`REVERT_APP_PRIVATE_KEY\` secret) for a GitHub App installation that
has **Workflows: read and write**.
EOF
gh issue create --title "$title" --body-file /tmp/revert-issue.md
exit 0
fi

# Skip if a revert PR for this SHA already exists.
if gh pr list --head "$branch" --state all --json number --jq 'length' | grep -q '^[1-9]'; then
echo "revert PR for $short already exists — skipping"
Expand Down
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,35 @@ project uses [SemVer](https://semver.org/) for the `vMAJOR.MINOR.PATCH` tags.

## [Unreleased]

### Fixed

- **`auto-revert-on-main-failure.yml` no longer fails outright when the bad
commit touched a workflow file.** It 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 that could be corrected. The
push was rejected *after* the revert commit was made, and the job died with
`refusing to allow a GitHub App to create or update workflow ... without
'workflows' permission`: a red check that reads like a setup error and buries
the CI failure it was reacting to. The job now detects that case before
committing and **opens an issue** instead, so the regression is still
surfaced. Callers need `issues: write` for that path (the example stub is
updated).

Two latent bugs found while fixing it: the path check has to diff against the
first parent, because `git diff-tree -r <sha>` 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
Expand Down
29 changes: 29 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading