Skip to content

Maintenance: enable auto-merge for Dependabot dev-dependency PRs #5491

Description

@svozza

Summary

Dependabot opens daily PRs for npm dev dependencies (plus grouped weekly PRs for AWS SDK/CDK). Every one of these requires a maintainer to approve, wait for checks, and merge by hand — and because branch protection enforces strict up-to-date checks (strict: true), each merge invalidates the other open Dependabot PRs, triggering a rebase/re-approve cycle across all of them. This is a recurring, low-value drain on maintainer time.

This issue proposes enabling GitHub native auto-merge for a narrowly-scoped subset of Dependabot PRs — direct dev dependencies, non-major updates only — via a hardened workflow that verifies the PR's provenance and commit contents before approving. It also closes two gaps in our required status checks that exist today and become more relevant once human review is removed from these merges.

Why is this needed?

Maintainer toil. Dev-dependency bumps (e.g. lint-staged, typedoc, the vitest/typescript groups) are mechanical: CI either passes or it doesn't, and the human "review" is a rubber stamp. Automating them frees maintainer time for actual reviews.

Current state (verified via API on 2026-07-21):

  • Branch protection on main: 1 required approving review, dismiss_stale_reviews: true, require_last_push_approval: true, enforce_admins: true, linear history, squash-only.
  • Required status checks are only: Acknowledgment Check, Related Issue Check, Semantic PR Title Check, CodeQL.
  • Gap 1: the ~35 lint/unit-test matrix contexts from pr-run-linting-check-and-unit-tests.yml are required=false (verified on chore(deps-dev): bump lint-staged from 17.0.8 to 17.1.0 #5483). Human merges catch red tests informally; auto-merge only waits on required checks, so a dev-dep bump that breaks the build would merge green.
  • Gap 2: dependency-review (the workflow that blocks known-vulnerable package versions — our main supply-chain gate for dependency PRs) is also required=false. Its own header comment says it only blocks "if the workflow run is marked as required".
  • allow_auto_merge is currently false at the repo level; GitHub Actions is permitted to approve PRs (can_approve_pull_request_reviews: true).

Fixing the two required-check gaps is worthwhile independently of auto-merge.

Which area does this relate to?

Automation, Governance

Solution

Threat model

The primary risk is not "Dependabot proposes a bad version" — that risk exists with manual merging too and is mitigated by dependency-review and CI. The risk auto-merge introduces is the automation approving code no human reviewed:

  1. Non-Dependabot PR gets auto-merged. An attacker (or a bug) causes the workflow to approve an arbitrary PR.
  2. Poisoned Dependabot PR. A Dependabot PR is open; an account with write access (possibly compromised) pushes an extra commit onto the dependabot/* branch. The PR author is still dependabot[bot], so an author-only check passes, and the automation merges unreviewed code.
  3. Race: the malicious commit lands after the workflow has approved and enabled auto-merge, but before checks complete.
  4. Compromised upstream package: a legitimately-published but malicious dev-dep release.

Design: layered controls

Workflow gating (threat 1) — the job runs only when all of the following hold: PR author is dependabot[bot], triggering actor is dependabot[bot], head branch is in this repo (not a fork), branch name starts with dependabot/, and repository is aws-powertools/powertools-lambda-typescript. Additionally dependabot/fetch-metadata itself hard-fails on non-Dependabot events. The dependabot[bot] app identity cannot be spoofed.

Commit content verification (threat 2) — before approving, the workflow lists every commit on the PR via the API and requires each one to be (a) authored by dependabot[bot] and (b) verification.verified == true, i.e. signed by GitHub's key. Locally forging user.name = "dependabot[bot]" cannot produce a GitHub-verified signature. One foreign or unsigned commit → hard fail, no approval, PR falls back to normal human review.

Branch protection closes the race (threat 3) — existing settings do the heavy lifting:

  • dismiss_stale_reviews: true: any push after approval instantly dismisses the bot's approval, blocking auto-merge again.
  • The push re-triggers the workflow, which now fails either the actor condition (a human pushed) or commit verification — so no re-approval is issued.
  • require_last_push_approval: true: the last pusher can never satisfy the review requirement themselves.

Net effect: a poisoned PR doesn't merge; it gets stuck waiting for a human. Auto-merge enablement is a request that branch protection re-validates continuously, not a one-time decision.

Scope limits (threat 4 + blast radius):

  • Only dependency-type == 'direct:development' and update-type != 'version-update:semver-major'. Production dependencies and majors always get human review. For grouped PRs, fetch-metadata reports direct:development only when every dependency in the group is a dev dependency, so mixed groups fall through to manual review.
  • Making dependency-review required blocks known-vulnerable/malicious versions regardless of who merges. Optionally, a Dependabot cooldown (e.g. 3–5 days) lets the ecosystem surface bad releases before we ingest them.

Workflow hardening: permissions: {} at workflow level with contents: write + pull-requests: write granted only to the job; no actions/checkout — the workflow never fetches or executes PR code; all actions pinned to commit SHAs per secure-workflows.yml policy.

Required-checks fix

The unit-test matrix fans out into ~35 contexts; requiring each by name is brittle (renaming a package or bumping a Node version would silently block merges). Instead, add one aggregating job to reusable-run-linting-check-and-unit-tests.yml:

  unit-tests-complete:
    runs-on: ubuntu-latest
    needs: [code-quality, check-examples, check-layer-publisher, check-docs-snippets, check-docs]
    if: always()
    steps:
      - name: All checks passed
        run: |
          if [ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" = "true" ]; then
            exit 1
          fi

if: always() + explicit failure check prevents a skipped aggregate job from satisfying the required check. Both pr-run-linting-check-and-unit-tests.yml and dependency-review.yml run on all PRs with no paths: filter (verified), so making them required cannot strand a PR whose checks never report. While in there, add reopened to the PR workflow's trigger types so reopened PRs re-report the required context. The reusable workflow is also called by make-release.yml; the aggregate job is harmless there.

Auto-merge workflow (sketch)

name: Dependabot auto-merge
on: pull_request

permissions: {}

jobs:
  automerge:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      pull-requests: write
    if: >-
      github.repository == 'aws-powertools/powertools-lambda-typescript' &&
      github.event.pull_request.user.login == 'dependabot[bot]' &&
      github.actor == 'dependabot[bot]' &&
      github.event.pull_request.head.repo.full_name == github.repository &&
      startsWith(github.event.pull_request.head.ref, 'dependabot/')
    steps:
      - name: Fetch Dependabot metadata
        id: metadata
        uses: dependabot/fetch-metadata@<pinned-sha> # v2.x
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
      - name: Verify every commit is authored by Dependabot and signed by GitHub
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
        run: |
          set -euo pipefail
          gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/commits" --paginate \
            --jq '.[] | [(.author.login // "unknown"), (.commit.verification.verified | tostring)] | @tsv' \
          | while IFS=$'\t' read -r login verified; do
              if [ "$login" != "dependabot[bot]" ] || [ "$verified" != "true" ]; then
                echo "::error::Commit not authored+signed by dependabot[bot] (author: ${login}, verified: ${verified})"
                exit 1
              fi
            done
      - name: Approve and enable auto-merge (dev dependencies, non-major only)
        if: >-
          steps.metadata.outputs.dependency-type == 'direct:development' &&
          steps.metadata.outputs.update-type != 'version-update:semver-major'
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          PR_URL: ${{ github.event.pull_request.html_url }}
        run: |
          gh pr review --approve "$PR_URL"
          gh pr merge --auto --squash "$PR_URL"

Because the workflow triggers on synchronize, it automatically re-approves after every legitimate Dependabot rebase — eliminating the dismiss/re-approve churn caused by strict: true + dismiss_stale_reviews.

Rollout plan

Order matters — required checks must exist before auto-merge is enabled:

  1. Add unit-tests-complete aggregate job + reopened trigger type (PR).
  2. Wait for one PR run so the new context is reported.
  3. Add run-unit-tests / unit-tests-complete and dependency-review to required status checks (repo admin).
  4. Enable allow_auto_merge in repo settings (repo admin).
  5. Land dependabot-auto-merge.yml (PR), with fetch-metadata pinned to the current release SHA.
  6. Observe the next few Dependabot dev-dep PRs merge unattended; confirm prod-dep and grouped SDK PRs still require manual review.

Steps 1–3 are valuable even if auto-merge is ultimately rejected.

Residual risk

A compromised upstream dev-dependency release that passes CI and dependency-review would auto-merge. This risk is materially unchanged from today: a human approving a routine dev-dep bump does not audit package tarballs. Mitigations: dependency-review as a required check (this proposal), optional Dependabot cooldown, and the fact that dev dependencies do not ship in published packages or the Lambda layer.

Acknowledgment

Future readers

Please react with 👍 and your use case to help us understand customer demand.

Metadata

Metadata

Assignees

Labels

internalPRs that introduce changes in governance, tech debt and chores (linting setup, baseline, etc.)triageThis item has not been triaged by a maintainer, please wait

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions