Saturday, July 24, 2021

Dynamic Branch Matching with WhiteSource Renovate

In addition to first-party libraries, such as Angular Material, most enterprise-level Angular applications make use of dozens of third-party dependencies. These handy reusable libraries are published as npm packages that are ready to be integrated into your applications via the Angular CLI.

Reusable code is a great thing, but there are downsides. One of the main ones is that you could be missing out on important security updates as well as other improvements that might help your application perform better. Moreover, the accumulation of stale dependencies can leave the projects prone to errors and even crashing. You can always visit each library’s website and see what’s new, but who has the time or inclination to do so?

Now there’s a free tool by WhiteSource called Renovate to manage dependency updates automatically. Renovate keeps project dependencies up-to-date using Pull Requests (PRs) and/or branches. It scans repositories for dependencies inside package manager files such as package.json (npm/yarn) and submits PRs with updated versions whenever they are found.

I recently had the opportunity to use Renovate to maintain my organization’s own private libraries. As I got into it, I soon discovered that neither matching branches dynamically nor targeting specific dependencies are as straight-forward as I hoped they would be. Not to discourage anyone from using Renovate, but my task soon ballooned into a long string of challenges that spanned several weeks of full-time effort. On the plus side, I learned a lot about Renovate, and got to correspond with some of the product collaborators, who were very helpful in getting my adoption of Renovate on the right track.

This tutorial will be broken down into 2 parts: the first will cover how to target GitHub branches dynamically; part 2 will be all about targeting specific dependencies.

Note: this tutorial assumes that you have a basic working of Renovate, so we’ll be skipping over the installation procedure and other steps for getting started.

Running Renovate

In order to look up GitHub branches dynamically, you should know that there are two ways to trigger Renovate scanning:

  1. As a GitHub hook that runs when projects are merged into master
  2. As a self-hosted job

We decided to use the second option, using the Docker build, as it provides several distinct advantages:

  • We can run the job any time.
  • We can scan several repositories at once.
  • We can configure the rules more dynamically.

Our Renovate job is hosted on GitHub. It contains a Jenkins pipeline (Jenkinsfile) and Renovate app configuration (renovate-config.js). The renovate-config.js file currently defines the common default configuration for the organization, but we are planning to move some options to each project’s renovate.json configuration as we hand off more applications and dependency updates to Renovate.

Obtaining the List of Active Branches

One of the first challenges I encountered was in setting the branches to scan. Unfortunately, neither the baseBranches nor matchBaseBranches support wildcards or Regular Expressions (RegExes). That was a problem because we wanted to include sprint and release branches, which are both named “Sprint-xxx” and “Release-xxx” where the xxx was the Sprint/Release number.

Our solution was to read the list of active branches from the Yaml file and pass it to our renovate-config.js script via an environment variable. Here’s the relevant code from our Jenkinsfile that collects the branch names and launches the renovate bot:

stage('prepare') {
  repo(env.REPO, env.BRANCH)
  
  // Read active branches from build configuration
  repo('build', 'master', 'build-master')
  def branchData = readYaml file: "build-master/$env.BRANCHES_FILE"
  activeBranches = branchData.collect { k, stops ->
    stops.findAll { stop, branch ->
      stop != 'prod' && branch != 'master' && branch != 'ignore'
    }.collect { s, branch -> branch }
  }.flatten().unique()
}
stage('renovate') {
  withEnv([
    "RENOVATE_CONFIG_FILE=$WORKSPACE/renovate-config.js",
    "LOG_LEVEL=info",
    "ACTIVE_BRANCHES=${activeBranches.join(',')}",
  ]) {
    withCredentials([
      string(credentialsId: 'jenkins-acme-com', variable: 'RENOVATE_TOKEN'),
      string(credentialsId: 'jenkins-acme-com', variable: 'GITHUB_COM_TOKEN'),
    ]) {
      def r = docker.image('renovate/renovate:24')
      def args = [
        "-v /home/jenkins/.npmrc:$WORKSPACE/.npmrc",
        '-v /home/jenkins/.gradle:/.gradle-user-home:rw,z',
      ].join(' ')
        r.inside(args) {
        sh "renovate --print-config true ${repos.join(' ')}"
      }
    }
  }
}

In the renovate-config.js file, we can then access the ACTIVE_BRANCHES variable, to which we append the master branch. The baseBranches property expects an array, so we have to split the list into an array of strings:

module.exports = {
  "baseBranches": (process.env.ACTIVE_BRANCHES + ",master").split(","),
  "branchPrefix": "renovate/",
  "onboarding": true,
  "onboardingConfig": {
    "extends": [":disableHost(registry.npmjs.org)"]
  },
  "platform": "github",
  "rangeStrategy": "bump",
  "packageRules": [
    // ...
  ]
};

Branch Naming

We assigned a branchPrefix of “renovate/” in order to easily identify Renovate branches. The rest of the branch name consists of:

  1. The branch that was scanned.
  2. The package group.
  3. The dependency that was upgraded.
  4. Which part of the semantic version number is being updated, expressed as: x.0.0, 1.x.0, or 1.0.x.According to Renovate collaborators, they name branches thusly because:
    • Branches often receive updates (e.g. new patches) before they’re merged.
    • Naming the branch like 1.x means its name still makes sense if a 1.2.1 release happens.

    You can configure the branch names further by using the string template branchName and/or its sub-templates branchPrefix and branchTopic.

Each of the above name parts are strung together by hyphen (-). Here’s an example branch name:

renovate/master-acme-common-assets-1.x

We can verify after a successful run that three branches and PRs have been created – one for each of our target branches:

angular branches

Conclusion

I read somewhere on the Renovate Help board that the reason that the developers of Renovate chose to use exact branch matching is because they wanted to keep things simple. It might be simpler for them, but for some users of their plugin, not so much! We can only hope that they add wildcard or RegEx support to their branch matching soon. Until then, at least there’s a workaround.

In the next article, we’ll learn how to target specific dependencies. Although that is 100% achievable using the built-in Renovate configuration rules, it takes some know-how to get it working.

Rob Gravelle
Rob Gravelle
Rob Gravelle resides in Ottawa, Canada, and has been an IT guru for over 20 years. In that time, Rob has built systems for intelligence-related organizations such as Canada Border Services and various commercial businesses. In his spare time, Rob has become an accomplished music artist with several CDs and digital releases to his credit.

Popular Articles

Featured