diff --git a/pipeline-model-definition/src/main/java/org/jenkinsci/plugins/pipeline/modeldefinition/actions/RestartDeclarativePipelineAction.java b/pipeline-model-definition/src/main/java/org/jenkinsci/plugins/pipeline/modeldefinition/actions/RestartDeclarativePipelineAction.java index 62464465b..719f7df6b 100644 --- a/pipeline-model-definition/src/main/java/org/jenkinsci/plugins/pipeline/modeldefinition/actions/RestartDeclarativePipelineAction.java +++ b/pipeline-model-definition/src/main/java/org/jenkinsci/plugins/pipeline/modeldefinition/actions/RestartDeclarativePipelineAction.java @@ -38,6 +38,7 @@ import org.jenkinsci.plugins.pipeline.StageStatus; import org.jenkinsci.plugins.pipeline.modeldefinition.Utils; import org.jenkinsci.plugins.pipeline.modeldefinition.ast.ModelASTStage; +import org.jenkinsci.plugins.pipeline.modeldefinition.ast.ModelASTStages; import org.jenkinsci.plugins.pipeline.modeldefinition.causes.RestartDeclarativePipelineCause; import org.jenkinsci.plugins.workflow.cps.CpsFlowExecution; import org.jenkinsci.plugins.workflow.flow.FlowExecution; @@ -170,6 +171,30 @@ public List getRestartableStages() { return stages; } + /** + * Returns whether a stage is restartable. + * @param stageName the stage name + * @return true is restartable, false otherwise + */ + public boolean isRestartable(String stageName) { + FlowExecution execution = getExecution(); + if (execution != null) { + ExecutionModelAction execAction = run.getAction(ExecutionModelAction.class); + if (execAction != null) { + ModelASTStages stages = execAction.getStages(); + if (stages != null) { + for (ModelASTStage s : stages.getStages()) { + if(s.getName().equals(stageName)) { + return !Utils.stageHasStatusOf(s.getName(), execution, + StageStatus.getSkippedForFailure(), StageStatus.getSkippedForUnstable()); + } + } + } + } + } + return false; + } + public String getCheckUrl() { return Jenkins.get().getRootUrl() + run.getUrl() + getUrlName() + "/" + "checkStageName"; } diff --git a/pipeline-model-definition/src/test/java/org/jenkinsci/plugins/pipeline/modeldefinition/actions/RestartDeclarativePipelineActionTest.java b/pipeline-model-definition/src/test/java/org/jenkinsci/plugins/pipeline/modeldefinition/actions/RestartDeclarativePipelineActionTest.java index 068c1f53e..38bcdf9e7 100644 --- a/pipeline-model-definition/src/test/java/org/jenkinsci/plugins/pipeline/modeldefinition/actions/RestartDeclarativePipelineActionTest.java +++ b/pipeline-model-definition/src/test/java/org/jenkinsci/plugins/pipeline/modeldefinition/actions/RestartDeclarativePipelineActionTest.java @@ -72,6 +72,7 @@ import java.net.URL; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import static org.hamcrest.Matchers.is; @@ -729,6 +730,53 @@ public void skippedParallelStagesMarkedNotExecuted() throws Exception { assertFalse(stageStatusPredicate("second-parallel", StageStatus.getFailedAndContinued()).apply(secondRunSecondParallelStart)); } + @Issue("JENKINS-70808") + @Test + public void isRestartable() throws Exception { + WorkflowRun original = expect(Result.FAILURE, "restart", "isRestartable") + .logContains("This shouldn't show up on second run", "Odd numbered build, failing") + .logNotContains("This should only run on restart") + .go(); + + WorkflowJob p = original.getParent(); + + FlowExecution firstExecution = original.getExecution(); + assertNotNull(firstExecution); + assertNotNull(firstExecution.getCauseOfFailure()); + + RestartDeclarativePipelineAction action = original.getAction(RestartDeclarativePipelineAction.class); + assertNotNull(action); + assertTrue(action.isRestartEnabled()); + List restartableStages = action.getRestartableStages(); + assertThat(restartableStages, is(Arrays.asList("skip-on-restart", "restart"))); + assertTrue(action.isRestartable("skip-on-restart")); + assertTrue(action.isRestartable("restart")); + assertFalse(action.isRestartable("post-restart")); + + HtmlPage redirect = restartFromStageInUI(original, "restart"); + + assertNotNull(redirect); + assertEquals(p.getAbsoluteUrl(), redirect.getUrl().toString()); + + j.waitUntilNoActivity(); + WorkflowRun b2 = p.getBuildByNumber(2); + assertNotNull(b2); + j.assertBuildStatusSuccess(b2); + j.assertLogContains("Even numbered build, success", b2); + j.assertLogContains("Now we're post-restart", b2); + j.assertLogNotContains("This shouldn't show up on second run", b2); + + action = b2.getAction(RestartDeclarativePipelineAction.class); + assertNotNull(action); + assertTrue(action.isRestartEnabled()); + restartableStages = action.getRestartableStages(); + assertThat(restartableStages, is(Arrays.asList("skip-on-restart", "restart", "post-restart"))); + assertTrue(action.isRestartable("skip-on-restart")); + assertTrue(action.isRestartable("restart")); + assertTrue(action.isRestartable("post-restart")); + + } + @Test public void testDoRestartPipeline() throws Exception { String jobName = "simplePipeline"; diff --git a/pipeline-model-definition/src/test/resources/restart/isRestartable.groovy b/pipeline-model-definition/src/test/resources/restart/isRestartable.groovy new file mode 100644 index 000000000..27c935933 --- /dev/null +++ b/pipeline-model-definition/src/test/resources/restart/isRestartable.groovy @@ -0,0 +1,51 @@ +/* + * 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 any + + stages { + stage('skip-on-restart') { + steps { + echo "This shouldn't show up on second run" + } + } + stage('restart') { + steps { + script { + if (currentBuild.getNumber() % 2 == 1) { + error("Odd numbered build, failing") + } else { + echo "Even numbered build, success" + } + } + } + } + stage('post-restart') { + steps { + echo "Now we're post-restart" + } + } + } +}