From Dependabot Config to Auto-Merge Pipeline

In the Dependabot configuration deep dive, I left one thing hanging: the moment after Dependabot opens the PR.
A good dependabot.yml can keep the inbox sane. It can group related updates, delay fresh releases with cooldown, label PRs, split majors away from smaller bumps, and put dependency work on a schedule that does not surprise the team. But it still cannot answer the question that matters on a Tuesday morning.
Which of these PRs can merge without a person pressing the button?
This follow-up is based on a real setup. I am going to keep repo names, account names, token values, and private operational details out of it, because they do not make the pattern more useful. The interesting part is the shape of the pipeline: Dependabot prepares the PR, one workflow decides whether it is eligible, and a second workflow merges only after CI passes.
The Config Is Only Intake#
A modern dependabot.yml does the intake work. In this setup, Dependabot updates GitHub Actions and npm dependencies on a weekly schedule, applies cooldown windows, and groups updates by risk.
For GitHub Actions, minor and patch updates are grouped together. Security updates get their own group rule because version-update grouping and security-update grouping are separate in Dependabot. For npm, major updates are split from minor and patch updates, and security updates get matching major and minor or patch groups. Dependabot is only helpful when the update it proposes can run in your real environment.
A simplified version looks like this:
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
versioning-strategy: increase
open-pull-requests-limit: 20
schedule:
interval: "weekly"
cooldown:
default-days: 5
semver-major-days: 30
semver-minor-days: 7
semver-patch-days: 3
groups:
npm-major:
applies-to: version-updates
patterns: ["*"]
update-types: ["major"]
npm-minor-patch:
applies-to: version-updates
patterns: ["*"]
update-types: ["minor", "patch"]
npm-security-minor-patch:
applies-to: security-updates
patterns: ["*"]
update-types: ["minor", "patch"]
That is a cleaner inbox, however it is not a merge policy. It still needs to answer a few questions that are outside Dependabot's config surface:
- Is the PR really from Dependabot?
- Is the update type low enough risk to merge automatically?
- Did CI pass on the exact branch update?
- Has anyone requested changes?
- Which token is allowed to approve and merge?
- What happens when the repository cannot use GitHub's built-in auto-merge setting?
Those questions belong in workflows, not in dependabot.yml.
The Shape of the Pipeline#
My setup uses two workflows.
First one runs on pull_request_target. It never checks out PR code, but inspects the Dependabot PR from the base-branch context, fetches Dependabot metadata, approves only patch or minor updates, and adds an eligibility label.
The second one runs on workflow_run after the normal CI workflow completes, and it only considers successful CI runs from Dependabot PRs. Then it resolves the PR attached to the commit, checks for the eligibility label, checks review state again, and squash-merges.
That split is the important design choice. The approval workflow answers, "Is this update eligible?" The merge workflow answers, "Did the eligible update pass CI?"
That eligibility label is the handoff between those two decisions.
This is more verbose than a single workflow that approves and merges immediately, but the tradeoff is worth it: CI stays explicit, visible, and required.
Workflow One: Decide Whether the PR Is Safe#
The approval workflow starts with Dependabot PR activity:
Dependabot Approve workflow
name: Dependabot Approve
on:
pull_request_target:
types: [opened, synchronize, reopened]
permissions:
contents: read
pull-requests: write
jobs:
approve:
name: Approve and label safe Dependabot updates
runs-on: ubuntu-latest
if: github.event.pull_request.user.login == 'dependabot[bot]'
steps:
- name: Fetch Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@<pinned-commit-sha>
with:
alert-lookup: false
compat-lookup: false
skip-commit-verification: true
skip-verification: true
- name: Approve and label
env:
GH_TOKEN: <fine-grained token with contents and pull request write access>
REPO: <owner/repo>
PR_NUMBER: <pull request number>
UPDATE_TYPE: <dependabot update type>
run: |
set -euo pipefail
if update type is not patch or minor; then
add auto-merge-skipped
remove auto-merge-eligible
stop
fi
if review decision is CHANGES_REQUESTED; then
add auto-merge-skipped
remove auto-merge-eligible
stop
fi
approve the PR if it is not already approved
add auto-merge-eligible
remove auto-merge-skipped
The job gate checks the PR author, not the event actor:
if: github.event.pull_request.user.login == 'dependabot[bot]'
That distinction matters. A human can reopen a Dependabot PR, which changes the event actor. The PR author is the GitHub-asserted identity that created the PR. For this gate, the question is not "who clicked reopen?" It is "who owns this PR?"
The workflow then fetches Dependabot metadata. In the real implementation, the action is pinned to a commit SHA instead of a floating tag. That is a small piece of supply-chain hygiene: a workflow that can approve PRs should not casually pull a moving third-party action reference.
Patch and minor version updates are eligible. Major updates are not. Unknown update types are not. Security updates can still be grouped and surfaced by Dependabot, but the auto-merge policy should name exactly what it trusts.
Only after those gates pass does the workflow approve the PR and add the auto-merge-eligible label. The merge workflow treats that label as permission to continue, not as permission to skip CI.
Workflow Two: Merge Only After CI Passes#
The merge workflow waits for CI to finish:
Dependabot Auto Merge workflow
name: Dependabot Auto Merge
on:
workflow_run:
workflows: ["CI Pipeline"]
types: [completed]
permissions:
contents: write
pull-requests: write
jobs:
auto_merge:
name: Auto-merge safe Dependabot updates
runs-on: ubuntu-latest
if: >-
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.actor.login == 'dependabot[bot]' &&
github.repository == '<owner/repo>'
steps:
- name: Resolve PR number
env:
GH_TOKEN: <fine-grained token with contents and pull request write access>
HEAD_SHA: <workflow run head sha>
HEAD_BRANCH: <workflow run head branch>
run: |
set -euo pipefail
find the PR attached to HEAD_SHA and HEAD_BRANCH
write the PR number to step output
- name: Check eligibility label
if: PR number was found
env:
GH_TOKEN: <fine-grained token with contents and pull request write access>
REPO: <owner/repo>
PR_NUMBER: <resolved pull request number>
run: |
set -euo pipefail
read labels from the PR
continue only when auto-merge-eligible is present
- name: Squash-merge
if: eligibility is true
env:
GH_TOKEN: <fine-grained token with contents and pull request write access>
REPO: <owner/repo>
PR_NUMBER: <resolved pull request number>
run: |
set -euo pipefail
if review decision is CHANGES_REQUESTED; then
stop
fi
squash-merge the PR and delete the branch
The job gate is deliberately strict:
if: >-
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.actor.login == 'dependabot[bot]'
This is the other half of the trust model: the label alone does not merge anything, and the workflow only continues after CI passes on a Dependabot-authored pull request run.
If the label is missing, the workflow exits without merging. That means a successful CI run is necessary but insufficient. The update also needs an earlier policy decision.
Before merging, the workflow checks review state one more time. This is cheap and valuable. Someone may request changes after the approval workflow runs but before CI finishes. The final merge step should notice that.
This version does not use --auto. It runs after CI has already passed, so it can squash-merge the PR directly without depending on GitHub's built-in auto-merge setting.
Why the Label Matters#
The label is the smallest durable state shared between the two workflows.
Without it, the merge workflow would have to repeat the full Dependabot metadata decision after CI. That is possible, but it makes the post-CI workflow responsible for both policy and execution. With the label, the responsibilities stay separate:
| Workflow | Responsibility | Durable output |
|---|---|---|
| Approval workflow | Decide whether the update is safe enough to auto-merge | auto-merge-eligible or auto-merge-skipped |
| CI workflow | Test the exact PR branch | Successful or failed workflow run |
| Merge workflow | Merge only when policy and CI both pass | Squash merge or skip |
The label also gives humans something obvious to inspect. If a Dependabot PR is not merging, the first check is whether it was ever marked eligible. That is much better than forcing the whole decision into workflow logs.
The Security Tradeoffs#
The awkward part of this design is the same part that makes it useful: the automation needs enough permission to approve and merge.
The implementation uses a dedicated token exposed as a repository secret. In examples, AUTO_MERGE_TOKEN is just a placeholder name. The token should be scoped as narrowly as the platform allows. For this pattern, the important capabilities are pull request writes and content writes on the repository being automated.
There are a few guardrails that make the risk manageable:
- The privileged workflow never checks out PR code.
- The workflow gates on GitHub's PR author identity for Dependabot.
- The metadata action is pinned to a commit SHA.
- Patch and minor updates are the only auto-merge candidates.
- Major updates stay manual.
- The merge workflow reruns the review-state check before merging.
- The merge workflow requires both the eligibility label and successful CI.
None of that makes automated dependency merging risk-free. It does make the trust boundary explicit enough to audit.
The biggest mistake would be treating pull_request_target like a normal pull_request workflow. It is not. If you checkout and run untrusted PR code in that context, you have mixed write credentials with attacker-controlled execution. The safe version is deliberately boring: inspect metadata, call GitHub APIs, set labels, and leave.
The Part Worth Reusing#
Dependabot config should shape the intake: cooldowns, grouping, schedules, and labels. The approval workflow should stay small and privileged. It decides whether a PR fits the policy, then gets out of the way. Project CI remains ordinary CI. The merge workflow comes last, after both signals are present: the update was marked eligible and the checks passed.
The manual lane matters as much as the automation. Major bumps, failed CI, requested changes, and unclear metadata should stay visible. The goal is not to empty the dependency inbox at any cost. The goal is to remove the boring low-risk path without hiding the cases that need judgment.
Once those responsibilities blur, the setup gets harder to audit and easier to trust for the wrong reasons.
Originally published at https://iuriio.com/blog/posts/2026/07/dependabot-auto-merge-pipeline

