How-to › Release and secure packages

How to automate dependency updates with Dependabot or Renovate#

Configure Dependabot and Renovate side by side for an SDK repository: a release-age delay on every update, and automerge only for the updates that earn it.

Audience
Library maintainer
Level
beginner
Topic
Secure the supply chain
Verified

Forty pull requests from a bot sit open, one package each, because nobody has merged one in a month. Then someone merges them all in an afternoon, including the release that reached the registry that morning. The update queue is either ignored or trusted wholesale, and an SDK repository with four languages produces four queues.

What you get

You will end up with a dependabot.yml and a renovate.json that state one policy, a merge rule naming the updates that need no person, and a script proving the two files agree. This is for you if you maintain an SDK in several languages and the update queue has become noise.

Short answer

Let the bot open the pull requests and let a release-age delay decide when: three days by default, seven for a minor, fourteen for a major. Automerge patches of stable dependencies alone, and only once the test suite calls the code that changed. Dependabot needs a workflow to merge; Renovate has automerge in its config. Everything else waits for a person.

You will need

A repository on GitHub, or on any of the hosts Renovate runs against. A lockfile in every language you ship, so a bump is one reviewable diff and one clean revert. A test suite that calls the dependencies it claims to cover, which the last section of this page measures. Node 22 or later for the checking script. Verified 2026-09-24 against Node 22.22.2 and yaml 2.9.1.

Approaches compared

ApproachWhen it fitsWhat it costs youWhen to pick something else
DependabotThe repository is on GitHub and you want nothing to install or hostNo automerge of its own, so the merge decision is a workflow you write, and groups are keyed on name patterns, dependency type and semver levelThe repository is on GitLab, or a rule needs to look at more than the version number
RenovateAny host, a long list of package managers, and rules keyed on update type, dependency type, and managerA configuration reference of several hundred options, and a bot you run yourself or a hosted app you installThe repository is on GitHub and the two-file policy on this page is all you need
Security updates onlyA library with few dependencies, bumped by hand at each releaseDrift between releases, and a hand upgrade that touches ten packages at once when a release is dueDependencies change faster than you release

The split is between a bot GitHub runs for you and a bot you configure in depth. Dependabot costs one file and offers one set of levers per ecosystem: a cooldown, a schedule, and groups by name, dependency type and semver level. Renovate costs a larger file and gives you rules that combine update type, dependency type, and manager, on any host. Security updates alone cost nothing and leave every other bump to release day.

Wait before you update

A release-age delay is the part of the policy that protects you, and it is the part that gets switched off first. Dependabot applies a cooldown of three days to every version update even when the file says nothing, and lets you lengthen it by semver level.1 The delay applies to version updates only. A security update goes out as soon as the advisory does.

version: 2
updates:
  - package-ecosystem: npm
    directory: /ts
    schedule:
      interval: weekly
      day: monday
    cooldown:
      default-days: 3
      semver-minor-days: 7
      semver-major-days: 14
    groups:
      dev-tooling:
        dependency-type: development
        update-types: [minor, patch]
      runtime-patches:
        dependency-type: production
        update-types: [patch]
    open-pull-requests-limit: 10

One entry per ecosystem, and the SDK has four, so the file repeats itself four times. The semver overrides are supported for npm, pip, and Go modules and not for GitHub Actions, which get default-days alone. The groups block folds every development minor and patch into one pull request a week, which is the difference between forty open pull requests and four. The file has a schema on SchemaStore, so an editor can check it before Dependabot does.

Renovate spells the same delay minimumReleaseAge, once at the top level and again inside any rule that needs a longer one.

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": ["config:recommended"],
  "minimumReleaseAge": "3 days",
  "packageRules": [
    {
      "matchUpdateTypes": ["minor"],
      "minimumReleaseAge": "7 days"
    },
    {
      "matchUpdateTypes": ["major"],
      "minimumReleaseAge": "14 days"
    },

config:recommended brings the dependency dashboard, the curated monorepo groupings, and the age and confidence badges on each pull request. Both bots wait. Dependabot does not open the pull request until the cooldown has passed. Renovate, under its default internalChecksFilter of strict, creates neither branch nor pull request until the age is reached, whatever prCreation says.

Decide what merges without a person

Dependabot has no automerge setting. GitHub’s own recipe is a workflow that reads the pull request’s metadata and asks gh to merge it once the required checks pass. The one below says yes to a patch of a stable dependency and to nothing else.

jobs:
  dependabot:
    runs-on: ubuntu-latest
    if: github.event.pull_request.user.login == 'dependabot[bot]'
    steps:
      - name: Read what Dependabot changed
        id: metadata
        uses: dependabot/fetch-metadata@v2
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
      # A grouped pull request carries several dependencies and `new-version`
      # names only the first, so every version in the pull request is read.
      - name: Check that no dependency in the pull request is 0.x
        id: stable
        run: echo "all=$(jq 'all(.[]; .newVersion | startswith("0.") | not)' <<< "$UPDATED")" >> "$GITHUB_OUTPUT"
        env:
          UPDATED: ${{ steps.metadata.outputs.updated-dependencies-json }}
      - name: Merge a patch of stable dependencies once the checks pass
        if: steps.metadata.outputs.update-type == 'version-update:semver-patch' && steps.stable.outputs.all == 'true'
        run: gh pr merge --auto --squash "$PR_URL"
        env:
          PR_URL: ${{ github.event.pull_request.html_url }}
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

gh pr merge --auto merges when every required check has passed, so the branch protection rule that requires the test suite is part of this policy and not a separate decision. Without that rule the merge happens the moment the workflow runs. The jq step exists because semver says a 0.x package may change anything at any time, so a 0.x “patch” is a word rather than a promise. It reads every dependency, because a grouped pull request carries several and new-version names only the first.

Renovate puts the same decision in the file that holds everything else, as automerge on a rule. A major is left to a person under both tools, which is the one decision nobody argues with.2

    {
      "matchUpdateTypes": ["patch"],
      "matchCurrentVersion": "!/^0/",
      "automerge": true
    },
    {
      "matchDepTypes": ["devDependencies"],
      "matchUpdateTypes": ["minor", "patch"],
      "matchCurrentVersion": "!/^0/",
      "automerge": true
    },
    {
      "matchManagers": ["github-actions"],
      "matchUpdateTypes": ["minor", "patch", "digest"],
      "matchCurrentVersion": "!/^v?0/",
      "automerge": true
    }
  ]
}

Rules apply in order and a later match overrides an earlier one. So the file reads top to bottom as a policy: patches of stable dependencies, development minors, and action bumps merge, unless the dependency is at 0.x. matchCurrentVersion with a negated pattern is how the Renovate documentation excludes 0.x, and the action rule allows for the v most tags carry, because Renovate matches the tag as written. Renovate refuses to automerge until every status check has passed, and the only way past that is ignoreTests, which is named so that nobody sets it by accident.

Check it worked

policy.mjs reads the three files above with a YAML parser and applies their documented rules to a table of updates. It covers the five match keys renovate.json uses and nothing more, so it is a reading of the files rather than a copy of either bot.

node demo.mjs
update                           type        age  Dependabot         Renovate
undici 6.20.1 -> 6.20.2          patch       1d   holds 2d more      holds 2d more
undici 6.20.1 -> 6.20.2          patch       4d   merges itself      merges itself
undici 6.20.1 -> 6.21.0          minor       4d   holds 3d more      holds 3d more
undici 6.21.0 -> 7.0.0           major       30d  waits for a human  waits for a human
typescript 5.6.2 -> 5.7.0        dev minor   10d  waits for a human  merges itself
yaml-lint 0.4.1 -> 0.4.2         patch (0.x) 10d  waits for a human  waits for a human
requests 2.32.3 -> 2.32.4        patch       4d   merges itself      merges itself
actions/checkout 4.2.1 -> 4.2.2  patch       4d   merges itself      merges itself

Seven of the eight rows agree, and the fifth is the shape of the difference between the tools. A development minor merges itself under Renovate because one rule says so. Under Dependabot the preceding workflow looks at the update type alone, so the same bump waits for a person until you extend the condition with the dependency-type output that fetch-metadata also provides. The test file pins the properties the page promises.

node --test policy.test.mjs
1..6
# tests 6
# suites 0
# pass 6
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 137.052934

Test one: a major never merges itself under either bot, however old the release. Test two: nothing merges on the day it is released. Test three: a 0.x patch waits for a person. Test five lists the rows where the two files disagree and fails if that list changes. An edit to either file that opens a gap between them then fails the build rather than the release.

When it goes wrong

The check is green and the dependency is broken. The test suite never called the code that changed, so the merge rule was fed a color with nothing behind it. Two suites below run against a padding helper at 1.0.0 and at a patch release that flips the alignment.

node pitfall.mjs
suite              pad 1.0.0   pad 1.0.1 (the patch)
shallow.test.mjs   green       green
deep.test.mjs      green       red

The shallow suite tests the repository’s own arithmetic and imports nothing from the dependency, so it stays green while every statement line comes out left-aligned. Before enabling automerge, list the dependencies the policy will merge and find the test that calls each one. A dependency with no such test is a dependency the rule has to exclude.

The pull request Renovate opened does not merge, even though its checks are green. Renovate merges one branch per target branch per run, and only when the branch is up to date with its base. Its documentation asks for about two hours before you call it a fault. If it still has not merged, look for a required review in the branch protection rules. Renovate cannot approve its own pull request.

Dependabot grouped a patch with a minor and the workflow did not merge it. The update-type output reports the highest semver change in the pull request, so a group carrying one minor is a minor. That is correct behaviour. If you want the patches merged on their own, give them their own group, as runtime-patches does in the file shown earlier.

When not to do this

Do not switch on automerge before measuring what the suite exercises. A green check on a suite that never imports the dependency is a green check on nothing, and the merge rule cannot tell the difference.

Do not automerge a 0.x dependency, whatever the bump is called. The version number promises nothing below 1.0.0, and both configurations on this page carry a guard for exactly that reason.

Do not set the cooldown to zero to stay current. The three days are the window in which someone else reads the package, files the advisory, and gets the release pulled.3 A repository that installs a release the hour it is published has volunteered to be that someone.

Do not run both bots on one repository. Each opens its own pull requests for the same bumps, each rebases the other’s branches, and the queue you meant to shorten doubles.

Do not expect the cooldown to hold a security update. It applies to version updates only, and that is right: the advisory is the review.

Last verified

Verified 2026-09-24 against Node 22.22.2 and yaml 2.9.1. Every output block is what the command preceding it printed. Neither bot was run against a repository. policy.mjs reads the two configuration files and the workflow and applies their documented rules, so it checks that the files agree. It stands in for neither bot. Validate the files themselves with the config validator for Renovate and the Dependabot schema before relying on them.

Footnotes

  1. The option was called stabilityDays before it was minimumReleaseAge, and the documentation for Renovate allows that other ecosystems may call the same thing a dependency cooldown, which is what Dependabot calls it. Three names for one number, and the number is the only part either bot enforces. ↩︎ Back to text

  2. The reference for the options of Renovate carries a banner recording that Renovate 44 was released as a major version by accident, the changes being non-breaking. A tool whose work includes holding every major for a person shipped one that no person needed to read. The banner is the sort of thing a fourteen-day wait exists to let you find. ↩︎ Back to text

  3. GitHub’s account of the Shai-Hulud worm says it was notified on September 14, 2025 and removed more than 500 packages. Dependabot waits three days by default. The example in the documentation for Renovate is fourteen, chosen to give researchers and scanners time to notice. Neither page derives its figure, and a delay of any length is a bet that somebody else reads the package first. ↩︎ Back to text

Read this page as markdown · All how-to guides

Generate the client instead of writing it#

Retries, timeouts, pagination and auth are the same problems in every client. Voxgig generates them from your OpenAPI description, in 23 languages, from one model.

Get the Voxgig dispatch

Short notes on building SDKs, CLIs, REPLs, and MCPs for API-first teams, plus the occasional Fireside episode pick.

By signing up you agree to our Terms and Conditions.