Skip to content

Dependabot and Renovate approval: gate major dependency updates

A copy-paste dependency update approval workflow for Dependabot and Renovate that auto-merges safe updates and routes major changes to a human.

Developers reviewing code together on a laptop

Dependabot opens a PR. CI is green. Nobody clicks approve, so the PR waits — and your package.json quietly accumulates 30 stale bumps. Or the team auto-merges everything and a major version lands in production with a breaking change nobody read. Both are failure modes of the same missing step: a decision about this update.

The fix is a gate with an actual rule: patches and minors with passing CI merge automatically; majors and grouped updates go to a human — with the changelog, the version jump, and the CI status attached.

Architecture Flow
Yes No Approve Reject / timeout Dependency PR CI + changelog Patch or minor? Auto-merge when green Actionbox approval Merge update Keep PR open

The pattern: tier the risk, then decide

UpdateVersion jumpGate
Patch1.2.3 → 1.2.4Auto-approve + merge when CI passes
Minor1.2.3 → 1.3.0Auto-approve + merge when CI passes
Major1.2.3 → 2.0.0Ask a human — breaking changes expected
Grouped PRmultiple packagesAsk a human — changes are bundled

Step 1: auto-approve the safe updates

The workflow approves bot PRs only when the version jump is safe and CI is green:

yaml
name: dependency-approval
on:
  pull_request:
    types: [opened, synchronize, reopened]

permissions:
  contents: write
  pull-requests: write
  checks: read

jobs:
  gate:
    runs-on: ubuntu-latest
    if: github.actor == 'dependabot[bot]' || github.actor == 'renovate[bot]'
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Wait for CI checks
        uses: lewagon/wait-on-check-action@v1.3.4
        with:
          ref: ${{ github.event.pull_request.head.sha }}
          repo-token: ${{ secrets.GITHUB_TOKEN }}
          running-workflow-name: 'dependency-approval'

      - name: Classify the update
        id: classify
        run: |
          TITLE="${{ github.event.pull_request.title }}"
          if echo "$TITLE" | grep -qiE '\(major\)|major'; then
            echo "risk=major" >> "$GITHUB_OUTPUT"
          elif echo "$TITLE" | grep -qiE 'group with|grouped'; then
            echo "risk=grouped" >> "$GITHUB_OUTPUT"
          else
            echo "risk=auto" >> "$GITHUB_OUTPUT"
          fi

      - name: Approve + enable auto-merge for safe updates
        if: steps.classify.outputs.risk == 'auto'
        run: |
          gh pr review --approve "${{ github.event.pull_request.html_url }}"
          gh pr merge --auto --squash "${{ github.event.pull_request.html_url }}"
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Only the CI-verified, non-major PRs get a bot approval. Everything else stays open for a human.

Step 2: major and grouped updates ask a human

The risky class never touches the auto-approve path — it creates a decision with the changelog context instead:

yaml
      - name: Install Actionbox CLI
        if: steps.classify.outputs.risk != 'auto'
        run: python -m pip install actionbox

      - name: Request human approval for major / grouped updates
        if: steps.classify.outputs.risk != 'auto'
        id: approval
         env:
           ACTIONBOX_TOKEN: ${{ secrets.ACTIONBOX_TOKEN }}
         run: |
           result=$(actionbox ask "Approve dependency update: ${{ github.event.pull_request.title }}" \
             --option approve="Approve and merge" \
             --option reject="Reject update" \
             --context-json "[{\"type\":\"key_value\",\"items\":{\"risk\":\"${{ steps.classify.outputs.risk }}\",\"pr\":\"${{ github.event.pull_request.html_url }}\",\"ci_status\":\"passing\",\"check\":\"wait-on-check + test suite\"}}]" \
             --wait --timeout 24h --json)
           echo "decision=$(jq -r '.decision // empty' <<< "$result")" >> "$GITHUB_OUTPUT"

      - name: Approve + auto-merge on human approval
        if: steps.approval.outputs.decision == 'approve'
        run: |
          gh pr review --approve "${{ github.event.pull_request.html_url }}"
          gh pr merge --auto --squash "${{ github.event.pull_request.html_url }}"
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Comment on reject or timeout
        if: steps.approval.outputs.decision != 'approve'
        run: |
          gh pr comment "${{ github.event.pull_request.html_url }}" \
            --body "Dependency update not approved (decision: ${{ steps.approval.outputs.decision }}). Review the changelog before merging."
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

The reviewer sees the PR link, the risk class, and the CI status in the dashboard — then decides from the phone instead of losing the PR in a review queue.

Step 3: fail closed on timeout

A stale major bump must not merge silently on the 7th day. The decision expires explicitly:

yaml
      - name: Fail closed on timeout
        if: steps.approval.outputs.decision == 'expired'
        run: |
          gh pr comment "${{ github.event.pull_request.html_url }}" \
            --body "Update expired without approval — re-request when ready."
          exit 0
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

The audit record shows the request, the decision, and the reason for every dependency that ever merged — so "who approved the next bump?" is one query, not a git-blame hunt.

Full example: Renovate with per-package rules

json
{
  "extends": ["config:base"],
  "automerge": false,
  "packageRules": [
    {
      "matchUpdateTypes": ["patch", "minor"],
      "automerge": true,
      "platformAutomerge": true
    },
    {
      "matchUpdateTypes": ["major"],
      "assignees": ["@release-owner"],
      "reviewers": ["@release-owner"]
    }
  ]
}

Renovate merges the safe class itself; majors land in the approval queue with a named owner. Combine with the workflow above and every class of update has exactly one decision path.

Why this beats "auto-merge everything"

  • Safe updates ship on their own — patches and minors with green CI, no sign-off needed
  • Major bumps get judgment — a person sees the version jump and the changelog before merge
  • The rule is visible — the tier table is the policy; nobody decides case-by-case
  • Every merge is on record — who approved what update, when, and why

Sources & Research

Try it

  1. Install the CLI on your runner
  2. Create a Source → ACTIONBOX_TOKEN
  3. Copy the workflow and set your tier rules

Create a free Source · GitHub Actions reference · Deploy approval gates in GitHub Actions

About the author

Suson Sapkota

Suson founded ActionBox and works in software and data engineering. He writes about approval workflows, background jobs, and how to verify what happened after a human decision.

Turn the next risky operation into a reviewable decision.

Create a free Source, run the example from this guide, and keep the decision and execution outcome connected.