I sat down to clear a Dependabot backlog across eight repositories in one pass. The script was simple: for every open Dependabot PR, check that CI is green and the PR is mergeable, then squash-merge it. Most repos emptied out cleanly. One did not. The script merged a single PR and left five open, and all five were passing CI.
The five were npm bumps, and they all edited the same file: package-lock.json.
Lockfile contention explains it. Almost every dependency PR rewrites the same mutable file. Two PRs can each merge cleanly against the current base. Once one lands, GitHub marks the others BEHIND because their branches no longer include the new base.
This repository requires branches to be current before merge. Each remaining PR therefore needed a rebase and a regenerated package-lock.json. A parallel "merge all the green ones" loop merged the first PR, found the others no longer mergeable, and moved on.
The useful field is mergeStateStatus, exposed by both gh and the GraphQL API. In this repository, I treated CLEAN as ready to merge and BEHIND as a prompt to update the branch by asking Dependabot to rebase it. BLOCKED cost me time: in this run, it usually meant a required check was still running after the rebase, not that the check had failed. The driver needed to poll that state, not skip it. DIRTY is different: GitHub cannot create the merge cleanly. Ask Dependabot to rebase first; if the lockfile conflict remains, @dependabot recreate regenerates the PR but overwrites any edits made to its branch.
One PR walking through the whole sequence, shown here with an illustrative PR number and polled with gh:
$ gh pr view 1234 --json mergeStateStatus -q .mergeStateStatus
BEHIND # a sibling PR just merged; base moved out from under it
$ gh pr comment 1234 --body "@dependabot rebase" # then wait
$ gh pr view 1234 --json mergeStateStatus -q .mergeStateStatus
BLOCKED # rebased onto new base; required check is running, not failed
$ gh pr view 1234 --json mergeStateStatus -q .mergeStateStatus
BLOCKED # still running
$ gh pr view 1234 --json mergeStateStatus -q .mergeStateStatus
CLEAN # check passed; now it merges
I picked the oldest PR, asked Dependabot to rebase it (@dependabot rebase in a comment), waited for CI, merged it, and repeated the sequence against the new base. Each cycle took a couple of minutes. The serial driver cleared the five PRs the first merge had left behind.
For new backlogs, I prevent the cascade upstream. Dependabot's documented groups configuration batches compatible updates into fewer pull requests, with fewer lockfile changes and CI runs:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: weekly
groups:
npm-minor-and-patch:
update-types:
- minor
- patch
One of my repos already had grouping on its npm updates, and it never showed up in the backlog at all - its bumps had been merging as single grouped PRs the whole time. The repos that generated the cascade were the ones still getting a PR per package.
Batch merging fails the same way when several PRs touch a generated client, checked-in schema, or shared versions.tf. I now group routine updates before they pile up. When the backlog already exists, I merge it serially.
See also
- From Vibes to Production - why deterministic review and validation lanes matter more as generated changes scale