Dependabot Auto-Merge with Policy-as-Code Guardrails
You want Dependabot to keep dependencies current without a human clicking merge on every routine patch — but not so autonomously that a compromised or breaking release sails into production unattended. The safe middle ground is a small amount of configuration that encodes a strict rule: auto-merge only patch and minor updates that pass every check, and route everything else to a person. This walkthrough gives you the three files that implement it — a dependabot.yml, a GitHub Actions workflow using dependabot/fetch-metadata and gh pr merge --auto, and the branch-protection settings that make the whole thing enforceable. It is the implementation companion to Automated Dependency Updates as Policy-as-Code, within Supply Chain & Dependency Security. The merge decision leans on the pass/fail signal from dependency scanning CI gates, so have that gate in place first.
Prerequisites
- A GitHub repository with Actions enabled and admin access to configure branch protection
- An existing CI workflow that runs your test suite and a dependency vulnerability scan on pull requests
- Committed lockfiles so update PRs produce deterministic diffs
- The
ghCLI is available on GitHub-hosted runners by default; no extra install needed - Allow auto-merge enabled in repository Settings → General → Pull Requests
Expected Outcomes
- Dependabot opens grouped, scheduled update PRs against a defined ecosystem
- Patch and minor updates that pass all required checks merge automatically
- Major version bumps are blocked and labelled for human review
- Branch protection guarantees no update merges without green required checks
Step 1: Author dependabot.yml
Start with the policy file that tells Dependabot what to watch and how to shape its pull requests. Grouping non-major updates into one PR keeps the auto-merge surface small and reviewable, and separating majors makes them easy to route to a human.
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
open-pull-requests-limit: 10
labels:
- "dependencies"
groups:
# One PR for all low-risk updates — the auto-merge target
non-major:
update-types:
- "minor"
- "patch"
commit-message:
prefix: "deps"
The groups block is the key policy choice: patch and minor updates arrive as a single grouped PR, while any major update comes as its own separate PR that will fall outside the auto-merge condition in Step 2. Commit this file to your default branch; Dependabot picks it up on its next scheduled run.
Step 2: Write the Auto-Merge Workflow with fetch-metadata
This workflow reads the update metadata Dependabot attaches to each PR and enables GitHub’s native auto-merge only when the update is a patch or minor. It uses pull_request_target so the GITHUB_TOKEN has the write access needed to approve and merge — Dependabot PRs get a read-only token under the plain pull_request event. Because pull_request_target is privileged, the workflow deliberately does not check out or run the PR’s code; it only calls fetch-metadata and gh.
# .github/workflows/dependabot-auto-merge.yml
name: dependabot-auto-merge
on: pull_request_target
permissions:
contents: write
pull-requests: write
jobs:
auto-merge:
runs-on: ubuntu-latest
if: github.actor == 'dependabot[bot]'
steps:
- name: Fetch Dependabot metadata
id: meta
uses: dependabot/fetch-metadata@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Approve patch and minor updates
if: steps.meta.outputs.update-type == 'version-update:semver-patch' || steps.meta.outputs.update-type == 'version-update:semver-minor'
run: gh pr review --approve "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Enable auto-merge for patch and minor updates
if: steps.meta.outputs.update-type == 'version-update:semver-patch' || steps.meta.outputs.update-type == 'version-update:semver-minor'
run: gh pr merge --auto --squash "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
The update-type output takes exactly one of version-update:semver-patch, version-update:semver-minor, or version-update:semver-major. The two conditional steps run only for the first two, so a major bump skips both the approval and the gh pr merge call — it simply sits open, approved by nobody, waiting for a reviewer. The --auto flag does not merge immediately; it queues the PR to merge once branch protection’s required checks pass, which is what Step 3 configures.
Step 3: Enforce Branch Protection and Required Checks
The workflow above is only a request to merge. The actual enforcement — that nothing merges without green checks and that majors need a human — lives in branch protection. Without this step, --auto would merge a patch as soon as it is approved even if tests were failing. The decision flow below shows how the three files interlock.
Configure branch protection on the default branch so the required checks and a review are mandatory. This can be done in the UI or with the API:
gh api -X PUT repos/:owner/:repo/branches/main/protection \
--input - <<'JSON'
{
"required_status_checks": {
"strict": true,
"contexts": ["verify", "dependency-review"]
},
"required_pull_request_reviews": { "required_approving_review_count": 1 },
"enforce_admins": true,
"restrictions": null
}
JSON
With contexts listing your CI test job and dependency-review job, gh pr merge --auto cannot complete until both pass. The required_approving_review_count of 1 is satisfied for patch and minor updates by the workflow’s own approval step, but a major update — which the workflow never approves — stays blocked until a human approves it.
Verification
Simulate both a patch and a major update and confirm each takes the expected path.
# 1. Confirm auto-merge is enabled on a live patch/minor PR
gh pr view <patch-pr-number> --json autoMergeRequest,reviewDecision
# Expected: autoMergeRequest is non-null (queued), reviewDecision "APPROVED"
# 2. Confirm a major PR is NOT queued and awaits review
gh pr view <major-pr-number> --json autoMergeRequest,reviewDecision
# Expected: autoMergeRequest is null, reviewDecision "REVIEW_REQUIRED"
# 3. Inspect the workflow run to see which steps executed
gh run view <run-id> --log | grep -E "Enable auto-merge|semver-major"
# Expected: for a major update, the "Enable auto-merge" step is skipped
# 4. Prove required checks gate the merge: temporarily fail a check
# on a patch PR and confirm it stays open despite being queued.
gh pr checks <patch-pr-number>
# Expected: merge does not complete while any required check is failing
A correctly configured setup shows the patch PR queued and approved but merging only after every check is green, and the major PR neither approved nor queued, sitting in REVIEW_REQUIRED.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Workflow fails with “Resource not accessible by integration” | Using pull_request event, whose token is read-only for Dependabot |
Switch the trigger to pull_request_target and grant pull-requests: write and contents: write |
| Auto-merge enabled but PR never merges | Repository setting “Allow auto-merge” is off, or no required checks are configured | Enable auto-merge in Settings → Pull Requests and add required status checks in branch protection |
| Major updates are auto-merging | The if condition is missing or matches semver-major |
Restrict the condition to version-update:semver-patch and version-update:semver-minor only |
gh: command not found |
Self-hosted runner without the GitHub CLI | Install gh on the runner, or run on a GitHub-hosted runner where it is preinstalled |
| Patch PR merges with tests failing | --auto used but the test job is not a required check |
Add the test job’s check name to required_status_checks.contexts in branch protection |
Common Implementation Mistakes
Frequently Asked Questions
Does gh pr merge --auto bypass required status checks?
No. The --auto flag queues the pull request to merge only after every required status check configured in branch protection has passed. If branch protection requires your CI and dependency-review checks, the merge waits for them to go green before completing. Auto-merge is a scheduling instruction, not an override — the enforcement lives entirely in branch protection. This is why Step 3 is mandatory: without required checks defined, --auto would merge as soon as the PR is approved, which for a patch happens immediately via the workflow’s own approval step.
Why use pull_request_target instead of pull_request for the workflow?
Dependabot pull requests run with a read-only GITHUB_TOKEN under the ordinary pull_request event, so a workflow triggered that way cannot approve the PR or enable auto-merge. The pull_request_target event runs in the context of the base branch and receives a token with write access, which is what the approval and merge steps need. Because that event is more privileged, never check out or execute untrusted PR code within it — restrict the job to calling fetch-metadata and gh, neither of which runs the dependency’s own code, so an attacker-controlled release cannot exploit the elevated token.
How do I stop major version bumps from auto-merging?
Gate the merge step on the update-type output from dependabot/fetch-metadata, allowing only version-update:semver-patch and version-update:semver-minor. A major update produces version-update:semver-major, which fails that condition, so both the approval and gh pr merge steps are skipped and the PR waits. Keep branch protection requiring at least one approving review; since the workflow never approves a major update, it cannot merge until a human signs off. For the broader reasoning on why majors carry the most risk, see the parent Automated Dependency Updates as Policy-as-Code guide.
Related
- Automated Dependency Updates as Policy-as-Code — the parent guide on semver scoping, cooldown windows, and guarded auto-merge policy
- Dependency Scanning CI Gates — the vulnerability gate whose pass/fail signal your required checks consume
- Threat Model Documentation Patterns — recording auto-merge policy decisions and their threat rationale in your living threat model