22

Anyone have a Jenkins Pipeline script that can stuff all the changes since the previous successful build in a variable? I'm using git and a multibranch pipeline job.

StephenKing
  • 36,187
  • 11
  • 83
  • 112
CaptRespect
  • 1,985
  • 1
  • 16
  • 24

6 Answers6

20

Well I managed to cobble something together. I'm pretty sure you can iterate the arrays better but here's what I've got for now:

node('Android') {
  passedBuilds = []

  lastSuccessfulBuild(passedBuilds, currentBuild);

  def changeLog = getChangeLog(passedBuilds)
  echo "changeLog ${changeLog}"
}

def lastSuccessfulBuild(passedBuilds, build) {
  if ((build != null) && (build.result != 'SUCCESS')) {
      passedBuilds.add(build)
      lastSuccessfulBuild(passedBuilds, build.getPreviousBuild())
   }
}

@NonCPS
def getChangeLog(passedBuilds) {
    def log = ""
    for (int x = 0; x < passedBuilds.size(); x++) {
        def currentBuild = passedBuilds[x];
        def changeLogSets = currentBuild.rawBuild.changeSets
        for (int i = 0; i < changeLogSets.size(); i++) {
            def entries = changeLogSets[i].items
            for (int j = 0; j < entries.length; j++) {
                def entry = entries[j]
                log += "* ${entry.msg} by ${entry.author} \n"
            }
        }
    }
    return log;
  }
Etienne Neveu
  • 12,604
  • 9
  • 36
  • 59
CaptRespect
  • 1,985
  • 1
  • 16
  • 24
  • Unfortunately the very first build for a job returns 0 changes. Ideally I'd consider every file as changed on the very first build. ref: https://issues.jenkins-ci.org/browse/JENKINS-26354 – RaGe Sep 05 '19 at 04:32
  • How would you modify this to accept a certain Jenkins job as parameter? – Chris F Dec 31 '19 at 16:31
  • This always gives me an empty change log. In the `lastSuccessfulBuild()` function, why is the `if()` statement `build.result != SUCCESS`? – Chris F Dec 31 '19 at 23:13
  • @RaGe: After having struggled, I realized in an empiric way that the `currentBuild.rawBuild.changeSets` is initialized during a call of `checkout scm`. If your calls are made before this instruction, you won't get anything. – Max Jun 17 '20 at 12:59
  • if statement inside the lastSuccessfulBuild would be. `if ((build != null) && (build.getResult().toString() == 'SUCCESS'))`. build.result will return class hudson.model.Result, not s string. – Biju Jan 06 '23 at 19:28
13

Based on the answer from CaptRespect I came up with the following script for use in the declarative pipeline:

def changes = "Changes:\n"
build = currentBuild
while(build != null && build.result != 'SUCCESS') {
    changes += "In ${build.id}:\n"
    for (changeLog in build.changeSets) {
        for(entry in changeLog.items) {
            for(file in entry.affectedFiles) {
                changes += "* ${file.path}\n"
            }
        }
    }
    build = build.previousBuild
}
echo changes

This is quite useful in stage->when->expression parts to run a stage only when certain files were changed. I haven't gotten to that part yet though, I'd love to create a shared library from this and make it possible to pass it some globbing patterns to check for.

EDIT: Check the docs btw, in case you want to delve a little deeper. You should be able to convert all the object.getSomeProperty() calls into just entry.someProperty.

Lino
  • 5,084
  • 3
  • 21
  • 39
andsens
  • 6,716
  • 4
  • 30
  • 26
  • Nice. There is a github repo with some pipeline examples if you care to contrubute: https://github.com/jenkinsci/pipeline-examples – CaptRespect Aug 07 '17 at 19:51
  • This is not the in the declarative pipeline style but the scripted pipeline! – Lincoln Sep 13 '17 at 13:33
  • 1
    @Lincoln, yup. You can't do it declaratively. What you need to do is wrap it in a script block or use it from a shared library. – andsens Sep 13 '17 at 13:52
1

In order to return the changes as a list of strings, instead of just printing them, you may use this function (based on @andsens answer):

def getChangesSinceLastSuccessfulBuild() {
    def changes = []
    def build = currentBuild

    while (build != null && build.result != 'SUCCESS') {
        changes += (build.changeSets.collect { changeSet ->
            (changeSet.items.collect { item ->
                (item.affectedFiles.collect { affectedFile ->
                    affectedFile.path
                }).flatten()
            }).flatten()
        }).flatten()

        build = build.previousBuild
    }

    return changes.unique()
}
AlonL
  • 6,100
  • 3
  • 33
  • 32
  • Thanks for making this a copy-pastable snippet! It was exactly what I needed and worked flawlessly. You saved me quite some headache. :) – moertel Jun 26 '23 at 07:54
1

This is what I've used:

def listFilesForBuild(build) {
  def files = []
  currentBuild.changeSets.each {
    it.items.each {
      it.affectedFiles.each {
        files << it.path
      }
    }
  }
  files
}

def filesSinceLastPass() {
  def files = []
  def build = currentBuild
  while(build.result != 'SUCCESS') {
    files += listFilesForBuild(build)
    build = build.getPreviousBuild()
  }
  return files.unique()
}

def files = filesSinceLastPass()
daniel
  • 754
  • 4
  • 11
  • Does `currentBuild` require a declarative pipeline? When I try this in my scripted pipeline, I get `java.lang.NullPointerException: Cannot get property 'result' on null object`, which presumably is because `currentBuild` is `null`. – Shane Bishop May 13 '22 at 18:14
  • Also, the `build` parameter in `listFilesForBuild()` is unused (you use `currentBuild` in that function instead). – Shane Bishop May 13 '22 at 18:15
0

There's the Changes Since Last Success Plugin that could help you with that.

Rogério Peixoto
  • 2,176
  • 2
  • 23
  • 31
0

For anyone using Accurev here is an adaptation of andsens answer. andsens answer can't be used because the Accurev plugin doesn't implement getAffectedFiles. Documentation for the AccurevTransaction that extends the ChangeLogSet.Entry class can be found at here.

import hudson.plugins.accurev.*

def changes = "Changes: \n"
build = currentBuild
// Go through the previous builds and get changes until the
// last successful build is found.
while (build != null && build.result != 'SUCCESS') {
    changes += "Build ${build.id}:\n"

    for (changeLog in build.changeSets) {
        for (AccurevTransaction entry in changeLog.items) {
            changes += "\n    Issue: " + entry.getIssueNum()
            changes += "\n    Change Type: " + entry.getAction()
            changes += "\n    Change Message: " + entry.getMsg()
            changes += "\n    Author: " + entry.getAuthor()
            changes += "\n    Date: " + entry.getDate()
            changes += "\n    Files: "
            for (path in entry.getAffectedPaths()) {
                changes += "\n        " + path;
            }
            changes += "\n"
        }
    }
    build = build.previousBuild
}
echo changes
writeFile file: "changeLog.txt", text: changes
Bakon Jarser
  • 703
  • 6
  • 20