In-Range Dependency Updates with Renovate

Renovate is a tool to keep dependencies up-to-date in your software development projects. By default it is very eager to update dependencies regardless of defined ranges. This way you can avoid that updates keep pilling up.

I have a few internal packages that are actively hardened in different, parallel software releases. To better align for different teams working on those, different minor versions are being tracked per software release. For example productA relies @internal/package@1.3.x for an ongoing release, and productB tracks @internal/package@1.4.x. And to increase complexity newer features already land in the @internal/package@1.5.x versions.

I want the updates to be automatically distributed to keep manual efforts and overhead low. Enter renovate: Whenever a new version of @internal/package is built, a pipeline running renovate is triggered to update all consumers.

In short I'm aiming to get these changes from renovate:

# package.json
-     "@internal/packageA": "~1.3.3",
+     "@internal/packageA": "~1.3.4",

# package-lock.json
# "node_modules/@internal/packageA": {
-             "version": "1.3.3",    
+             "version": "1.3.4",    

With the base recommended rules from renovate, it would ignore an update on the 1.3.x branch and directly update to the latest 1.5.x version. So I dove deeper into the options how to solve this with renovate.

rangeStrategy: 'in-range-only' is not sufficient

The config parameter rangeStrategy does offer a way to handle only in-range updates: in-range-only. This will update the package in the package-lock.json only.

However this is not ideal in my use-case because some of the consuming packages themselves are a dependency in another project. So this update might get lost as only the data from the package.json is used to resolve the appropriate versions.

Solution: Individual rules with matchCurrentValue

So I needed to go a level deeper and trigger individual update rules based on the currently tracked version. This can be done with matchCurrentValue which contains the actual string from the package.json.

Here a full example to have patch updates for tilde-ranges, and minor and patch updates for caret-ranges:

// `config.js` for a self-hosted renovate setup to only update the `@internal/package`
// triggered from pipelines whenever a new version of `@internal/package` has been published

module.exports = {
  // ... standalone config
  repositories: [
      // ... repository config
      enabledManagers: ['npm'],
      major: { enabled: false },
      minor: { enabled: false },
      patch: { enabled: false },
      rangeStrategy: 'bump', // ensures that entries in the `package.json` is being updated
      separateMinorPatch: true, // otherwise renovate would not track updates to patch versions, 
                                // if also a new minor exists
      packageRules: [
          // disable all dependencies
          enabled: false,
          matchDepTypes: ['devDependencies', 'dependencies', 'peerDependencies'],
          matchPackagePatterns: ['*'],
          matchUpdateTypes: ['minor', 'patch', 'bump']
          // Allow minor and patch updates for caret-ranges, e.g. `^1.3.0`
          enabled: true,
          matchDepTypes: ['devDependencies', 'dependencies'],
          matchPackageNames: ['@internal/package'],
          matchUpdateTypes: ['minor', 'patch'],
          // RegExp to cover all version strings starting with a caret
          matchCurrentValue: "/^\\^/"
          // Allow patch updates for tilde-ranges, e.g. `~1.2.0`
          enabled: true,
          matchDepTypes: ['devDependencies', 'dependencies'],
          matchPackageNames: ['@internal/package'],
          matchUpdateTypes: ['patch'],
          // RegExp to cover all version strings starting with a tilde
          matchCurrentValue: "/^~/"

With automerge enabled and renovate triggered once a new version of @internal/package is published, this removes pretty much any manual involvement. So we can rely that after a few minutes the changes will automatically land in the consuming project. 🚀