Skip to content

Commit

Permalink
Merge pull request #98 from jenkinsci/JENKINS-46511
Browse files Browse the repository at this point in the history
[JENKINS-46511] Only trigger downstream pipelines if maven builds reach a threshold lifecycle phase
  • Loading branch information
Cyrille Le Clerc committed Sep 27, 2017
2 parents 85f712d + 509c09c commit 83a7ad3
Show file tree
Hide file tree
Showing 10 changed files with 819 additions and 1,141 deletions.
Expand Up @@ -3,6 +3,7 @@
import hudson.Extension; import hudson.Extension;
import hudson.model.Run; import hudson.model.Run;
import hudson.model.TaskListener; import hudson.model.TaskListener;
import hudson.util.ListBoxModel;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.jenkinsci.Symbol; import org.jenkinsci.Symbol;
import org.jenkinsci.plugins.pipeline.maven.GlobalPipelineMavenConfig; import org.jenkinsci.plugins.pipeline.maven.GlobalPipelineMavenConfig;
Expand Down Expand Up @@ -50,10 +51,10 @@ public class PipelineGraphPublisher extends MavenPublisher {
private boolean skipDownstreamTriggers; private boolean skipDownstreamTriggers;


/** /**
* Skip trigger of downstream pipelines if the artifact has just been * Lifecycle phase threshold to trigger downstream pipelines, "deploy" or "install" or "package" or ...
* packaged (mvn package) or installed locally (mvn install) * If this phase has not been successfully reached during the build, then we don't trigger downstream pipelines
*/ */
private boolean skipDownstreamTriggersForNonDeployedArtifacts; private String lifecycleThreshold = "deploy";


private boolean ignoreUpstreamTriggers; private boolean ignoreUpstreamTriggers;


Expand Down Expand Up @@ -83,7 +84,7 @@ public void process(@Nonnull StepContext context, @Nonnull Element mavenSpyLogsE
PipelineMavenPluginDao dao = GlobalPipelineMavenConfig.get().getDao(); PipelineMavenPluginDao dao = GlobalPipelineMavenConfig.get().getDao();


recordDependencies(mavenSpyLogsElt, run, listener, dao); recordDependencies(mavenSpyLogsElt, run, listener, dao);
recordGeneratedArtifacts(mavenSpyLogsElt,run,listener, dao); recordGeneratedArtifacts(mavenSpyLogsElt, run, listener, dao);
} }


protected void recordDependencies(@Nonnull Element mavenSpyLogsElt, @Nonnull Run run, @Nonnull TaskListener listener, @Nonnull PipelineMavenPluginDao dao) { protected void recordDependencies(@Nonnull Element mavenSpyLogsElt, @Nonnull Run run, @Nonnull TaskListener listener, @Nonnull PipelineMavenPluginDao dao) {
Expand Down Expand Up @@ -140,40 +141,40 @@ protected void recordDependencies(List<MavenSpyLogProcessor.MavenDependency> dep


protected void recordGeneratedArtifacts(@Nonnull Element mavenSpyLogsElt, @Nonnull Run run, @Nonnull TaskListener listener, @Nonnull PipelineMavenPluginDao dao) { protected void recordGeneratedArtifacts(@Nonnull Element mavenSpyLogsElt, @Nonnull Run run, @Nonnull TaskListener listener, @Nonnull PipelineMavenPluginDao dao) {
List<MavenSpyLogProcessor.MavenArtifact> generatedArtifacts = listArtifacts(mavenSpyLogsElt); List<MavenSpyLogProcessor.MavenArtifact> generatedArtifacts = listArtifacts(mavenSpyLogsElt);
List<Element> mvnDeployEvents = XmlUtils.getExecutionEvents(mavenSpyLogsElt, "MojoSucceeded", "org.apache.maven.plugins", "maven-deploy-plugin", "deploy");
List<Element> nexusDeployEvents = XmlUtils.getExecutionEvents(mavenSpyLogsElt, "MojoSucceeded", "org.sonatype.plugins", "nexus-staging-maven-plugin", "deploy");


boolean artifactsHaveBeenMvnDeployed = !mvnDeployEvents.isEmpty() || !nexusDeployEvents.isEmpty(); List<String> executedLifecyclePhases = XmlUtils.getExecutedLifecyclePhases(mavenSpyLogsElt);

recordGeneratedArtifacts(generatedArtifacts, executedLifecyclePhases, run, listener, dao);
recordGeneratedArtifacts(generatedArtifacts, artifactsHaveBeenMvnDeployed, run, listener, dao);
} }


/** /**
* * @param generatedArtifacts deployed artifacts
* @param generatedArtifacts deployed artifacts * @param executedLifecyclePhases Maven lifecycle phases that have been gone through during the Maven execution (e.g. "..., compile, test, package..." )
* @param artifactsHaveBeenMvnDeployed artifacts have been deployed using "mvn deploy" or "mvn nexus-staging-maven-plugin:deploy"
* @param run * @param run
* @param listener * @param listener
* @param dao * @param dao
*/ */
protected void recordGeneratedArtifacts(List<MavenSpyLogProcessor.MavenArtifact> generatedArtifacts, boolean artifactsHaveBeenMvnDeployed, @Nonnull Run run, @Nonnull TaskListener listener, @Nonnull PipelineMavenPluginDao dao) { protected void recordGeneratedArtifacts(List<MavenSpyLogProcessor.MavenArtifact> generatedArtifacts, List<String> executedLifecyclePhases, @Nonnull Run run, @Nonnull TaskListener listener, @Nonnull PipelineMavenPluginDao dao) {
if (LOGGER.isLoggable(Level.FINE)) { if (LOGGER.isLoggable(Level.FINE)) {
listener.getLogger().println("[withMaven] pipelineGraphPublisher - recordGeneratedArtifacts..."); listener.getLogger().println("[withMaven] pipelineGraphPublisher - recordGeneratedArtifacts...");
} }
for(MavenSpyLogProcessor.MavenArtifact artifact: generatedArtifacts) { for (MavenSpyLogProcessor.MavenArtifact artifact : generatedArtifacts) {
boolean skipDownstreamPipelines = this.skipDownstreamTriggers || boolean skipDownstreamPipelines = this.skipDownstreamTriggers ||
(this.skipDownstreamTriggersForNonDeployedArtifacts && !artifactsHaveBeenMvnDeployed); (!executedLifecyclePhases.contains(this.lifecycleThreshold));


if (LOGGER.isLoggable(Level.FINE)) { if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "Build {0}#{1} - record generated {2}:{3}, version:{4}, skipDownstreamTriggers:{5}, artifactsHaveBeenMvnDeployed: {6}", LOGGER.log(Level.FINE, "Build {0}#{1} - record generated {2}:{3}, version:{4}, " +
"executedLifecyclePhases: {5}, " +
"skipDownstreamTriggers:{6}, lifecycleThreshold: {7}",
new Object[]{run.getParent().getFullName(), run.getNumber(), new Object[]{run.getParent().getFullName(), run.getNumber(),
artifact.getId(), artifact.type, artifact.version, artifact.getId(), artifact.type, artifact.version,
skipDownstreamTriggers, artifactsHaveBeenMvnDeployed}); executedLifecyclePhases,
skipDownstreamTriggers, lifecycleThreshold});
listener.getLogger().println("[withMaven] pipelineGraphPublisher - Record generated artifact: " + artifact.getId() + ", version: " + artifact.version + listener.getLogger().println("[withMaven] pipelineGraphPublisher - Record generated artifact: " + artifact.getId() + ", version: " + artifact.version +
", skipDownstreamTriggers: " + skipDownstreamTriggers + ", artifactsHaveBeenMvnDeployed:" + artifactsHaveBeenMvnDeployed + ", executedLifecyclePhases: " + executedLifecyclePhases +
", skipDownstreamTriggers: " + skipDownstreamTriggers + ", lifecycleThreshold:" + lifecycleThreshold +
", file: " + artifact.file); ", file: " + artifact.file);
} }
dao.recordGeneratedArtifact(run.getParent().getFullName(), run.getNumber(), dao.recordGeneratedArtifact(run.getParent().getFullName(), run.getNumber(),
artifact.groupId, artifact.artifactId, artifact.version, artifact.type, artifact.baseVersion, artifact.groupId, artifact.artifactId, artifact.version, artifact.type, artifact.baseVersion,
skipDownstreamPipelines); skipDownstreamPipelines);
} }
Expand Down Expand Up @@ -241,6 +242,7 @@ public String toString() {
"versions={snapshot:" + isIncludeSnapshotVersions() + ", release:" + isIncludeReleaseVersions() + "}" + "versions={snapshot:" + isIncludeSnapshotVersions() + ", release:" + isIncludeReleaseVersions() + "}" +
']'; ']';
} }

/** /**
* @param mavenSpyLogs Root XML element * @param mavenSpyLogs Root XML element
* @return list of {@link MavenSpyLogProcessor.MavenArtifact} * @return list of {@link MavenSpyLogProcessor.MavenArtifact}
Expand Down Expand Up @@ -346,13 +348,13 @@ public void setIgnoreUpstreamTriggers(boolean ignoreUpstreamTriggers) {
this.ignoreUpstreamTriggers = ignoreUpstreamTriggers; this.ignoreUpstreamTriggers = ignoreUpstreamTriggers;
} }


public boolean isSkipDownstreamTriggersForNonDeployedArtifacts() { public String getLifecycleThreshold() {
return skipDownstreamTriggersForNonDeployedArtifacts; return lifecycleThreshold;
} }


@DataBoundSetter @DataBoundSetter
public void setSkipDownstreamTriggersForNonDeployedArtifacts(boolean skipDownstreamTriggersForNonDeployedArtifacts) { public void setLifecycleThreshold(String lifecycleThreshold) {
this.skipDownstreamTriggersForNonDeployedArtifacts = skipDownstreamTriggersForNonDeployedArtifacts; this.lifecycleThreshold = lifecycleThreshold;
} }


@Symbol("pipelineGraphPublisher") @Symbol("pipelineGraphPublisher")
Expand All @@ -374,5 +376,19 @@ public int ordinal() {
public String getSkipFileName() { public String getSkipFileName() {
return ".skip-pipeline-graph"; return ".skip-pipeline-graph";
} }

/**
* Only propose "package", "install" and "deploy" because the other lifecycle phases are unlikely to be useful
* @return
*/
public ListBoxModel doFillLifecycleThresholdItems() {
ListBoxModel options = new ListBoxModel();

options.add("package");
options.add("install");
options.add("deploy");

return options;
}
} }
} }
Expand Up @@ -242,6 +242,30 @@ public static List<Element> getExecutionEvents(@Nonnull Element mavenSpyLogs, St
return result; return result;
} }


/*
<ExecutionEvent type="MojoSucceeded" class="org.apache.maven.lifecycle.internal.DefaultExecutionEvent" _time="2017-09-26 23:55:44.188">
<project baseDir="/Users/cyrilleleclerc/git/cyrille-leclerc/my-jar" file="/Users/cyrilleleclerc/git/cyrille-leclerc/my-jar/pom.xml" groupId="com.example" name="my-jar" artifactId="my-jar" version="0.3-SNAPSHOT">
<build sourceDirectory="/Users/cyrilleleclerc/git/cyrille-leclerc/my-jar/src/main/java" directory="/Users/cyrilleleclerc/git/cyrille-leclerc/my-jar/target"/>
</project>
<plugin executionId="default-jar" goal="jar" lifecyclePhase="package" groupId="org.apache.maven.plugins" artifactId="maven-jar-plugin" version="2.4">
<finalName>${jar.finalName}</finalName>
<outputDirectory>${project.build.directory}</outputDirectory>
</plugin>
</ExecutionEvent>
*/
@Nonnull
public static List<String> getExecutedLifecyclePhases(@Nonnull Element mavenSpyLogs) {
List<String> lifecyclePhases = new ArrayList<>();
for (Element mojoSucceededEvent :getExecutionEvents(mavenSpyLogs, "MojoSucceeded")) {
Element pluginElement = getUniqueChildElement(mojoSucceededEvent, "plugin");
String lifecyclePhase = pluginElement.getAttribute("lifecyclePhase");
if (!lifecyclePhases.contains(lifecyclePhase)) {
lifecyclePhases.add(lifecyclePhase);
}
}

return lifecyclePhases;
}


/** /**
* Relativize path * Relativize path
Expand Down
Expand Up @@ -39,7 +39,11 @@ THE SOFTWARE.
<f:checkbox title="${%Provided}" field="includeScopeProvided" default="true"/> <f:checkbox title="${%Provided}" field="includeScopeProvided" default="true"/>
<f:checkbox title="${%Test}" field="includeScopeTest"/> <f:checkbox title="${%Test}" field="includeScopeTest"/>
</f:entry> </f:entry>

</f:section>
<f:section title="${%Downstream Pipeline Triggers}">
<f:entry title="${%Maven lifecycle phase threshold}" field="lifecycleThreshold" description="${%Threshold to trigger downstream pipelines. Ignored if 'Skip Downstream Triggers' is activated}">
<f:select default="deploy" />
</f:entry>
<f:entry title="${%Triggers}" field="dependenciesTriggers"> <f:entry title="${%Triggers}" field="dependenciesTriggers">
<f:checkbox title="${%Skip Downstream Triggers}" field="skipDownstreamTriggers" /> <f:checkbox title="${%Skip Downstream Triggers}" field="skipDownstreamTriggers" />
<f:checkbox title="${%Ignore Upstream Triggers}" field="ignoreUpstreamTriggers" /> <f:checkbox title="${%Ignore Upstream Triggers}" field="ignoreUpstreamTriggers" />
Expand Down
@@ -0,0 +1,24 @@
<div>
<p>
Threshold to trigger downstream pipelines based on the <a href="https://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html">Maven lifecycle</a>
phase successfully reached during the Maven execution.
</p>
<p>
If "install" is selected then downstream pipelines will be triggered for "<code>mvn clean install</code>", "<code>mvn clean deploy</code>"
but NOT "<code>mvn clean verify</code>" or "<code>mvn clean package</code>".
</p>

<h2>Example</h2>
<p>Configure a Jenkins Multibranch Pipeline with
<ul>
<li>Threshold: "<code>deploy</code>"</li>
<li>execution of "<code>mvn clean deploy</code>" on branches (incl. master) and execution of "<code>mvn clean
verify</code> on pull requests</li>
</ul>
So that:
<ul>
<li>The builds of branches (incl. "master") would upload the generated jar/war file to your enterprise Maven repository and would trigger downstream pipelines</li>
<li>The builds of pull request would only build the package but NOT upload the generated jar/war file to your enterprise Maven repository and would NOT trigger downstream pipelines</li>
</ul>
</p>
</div>
Expand Up @@ -6,17 +6,23 @@
import jenkins.branch.BranchSource; import jenkins.branch.BranchSource;
import jenkins.plugins.git.GitSCMSource; import jenkins.plugins.git.GitSCMSource;
import jenkins.plugins.git.GitSampleRepoRule; import jenkins.plugins.git.GitSampleRepoRule;
import org.jenkinsci.plugins.pipeline.maven.publishers.PipelineGraphPublisher;
import org.jenkinsci.plugins.pipeline.maven.trigger.WorkflowJobDependencyTrigger; import org.jenkinsci.plugins.pipeline.maven.trigger.WorkflowJobDependencyTrigger;
import org.jenkinsci.plugins.pipeline.maven.util.WorkflowMultibranchProjectTestsUtils; import org.jenkinsci.plugins.pipeline.maven.util.WorkflowMultibranchProjectTestsUtils;
import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
import org.jenkinsci.plugins.workflow.job.WorkflowJob; import org.jenkinsci.plugins.workflow.job.WorkflowJob;
import org.jenkinsci.plugins.workflow.job.WorkflowRun; import org.jenkinsci.plugins.workflow.job.WorkflowRun;
import org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject; import org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject;
import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;


import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Future; import java.util.concurrent.Future;


import javax.inject.Inject;

import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*; import static org.junit.Assert.*;


Expand All @@ -28,6 +34,23 @@ public class DependencyGraphTest extends AbstractIntegrationTest {
@Rule @Rule
public GitSampleRepoRule mavenWarRepoRule = new GitSampleRepoRule(); public GitSampleRepoRule mavenWarRepoRule = new GitSampleRepoRule();


@Inject
public GlobalPipelineMavenConfig globalPipelineMavenConfig;

@Before
@Override
public void setup() throws Exception {
super.setup();
PipelineGraphPublisher publisher = new PipelineGraphPublisher();
publisher.setLifecycleThreshold("install");

List<MavenPublisher> publisherOptions = GlobalPipelineMavenConfig.get().getPublisherOptions();
if (publisherOptions == null) {
publisherOptions = new ArrayList<>();
GlobalPipelineMavenConfig.get().setPublisherOptions(publisherOptions);
}
publisherOptions.add(publisher);
}


/** /**
* The maven-war-app has a dependency on the maven-jar-app * The maven-war-app has a dependency on the maven-jar-app
Expand Down
Expand Up @@ -2,16 +2,19 @@


import hudson.FilePath; import hudson.FilePath;
import org.hamcrest.CoreMatchers; import org.hamcrest.CoreMatchers;
import org.hamcrest.Matchers;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.Issue;
import org.w3c.dom.Document;
import org.w3c.dom.Element; import org.w3c.dom.Element;
import org.xml.sax.InputSource; import org.xml.sax.InputSource;
import org.xml.sax.SAXException; import org.xml.sax.SAXException;


import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader; import java.io.StringReader;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
Expand Down Expand Up @@ -247,4 +250,14 @@ public void test_getPathInWorkspace_macosx_edge_case(){
String expected = "pom.xml"; String expected = "pom.xml";
Assert.assertThat(actual, CoreMatchers.is(expected)); Assert.assertThat(actual, CoreMatchers.is(expected));
} }

@Test
public void test_getExecutedLifecyclePhases() throws Exception {
InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream("org/jenkinsci/plugins/pipeline/maven/maven-spy-package-jar.xml");
in.getClass(); // check non null
Element mavenSpyLogs = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(in).getDocumentElement();
List<String> executedLifecyclePhases = XmlUtils.getExecutedLifecyclePhases(mavenSpyLogs);
System.out.println(executedLifecyclePhases);
Assert.assertThat(executedLifecyclePhases, Matchers.contains("process-resources", "compile", "process-test-resources", "test-compile", "test", "package"));
}
} }

0 comments on commit 83a7ad3

Please sign in to comment.