Skip to content

fix: retry the Dependabot merge when another merge modifies the base branch - #183

Merged
dnlopes merged 1 commit into
mainfrom
fm/rw-merge-race-r1
Oct 24, 2026
Merged

dnlopes merged 1 commit into
mainfrom
fm/rw-merge-race-r1

Conversation

@dnlopes

@dnlopes dnlopes commented Oct 24, 2026

Copy link
Copy Markdown
Contributor

Intent

Whenever "Looks good to me" (LGTM) is commented on a Dependabot PR, the PR should merge automatically, but some PRs do not. Troubleshoot it; if the problem is in the reusable workflow, open a pull request in that repository and fix it.

Context from firstmate's diagnosis: in dnlopes/Blog.TodosParaAMesa, PR 150 was approved by the code owner, and the reusable dependabot-merge.yaml job then ran gh pr merge 150 --auto --squash. All required checks had already passed, so gh tried to merge immediately. PR 151 merged into main at 2026-10-24T15:59:02Z; one second later the PR 150 merge failed with GraphQL: Base branch was modified. Review and try the merge again. (mergePullRequest) and the job exited 1. Nothing retries, so the PR stays approved but unmerged and without auto-merge. This race happens whenever two or more Dependabot PRs are approved close together.

What Changed

  • Adds a request_merge helper to .github/workflows/dependabot-merge.yaml. The helper wraps gh pr merge --auto --squash and retries up to 5 times when the merge fails with Base branch was modified. It waits 5, 10, 15, then 20 seconds between attempts.
  • Fails at once on any other merge error, and on the fifth failed attempt. The helper writes each failed attempt's output to stderr.
  • Calls request_merge instead of gh pr merge when a code owner submits a review. This fixes the race in which 2 Dependabot PRs are approved close together. Before, the second merge failed and the PR stayed approved but unmerged.

🤖 Generated with Claude Code

Risk Assessment

✅ Low: The change adds a bounded retry of about 50 seconds to the only merge call, pull_request_review, and retries only on the exact Base branch was modified error from the diagnosis; other errors still fail as before.

Testing

I parsed the real step script out of the workflow YAML at the base and target commits and ran it with bash 3.2. A fake gh simulated the GitHub API, and a fake sleep recorded back-off timing. The base commit exits 1 on one "Base branch was modified" failure, which reproduces the reported bug. The fix retries and merges, and every adversarial case behaves as intended. GitHub was stubbed, so no scenario counts as live. A live run needs a real repository, a code-owner PAT, and two Dependabot PRs approved close together. This is a CI-workflow-only change with no surface no-mistakes can drive. The worktree stays clean, and the harness and transcript live in the evidence directory.

  • Live validation: ⚠️ no-surface - 0 of 6 scenarios driven live against the product
Scenario Result Live Evidence
Code owner approves PR 150 just after PR 151 merges: the first merge fails with 'Base branch was modified', the job retries and merges PR 150, and the job exits 0 ⏸️ untested no No live surface: this is a reusable GitHub Actions workflow and runs only on GitHub-hosted runners. A live run needs real Dependabot PRs on a GitHub repository and a code-owner DEPENDABOT_MERGE_TOKEN…
Approval with no concurrent merge: one merge call, no retry or sleep, and the job exits 0 ⏸️ untested no No live surface (CI workflow only). I drove the real script against a fake gh.
Adversarial: the base branch changes on every attempt, so the job stops after 5 attempts with 5/10/15/20 s back-off and exits 1 instead of looping forever ⏸️ untested no No live surface (CI workflow only). I drove the real script against a fake gh.
Adversarial: an unrelated merge error ('Pull request is not mergeable') is not retried, and the job fails at once with exit 1 ⏸️ untested no No live surface (CI workflow only). I drove the real script against a fake gh.
The race clears on a later attempt (the 4th): the job merges and exits 0 ⏸️ untested no No live surface (CI workflow only). I drove the real script against a fake gh.
An approval from someone who is not a code owner triggers no merge attempt ⏸️ untested no No live surface (CI workflow only). I drove the real script against a fake gh.
Evidence: Merge race harness transcript (base and fix, 7 runs)
=== S1-before: race at base commit (1 transient failure) (run-base.sh) ===
  out| GraphQL: Base branch was modified. Review and try the merge again. (mergePullRequest)
  call| gh pr merge 150 -R dnlopes/Blog.TodosParaAMesa --auto --squash
  exit code: 1
=== S1: race, merge of PR 151 lands first (1 transient failure) (run-head.sh) ===
  out| GraphQL: Base branch was modified. Review and try the merge again. (mergePullRequest)
  out| ✓ Squashed and merged pull request dnlopes/Blog.TodosParaAMesa#150
  call| gh pr merge 150 -R dnlopes/Blog.TodosParaAMesa --auto --squash
  call| sleep 5
  call| gh pr merge 150 -R dnlopes/Blog.TodosParaAMesa --auto --squash
  call| gh pr view 150 -R dnlopes/Blog.TodosParaAMesa --json state,headRefOid --jq "\(.state) \(.headRefOid)"
  exit code: 0
=== S2: no race, checks passed (run-head.sh) ===
  out| ✓ Squashed and merged pull request dnlopes/Blog.TodosParaAMesa#150
  call| gh pr merge 150 -R dnlopes/Blog.TodosParaAMesa --auto --squash
  call| gh pr view 150 -R dnlopes/Blog.TodosParaAMesa --json state,headRefOid --jq "\(.state) \(.headRefOid)"
  exit code: 0
=== S3: race persists on every attempt (run-head.sh) ===
  out| GraphQL: Base branch was modified. Review and try the merge again. (mergePullRequest)
  out| GraphQL: Base branch was modified. Review and try the merge again. (mergePullRequest)
  out| GraphQL: Base branch was modified. Review and try the merge again. (mergePullRequest)
  out| GraphQL: Base branch was modified. Review and try the merge again. (mergePullRequest)
  out| GraphQL: Base branch was modified. Review and try the merge again. (mergePullRequest)
  call| gh pr merge 150 -R dnlopes/Blog.TodosParaAMesa --auto --squash
  call| sleep 5
  call| gh pr merge 150 -R dnlopes/Blog.TodosParaAMesa --auto --squash
  call| sleep 10
  call| gh pr merge 150 -R dnlopes/Blog.TodosParaAMesa --auto --squash
  call| sleep 15
  call| gh pr merge 150 -R dnlopes/Blog.TodosParaAMesa --auto --squash
  call| sleep 20
  call| gh pr merge 150 -R dnlopes/Blog.TodosParaAMesa --auto --squash
  exit code: 1
=== S4: unrelated merge error must not retry (run-head.sh) ===
  out| GraphQL: Pull request is not mergeable (mergePullRequest)
  call| gh pr merge 150 -R dnlopes/Blog.TodosParaAMesa --auto --squash
  exit code: 1
=== S5: review by non-code-owner (run-head.sh) ===
  exit code: 0
=== S6: race succeeds on attempt 4 (run-head.sh) ===
  out| GraphQL: Base branch was modified. Review and try the merge again. (mergePullRequest)
  out| GraphQL: Base branch was modified. Review and try the merge again. (mergePullRequest)
  out| GraphQL: Base branch was modified. Review and try the merge again. (mergePullRequest)
  out| ✓ Squashed and merged pull request dnlopes/Blog.TodosParaAMesa#150
  call| gh pr merge 150 -R dnlopes/Blog.TodosParaAMesa --auto --squash
  call| sleep 5
  call| gh pr merge 150 -R dnlopes/Blog.TodosParaAMesa --auto --squash
  call| sleep 10
  call| gh pr merge 150 -R dnlopes/Blog.TodosParaAMesa --auto --squash
  call| sleep 15
  call| gh pr merge 150 -R dnlopes/Blog.TodosParaAMesa --auto --squash
  call| gh pr view 150 -R dnlopes/Blog.TodosParaAMesa --json state,headRefOid --jq "\(.state) \(.headRefOid)"
  exit code: 0
  • Evidence: Harness driver, fake gh, and extracted run scripts (local file: ~/.no-mistakes/evidence/01M5QBSCNMWB1F391NXBVZ6WG9/harness)
  • Outcome: ⚠️ 1 warning across 1 run (1m12s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

⚠️ **Test** - 1 warning
  • ⚠️ this change has no live-validatable surface; proceed without live validation? (0 of 6 scenarios were driven live against the product); Code owner approves PR 150 just after PR 151 merges: the first merge fails with 'Base branch was modified', the job retries and merges PR 150, and the job exits 0: No live surface: this is a reusable GitHub Actions workflow and runs only on GitHub-hosted runners. A live run needs real Dependabot PRs on a GitHub repository and a code-owner DEPENDABOT_MERGE_TOKEN PAT, and it publishes merges outward. I drove the real step script with bash against a fake gh, which is not live.; Approval with no concurrent merge: one merge call, no retry or sleep, and the job exits 0: No live surface (CI workflow only). I drove the real script against a fake gh.; Adversarial: the base branch changes on every attempt, so the job stops after 5 attempts with 5/10/15/20 s back-off and exits 1 instead of looping forever: No live surface (CI workflow only). I drove the real script against a fake gh.; Adversarial: an unrelated merge error ('Pull request is not mergeable') is not retried, and the job fails at once with exit 1: No live surface (CI workflow only). I drove the real script against a fake gh.; The race clears on a later attempt (the 4th): the job merges and exits 0: No live surface (CI workflow only). I drove the real script against a fake gh.; An approval from someone who is not a code owner triggers no merge attempt: No live surface (CI workflow only). I drove the real script against a fake gh.
  • Live validation: ⚠️ no-surface - 0 of 6 scenarios driven live against the product
Scenario Result Live Evidence
Code owner approves PR 150 just after PR 151 merges: the first merge fails with 'Base branch was modified', the job retries and merges PR 150, and the job exits 0 ⏸️ untested no No live surface: this is a reusable GitHub Actions workflow and runs only on GitHub-hosted runners. A live run needs real Dependabot PRs on a GitHub repository and a code-owner DEPENDABOT_MERGE_TOKEN…
Approval with no concurrent merge: one merge call, no retry or sleep, and the job exits 0 ⏸️ untested no No live surface (CI workflow only). I drove the real script against a fake gh.
Adversarial: the base branch changes on every attempt, so the job stops after 5 attempts with 5/10/15/20 s back-off and exits 1 instead of looping forever ⏸️ untested no No live surface (CI workflow only). I drove the real script against a fake gh.
Adversarial: an unrelated merge error ('Pull request is not mergeable') is not retried, and the job fails at once with exit 1 ⏸️ untested no No live surface (CI workflow only). I drove the real script against a fake gh.
The race clears on a later attempt (the 4th): the job merges and exits 0 ⏸️ untested no No live surface (CI workflow only). I drove the real script against a fake gh.
An approval from someone who is not a code owner triggers no merge attempt ⏸️ untested no No live surface (CI workflow only). I drove the real script against a fake gh.
  • ruby extract.rb .github/workflows/dependabot-merge.yaml (parses the workflow YAML and pulls out the run script of the step named "Approve, merge, or rebase", for the target and base commits)
  • drive.sh run-base.sh with MERGE_FAILS=1: base-commit reproduction of the race
  • drive.sh run-head.sh with MERGE_FAILS set to 0, 1, 3, and 99 (race scenarios)
  • drive.sh run-head.sh with MERGE_ERROR='Pull request is not mergeable' (unrelated error, no retry expected)
  • drive.sh run-head.sh with REVIEW_AUTHOR=someone-else (review by someone who is not a code owner)
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@dnlopes
dnlopes merged commit de12d60 into main Oct 24, 2026
1 check passed
@dnlopes
dnlopes deleted the fm/rw-merge-race-r1 branch October 24, 2026 16:32
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 1.27.10 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

@github-actions github-actions Bot added the released Issue already released label Oct 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

released Issue already released

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant