Skip to content
Permalink
Browse files

[JENKINS-46511] Only trigger downstream pipelines if maven builds rea…

…ch a threshold lifecycle phase.

The default minimum lifecycle phase is "deploy", can be configured to trigger downstream pipeline on "install" or on "package"
  • Loading branch information...
cyrille-leclerc committed Sep 27, 2017
1 parent 221da6c commit 509c09cce18728b7a9702798b328ee11dac08f76
@@ -3,6 +3,7 @@
import hudson.Extension;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.util.ListBoxModel;
import org.apache.commons.lang.StringUtils;
import org.jenkinsci.Symbol;
import org.jenkinsci.plugins.pipeline.maven.GlobalPipelineMavenConfig;
@@ -50,10 +51,10 @@
private boolean skipDownstreamTriggers;

/**
* Skip trigger of downstream pipelines if the artifact has just been
* packaged (mvn package) or installed locally (mvn install)
* Lifecycle phase threshold to trigger downstream pipelines, "deploy" or "install" or "package" or ...
* 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;

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

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) {
@@ -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) {
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();

recordGeneratedArtifacts(generatedArtifacts, artifactsHaveBeenMvnDeployed, run, listener, dao);
List<String> executedLifecyclePhases = XmlUtils.getExecutedLifecyclePhases(mavenSpyLogsElt);
recordGeneratedArtifacts(generatedArtifacts, executedLifecyclePhases, run, listener, dao);
}

/**
*
* @param generatedArtifacts deployed artifacts
* @param artifactsHaveBeenMvnDeployed artifacts have been deployed using "mvn deploy" or "mvn nexus-staging-maven-plugin:deploy"
* @param generatedArtifacts deployed artifacts
* @param executedLifecyclePhases Maven lifecycle phases that have been gone through during the Maven execution (e.g. "..., compile, test, package..." )
* @param run
* @param listener
* @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)) {
listener.getLogger().println("[withMaven] pipelineGraphPublisher - recordGeneratedArtifacts...");
}
for(MavenSpyLogProcessor.MavenArtifact artifact: generatedArtifacts) {
for (MavenSpyLogProcessor.MavenArtifact artifact : generatedArtifacts) {
boolean skipDownstreamPipelines = this.skipDownstreamTriggers ||
(this.skipDownstreamTriggersForNonDeployedArtifacts && !artifactsHaveBeenMvnDeployed);
(!executedLifecyclePhases.contains(this.lifecycleThreshold));

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(),
artifact.getId(), artifact.type, artifact.version,
skipDownstreamTriggers, artifactsHaveBeenMvnDeployed});
executedLifecyclePhases,
skipDownstreamTriggers, lifecycleThreshold});
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);
}
dao.recordGeneratedArtifact(run.getParent().getFullName(), run.getNumber(),
dao.recordGeneratedArtifact(run.getParent().getFullName(), run.getNumber(),
artifact.groupId, artifact.artifactId, artifact.version, artifact.type, artifact.baseVersion,
skipDownstreamPipelines);
}
@@ -241,6 +242,7 @@ public String toString() {
"versions={snapshot:" + isIncludeSnapshotVersions() + ", release:" + isIncludeReleaseVersions() + "}" +
']';
}

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

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

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

@Symbol("pipelineGraphPublisher")
@@ -374,5 +376,19 @@ public int ordinal() {
public String getSkipFileName() {
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;
}
}
}
@@ -242,6 +242,30 @@ public static String toString(@Nullable Node node) {
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
@@ -39,7 +39,11 @@ THE SOFTWARE.
<f:checkbox title="${%Provided}" field="includeScopeProvided" default="true"/>
<f:checkbox title="${%Test}" field="includeScopeTest"/>
</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:checkbox title="${%Skip Downstream Triggers}" field="skipDownstreamTriggers" />
<f:checkbox title="${%Ignore Upstream Triggers}" field="ignoreUpstreamTriggers" />
@@ -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>
@@ -6,17 +6,23 @@
import jenkins.branch.BranchSource;
import jenkins.plugins.git.GitSCMSource;
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.util.WorkflowMultibranchProjectTestsUtils;
import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
import org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;

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

import javax.inject.Inject;

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

@@ -28,6 +34,23 @@
@Rule
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
@@ -2,16 +2,19 @@

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

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.util.Arrays;
import java.util.List;
@@ -247,4 +250,14 @@ public void test_getPathInWorkspace_macosx_edge_case(){
String expected = "pom.xml";
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 509c09c

Please sign in to comment.
You can’t perform that action at this time.