peter-evans/create-pull-request: one updated PR, and why its checks stall
peter-evans/create-pull-request commits whatever earlier steps changed to one fixed branch, create-pull-request/patch unless you name another, and creates, updates or closes a single pull request from it, so a scheduled job keeps one pull request current instead of opening one per run. Opened with the default GITHUB_TOKEN, that pull request's checks wait for someone to approve them and its pushes start no workflows, which is why most setups pass a GitHub App token.
peter-evans/create-pull-request is built for jobs that change files and want a person to review the result: dependency bumps, regenerated clients, translation syncs, formatted code. Earlier steps edit the workspace; this step notices the diff, commits it, pushes the branch and opens the pull request.
Two things surprise people the first time. The branch is rebuilt and force-pushed on every update, by design. And the pull request's checks do not run the way they do for a person's pull request.

Create, update, close, or nothing
Each run compares the workspace with the base branch, which is the branch the job checked out unless you set base. Untracked files, modified files and local commits that were never pushed all count as changes. With a diff and no open pull request, it creates the branch and the pull request. With a diff and an open one, it rebuilds the branch on the current base and force-pushes it, so the pull request always shows the current diff. With no diff and an open pull request, it closes it, and deletes the branch if delete-branch is true. With no diff and nothing open, it exits quietly.
The pull-request-operation output says which happened: created, updated, closed or none. Branch on it rather than on the step's success, because all four are successes.
name: Regenerate API client
on:
schedule:
- cron: '17 6 * * 1-5'
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
regenerate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- run: make generate-client
- id: cpr
uses: peter-evans/create-pull-request@3ffccacd46053766916f087ce41d627c0b8cf2d1 # v8.1.3
with:
branch: bot/generated-client
commit-message: 'chore: regenerate API client'
title: 'chore: regenerate API client'
labels: automated
delete-branch: true
- if: steps.cpr.outputs.pull-request-operation == 'created'
env:
PR_URL: ${{ steps.cpr.outputs.pull-request-url }}
run: echo "Opened $PR_URL"Inputs, and two repository settings
Every input is optional. Two settings outside the workflow also have to be right. The repository, or its organization, must allow GitHub Actions to create pull requests under Settings, Actions, General, Workflow permissions; without it the step fails with GitHub Actions is not permitted to create or approve pull requests. And a repository created after 2 February 2023 gives GITHUB_TOKEN read-only access unless the workflow asks for contents: write and pull-requests: write.
| Input | Default | Notes |
|---|---|---|
token | GITHUB_TOKEN | Creates and updates the pull request |
branch-token | the value of token | Creates and updates the branch; rarely needed |
path | GITHUB_WORKSPACE | Repository path relative to the workspace |
add-paths | all new and modified files | Git pathspecs to commit; other changes are stashed and restored |
commit-message | [create-pull-request] automated change | Text after a blank line becomes the commit description |
committer, author | github-actions[bot], the triggering actor | Both ignored when sign-commits is true |
signoff | false | Adds a Signed-off-by line |
branch | create-pull-request/patch | The pull request branch; the action owns it |
delete-branch | false | Delete branch when its pull request no longer has a diff |
branch-suffix | none | random, timestamp or short-commit-hash: a new branch per run |
base | the checked-out branch | Required for events that check out a commit |
push-to-fork | none | owner/repo-fork to push the branch to |
sign-commits | false | Sign as github-actions[bot] or as your App's bot |
title, body, body-path | generic text | body-path wins over body |
labels, assignees, reviewers, team-reviewers, milestone | none | team-reviewers needs a PAT or App token |
draft | false | true on create only, always-true on create and update |
maintainer-can-modify | true | Whether maintainers can push to the branch |
The branch belongs to the action
The action rebuilds the pull request branch from the base and force-pushes it on every update. Its documentation gives the reason: creating and updating should always produce the same result, and rebasing then force-pushing is the simplest way to guarantee it. The practical consequence is to never push your own commits to that branch, because the next run replaces them.
For the same reason, pointing branch at an existing branch that already holds your changes does not open a pull request for it; the action expects to create and manage the branch itself. For that case its documentation recommends gh pr create. branch-suffix creates a fresh branch per run instead, which the README marks as not recommended because it can pile up pull requests.
Events that check out a commit rather than a branch, such as pull_request (a merge commit) and release (a tag), need base set explicitly, for example base: ${{ github.head_ref }} or base: main.
Why checks on the pull request do not run
GitHub does not start workflows from events caused by GITHUB_TOKEN, so that a workflow cannot trigger itself. Pull requests now have an exception: when a workflow using GITHUB_TOKEN opens or updates one, its pull_request runs for opened, synchronize and reopened are created in an approval-required state, and someone with write access has to select Approve workflows to run. push workflows on the branch still do not run at all. The action's README still says the checks never run; either way a reviewer sees required checks that are not green.
The fix with the best trade-off is a GitHub App token from actions/create-github-app-token, passed as token: both push and pull_request workflows run, the pull request is opened by <app-slug>[bot], and the token is scoped to one installation and lives an hour. The other workarounds from the action's documentation are in the table.
- id: app
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
with:
client-id: ${{ vars.APP_CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write
- uses: peter-evans/create-pull-request@3ffccacd46053766916f087ce41d627c0b8cf2d1 # v8.1.3
with:
token: ${{ steps.app.outputs.token }}
sign-commits: true| Workaround | push workflows | pull_request workflows | Cost |
|---|---|---|---|
GITHUB_TOKEN as it is | Do not run | Wait for approval | A click on every update |
| GitHub App token | Run | Run | A one-time App setup |
| PAT on a machine account | Run | Run | An extra account; it cannot approve its own pull requests |
| SSH deploy key for the push | Run | Do not run | A key per repository |
Machine account pushing to its fork (push-to-fork) | Do not run | Run | A fork to maintain |
draft: always-true with ready_for_review | Do not run | Run when a person marks it ready | A manual click per update |
Signed commits
Since v7 the action can sign its commits without a GPG key. Set sign-commits: true and the commits are signed as github-actions[bot] with GITHUB_TOKEN, or as your App's bot with an App token, and show as verified. A personal access token still creates the pull request but its commits are not signed, and committer and author are ignored whenever signing is on.
This path has a size limit: the GitHub API refuses blobs over 40 MiB, so a file that large in the pull request raises an error, and the documentation points to GPG signing for those repositories. The pull-request-commits-verified output reports whether GitHub considers the branch's commits verified, which is what a rule requiring signed commits checks.
Upgrade past v7.0.9 if you use checkout v6
One cross-action break matters more than any row here: with actions/checkout v6 or later, versions of this action older than v7.0.9 fail at the push with remote: Duplicate header: "Authorization". The maintainer's answer in issue #4272 was to upgrade; v8 is not affected.
| Major | Change |
|---|---|
| v5 | Leaves the repository on the branch it started on; add-paths stashes and restores other changes; body-path input |
| v6 | Node 20; default author and committer gained the numeric ID prefix; git-token input |
| v7 | git-token renamed branch-token; sign-commits; draft: always-true; maintainer-can-modify; primary rate limits retried; pull-request-operation can be none |
| v8 | Node 24, needing runner v2.327.1 or later |
Key takeaways
- Four outcomes per run,
created,updated,closedornone, all reported inpull-request-operation. - The branch is rebuilt and force-pushed on every update; never commit to it by hand.
- With
GITHUB_TOKEN,pull_requestchecks wait for approval andpushworkflows never run; an App token fixes both. sign-commits: truegives verified commits withGITHUB_TOKENor an App token, not with a PAT.
Frequently asked questions
Why don't pull requests created by create-pull-request trigger other workflows?
GITHUB_TOKEN, and GitHub does not start workflows from that token's events, except that pull_request runs are now created waiting for approval. Pass a GitHub App token or a machine account's PAT as token and the checks run as they do for a person.Why are actions still not triggering with a custom App token?
on: pull_request: branches: filter named the pull request's own branch. That filter matches the base branch, the one the pull request targets. Check that too, and that the App is installed on the repository.How do I fix Error: GitHub Actions is not permitted to create or approve pull requests?
Can I use an existing branch as the pull request branch?
branch at an existing branch with changes will not work. To open a pull request for a branch you already have, run gh pr create in a step.