DevOps Multi-Cloud

Automated Dependency Management at Scale with Renovate: Grouping, Policies, and Auto-Merge

Dependency rot is a slow-motion incident. Every week a repo sits unpatched, the diff to current widens, the CVE backlog grows, and the eventual upgrade goes from a one-line bump to a weekend migration. Dependabot handles the simple case, but once you have dozens of repos, monorepos, internal registries, and a real auto-merge policy, you want Renovate: it speaks far more package managers, its config is composable across an entire org, and its grouping and scheduling controls are what keep automation from becoming a PR flood nobody reads.

This guide sets up Renovate as a fleet-wide platform: one shared preset that encodes policy, grouped and rate-limited updates, vulnerability fixes jumped to the front of the queue, and auto-merge that only fires when CI is green and a package has had time to bake. The goal is the same one I hold every dependency platform to: the boring 80% of updates merge themselves overnight, and humans only look at the 20% that actually need judgment.

1. How Renovate discovers managers, datasources, and versioning

Before configuring anything, understand the three-layer model Renovate uses, because almost every “why didn’t it update X” question maps to one of these layers.

You can preview exactly what a manager finds without opening any PRs by running an explain pass against one repo:

LOG_LEVEL=debug renovate --dry-run=full my-org/my-service 2>&1 | grep -E 'depName|datasource|currentValue|skipReason'

The skipReason field is the single most useful debugging output in the tool. disabled-by-config, is-pinned, unsupported-datasource, and invalid-version each tell you which layer to fix.

Renovate’s defaults are conservative on purpose: it will not update a dependency it cannot resolve a datasource for, and it will not change versioning it does not understand. When a dep is silently ignored, run the dry run before assuming a bug.

2. Self-hosting the bot vs. the hosted app, and the platform token

You have two deployment models. Mend Renovate Cloud (the hosted GitHub App, formerly the “Renovate” app on the Marketplace) requires no infrastructure: install it, grant repo access, done. Self-hosting runs the open-source CLI on your own schedule against a platform token, which you want once you need private registries unreachable from the internet, custom run cadence, air-gapped environments, or strict data-residency.

For self-hosting on GitHub Actions, the canonical path is the official action driven by a cron:

# .github/workflows/renovate.yml in your "renovate-config" repo
name: Renovate
on:
  schedule:
    - cron: '0 */4 * * *'   # every 4 hours; Renovate's own schedule narrows this further
  workflow_dispatch:
    inputs:
      logLevel:
        default: 'info'

permissions:
  contents: read

jobs:
  renovate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: renovatebot/github-action@v41
        with:
          token: ${{ secrets.RENOVATE_TOKEN }}
          configurationFile: config.js
        env:
          LOG_LEVEL: ${{ inputs.logLevel || 'info' }}

The token is the critical decision. A classic Personal Access Token works but ties the bot to a human and PRs show up as that user. The right answer at scale is a GitHub App: create an app, give it Contents: read/write, Pull requests: read/write, and Checks: read, install it on the org, then mint a short-lived installation token at runtime. Renovate reads it from RENOVATE_TOKEN. App identity means a clean bot author, per-repo install control, and no token rotation tied to staff turnover.

// config.js -- self-hosted (global) config, distinct from per-repo presets
module.exports = {
  platform: 'github',
  autodiscover: true,
  autodiscoverFilter: ['my-org/*'],
  // Required so PRs that change CI run against your *real* workflows:
  onboardingConfig: { extends: ['local>my-org/renovate-config'] },
  // Self-hosted-only knobs:
  prHourlyLimit: 2,
  dryRun: process.env.RENOVATE_DRY_RUN ? 'full' : null,
};

Some options are self-hosted only (autodiscover, binarySource, dryRun) and are rejected if placed in a repo’s renovate.json. Keep host-level settings in config.js and policy in the shared preset. Mixing them is the most common onboarding failure.

3. One shared preset for the whole org

The thing that makes Renovate a platform rather than per-repo YAML sprawl is config presets: a repo can extends a config published in another repo, and you change policy for the entire fleet by editing one file. Create a renovate-config repo with a default.json:

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": [
    "config:recommended",
    ":dependencyDashboard",
    ":semanticCommits",
    "group:monorepos",
    "group:recommended"
  ],
  "timezone": "Europe/London",
  "schedule": ["after 9pm and before 6am every weekday", "every weekend"],
  "prHourlyLimit": 2,
  "prConcurrentLimit": 10,
  "rangeStrategy": "bump",
  "labels": ["dependencies"],
  "packageRules": [
    {
      "description": "Group all non-major npm devDependencies",
      "matchManagers": ["npm"],
      "matchDepTypes": ["devDependencies"],
      "matchUpdateTypes": ["minor", "patch"],
      "groupName": "dev dependencies (non-major)"
    }
  ]
}

Every other repo then carries a one-line renovate.json:

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": ["local>my-org/renovate-config"]
}

The local> prefix resolves on the same platform without a network round trip to the public registry, which matters for GitHub Enterprise Server and self-hosted GitLab. Now group:monorepos and your schedule apply everywhere, and a policy change is a single PR against renovate-config. The config:recommended preset (the modern replacement for the deprecated config:base) supplies the sane defaults you would otherwise re-derive.

4. Grouping, scheduling, and rate-limiting to kill the PR flood

The failure mode of naive automation is forty PRs on Monday morning. Three levers fix it.

Grouping collapses related updates into a single PR. Group by ecosystem so one review covers a coherent set:

{
  "packageRules": [
    {
      "matchManagers": ["github-actions"],
      "groupName": "github actions",
      "schedule": ["before 6am on monday"]
    },
    {
      "matchPackageNames": ["/^@aws-sdk//"],
      "matchUpdateTypes": ["minor", "patch"],
      "groupName": "aws sdk v3"
    },
    {
      "matchDatasources": ["docker"],
      "matchUpdateTypes": ["patch"],
      "groupName": "docker base image patches"
    }
  ]
}

Renovate v40+ treats values wrapped in slashes as regex for matchPackageNames (e.g. "/^@aws-sdk//"). The older matchPackagePatterns/matchPackagePrefixes fields are deprecated – use the unified matchPackageNames with bare strings for exact matches and /regex/ for patterns.

Scheduling keeps PRs out of working hours so they batch overnight and CI runs when nobody is fighting for runners. Times use Renovate’s own later syntax, anchored to your timezone.

Rate-limiting caps the firehose. prHourlyLimit throttles creation velocity; prConcurrentLimit caps how many open Renovate PRs a repo carries at once. With prConcurrentLimit: 10, Renovate opens its highest-priority ten, and as each merges it backfills the next from the queue – so the backlog drains steadily instead of arriving all at once.

5. Prioritizing vulnerability fixes

Routine bumps can wait for the overnight window. A known-exploited CVE in a transitive dep cannot. Renovate pulls advisory data from the OSV database and GitHub’s advisory feed, and you want those updates to ignore your schedule and limits:

{
  "vulnerabilityAlerts": {
    "enabled": true,
    "labels": ["security", "priority/high"],
    "schedule": ["at any time"],
    "prCreation": "immediate"
  },
  "osvVulnerabilityAlerts": true
}

Setting schedule: ["at any time"] inside vulnerabilityAlerts overrides the org-wide overnight window for security PRs specifically; prCreation: "immediate" skips the internal stability delay. The osvVulnerabilityAlerts flag broadens coverage beyond GitHub’s feed to the full OSV dataset, which matters for ecosystems GitHub indexes less completely (Go modules, crates, PyPI transitive chains). On GitHub the underlying alerts require Dependency Graph and Dependabot alerts enabled at the org level – Renovate reads them, it does not turn them on for you.

The payoff: a security fix jumps the queue, lands a PR within minutes carrying a security label, and – if you trust your tests – can auto-merge the moment CI passes, which is exactly the behavior we wire next.

6. Safe auto-merge gated on required checks and stability days

Auto-merge is where teams either save hundreds of review-hours or cause an outage. The discipline is simple: never auto-merge anything CI did not vet, and never auto-merge a release that is hours old.

Two safety mechanisms combine. Branch protection with required status checks means Renovate physically cannot merge a red PR – it sets the PR to auto-merge and the platform completes it only after checks pass. minimumReleaseAge (the renamed, current form of the old stabilityDays) refuses to even open a PR until a release has existed for N days, which dodges the all-too-common “package author published a broken patch, then yanked it an hour later” trap.

{
  "minimumReleaseAge": "3 days",
  "internalChecksFilter": "strict",
  "packageRules": [
    {
      "description": "Auto-merge non-major dev/CI updates once CI is green and aged",
      "matchDepTypes": ["devDependencies"],
      "matchManagers": ["npm", "github-actions"],
      "matchUpdateTypes": ["minor", "patch", "pin", "digest"],
      "automerge": true,
      "automergeType": "pr",
      "platformAutomerge": true
    },
    {
      "description": "Never auto-merge majors -- humans only",
      "matchUpdateTypes": ["major"],
      "automerge": false,
      "addLabels": ["needs-human-review"]
    }
  ]
}

platformAutomerge: true hands the actual merge to GitHub/GitLab’s native auto-merge, so it happens the instant the last required check turns green even between Renovate runs – no waiting for the next cron tick. internalChecksFilter: "strict" makes Renovate honor minimumReleaseAge as a hard gate rather than a soft preference.

Auto-merge requires required status checks configured in branch protection. Without them, “auto-merge a green PR” is meaningless – there is nothing defining “green.” If your repos have no required checks, fix that before enabling automerge, or you are just merging unverified diffs on a timer.

Start auto-merge narrow: dev dependencies, lockfile maintenance, and digest pins only. Promote runtime minor/patch to auto-merge per ecosystem once you trust each one’s test coverage. Majors stay human, always.

7. Monorepos, lockfile maintenance, and post-upgrade tasks

Monorepos break naive tooling because a single version bump must propagate to many lockfiles and workspaces atomically. Renovate handles this natively but you steer it.

The group:monorepos preset (already in our base) keeps families like @nestjs/*, @angular/*, or the AWS SDK on matched versions so you never get a half-upgraded framework. For your own internal workspaces, enable lockfile maintenance to refresh transitive deps that no direct bump would touch:

{
  "lockFileMaintenance": {
    "enabled": true,
    "schedule": ["before 6am on the first day of the month"],
    "commitMessageAction": "Refresh lock file",
    "automerge": true
  }
}

For repos where a dependency bump must trigger codegen, schema regeneration, or a re-vendored file, post-upgrade tasks run commands and commit the result into the same PR. These require self-hosting and an explicit allowedCommands allowlist for safety:

{
  "postUpgradeTasks": {
    "commands": ["npm run codegen", "npm run format"],
    "fileFilters": ["src/generated/**", "**/*.ts"],
    "executionMode": "branch"
  }
}
// config.js -- the allowlist that authorizes the above
module.exports = {
  allowedCommands: ['^npm run codegen$', '^npm run format$'],
};

executionMode: "branch" runs the commands once per branch after all updates are applied, rather than once per dependency – correct for codegen, where you want one regeneration over the final state, not N intermediate ones.

8. Dashboard, pinning strategy, and measuring lead time

The Dependency Dashboard (enabled via :dependencyDashboard in the base preset) creates a single tracking issue per repo listing every pending, rate-limited, and errored update, with checkboxes to force a specific PR or rebase. It is your fleet’s control surface – the place an engineer goes to say “open the React 19 major PR now” without waiting for the schedule.

On pinning strategy, the principal-level call is application-versus-library. Applications (anything you deploy) should pin exact versions so builds are reproducible and the lockfile is the source of truth – this is what config:recommended does for app dependencies. Libraries (anything you publish) should keep wide semver ranges so they do not force version constraints on consumers. Encode the split explicitly:

{
  "packageRules": [
    {
      "description": "Apps: pin exact for reproducible deploys",
      "matchFileNames": ["services/**/package.json"],
      "rangeStrategy": "pin"
    },
    {
      "description": "Published libs: keep ranges wide",
      "matchFileNames": ["packages/**/package.json"],
      "rangeStrategy": "widen"
    }
  ]
}

For lead time – the metric that proves the platform works – measure the gap between a release being published upstream and your repo merging it. Renovate stamps every branch with structured commit metadata; the practical measurement is the delta between a dependency’s date_published (from the datasource) and your PR’s merge time. Track the org-wide p50 and p90. A healthy fleet sits at a p50 of single-digit days for patches and trends down over time. When p90 spikes, it is almost always a stuck repo with failing CI blocking auto-merge – the dashboard issue for that repo will already be flagging it.

Verify

Run these before declaring the rollout done.

  1. Dry run finds dependencies. Confirm the explain pass lists real deps with no unexpected skipReason:
LOG_LEVEL=debug renovate --dry-run=full my-org/my-service 2>&1 \
  | grep -E 'depName|skipReason' | sort | uniq -c | sort -rn
  1. The shared preset resolves. A repo’s onboarding PR or dashboard should show your org defaults (the dependencies label, your schedule). Validate any preset edit locally before it hits the fleet:
npx --package renovate -- renovate-config-validator default.json
  1. Security PRs ignore the schedule. Introduce a knowingly-vulnerable pin in a sandbox repo and confirm Renovate opens a security-labeled PR immediately, not in the overnight window.
  2. Auto-merge respects CI. Open a grouped dev-dependency PR, force a test to fail, and confirm it does not merge. Fix the test and confirm it merges without human action.
  3. minimumReleaseAge holds. Confirm a release published today does not get a PR until it clears the configured age – check the dashboard’s “Pending Status Checks” / “Awaiting Schedule” section.

Enterprise scenario

A platform team running ~280 microservice repos on GitHub Enterprise Server moved off Dependabot because it could not reach their internal Artifactory npm and Docker registries, and because every repo’s dependabot.yml had drifted into a bespoke snowflake. They self-hosted Renovate on a scheduled Actions workflow with a GitHub App token and a single renovate-config preset repo.

The constraint that bit them: auto-merge worked for two weeks, then a routine patch took down a payments service. Root cause was not Renovate – it was that the payments repo’s “required” check was advisory, not enforced in branch protection, so a green-looking-but-not-actually-required test let a bad bump merge at 3am. The same automerge: true policy was correct for 279 repos and catastrophic for the one without enforced checks.

The fix was twofold. First, they made enforced-required-checks a precondition: a small org-policy job audits branch protection and Renovate auto-merge is gated behind it via a packageRules entry that disables auto-merge on any repo missing the marker file. Second, they added a blast-radius brake – payments and other tier-0 services keep minimumReleaseAge: "7 days" and never auto-merge runtime deps, only dev/CI tooling:

{
  "packageRules": [
    {
      "description": "Tier-0 services: longer bake, no runtime auto-merge",
      "matchFileNames": ["services/payments/**", "services/ledger/**"],
      "minimumReleaseAge": "7 days",
      "matchDepTypes": ["dependencies"],
      "automerge": false
    }
  ]
}

Six months in, ~70% of all dependency PRs across the fleet merge with zero human touch, security fixes land a PR within minutes of disclosure, and the median patch lead time dropped from 41 days (the Dependabot-era backlog) to 4. The lesson the team internalized: auto-merge is only as safe as your weakest branch protection, so the policy that enables it must verify the gate exists, not assume it.

Checklist

renovatedependency-managementci-cdsupply-chainautomation

Comments

Keep Reading