ActionBoxBlog

DevOpsPlatformBackendQA

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.

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.

The pattern: tier the risk, then decide

UpdateVersion jumpGate
Patch1.2.31.2.4Auto-approve + merge when CI passes
Minor1.2.31.3.0Auto-approve + merge when CI passes
Major1.2.32.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/[email protected]
        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: curl -fsSL https://actionbox.cloud/install.sh | sh

      - 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

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

Try this workflow in minutes

Create a free Source, then run the exact commands from this post against the live API — no approval infrastructure to build.

S
Suson Sapkota

Founder, Actionbox