Jenkinsfile : the auto-upgrading project

A dream project is when your codebase runs on the latest and safest version of your libraries.
No need to worry about deprecation, adaptation, breaking changes, or sticking with legacy tools.

Today GitHub/GitLab tend to make it easier with assisted upgrades : dependabot, security alerts, all you have to do is to activate it and to merge on case by case basis.
You can even activate the auto-merge feature.

But sometimes having dedicated pull-requests is a bit overkill.
What you want to do is only to make sure you stay in sync with your dependencies.
Then you can just ask Jenkins to schedule a job to upgrade your dependencies on a daily / weekly / monthly basis.

I want to show you how I adressed the challenge by just re running an upgrade with npm at each run.

pipeline {
  triggers {
   
    // you can trigger a build when you know for sure that a dependency has been published just before
    upstream(upstreamProjects:
      ‘dependency-a/master,dependency-b/master’,
      threshold: hudson.model.Result.SUCCESS
    )
    cron(‘H 18 * * 1-7’)
  }
  agent {
    kubernetes {
      yaml “””
        apiVersion: v1
        kind: Pod
        spec:
          securityContext:
            runAsNonRoot: false
            runAsUser: 0
            runAsGroup: 0
            fsGroup: 0
          containers:
          – name: node
            image: node:16.13.1
            command:
            – cat
            tty: true
        “””
    }
  }
  stages {
    stage(🧰 install”) {
      steps {
        container(‘node’) {
          script {
            // we will use the magic word “[upgrade]” to prevent jenkins from cycling with builds
            def lastCommitIsUpgrade = sh(script: ‘git log -1 –pretty=%B | cat’, returnStdout: true)?.contains(‘[upgrade]’)
            // even if the last commit is “[upgrade]”, we can allow jobs to be manually triggered
            def triggeredFromGit = currentBuild.rawBuild.getCauses().any {
              it.toString().contains(“jenkins.branch.BranchEventCause”)
            }
            env.JUST_UPGRADED = lastCommitIsUpgrade && triggeredFromGit ? ‘true’ : ‘false’
            if (env.JUST_UPGRADED == ‘false’) {
              sh “npm install -g npm-check-updates”
              // to be done if you need to re-auth with npm, otherwise please remove
              sh “npm install -g npm-cli-login”
            }
          }
        }
      }
    }
    stage(🔄 Auto-upgrade’) {
      when {
          environment name: ‘JUST_UPGRADED’, value: ‘false’
      }
      steps {
        container(‘node’) {
          script {
            withNPM(npmrcConfig: ‘npmrcConfig’) {
              sh “mv .npmrc /root”
              // to be done if you need to re-auth with npm, otherwise please remove from here…
              def credentials = sh(script: “cat /root/.npmrc | grep _auth | head -n 1 | sed -E ‘s/[^=]+= //’ | base64 -d”, returnStdout: true).trim().split(‘:’);
              def email = sh(script: “cat /root/.npmrc | grep email | head -n 1 | sed -E ‘s/[^=]+= //'”, returnStdout: true).trim();
              def login = credentials[0];
              def password = credentials[1];
              sh “npm-cli-login -u ${login} -p ${password} -e ${email} -r https://registry.npmjs.org/”
              // …to here
              // we will run npm-check-updates to upgrade all the dependencies at once.
              sh “ncu –loglevel verbose –packageFile package.json -u –timeout 9000000 –filterVersion ‘/^\\^?[0-9]+\\.[0-9]+\\.[0-9]+\$/'”
              // we run npm install to update the package-lock.json
              sh “npm install –force”
            }
          }
        }
      }
    }
    stage(👷 Build’) {
      when {
          environment name: ‘JUST_UPGRADED’, value: ‘false’
      }
      steps {
        container(‘node’) {
          withNPM(npmrcConfig: ‘npmrcConfig’) {
            sh “mv .npmrc /root”
            sh “npm run build”
          }
        }
      }
    }
    // this stage is only triggered if the build is successful
    // Thus, the upgraded package.json is saved only
    // if the project was built successfully
    stage(🔄 Push upgrade to master”) {
      when {
          allOf {
            environment name: ‘BRANCH_NAME’, value: ‘master’
            environment name: ‘JUST_UPGRADED’, value: ‘false’
          }
      }
      steps {
        container(“node”) {
          // my company uses withCredentials to inject password variables in the pipeline
          withCredentials([sshUserPrivateKey(credentialsId: ‘credentials-github’, keyFileVariable: ‘SSH_KEY’)]) {
            sh ‘echo ssh -i $SSH_KEY -l git -o StrictHostKeyChecking=no \\\\$@\\” > ../local_ssh.sh’
            sh ‘chmod +x ../local_ssh.sh’
            withEnv([‘GIT_SSH=../local_ssh.sh’]) {
              sh ‘git config –global user.email “devops@acme.com”‘
              sh ‘git config –global user.name “dev-ops”‘
              sh “git remote set-url origin git@github.com:user/project.git”
              sh “git checkout master”
              sh ‘git add package.json package-lock.json’
              sh “git commit -m ‘[upgrade]’ || true”
              sh ‘git push || true’
            }
          }
        }
      }
    }
  }
}
The “upgrade-everything” task is no longer part of our checklist… It all works by itself.
Ok, this is the last post for 2021. Best wishes. See you soon

Leave a Reply

Your email address will not be published. Required fields are marked *

*

This site uses Akismet to reduce spam. Learn how your comment data is processed.