Skip to content
Permalink
Browse files
[FIXED JENKINS-44461] Add beforeAgent flag for when
If true, the when condition will be evaluated before entering the
stage's agent (if defined). The default is false - better safe than
sorry there.
  • Loading branch information
abayer committed Dec 20, 2017
1 parent 32c5e41 commit ae2406d833ccb9b199e9c4865d302ef882ed1dae
Showing with 332 additions and 26 deletions.
  1. +21 −1 ...line-model-api/src/main/java/org/jenkinsci/plugins/pipeline/modeldefinition/ast/ModelASTWhen.java
  2. +3 −0 pipeline-model-api/src/main/resources/ast-schema.json
  3. +8 −0 ...ion/src/main/groovy/org/jenkinsci/plugins/pipeline/modeldefinition/model/StageConditionals.groovy
  4. +4 −0 ...efinition/src/main/groovy/org/jenkinsci/plugins/pipeline/modeldefinition/parser/JSONParser.groovy
  5. +24 −10 ...finition/src/main/groovy/org/jenkinsci/plugins/pipeline/modeldefinition/parser/ModelParser.groovy
  6. +2 −1 ...rc/main/groovy/org/jenkinsci/plugins/pipeline/modeldefinition/parser/RuntimeASTTransformer.groovy
  7. +1 −1 ...-definition/src/main/resources/org/jenkinsci/plugins/pipeline/modeldefinition/Messages.properties
  8. +28 −12 ...inition/src/main/resources/org/jenkinsci/plugins/pipeline/modeldefinition/ModelInterpreter.groovy
  9. +1 −0 ...definition/src/test/java/org/jenkinsci/plugins/pipeline/modeldefinition/AbstractModelDefTest.java
  10. +1 −1 ...-model-definition/src/test/java/org/jenkinsci/plugins/pipeline/modeldefinition/ValidatorTest.java
  11. +24 −0 ...-model-definition/src/test/java/org/jenkinsci/plugins/pipeline/modeldefinition/WhenStageTest.java
  12. +57 −0 pipeline-model-definition/src/test/resources/json/whenBeforeAgentTrue.json
  13. +53 −0 pipeline-model-definition/src/test/resources/whenBeforeAgentFalse.groovy
  14. +53 −0 pipeline-model-definition/src/test/resources/whenBeforeAgentTrue.groovy
  15. +52 −0 pipeline-model-definition/src/test/resources/whenBeforeAgentUnspecified.groovy
@@ -40,6 +40,8 @@ public class ModelASTWhen extends ModelASTElement {

private List<ModelASTWhenContent> conditions = new ArrayList<>();

private Boolean beforeAgent;

public ModelASTWhen(Object sourceLocation) {
super(sourceLocation);
}
@@ -52,18 +54,35 @@ public void setConditions(List<ModelASTWhenContent> conditions) {
this.conditions = conditions;
}

public Boolean getBeforeAgent() {
return beforeAgent;
}

public void setBeforeAgent(Boolean beforeAgent) {
this.beforeAgent = beforeAgent;
}

@Override
public Object toJSON() {
final JSONObject o = new JSONObject();
final JSONArray a = new JSONArray();
for (ModelASTWhenContent c : conditions) {
a.add(c.toJSON());
}
return new JSONObject().accumulate("conditions", a);
o.accumulate("conditions", a);

if (beforeAgent != null) {
o.accumulate("beforeAgent", beforeAgent);
}
return o;
}

@Override
public String toGroovy() {
StringBuilder result = new StringBuilder("when {\n");
if (beforeAgent != null && beforeAgent) {
result.append("beforeAgent true\n");
}
for (ModelASTWhenContent c : conditions) {
result.append(c.toGroovy()).append("\n");
}
@@ -83,6 +102,7 @@ public void removeSourceLocation() {
public String toString() {
return "ModelASTWhen{" +
"conditions=" + conditions +
", beforeAgent=" + beforeAgent +
"}";
}

@@ -412,6 +412,9 @@
"description": "Conditions to evaluate whether the stage should run or not",
"type": "object",
"properties": {
"beforeAgent": {
"type": "boolean"
},
"conditions": {
"type": "array",
"minItems": 1,
@@ -58,9 +58,17 @@ class StageConditionals implements Serializable {
)

final Closure rawClosure
final Boolean beforeAgent

@Deprecated
@Whitelisted
StageConditionals(Closure rawClosure) {
this(rawClosure, null)
}

@Whitelisted
StageConditionals(Closure rawClosure, Boolean beforeAgent) {
this.rawClosure = rawClosure
this.beforeAgent = beforeAgent
}
}
@@ -205,6 +205,10 @@ class JSONParser implements Parser {
@CheckForNull ModelASTWhen parseWhen(JsonTree j) {
ModelASTWhen when = new ModelASTWhen(j)

if (j.node.has("beforeAgent")) {
when.beforeAgent = j.node.get("beforeAgent")?.asBoolean()
}

JsonTree conditionsTree = j.append(JsonPointer.of("conditions"))
conditionsTree.node.eachWithIndex { JsonNode entry, int i ->
JsonTree condTree = conditionsTree.append(JsonPointer.of(i))
@@ -558,15 +558,7 @@ class ModelParser implements Parser {
stage.parallel = parseStages(s)
break
case 'failFast':
List<Expression> args = ((TupleExpression) mc.arguments).expressions

ConstantExpression exp = castOrNull(ConstantExpression.class, args[0])
if (exp == null || !(exp.value instanceof Boolean)) {
errorCollector.error(new ModelASTKey(mc.method),
Messages.ModelParser_ExpectedFailFast())
} else {
stage.setFailFast((Boolean)exp.value)
}
stage.setFailFast(parseBooleanMethod(mc))
break
default:
errorCollector.error(stage, Messages.ModelParser_UnknownStageSection(name))
@@ -582,12 +574,34 @@ class ModelParser implements Parser {
BlockStatement block = asBlock(stepsBlock.body.code)
ModelASTWhen w = new ModelASTWhen(statement)
block.statements.each { s ->
w.conditions.add(parseWhenContent(s))
def mc = matchMethodCall(s)
if (mc != null) {
def name = parseMethodName(mc)
if (name == "beforeAgent") {
w.beforeAgent = parseBooleanMethod(mc)
} else {
w.conditions.add(parseWhenContent(s))
}
}
}

return w
}

Boolean parseBooleanMethod(MethodCallExpression mc) {
List<Expression> args = ((TupleExpression) mc.arguments).expressions
def name = parseMethodName(mc)

ConstantExpression exp = castOrNull(ConstantExpression.class, args[0])
if (exp == null || !(exp.value instanceof Boolean)) {
errorCollector.error(new ModelASTKey(mc.method),
Messages.ModelParser_ExpectedBoolean(name))
return null
} else {
return (Boolean)exp.value
}
}

/**
* Parses a block of code into {@link ModelASTBranch}
*/
@@ -675,7 +675,8 @@ class RuntimeASTTransformer {
}

return ctorX(ClassHelper.make(StageConditionals.class),
args(closureX(block(returnS(closList)))))
args(closureX(block(returnS(closList))),
constX(original.beforeAgent != null ? original.beforeAgent : false)))
}
return constX(null)
}
@@ -39,9 +39,9 @@ ModelParser.DuplicateEnvVar=Duplicate environment variable name: "{0}"
ModelParser.ExpectedAgent=Expected an agent
ModelParser.ExpectedAgentKeyValue=Expected to find 'key "value"'
ModelParser.ExpectedBlock=Expected a block
ModelParser.ExpectedBoolean=Expected a boolean with {0}
ModelParser.ExpectedBuildParameter=Expected a build parameter definition
ModelParser.ExpectedClosureOrFailFast=Expected closure or failFast
ModelParser.ExpectedFailFast=Expected a boolean with failFast
ModelParser.ExpectedLibrary=Expected a "lib(...)" but got {0}
ModelParser.ExpectedMapMethod=Expected to find 'someKey "someValue"'
ModelParser.ExpectedOption=Expected an option
@@ -209,22 +209,38 @@ class ModelInterpreter implements Serializable {
script.parallel(getParallelStages(root, parentAgent, thisStage, firstError, parentStage, false, false, true))
}
} else {
inDeclarativeAgent(thisStage, root, thisStage.agent) {
if (evaluateWhen(thisStage.when)) {
withCredentialsBlock(thisStage.environment) {
withEnvBlock(thisStage.getEnvVars(script)) {
toolsBlock(thisStage.agent ?: root.agent, thisStage.tools, root) {
// Execute the actual stage and potential post-stage actions
executeSingleStage(root, thisStage, parentAgent)
}
def stageBody = {
withCredentialsBlock(thisStage.environment) {
withEnvBlock(thisStage.getEnvVars(script)) {
toolsBlock(thisStage.agent ?: root.agent, thisStage.tools, root) {
// Execute the actual stage and potential post-stage actions
executeSingleStage(root, thisStage, parentAgent)
}
}
} else {
Utils.logToTaskListener("Stage '${thisStage.name}' skipped due to when conditional")
Utils.markStageSkippedForConditional(thisStage.name)
isSkipped = true
}
}
// If beforeAgent is true, evaluate the when before entering the agent.
boolean whenPassed = false
if (thisStage.when?.beforeAgent != null && thisStage.when?.beforeAgent) {
whenPassed = evaluateWhen(thisStage.when)
if (whenPassed) {
inDeclarativeAgent(thisStage, root, thisStage.agent) {
stageBody.call()
}
}
} else {
inDeclarativeAgent(thisStage, root, thisStage.agent) {
whenPassed = evaluateWhen(thisStage.when)
if (whenPassed) {
stageBody.call()
}
}
}
if (!whenPassed) {
Utils.logToTaskListener("Stage '${thisStage.name}' skipped due to when conditional")
Utils.markStageSkippedForConditional(thisStage.name)
isSkipped = true
}
}
}
} catch (Exception e) {
@@ -158,6 +158,7 @@ public void setUp() throws Exception {
"whenNot",
"whenOr",
"whenAnd",
"whenBeforeAgentTrue",
"usernamePassword",
"environmentCrossReferences",
"nestedParallelStages",
@@ -253,7 +253,7 @@ public void emptyParallel() throws Exception {
@Test
public void parallelPipelineWithInvalidFailFast() throws Exception {
expectError("parallelPipelineWithInvalidFailFast")
.logContains(Messages.ModelParser_ExpectedFailFast())
.logContains(Messages.ModelParser_ExpectedBoolean("failFast"))
.go();
}

@@ -369,6 +369,30 @@ public void whenExprDurableTask() throws Exception {
.go();
}

@Issue("JENKINS-44461")
@Test
public void whenBeforeAgentTrue() throws Exception {
expect("whenBeforeAgentTrue")
.logContains("Heal it")
.go();
}

@Issue("JENKINS-44461")
@Test
public void whenBeforeAgentFalse() throws Exception {
expect("whenBeforeAgentFalse")
.logContains("Heal it")
.go();
}

@Issue("JENKINS-44461")
@Test
public void whenBeforeAgentUnspecified() throws Exception {
expect("whenBeforeAgentUnspecified")
.logContains("Heal it")
.go();
}

private void waitFor(Queue.Item item) throws InterruptedException, ExecutionException {
while (item != null && item.getFuture() == null) {
Thread.sleep(200);
@@ -0,0 +1,57 @@
{"pipeline": {
"stages": [
{
"name": "One",
"branches": [ {
"name": "default",
"steps": [ {
"name": "echo",
"arguments": [ {
"key": "message",
"value": {
"isLiteral": true,
"value": "Hello"
}
}]
}]
}]
},
{
"name": "Two",
"branches": [ {
"name": "default",
"steps": [ {
"name": "script",
"arguments": [ {
"key": "scriptBlock",
"value": {
"isLiteral": true,
"value": "echo \"World\"\n echo \"Heal it\""
}
}]
}]
}],
"agent": {
"type": "label",
"argument": {
"isLiteral": true,
"value": "here"
}
},
"when": {
"conditions": [ {
"name": "expression",
"arguments": [ {
"key": "scriptBlock",
"value": {
"isLiteral": true,
"value": "return getContext(hudson.FilePath) == null"
}
}]
}],
"beforeAgent": true
}
}
],
"agent": {"type": "none"}
}}
@@ -0,0 +1,53 @@
/*
* The MIT License
*
* Copyright (c) 2017, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

pipeline {
agent none

stages {
stage("One") {
steps {
echo "Hello"
}
}
stage("Two") {
when {
beforeAgent false
expression {
return getContext(hudson.FilePath) != null
}
}
agent {
label "here"
}
steps {
script {
echo "World"
echo "Heal it"
}

}
}
}
}

0 comments on commit ae2406d

Please sign in to comment.