Skip to content

Commit 59b67f5

Browse files
committed
[JENKINS-14701] Thorough Matrix support.
This allows you to only run the script once - on the parent job, before the "configuration" builds are run. This is supported through a new checkbox in the configuration UI.
1 parent 5d58561 commit 59b67f5

File tree

6 files changed

+258
-5
lines changed

6 files changed

+258
-5
lines changed

src/main/java/com/lookout/jenkins/EnvironmentScript.java

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44
import hudson.FilePath;
55
import hudson.Launcher;
66
import hudson.Util;
7+
import hudson.matrix.MatrixAggregatable;
8+
import hudson.matrix.MatrixAggregator;
9+
import hudson.matrix.MatrixRun;
10+
import hudson.matrix.MatrixBuild;
11+
import hudson.matrix.MatrixProject;
712
import hudson.model.BuildListener;
813
import hudson.model.AbstractBuild;
914
import hudson.model.AbstractProject;
@@ -21,18 +26,21 @@
2126
import jenkins.model.Jenkins;
2227

2328
import org.kohsuke.stapler.DataBoundConstructor;
29+
import org.kohsuke.stapler.StaplerRequest;
2430

2531
/**
2632
* Runs a specific chunk of code before each build, parsing output for new environment variables.
2733
*
2834
* @author Jørgen P. Tjernø
2935
*/
30-
public class EnvironmentScript extends BuildWrapper {
36+
public class EnvironmentScript extends BuildWrapper implements MatrixAggregatable {
3137
private final String script;
38+
private final boolean onlyRunOnParent;
3239

3340
@DataBoundConstructor
34-
public EnvironmentScript(String script) {
41+
public EnvironmentScript(String script, boolean onlyRunOnParent) {
3542
this.script = script;
43+
this.onlyRunOnParent = onlyRunOnParent;
3644
}
3745

3846
/**
@@ -42,12 +50,43 @@ public String getScript() {
4250
return script;
4351
}
4452

53+
public boolean shouldOnlyRunOnParent ()
54+
{
55+
return onlyRunOnParent;
56+
}
57+
4558
@SuppressWarnings("rawtypes")
4659
@Override
4760
public Environment setUp(AbstractBuild build,
4861
final Launcher launcher,
4962
final BuildListener listener) throws IOException, InterruptedException {
63+
if ((build instanceof MatrixRun) && shouldOnlyRunOnParent()) {
64+
// If this is a matrix run and we have the onlyRunOnParent option
65+
// enabled, we just retrieve the persisted environment from the
66+
// PersistedEnvironment Action.
67+
MatrixBuild parent = ((MatrixRun)build).getParentBuild();
68+
if (parent != null) {
69+
PersistedEnvironment persisted = parent.getAction(PersistedEnvironment.class);
70+
if (persisted != null) {
71+
return persisted.getEnvironment();
72+
} else {
73+
listener.error("[environment-script] Unable to load persisted environment from matrix parent job, not injecting any variables");
74+
return new Environment() {};
75+
}
76+
} else {
77+
// If there's no parent, then the module build was triggered
78+
// manually, so we generate a new environment.
79+
return generateEnvironment (build, launcher, listener);
80+
}
81+
} else {
82+
// Otherwise we generate a new one.
83+
return generateEnvironment (build, launcher, listener);
84+
}
85+
}
5086

87+
private Environment generateEnvironment(AbstractBuild<?, ?> build,
88+
final Launcher launcher,
89+
final BuildListener listener) throws IOException, InterruptedException {
5190
// First we create the script in a temporary directory.
5291
FilePath ws = build.getWorkspace();
5392
FilePath scriptFile;
@@ -75,7 +114,6 @@ public Environment setUp(AbstractBuild build,
75114
return null;
76115
}
77116

78-
79117
// Then we parse the variables out of it. We could use java.util.Properties, but it doesn't order the properties, so expanding variables with previous variables (like a shell script expects) doesn't work.
80118
String[] lines = commandOutput.toString().split("(\n|\r\n)");
81119
final Map<String, String> envAdditions = new HashMap<String, String>(lines.length);
@@ -137,6 +175,35 @@ public String[] buildCommandLine(FilePath scriptFile) {
137175
}
138176
}
139177

178+
/**
179+
* Create an aggregator that will calculate the environment once iff
180+
* onlyRunOnParent is true.
181+
*
182+
* The aggregator we return is called on the parent job for matrix jobs. In
183+
* it we generate the environment once and persist it in an Action (of type
184+
* {@link PersistedEnvironment}) if the job has onlyRunOnParent enabled. The
185+
* subjobs ("configuration runs") will retrieve this and apply it to their
186+
* environment, without performing the calculation.
187+
*/
188+
public MatrixAggregator createAggregator(MatrixBuild build, Launcher launcher, BuildListener listener) {
189+
if (!shouldOnlyRunOnParent()) {
190+
return null;
191+
}
192+
193+
return new MatrixAggregator(build, launcher, listener) {
194+
@Override
195+
public boolean startBuild() throws InterruptedException, IOException {
196+
Environment env = generateEnvironment(build, launcher, listener);
197+
if (env == null) {
198+
return false;
199+
}
200+
201+
build.addAction(new PersistedEnvironment(env));
202+
return true;
203+
}
204+
};
205+
}
206+
140207
/**
141208
* Descriptor for {@link EnvironmentScript}. Used as a singleton.
142209
* The class is marked as public so that it can be accessed from views.
@@ -155,6 +222,9 @@ public String getDisplayName() {
155222
public boolean isApplicable(AbstractProject<?, ?> project) {
156223
return true;
157224
}
225+
226+
public boolean isMatrix(StaplerRequest request) {
227+
return (request.findAncestorObject(AbstractProject.class) instanceof MatrixProject);
228+
}
158229
}
159230
}
160-
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.lookout.jenkins;
2+
3+
import hudson.EnvVars;
4+
import hudson.model.Action;
5+
import hudson.tasks.BuildWrapper.Environment;
6+
7+
public class PersistedEnvironment implements Action {
8+
private Environment environment;
9+
10+
public PersistedEnvironment (Environment environment) {
11+
this.environment = environment;
12+
}
13+
14+
public Environment getEnvironment () {
15+
return environment;
16+
}
17+
18+
public String getDisplayName() {
19+
return "Environment Script variables";
20+
}
21+
22+
public String getIconFileName() {
23+
// TODO Auto-generated method stub
24+
return null;
25+
}
26+
27+
public String getUrlName() {
28+
// TODO Auto-generated method stub
29+
return null;
30+
}
31+
32+
}

src/main/resources/com/lookout/jenkins/EnvironmentScript/config.jelly

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,17 @@
11
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
2+
<j:choose>
3+
<j:when test="${descriptor.isMatrix(request)}">
4+
<f:entry title="${%Run only on parent}"
5+
help="${resURL}/plugin/environment-script/help-runOnlyOnParent.html">
6+
<f:checkbox name="onlyRunOnParent"
7+
checked="${instance.shouldOnlyRunOnParent()}" />
8+
</f:entry>
9+
</j:when>
10+
<j:otherwise>
11+
<f:invisibleEntry name="runOnlyOnParent" value="false" />
12+
</j:otherwise>
13+
</j:choose>
14+
215
<f:entry title="${%Script Content}"
316
description="This script is executed before each build, and any output it produces will be evaluated as environment variables (key=value)."
417
help="${resURL}/plugin/environment-script/help-script.html">
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.lookout.jenkins;
2+
3+
import java.io.IOException;
4+
5+
import net.sf.json.JSONObject;
6+
7+
import org.kohsuke.stapler.StaplerRequest;
8+
9+
import hudson.Extension;
10+
import hudson.Launcher;
11+
import hudson.model.BuildListener;
12+
import hudson.model.AbstractBuild;
13+
import hudson.model.Descriptor;
14+
import hudson.tasks.Builder;
15+
16+
/**
17+
* {@link Builder} that simply counts how many times it was executed.
18+
*
19+
* @author Jørgen P. Tjernø <jorgen.tjerno@mylookout.com>
20+
*/
21+
public class CountBuilder extends Builder {
22+
int count = 0;
23+
24+
public int getCount() {
25+
return count;
26+
}
27+
28+
public synchronized boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
29+
count++;
30+
return true;
31+
}
32+
33+
@Extension
34+
public static final class DescriptorImpl extends Descriptor<Builder> {
35+
public Builder newInstance(StaplerRequest req, JSONObject data) {
36+
throw new UnsupportedOperationException();
37+
}
38+
39+
public String getDisplayName() {
40+
return "Count Number Of Builds";
41+
}
42+
}
43+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package com.lookout.jenkins;
2+
3+
import java.io.File;
4+
5+
import hudson.FilePath;
6+
import hudson.matrix.Axis;
7+
import hudson.matrix.AxisList;
8+
import hudson.matrix.MatrixRun;
9+
import hudson.matrix.DefaultMatrixExecutionStrategyImpl;
10+
import hudson.matrix.MatrixBuild;
11+
import hudson.matrix.MatrixProject;
12+
13+
import org.jvnet.hudson.test.CaptureEnvironmentBuilder;
14+
import org.jvnet.hudson.test.HudsonTestCase;
15+
16+
public class EnvironmentScriptMatrixTest extends HudsonTestCase {
17+
class MatrixTestJob {
18+
public MatrixProject project;
19+
public CaptureEnvironmentBuilder captureBuilder;
20+
public CountBuilder countBuilder;
21+
22+
public MatrixTestJob (String script, boolean onlyRunOnParent) throws Exception {
23+
project = createMatrixProject();
24+
25+
// This forces it to run the builds sequentially, to prevent any
26+
// race conditions when concurrently updating the 'counter' file.
27+
project.setExecutionStrategy(new DefaultMatrixExecutionStrategyImpl(true, null, null, null));
28+
29+
project.setAxes(new AxisList(new Axis("axis", "value1", "value2")));
30+
project.getBuildWrappersList().add(new EnvironmentScript(script, onlyRunOnParent));
31+
32+
captureBuilder = new CaptureEnvironmentBuilder();
33+
project.getBuildersList().add(captureBuilder);
34+
35+
countBuilder = new CountBuilder();
36+
project.getBuildersList().add(countBuilder);
37+
}
38+
}
39+
40+
final static String SCRIPT_COUNTER =
41+
"file='%s/counter'\n"
42+
+ "if [ -f $file ]; then\n"
43+
+ " let i=$(cat $file)+1\n"
44+
+ "else\n"
45+
+ " i=1\n"
46+
+ "fi\n"
47+
+ "echo 1 >was_run\n"
48+
+ "echo $i >$file\n"
49+
+ "echo seen=yes";
50+
51+
52+
// Explicit constructor so that we can call createTmpDir.
53+
public EnvironmentScriptMatrixTest () throws Exception {}
54+
55+
// Generate a random directory that we pass to the shell script.
56+
File tempDir = createTmpDir();
57+
String script = String.format(SCRIPT_COUNTER, tempDir.getPath());
58+
59+
public void testWithParentOnly () throws Exception {
60+
MatrixTestJob job = new MatrixTestJob(script, true);
61+
MatrixBuild build = buildAndAssert(job);
62+
63+
// We ensure that this was only run once (on the parent)
64+
assertEquals("1", new FilePath(tempDir).child("counter").readToString().trim());
65+
66+
// Then make sure that it was in fact in the parent's WS that we ran.
67+
assertTrue(build.getWorkspace().child("was_run").exists());
68+
for (MatrixRun run : build.getRuns())
69+
assertFalse(run.getWorkspace().child("was_run").exists());
70+
}
71+
72+
public void testWithEachChild () throws Exception {
73+
MatrixTestJob job = new MatrixTestJob(script, false);
74+
MatrixBuild build = buildAndAssert(job);
75+
76+
// We ensure that this was only run twice - once for each axis combination - but not on the parent.
77+
assertEquals("2", new FilePath(tempDir).child("counter").readToString().trim());
78+
79+
// Then make sure that it was in fact in the combination jobs' workspace.
80+
assertFalse(build.getWorkspace().child("was_run").exists());
81+
for (MatrixRun run : build.getRuns())
82+
assertTrue(run.getWorkspace().child("was_run").exists());
83+
}
84+
85+
private MatrixBuild buildAndAssert(MatrixTestJob job) throws Exception {
86+
MatrixBuild build = assertBuildStatusSuccess(job.project.scheduleBuild2(0).get());
87+
88+
// Make sure that the environment variables set in the script are properly propagated.
89+
assertEquals("yes", job.captureBuilder.getEnvVars().get("seen"));
90+
// Make sure that the builder was executed twice, once for each axis value.
91+
assertEquals(2, job.countBuilder.getCount());
92+
93+
return build;
94+
}
95+
}

src/test/java/com/lookout/jenkins/EnvironmentScriptTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public TestJob (String script) throws Exception {
1717
project = createFreeStyleProject();
1818
builder = new CaptureEnvironmentBuilder();
1919
project.getBuildersList().add(builder);
20-
project.getBuildWrappersList().add(new EnvironmentScript(script));
20+
project.getBuildWrappersList().add(new EnvironmentScript(script, false));
2121
}
2222
}
2323

0 commit comments

Comments
 (0)