GitHub Actions force-push gotchas
Force-pushing a PR branch 3+ times rapidly can permanently break CI triggers
Applies to: Any PR branch that receives multiple rapid force-pushes during active iteration
If you force-push a GitHub PR branch 3 or more times within a short window (≤30 minutes), GitHub Actions' pull_request synchronize CI trigger for that PR can enter a permanently broken state where subsequent pushes to the branch never queue new runs.
Root cause: GitHub Actions cancels in-flight runs on each force-push and debounces rapid successive synchronize events. After enough rapid iterations the PR's CI integration becomes stale — GitHub stops dispatching synchronize events for new commits to that branch.
Example failure:
# Push a commit to the PR branch — CI runs never appear in the Actions tab.
# No "queued", no "pending", no error — just silence.
git push --force-with-lease origin fix/my-feature
# (30 seconds pass, no new run appears in Actions → PR → Checks)In the PR #894 incident: 5 force-pushes in 40 minutes → zero CI runs triggered on all subsequent commits.
Diagnosis
- Check the PR's Checks tab — if there are no runs for the latest commit SHA but earlier commits have runs, CI dispatch is broken (not a flaky test).
- Confirm by running
gh run list --branch <branch>— if the latest commit SHA is absent from the list, GitHub never received the event. - A single empty commit is the fastest confirm: if CI still doesn't trigger after it, the PR's sync tracking is stale.
Fix
Push an empty commit to the branch — this forces a push event that re-establishes GitHub's tracking without needing a force-push:
git commit --allow-empty -m "trigger CI"
git push origin <branch>If the empty commit doesn't unblock CI, close and reopen the PR. This resets GitHub Actions' event tracking for the PR.
Prevention
Never force-push a PR branch during active iteration. Use a throwaway local branch to explore fixes freely, then force-push once to the PR branch when you have a final state. See Force-push iteration workflow below.
Force-push iteration workflow — use a throwaway branch, not the PR branch
Applies to: Any exploratory or iterative work (CI config tweaks, rebase cleanup, fix exploration)
When exploring fixes or iterating on CI config, doing that work directly on the PR branch risks triggering the CI breakage described above. Instead, keep the PR branch clean and use a local working branch for all iteration.
Workflow:
# 1. Create a throwaway working branch from the PR branch
git checkout -b wip/fix-exploration
# 2. Iterate freely — commit, amend, rebase, force-push to origin wip/ as needed
git add -p && git commit -m "wip: trying approach A"
git push --force-with-lease origin wip/fix-exploration
# (repeat until the approach is solid)
# 3. When ready, apply the final state to the PR branch — one clean force-push
git checkout fix/my-feature
git reset --hard wip/fix-exploration
git push --force-with-lease origin fix/my-feature
# 4. Delete the throwaway branch
git branch -D wip/fix-exploration
git push origin --delete wip/fix-explorationThis preserves GitHub's CI tracking on the PR branch (one force-push) while letting you iterate freely on the throwaway branch where CI trigger state doesn't matter.
Prevention checklist before force-pushing a PR branch:
- [ ] Is this the first force-push for this PR? (Safe — one force-push doesn't break CI)
- [ ] Has it been at least 30 minutes since the last force-push? (Safe — enough cooldown)
- [ ] If neither: use the throwaway branch workflow above instead
Related
- CI Secrets Checklist — GitHub Actions secret setup and the "assumed secrets" gotcha
.github/workflows/ci.yml— primary CI workflow triggered bypull_request synchronize
Last updated: 2026-05-26 (OBJ-692 / OBJ-703: force-push CI breakage documented from PR #894 post-mortem)