Skip to content
Latchkey LogoLatchkey home

git-auto-commit-action: commit back without loops or rejected pushes

stefanzweifel/git-auto-commit-action commits whatever the job changed and pushes it back to the branch the workflow ran on, which is how formatters and generators fix a pull request in place. It needs contents: write, it cannot push to a branch that requires pull requests, and with any token other than GITHUB_TOKEN its own commit starts the workflow again.

A formatter runs, files change, and you want them in the branch rather than in a failed check. That is the 80% case git-auto-commit-action names in its README, and it handles it in one step. The other 20% is where the questions come from: why nothing was committed, why the push was rejected, why CI did not run on the new commit, and how to stop a workflow that commits from triggering itself over and over.

Every behavior described below is read from entrypoint.sh at v7.2.1, the current release, on 2026-11-12, because that shell script, not the README, decides when the action commits and when it pushes.

Flow of one run: git status check, git add, staged diff check, commit, push, and what each token triggers
Two checks can end the run green with nothing pushed. After the push, the token decides whether the workflow runs again.

What one run does

The action looks for changes with git status -s, limited to file_pattern (default ., the whole repository) and status_options. A clean result prints Working tree clean. Nothing to commit., sets changes_detected to false and ends green. A dirty one runs git fetch --depth=1, stages the pattern with git add, and checks again with git diff --staged, because a change that is only line endings shows up in the first check and not the second.

Then it commits as github-actions[bot] with the triggering user as author, tags if you asked for a tag, and pushes. When branch is set, and it defaults to github.head_ref, the pull request's branch, the push is git push --set-upstream origin HEAD:<branch> --atomic; on a push event that value is empty and it runs a plain git push origin.

The clean-tree case, including the near-identical message git itself prints, is taken apart on git-auto-commit nothing to commit.

.github/workflows/format.yml
name: Format
on:
  pull_request:
permissions:
  contents: write
jobs:
  format:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          ref: ${{ github.head_ref }}
      - run: npx prettier --write .
      - id: commit
        uses: stefanzweifel/git-auto-commit-action@d0b01107b5db882071f96de5dd4b54961ea93f4c # v7.2.1
        with:
          commit_message: 'style: apply prettier'
          file_pattern: '*.ts *.tsx *.md'
      - if: steps.commit.outputs.changes_detected == 'true'
        env:
          SHA: ${{ steps.commit.outputs.commit_hash }}
        run: echo "Pushed $SHA"

The token checkout leaves is the token that pushes

The push uses whatever credential actions/checkout left in the git config, so the token you give checkout is the token that pushes. With GITHUB_TOKEN that means permissions: contents: write; without it the push is refused with a message naming github-actions[bot], which checkout persisted credentials push 403 walks through. On pull_request, also check out ref: ${{ github.head_ref }}, or the job starts on a detached merge commit.

InputDefaultNotes
commit_messageApply automatic changes
branch${{ github.head_ref }}Needed on pull_request; empty on push
file_pattern.Space separated pathspecs, used by both the dirty check and git add
repository.Path to the repository under the workspace
commit_options, add_options, status_options, push_optionsemptyPassed through to git, for example --no-verify
commit_user_name, commit_user_emailgithub-actions[bot] and its noreply addressThe committer
commit_authorthe triggering actorThe author
tag_name, tagging_messageemptyCreate and push a tag with the commit
skip_dirty_check, skip_fetch, skip_checkout, skip_pushfalseskip_push added in v7.1.0
create_branchfalseCreate branch locally and remotely if it is missing
create_git_tag_onlyfalseTag and push without committing
disable_globbingfalseStop the shell expanding file_pattern
before_*_hook, after_*_hookemptyEight shell hooks around add, commit, tag and push; added in v7.2.0
disable_pull_request_target_trigger_warningfalseSilence the v7.2.0 warning on pull_request_target

The push loop, and how to avoid it

With GITHUB_TOKEN there is no automatic loop, because GitHub does not start workflows from events that token causes. On a pull request branch the push still counts as a synchronize, and docs.github.com now says those pull_request runs are created in an approval-required state: they wait until someone with write access selects Approve workflows to run. push workflows do not run on the new commit at all, so a required check that comes from a push workflow waits for a status that never arrives.

Switch to a personal access token or an App token, which you do precisely to get CI on that commit, and the commit starts the workflow again. Usually the second run finds nothing to change and stops there. It loops when the job writes something different every run: a timestamp, a build number, a generated file with a date in it.

Three guards, strongest first. Make the job's output deterministic, so the second run is clean. Gate the job on the actor, as below, which stops the formatter from reacting to its own commit while the rest of CI still runs on it. Or add [skip ci] to commit_message; GitHub's documentation warns that a skipped workflow leaves its required checks pending, so a protected pull request cannot merge on that commit.

.github/workflows/format.yml
jobs:
  format:
    if: github.actor != 'my-format-bot[bot]'
    runs-on: ubuntu-latest
    steps:
      - 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
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          ref: ${{ github.head_ref }}
          token: ${{ steps.app.outputs.token }}
      - run: npx prettier --write .
      - uses: stefanzweifel/git-auto-commit-action@d0b01107b5db882071f96de5dd4b54961ea93f4c # v7.2.1
        with:
          commit_message: 'style: apply prettier'

Protected branches and rulesets

A branch rule or ruleset that requires pull requests rejects the push outright, with the message in the title of issue #71: remote: error: GH006: Protected branch update failed for refs/heads/master. The action does a normal push, so allowing force pushes does not help, and the maintainer's answer in that issue is that the action cannot get around branch protection.

Two designs work. Push to the pull request branch instead of the protected one, which is what happens by default on pull_request: the fix lands in the pull request and goes through the same review. Or, when the job runs on the protected branch itself, open a pull request with peter-evans/create-pull-request instead of pushing.

If a direct push really is the design, rulesets let you put a GitHub App on the bypass list. Push with that App's token from actions/create-github-app-token and its push is exempt, while people still go through review.

Two other rejected pushes

! [rejected] main -> main (non-fast-forward) means someone pushed to the branch after your job checked it out. The README lists this as a known limitation: the action never pulls before pushing, and matrix jobs that each commit run into it. The action leaves pulling to you, so serialize the writers with a concurrency group: a queued job checks out only after the one ahead of it has pushed.

A commit that touches .github/workflows/ fails with refusing to allow a GitHub App to create or update workflow followed by the file and without workflows permission, as in issue #322. The workflow permissions: key has no workflows scope, so GITHUB_TOKEN cannot get it; use an App token with permission-workflows: write, or keep workflow files out of file_pattern.

.github/workflows/format.yml
concurrency:
  group: autocommit-${{ github.head_ref || github.ref }}
  cancel-in-progress: false

v6 failed on a detached HEAD; v7 logs it

On v7.2.1 a detached HEAD logs Repository is in a detached HEAD state. git-auto-commit will likely handle this automatically. To avoid it, check out a branch using the ref option in actions/checkout. where v6 failed the step.

v7.2.0 also prints a warning whenever it runs on pull_request_target, because checking out and running a fork's code with a write token and secrets is how repositories get compromised. The README suggests running fixers on push to the default branch instead.

ReleaseChange
v5.0.0Node 20
v6.0.0Fails early on a detached HEAD; removed create_branch, skip_fetch and skip_checkout
v7.0.0Node 24; the three removed inputs restored; tag_name added beside tagging_message
v7.1.0skip_push
v7.2.0Eight hooks; a warning on pull_request_target
v7.2.1Accepts repositories where .git is a file

Key takeaways

  • Two checks can end the run green with nothing pushed: git status on file_pattern, then git diff --staged.
  • The token given to actions/checkout is the token that pushes; GITHUB_TOKEN needs contents: write.
  • A PAT or App token push re-triggers the workflow; deterministic output or an actor gate stops the loop.
  • Branch protection cannot be bypassed by the action; push to the PR branch, open a PR, or use a ruleset bypass App.

Frequently asked questions

Why do commits made by git-auto-commit not trigger new workflow runs?
Because the push used GITHUB_TOKEN, and GitHub does not start workflows from that token's events, to prevent recursive runs. On a pull request branch the pull_request runs are created but wait for someone with write access to approve them; push workflows do not run. Give actions/checkout an App token or a personal access token and the push triggers workflows normally, which is also when you need a guard against the push loop.
Why is git-auto-commit not working on a protected branch?
A rule that requires pull requests rejects any direct push, and the action does an ordinary git push, so it fails with GH006. Commit to the pull request branch, open a pull request with create-pull-request, or add a GitHub App to the ruleset bypass list and push with its token.
Why does the check say Waiting for status to be reported after the action commits?
The required checks ran on the commit before the action's. On the new head commit, a push made with GITHUB_TOKEN starts no push workflow, and its pull_request runs sit in an approval-required state until someone with write access approves them. Approve the runs, or push with an App token so CI runs on the new commit by itself.
Why is a change to a file not detected?
Check the .gitignore that applies to the file first; the README names it as the usual cause. Then check that file_pattern matches the path the earlier step wrote, and whether the only change was line endings, which the staged diff check drops.

References

Formatters run on every push. Latchkey 2 vCPU runners bill $0.0025/min. Start free → 30-day trial · No credit card