Uploaded image for project: 'Jenkins'
  1. Jenkins
  2. JENKINS-60457

NullPointerException in h.p.e.p.content.ScriptContent.renderTemplate

      I have not managed to reproduce this bug consistently, it looks to me like a race condition somewhere.

      When the build finishes, instead of getting a proper email, we are receving an email containing the following stack trace:

      Exception raised during template rendering: Cannot invoke method getParent() on null object java.lang.NullPointerException: Cannot invoke method getParent() on null object at org.codehaus.groovy.runtime.NullObject.invokeMethod(NullObject.java:91) at org.codehaus.groovy.runtime.callsite.PogoMetaClassSite.call(PogoMetaClassSite.java:48) at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:48) at org.codehaus.groovy.runtime.callsite.NullCallSite.call(NullCallSite.java:35) at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:48) at hudson.model.Queue$Executable$getParent.call(Unknown Source) at SimpleTemplateScript14$_run_closure4.doCall(SimpleTemplateScript14.groovy:35) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:93) at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:325) at org.codehaus.groovy.runtime.metaclass.ClosureMetaClass.invokeMethod(ClosureMetaClass.java:294) at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1022) at groovy.lang.Closure.call(Closure.java:414) at groovy.lang.Closure.call(Closure.java:430) at org.codehaus.groovy.runtime.DefaultGroovyMethods.each(DefaultGroovyMethods.java:2040) at org.codehaus.groovy.runtime.DefaultGroovyMethods.each(DefaultGroovyMethods.java:2025) at org.codehaus.groovy.runtime.DefaultGroovyMethods.each(DefaultGroovyMethods.java:2066) at org.codehaus.groovy.runtime.dgm$163.invoke(Unknown Source) at org.codehaus.groovy.runtime.callsite.PojoMetaMethodSite$PojoMetaMethodSiteNoUnwrapNoCoerce.invoke(PojoMetaMethodSite.java:274) at org.codehaus.groovy.runtime.callsite.PojoMetaMethodSite.call(PojoMetaMethodSite.java:56) at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:125) at SimpleTemplateScript14.run(SimpleTemplateScript14.groovy:33) at groovy.text.SimpleTemplateEngine$SimpleTemplate$1.writeTo(SimpleTemplateEngine.java:168) at groovy.text.SimpleTemplateEngine$SimpleTemplate$1.toString(SimpleTemplateEngine.java:180) at hudson.plugins.emailext.plugins.content.ScriptContent.renderTemplate(ScriptContent.java:151) at hudson.plugins.emailext.plugins.content.ScriptContent.evaluate(ScriptContent.java:81) at hudson.plugins.emailext.plugins.content.AbstractEvalContent.evaluate(AbstractEvalContent.java:76) at org.jenkinsci.plugins.tokenmacro.DataBoundTokenMacro.evaluate(DataBoundTokenMacro.java:202) at org.jenkinsci.plugins.tokenmacro.Parser.processToken(Parser.java:323) at org.jenkinsci.plugins.tokenmacro.Action$KiHW1UeqOdqAwZul.run(Unknown Source) at org.parboiled.matchers.ActionMatcher.match(ActionMatcher.java:96) at org.parboiled.parserunners.BasicParseRunner.match(BasicParseRunner.java:77) at org.parboiled.MatcherContext.runMatcher(MatcherContext.java:351) at org.parboiled.matchers.SequenceMatcher.match(SequenceMatcher.java:46) at org.parboiled.parserunners.BasicParseRunner.match(BasicParseRunner.java:77) at org.parboiled.MatcherContext.runMatcher(MatcherContext.java:351) at org.parboiled.matchers.FirstOfMatcher.match(FirstOfMatcher.java:41) at org.parboiled.parserunners.BasicParseRunner.match(BasicParseRunner.java:77) at org.parboiled.MatcherContext.runMatcher(MatcherContext.java:351) at org.parboiled.matchers.FirstOfMatcher.match(FirstOfMatcher.java:41) at org.parboiled.parserunners.BasicParseRunner.match(BasicParseRunner.java:77) at org.parboiled.MatcherContext.runMatcher(MatcherContext.java:351) at org.parboiled.matchers.ZeroOrMoreMatcher.match(ZeroOrMoreMatcher.java:39) at org.parboiled.parserunners.BasicParseRunner.match(BasicParseRunner.java:77) at org.parboiled.MatcherContext.runMatcher(MatcherContext.java:351) at org.parboiled.matchers.SequenceMatcher.match(SequenceMatcher.java:46) at org.parboiled.parserunners.BasicParseRunner.match(BasicParseRunner.java:77) at org.parboiled.MatcherContext.runMatcher(MatcherContext.java:351) at org.parboiled.parserunners.BasicParseRunner.run(BasicParseRunner.java:72) at org.parboiled.parserunners.ReportingParseRunner.runBasicMatch(ReportingParseRunner.java:86) at org.parboiled.parserunners.ReportingParseRunner.run(ReportingParseRunner.java:66) at org.parboiled.parserunners.AbstractParseRunner.run(AbstractParseRunner.java:81) at org.parboiled.parserunners.AbstractParseRunner.run(AbstractParseRunner.java:76) at org.jenkinsci.plugins.tokenmacro.Parser.process(Parser.java:85) at org.jenkinsci.plugins.tokenmacro.Parser.process(Parser.java:74) at org.jenkinsci.plugins.tokenmacro.TokenMacro.expand(TokenMacro.java:199) at org.jenkinsci.plugins.tokenmacro.TokenMacro.expandAll(TokenMacro.java:237) at hudson.plugins.emailext.plugins.ContentBuilder.transformText(ContentBuilder.java:80) at hudson.plugins.emailext.ExtendedEmailPublisher.addContent(ExtendedEmailPublisher.java:880) at hudson.plugins.emailext.ExtendedEmailPublisher.createMail(ExtendedEmailPublisher.java:753) at hudson.plugins.emailext.ExtendedEmailPublisher.sendMail(ExtendedEmailPublisher.java:451) at hudson.plugins.emailext.ExtendedEmailPublisher._perform(ExtendedEmailPublisher.java:441) at hudson.plugins.emailext.ExtendedEmailPublisher.perform(ExtendedEmailPublisher.java:349) at hudson.tasks.BuildStepMonitor$1.perform(BuildStepMonitor.java:20) at hudson.model.AbstractBuild$AbstractBuildExecution.perform(AbstractBuild.java:741) at hudson.model.AbstractBuild$AbstractBuildExecution.performAllBuildSteps(AbstractBuild.java:690) at hudson.model.Build$BuildExecution.cleanUp(Build.java:196) at hudson.model.Run.execute(Run.java:1862) at hudson.model.FreeStyleBuild.run(FreeStyleBuild.java:43) at hudson.model.ResourceController.execute(ResourceController.java:97) at hudson.model.Executor.run(Executor.java:429)
      

      We use:
      Jenkins ver. 2.190.1
      Email extension plugin : 2.68

          [JENKINS-60457] NullPointerException in h.p.e.p.content.ScriptContent.renderTemplate

          I forgot to mention that this is quite annoying for us because the email with the stacktrace is sent to every contributor of our project
          Looks like every email that is part of the git history is included

          Andrea Giardini added a comment - I forgot to mention that this is quite annoying for us because the email with the stacktrace is sent to every contributor of our project Looks like every email that is part of the git history is included

          Alex Earl added a comment -

          What template are you using? What does your config look like? 

          Alex Earl added a comment - What template are you using? What does your config look like? 

          Hello Alex and thank you for your answer

          This is the template we are using:

          <%
          /**
           * injected usable variables:
           * https://github.com/jenkinsci/email-ext-plugin/blob/master/src/main/java/hudson/plugins/emailext/plugins/content/ScriptContent.java#L120-L125
           */
          
          import hudson.model.Result
          import org.jenkinsci.plugins.tokenmacro.TokenMacro;
          
          def getBuildCauses = { c, sc ->
            if (c instanceof hudson.model.Cause.UpstreamCause) {
              sc.add(c)
              c.upstreamCauses.each { upstreamCause ->
                owner.call(upstreamCause, sc)
              }
            } else if (c instanceof java.util.Collection) {
              c.each { cause ->
                owner.call(cause, sc)
              }
            } else {
              // just add it
              sc.add(c)
            }
          }
          
          def buildCauses = []
          getBuildCauses(build.causes, buildCauses)
          buildCauses = buildCauses.reverse()
          
          def upstreamBuilds = buildCauses.findAll{ it instanceof hudson.model.Cause.UpstreamCause }
            .collect{ return it.getUpstreamRun() }
          def changeSetsByBuild = [:]
          upstreamBuilds.each{
            changeSetsByBuild.put(
              it.getParent().name,
              ['changesets': it.changeSets, 'buildurl': rooturl + it.getUrl()]
            )
          }
          changeSetsByBuild.put(
            project.name,
            ['changesets': build.changeSets, 'buildurl': rooturl + build.getUrl()]
          )
          %>
          <STYLE>
            BODY TABLE, TD, TH, P, H1, H2 { margin:0; font:normal normal 100% Georgia, Serif; background-color: #ffffff; }
          H1, H2 { border-bottom:dotted 1px #999999; padding:5px; margin-top:10px; margin-bottom:10px; color: #000000; font: normal bold 130% Georgia,Serif; background-color:#f0f0f0; }
          H2 { padding:5px; margin-top:5px; margin-bottom:5px; font: italic bold 110% Georgia,Serif; }
            .bg2 { color:black; background-color:#E0E0E0; font-size:110%; }
          TH { font-weight: bold; }
          TR, TD, TH { padding:2px; }
          TR.gray { background-color:#f0f0f0; }
          TD.test_passed { color:blue; }
          TD.test_failed { color:red; }
          TD.test_skipped { color:grey; }
            .console { font: normal normal 90% Courier New, monotype; padding:0px; margin:0px; }
          DIV.content, DIV.header { background: #ffffff; border: line; 1px #666; margin: 2px; content: 2px; padding: 2px; }
          TABLE.border, TH.border, TD.border { border: 1px solid black; border-collapse:collapse; }
          TD.right { text-align:right; }
          </STYLE>
          <BODY>
              <DIV class="header">
              <!-- GENERAL INFO -->
                  <TABLE width="100%">
                      <TH><TR><TD colspan="2" valign="center"><H2><IMG SRC="${rooturl}static/e59dfe28/images/24x24/<%= (build.result == null || build.result == Result.SUCCESS) ? 'blue.gif' : build.result == Result.FAILURE ? 'red.gif' : 'yellow.gif' %>" /><B>BUILD ${build.result}</B></TD></TR></TH>
          <TR><TD>URL</TD><TD><A href="${rooturl}${build.url}">${rooturl}${build.url}</A></TD></TR>
          <TR><TD>Project:</TD><TD>${project.name}</TD></TR>
                      <TR><TD>Git SHA:</TD><TD><%= TokenMacro.expand( build, listener, '${GIT_REVISION} on branch ${GIT_BRANCH}' ) %></TD></TR>
            <TR><TD>Date:</TD><TD>${it.timestampString}</TD></TR>
                      <TR><TD>Duration:</TD><TD>${build.durationString}</TD></TR>
            <TR>
            <TD>Build causes:</TD>
                          <TD>
                          <% buildCauses.each { buildCause -> %>
                          ${buildCause.shortDescription}<BR/>
            <% } %>
          </TD>
                      </TR>
            <TR><TD>Built on node:</TD><TD><%= build.builtOnStr == '' ? 'master' : build.builtOnStr %></TD></TD></TR>
          </TABLE>
              </DIV>
          
            <% if(changeSetsByBuild) { %>
            <!-- CHANGE SETS -->
              <DIV class="content">
            <H2>Changes</H2>
                  <TABLE width="100%">
                  <%  changeSetsByBuild.each() { projectName, values -> %>
                      <TR><TD class="bg2" colspan="2"><B>Job: <A href="${values['buildurl']}">${projectName}</B></a></TD></TR>
                      <%
                          def changeSet = values['changesets'][0] ?: null
                          if(changeSet != null && !changeSet.isEmptySet()) {
                              changeSet.each { cs ->
                                  commitId = cs.id
                                  author = cs.author
                                  commitMsg = cs.msgAnnotated
                                  commitUrl = changeSet.getBrowser().getChangeSetLink(cs)
                      %>
                      <TR><TD colspan="2">&nbsp;&nbsp;<B>${commitMsg}</B> - <A href="${commitUrl}" target="_blank">${commitId}</A> by <%= author %></TD></TR>
                      <%      }
                          }
                          else {
                      %>
                      <TR><TD colspan="2">No Changes</TD></TR>
                      <%
                          }
                      }
                      %>
                  </TABLE>
            </DIV>
          <% } %>
          
          <% if (build.result != Result.SUCCESS) { %>
              <!-- BUILD FAILURE ANALYZER -->
              <DIV class="content">
                  <H2>Build Failure Analyzer</H2>
            <%= TokenMacro.expand(build, listener, '${BUILD_FAILURE_ANALYZER, includeTitle=false, includeIndications=true, useHtmlFormat=true, noFailureText="No build failure cause detected."}') %>
            </DIV>
          <% } %>
          
          <% if (build.result != Result.SUCCESS && !it.JUnitTestResult.isEmpty()) { %>
              <!-- JUnit TEMPLATE -->
              <DIV class="content">
                  <A href="${rooturl}${build.url}/testreport"><H2>Failed JUnit Tests</H2></A>
            <TABLE class="border">
            <TR>
            <TH class="border">Package</TH>
                          <TH class="border">Failed</TH>
            <TH class="border">Passed</TH>
                          <TH class="border">Skipped</TH>
            <TH class="border">Total</TH>
                      </TR>
            <% it.JUnitTestResult.each { junitResult -> %>
            <% junitResult.getChildren().each { packageResult -> %>
            <% if (packageResult.getFailCount() > 0) { %>
            <TR>
            <TD class="border"><TT>${packageResult.getName()}</TT></TD>
            <TD class="border test_failed">${packageResult.getFailCount()}</TD>
                          <TD class="border test_passed">${packageResult.getPassCount()}</TD>
            <TD class="border test_skipped">${packageResult.getSkipCount()}</TD>
                          <TD class="border"><B>${packageResult.getPassCount()+packageResult.getFailCount()+packageResult.getSkipCount()}</B></TD>
                      </TR>
            <% packageResult.getFailedTests().each { failed_test -> %>
            <TR><TD class="test_failed" colspan="5"><TT>${failed_test.getFullName()}</TT></TD></TR>
                                  <% } %>
                              <% } %>
                          <% } %>
                      <% } %>
                  </TABLE>
            <BR />
            </DIV>
          <% } %>
          
          <% if (build.result != Result.SUCCESS) { %>
              <!-- CONSOLE OUTPUT -->
              <DIV class="content">
                  <A href="${rooturl}${build.url}/console"><H2>Console Output</H2></A>
            <TABLE class="console">
            <% build.getLog(50).each { line -> %>
            <TR><TD><TT>${org.apache.commons.lang.StringEscapeUtils.escapeHtml(line)}</TT></TD></TR>
                  <% } %>
                  </TABLE>
            <BR />
            </DIV>
          <% } %>
          
          <% if (build.result != Result.SUCCESS && build.artifacts && build.artifacts.size() > 0) { %>
              <!-- ARTIFACTS -->
              <DIV class="content">
                  <H2>Build Artifacts</H2>
            <UL>
            <% build.artifacts.each { artifact -> %>
            <LI><A href="${rooturl}${build.url}artifact/${artifact}">${artifact}</A></LI>
            <% } %>
            </UL>
              </DIV>
            <% } %>
            </BODY>
          

          It's added to the config files using Jobdsl:

          configFiles {
            groovyTemplateConfig {
              id('build-notification-mail-groovy')
              name('build-notification-mail-groovy')
              comment('Build notification mail template written in Groovy')
              content(readFileFromWorkspace('configfiles/emailext/build_notification_template.groovy'))
            }
          }
          

          And this is the CasC configuration:

          unclassified:
            extendedemailpublisher:
              adminRequiredForTemplateTesting: true
              defaultBody: '^${SCRIPT,template="managed:build-notification-mail-groovy"}'
              defaultPresendScript: |
                def failureCauses = build.actions?.find{ it instanceof com.sonyericsson.jenkins.plugins.bfa.model.FailureCauseBuildAction }
          
                if (failureCauses) {
                    def retryBuild = build.actions?.find{ it instanceof com.chikli.hudson.plugin.naginator.NaginatorAction }
          
                    if (retryBuild && retryBuild.getRetryCount() < retryBuild.getMaxRetryCount()) {
                        cancel = true
                        return
                    }
          
                    if (!retryBuild) {
                        // if no retryBuild, check if failureCauses contains the BFA retries,
                        // if true, we assume it is the first build where the error occurs because there won't be a NaginatorAction attached to the build
                        for (def ffc in failureCauses.getFoundFailureCauses()) {
                            if (ffc.categories.find { it ==~ /(Infrastructure|RFT)+/ }) {
                                cancel = true
                                return
                            }
                        }
                    }
                }
              defaultReplyTo: ''
              defaultSubject: '[$PROJECT_NAME] - $BUILD_STATUS - Build #$BUILD_NUMBER'
              listId: 'Build Notifications <ci.cloud>'
          

          Andrea Giardini added a comment - Hello Alex and thank you for your answer This is the template we are using: <% /** * injected usable variables: * https: //github.com/jenkinsci/email-ext-plugin/blob/master/src/main/java/hudson/plugins/emailext/plugins/content/ScriptContent.java#L120-L125 */ import hudson.model.Result import org.jenkinsci.plugins.tokenmacro.TokenMacro; def getBuildCauses = { c, sc -> if (c instanceof hudson.model.Cause.UpstreamCause) { sc.add(c) c.upstreamCauses.each { upstreamCause -> owner.call(upstreamCause, sc) } } else if (c instanceof java.util.Collection) { c.each { cause -> owner.call(cause, sc) } } else { // just add it sc.add(c) } } def buildCauses = [] getBuildCauses(build.causes, buildCauses) buildCauses = buildCauses.reverse() def upstreamBuilds = buildCauses.findAll{ it instanceof hudson.model.Cause.UpstreamCause } .collect{ return it.getUpstreamRun() } def changeSetsByBuild = [:] upstreamBuilds.each{ changeSetsByBuild.put( it.getParent().name, [ 'changesets' : it.changeSets, 'buildurl' : rooturl + it.getUrl()] ) } changeSetsByBuild.put( project.name, [ 'changesets' : build.changeSets, 'buildurl' : rooturl + build.getUrl()] ) %> <STYLE> BODY TABLE, TD, TH, P, H1, H2 { margin:0; font:normal normal 100% Georgia, Serif; background-color: #ffffff; } H1, H2 { border-bottom:dotted 1px #999999; padding:5px; margin-top:10px; margin-bottom:10px; color: #000000; font: normal bold 130% Georgia,Serif; background-color:#f0f0f0; } H2 { padding:5px; margin-top:5px; margin-bottom:5px; font: italic bold 110% Georgia,Serif; } .bg2 { color:black; background-color:#E0E0E0; font-size:110%; } TH { font-weight: bold; } TR, TD, TH { padding:2px; } TR.gray { background-color:#f0f0f0; } TD.test_passed { color:blue; } TD.test_failed { color:red; } TD.test_skipped { color:grey; } .console { font: normal normal 90% Courier New, monotype; padding:0px; margin:0px; } DIV.content, DIV.header { background: #ffffff; border: line; 1px #666; margin: 2px; content: 2px; padding: 2px; } TABLE.border, TH.border, TD.border { border: 1px solid black; border-collapse:collapse; } TD.right { text-align:right; } </STYLE> <BODY> <DIV class= "header" > <!-- GENERAL INFO --> <TABLE width= "100%" > <TH><TR><TD colspan= "2" valign= "center" ><H2><IMG SRC= "${rooturl} static /e59dfe28/images/24x24/<%= (build.result == null || build.result == Result.SUCCESS) ? 'blue.gif' : build.result == Result.FAILURE ? 'red.gif' : 'yellow.gif' %>" /><B>BUILD ${build.result}</B></TD></TR></TH> <TR><TD>URL</TD><TD><A href= "${rooturl}${build.url}" >${rooturl}${build.url}</A></TD></TR> <TR><TD>Project:</TD><TD>${project.name}</TD></TR> <TR><TD>Git SHA:</TD><TD><%= TokenMacro.expand( build, listener, '${GIT_REVISION} on branch ${GIT_BRANCH}' ) %></TD></TR> <TR><TD>Date:</TD><TD>${it.timestampString}</TD></TR> <TR><TD>Duration:</TD><TD>${build.durationString}</TD></TR> <TR> <TD>Build causes:</TD> <TD> <% buildCauses.each { buildCause -> %> ${buildCause.shortDescription}<BR/> <% } %> </TD> </TR> <TR><TD>Built on node:</TD><TD><%= build.builtOnStr == '' ? ' master' : build.builtOnStr %></TD></TD></TR> </TABLE> </DIV> <% if (changeSetsByBuild) { %> <!-- CHANGE SETS --> <DIV class= "content" > <H2>Changes</H2> <TABLE width= "100%" > <% changeSetsByBuild.each() { projectName, values -> %> <TR><TD class= "bg2" colspan= "2" ><B>Job: <A href= "${values[ 'buildurl' ]}" >${projectName}</B></a></TD></TR> <% def changeSet = values[ 'changesets' ][0] ?: null if (changeSet != null && !changeSet.isEmptySet()) { changeSet.each { cs -> commitId = cs.id author = cs.author commitMsg = cs.msgAnnotated commitUrl = changeSet.getBrowser().getChangeSetLink(cs) %> <TR><TD colspan= "2" >&nbsp;&nbsp;<B>${commitMsg}</B> - <A href= "${commitUrl}" target= "_blank" >${commitId}</A> by <%= author %></TD></TR> <% } } else { %> <TR><TD colspan= "2" >No Changes</TD></TR> <% } } %> </TABLE> </DIV> <% } %> <% if (build.result != Result.SUCCESS) { %> <!-- BUILD FAILURE ANALYZER --> <DIV class= "content" > <H2>Build Failure Analyzer</H2> <%= TokenMacro.expand(build, listener, '${BUILD_FAILURE_ANALYZER, includeTitle= false , includeIndications= true , useHtmlFormat= true , noFailureText= "No build failure cause detected." }' ) %> </DIV> <% } %> <% if (build.result != Result.SUCCESS && !it.JUnitTestResult.isEmpty()) { %> <!-- JUnit TEMPLATE --> <DIV class= "content" > <A href= "${rooturl}${build.url}/testreport" ><H2>Failed JUnit Tests</H2></A> <TABLE class= "border" > <TR> <TH class= "border" >Package</TH> <TH class= "border" >Failed</TH> <TH class= "border" >Passed</TH> <TH class= "border" >Skipped</TH> <TH class= "border" >Total</TH> </TR> <% it.JUnitTestResult.each { junitResult -> %> <% junitResult.getChildren().each { packageResult -> %> <% if (packageResult.getFailCount() > 0) { %> <TR> <TD class= "border" ><TT>${packageResult.getName()}</TT></TD> <TD class= "border test_failed" >${packageResult.getFailCount()}</TD> <TD class= "border test_passed" >${packageResult.getPassCount()}</TD> <TD class= "border test_skipped" >${packageResult.getSkipCount()}</TD> <TD class= "border" ><B>${packageResult.getPassCount()+packageResult.getFailCount()+packageResult.getSkipCount()}</B></TD> </TR> <% packageResult.getFailedTests().each { failed_test -> %> <TR><TD class= "test_failed" colspan= "5" ><TT>${failed_test.getFullName()}</TT></TD></TR> <% } %> <% } %> <% } %> <% } %> </TABLE> <BR /> </DIV> <% } %> <% if (build.result != Result.SUCCESS) { %> <!-- CONSOLE OUTPUT --> <DIV class= "content" > <A href= "${rooturl}${build.url}/console" ><H2>Console Output</H2></A> <TABLE class= "console" > <% build.getLog(50).each { line -> %> <TR><TD><TT>${org.apache.commons.lang.StringEscapeUtils.escapeHtml(line)}</TT></TD></TR> <% } %> </TABLE> <BR /> </DIV> <% } %> <% if (build.result != Result.SUCCESS && build.artifacts && build.artifacts.size() > 0) { %> <!-- ARTIFACTS --> <DIV class= "content" > <H2>Build Artifacts</H2> <UL> <% build.artifacts.each { artifact -> %> <LI><A href= "${rooturl}${build.url}artifact/${artifact}" >${artifact}</A></LI> <% } %> </UL> </DIV> <% } %> </BODY> It's added to the config files using Jobdsl: configFiles { groovyTemplateConfig { id( 'build-notification-mail-groovy' ) name( 'build-notification-mail-groovy' ) comment( 'Build notification mail template written in Groovy' ) content(readFileFromWorkspace( 'configfiles/emailext/build_notification_template.groovy' )) } } And this is the CasC configuration: unclassified: extendedemailpublisher: adminRequiredForTemplateTesting: true defaultBody: '^${SCRIPT,template= "managed:build-notification-mail-groovy" }' defaultPresendScript: | def failureCauses = build.actions?.find{ it instanceof com.sonyericsson.jenkins.plugins.bfa.model.FailureCauseBuildAction } if (failureCauses) { def retryBuild = build.actions?.find{ it instanceof com.chikli.hudson.plugin.naginator.NaginatorAction } if (retryBuild && retryBuild.getRetryCount() < retryBuild.getMaxRetryCount()) { cancel = true return } if (!retryBuild) { // if no retryBuild, check if failureCauses contains the BFA retries, // if true , we assume it is the first build where the error occurs because there won't be a NaginatorAction attached to the build for (def ffc in failureCauses.getFoundFailureCauses()) { if (ffc.categories.find { it ==~ /(Infrastructure|RFT)+/ }) { cancel = true return } } } } defaultReplyTo: '' defaultSubject: '[$PROJECT_NAME] - $BUILD_STATUS - Build #$BUILD_NUMBER' listId: 'Build Notifications <ci.cloud>'

          Alex Earl added a comment -

          You don't need to use token macro to expand those tokens. You can just use the tokens like functions in the template in a groovy context <%= %> and it will be used correctly

          Alex Earl added a comment - You don't need to use token macro to expand those tokens. You can just use the tokens like functions in the template in a groovy context <%= %> and it will be used correctly

          Thank you for helping out Do you think that's the problem?

          I was thinking about adding a null-check in:

          upstreamBuilds.each{
            changeSetsByBuild.put(
              it.getParent().name,
              ['changesets': it.changeSets, 'buildurl': rooturl + it.getUrl()]
            )
          } 

           

          Andrea Giardini added a comment - Thank you for helping out Do you think that's the problem? I was thinking about adding a null-check in: upstreamBuilds.each{ changeSetsByBuild.put( it.getParent().name, [ 'changesets' : it.changeSets, 'buildurl' : rooturl + it.getUrl()] ) }  

          Alex Earl added a comment -

          No, I don't think it's the problem. Adding a null check would be a good idea.

          Alex Earl added a comment - No, I don't think it's the problem. Adding a null check would be a good idea.

          Alex Earl added a comment -

          Did you ever get this resolved?

          Alex Earl added a comment - Did you ever get this resolved?

          Hi Alex

          I believe changing the template did the trick. We replaced the code above with:

          Map changeSetsByBuild = upstreamBuilds.grep().collectEntries {
            [(it.getParent().name): ['changesets': it.changeSets, 'buildurl': rooturl + it.getUrl()]]
          }

          Since then we didn't have another NPE triggered. But this was always difficult to reproduce so I can't be 100% sure.

          Does this make sense to you?

          Andrea Giardini added a comment - Hi Alex I believe changing the template did the trick. We replaced the code above with: Map changeSetsByBuild = upstreamBuilds.grep().collectEntries { [(it.getParent().name): [ 'changesets' : it.changeSets, 'buildurl' : rooturl + it.getUrl()]] } Since then we didn't have another NPE triggered. But this was always difficult to reproduce so I can't be 100% sure. Does this make sense to you?

          Alex Earl added a comment -

          The addition of the grep will basically filter out any null items. So, it does make sense. I'm not sure why there would be null items in the upstreamBuilds collection though. Do you feel ok closing this out?

          Alex Earl added a comment - The addition of the grep will basically filter out any null items. So, it does make sense. I'm not sure why there would be null items in the upstreamBuilds collection though. Do you feel ok closing this out?

          Makes sense, please close the issue and thank you for the feedback!

          Andrea Giardini added a comment - Makes sense, please close the issue and thank you for the feedback!

            slide_o_mix Alex Earl
            andrea_giardini Andrea Giardini
            Votes:
            0 Vote for this issue
            Watchers:
            2 Start watching this issue

              Created:
              Updated:
              Resolved: