diff --git a/src/main/java/org/jenkinsci/plugins/workflow/support/steps/build/WaitForBuildAction.java b/src/main/java/org/jenkinsci/plugins/workflow/support/steps/build/WaitForBuildAction.java new file mode 100644 index 00000000..c7e0e9ac --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/workflow/support/steps/build/WaitForBuildAction.java @@ -0,0 +1,15 @@ +package org.jenkinsci.plugins.workflow.support.steps.build; + +import hudson.model.InvisibleAction; +import org.jenkinsci.plugins.workflow.steps.StepContext; + +public class WaitForBuildAction extends InvisibleAction { + + final StepContext context; + final boolean propagate; + + WaitForBuildAction(StepContext context, boolean propagate) { + this.context = context; + this.propagate = propagate; + } +} diff --git a/src/main/java/org/jenkinsci/plugins/workflow/support/steps/build/WaitForBuildListener.java b/src/main/java/org/jenkinsci/plugins/workflow/support/steps/build/WaitForBuildListener.java new file mode 100644 index 00000000..aceb333e --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/workflow/support/steps/build/WaitForBuildListener.java @@ -0,0 +1,33 @@ +package org.jenkinsci.plugins.workflow.support.steps.build; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.model.Result; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.model.listeners.RunListener; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.jenkinsci.plugins.workflow.steps.FlowInterruptedException; +import org.jenkinsci.plugins.workflow.steps.StepContext; + +@Extension +public class WaitForBuildListener extends RunListener> { + + private static final Logger LOGGER = Logger.getLogger(WaitForBuildListener.class.getName()); + + @Override + public void onCompleted(Run run, @NonNull TaskListener listener) { + for (WaitForBuildAction action : run.getActions(WaitForBuildAction.class)) { + StepContext context = action.context; + LOGGER.log(Level.FINE, "completing {0} for {1}", new Object[] {run, context}); + if (!action.propagate || run.getResult() == Result.SUCCESS) { + context.onSuccess(new RunWrapper(run, false)); + } else { + Result result = run.getResult(); + context.onFailure(new FlowInterruptedException(result != null ? result : /* probably impossible */ Result.FAILURE, false, new DownstreamFailureCause(run))); + } + } + run.removeActions(WaitForBuildAction.class); + } +} diff --git a/src/main/java/org/jenkinsci/plugins/workflow/support/steps/build/WaitForBuildStep.java b/src/main/java/org/jenkinsci/plugins/workflow/support/steps/build/WaitForBuildStep.java new file mode 100644 index 00000000..bbc8ddcf --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/workflow/support/steps/build/WaitForBuildStep.java @@ -0,0 +1,83 @@ +package org.jenkinsci.plugins.workflow.support.steps.build; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.model.ItemGroup; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.util.FormValidation; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import org.apache.commons.lang.StringUtils; +import org.jenkinsci.plugins.workflow.graph.FlowNode; +import org.jenkinsci.plugins.workflow.steps.Step; +import org.jenkinsci.plugins.workflow.steps.StepContext; +import org.jenkinsci.plugins.workflow.steps.StepDescriptor; +import org.jenkinsci.plugins.workflow.steps.StepExecution; +import org.kohsuke.stapler.AncestorInPath; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; +import org.kohsuke.stapler.QueryParameter; + +public class WaitForBuildStep extends Step { + + private final String runId; + private boolean propagate = false; + + @DataBoundConstructor + public WaitForBuildStep(String runId) { + this.runId = runId; + } + + public String getRunId() { + return runId; + } + + public boolean isPropagate() { + return propagate; + } + + @DataBoundSetter public void setPropagate(boolean propagate) { + this.propagate = propagate; + } + + @Override + public StepExecution start(StepContext context) throws Exception { + return new WaitForBuildStepExecution(this, context); + } + + @Extension + public static class DescriptorImpl extends StepDescriptor { + + @Override + public String getFunctionName() { + return "waitForBuild"; + } + + @NonNull + @Override + public String getDisplayName() { + return "Wait for build to complete"; + } + + @Override + public Set> getRequiredContext() { + Set> context = new HashSet<>(); + Collections.addAll(context, FlowNode.class, Run.class, TaskListener.class); + return Collections.unmodifiableSet(context); + } + } + + @SuppressWarnings("rawtypes") + public FormValidation doCheckRunId(@AncestorInPath ItemGroup context, @QueryParameter String value) { + if (StringUtils.isBlank(value)) { + return FormValidation.warning(Messages.WaitForBuildStep_no_run_configured()); + } + Run run = Run.fromExternalizableId(value); + if (run == null) { + return FormValidation.error(Messages.WaitForBuildStep_cannot_find(value)); + } + return FormValidation.ok(); + } +} diff --git a/src/main/java/org/jenkinsci/plugins/workflow/support/steps/build/WaitForBuildStepExecution.java b/src/main/java/org/jenkinsci/plugins/workflow/support/steps/build/WaitForBuildStepExecution.java new file mode 100644 index 00000000..3effe641 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/workflow/support/steps/build/WaitForBuildStepExecution.java @@ -0,0 +1,43 @@ +package org.jenkinsci.plugins.workflow.support.steps.build; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.AbortException; +import hudson.console.ModelHyperlinkNote; +import hudson.model.Run; +import hudson.model.TaskListener; +import org.jenkinsci.plugins.workflow.steps.AbstractStepExecutionImpl; +import org.jenkinsci.plugins.workflow.steps.StepContext; + +public class WaitForBuildStepExecution extends AbstractStepExecutionImpl { + + private static final long serialVersionUID = 1L; + + private final transient WaitForBuildStep step; + + public WaitForBuildStepExecution(WaitForBuildStep step, @NonNull StepContext context) { + super(context); + this.step = step; + } + + @SuppressWarnings("rawtypes") + @Override + public boolean start() throws Exception { + Run run = Run.fromExternalizableId(step.getRunId()); + if (run == null) { + throw new AbortException("No build exists with runId " + step.getRunId()); + } + + String runHyperLink = ModelHyperlinkNote.encodeTo("/" + run.getUrl(), run.getFullDisplayName()); + TaskListener taskListener = getContext().get(TaskListener.class); + if (run.isBuilding()) { + run.addAction(new WaitForBuildAction(getContext(), step.isPropagate())); + taskListener.getLogger().println("Waiting for " + runHyperLink + " to complete"); + return false; + } else { + taskListener.getLogger().println(runHyperLink + " is already complete"); + getContext().onSuccess(null); + return true; + } + } + +} diff --git a/src/main/resources/org/jenkinsci/plugins/workflow/support/steps/build/Messages.properties b/src/main/resources/org/jenkinsci/plugins/workflow/support/steps/build/Messages.properties index 5e2b79ff..ef808906 100644 --- a/src/main/resources/org/jenkinsci/plugins/workflow/support/steps/build/Messages.properties +++ b/src/main/resources/org/jenkinsci/plugins/workflow/support/steps/build/Messages.properties @@ -30,3 +30,5 @@ BuildTriggerStep.unsupported=Building a {0} is not supported BuildTriggerStepExecution.building_=Building {0} BuildTriggerStepExecution.convertedParameterDescription=\ {0} (Automatically converted to {1} because {2} passed the parameter using a different type) +WaitForBuildStep.cannot_find=No such run with externalizable id {0} +WaitForBuildStep.no_run_configured=No runId configured diff --git a/src/main/resources/org/jenkinsci/plugins/workflow/support/steps/build/WaitForBuildStep/config.jelly b/src/main/resources/org/jenkinsci/plugins/workflow/support/steps/build/WaitForBuildStep/config.jelly new file mode 100644 index 00000000..c4f0fafe --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/workflow/support/steps/build/WaitForBuildStep/config.jelly @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/workflow/support/steps/build/WaitForBuildStep/help-propagate.html b/src/main/resources/org/jenkinsci/plugins/workflow/support/steps/build/WaitForBuildStep/help-propagate.html new file mode 100644 index 00000000..1298dd36 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/workflow/support/steps/build/WaitForBuildStep/help-propagate.html @@ -0,0 +1,6 @@ +

+ If enabled, then the result of this step is that of the downstream build being waited on (e.g., + success, unstable, failure, not built, or aborted). + If disabled (default state), then this step succeeds even if the downstream build is unstable, failed, etc.; + use the result property of the return value as needed. +

diff --git a/src/main/resources/org/jenkinsci/plugins/workflow/support/steps/build/WaitForBuildStep/help-runId.html b/src/main/resources/org/jenkinsci/plugins/workflow/support/steps/build/WaitForBuildStep/help-runId.html new file mode 100644 index 00000000..ee737805 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/workflow/support/steps/build/WaitForBuildStep/help-runId.html @@ -0,0 +1,3 @@ +

+ The externalizableId of the build to wait on. +

diff --git a/src/main/resources/org/jenkinsci/plugins/workflow/support/steps/build/WaitForBuildStep/help.html b/src/main/resources/org/jenkinsci/plugins/workflow/support/steps/build/WaitForBuildStep/help.html new file mode 100644 index 00000000..e483933d --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/workflow/support/steps/build/WaitForBuildStep/help.html @@ -0,0 +1,8 @@ +
+

+ Wait on a build to complete. +

+

+ Use the Pipeline Snippet Generator to generate a sample pipeline script for the waitforBuild step. +

+
diff --git a/src/test/java/org/jenkinsci/plugins/workflow/support/steps/build/WaitForBuildStepTest.java b/src/test/java/org/jenkinsci/plugins/workflow/support/steps/build/WaitForBuildStepTest.java new file mode 100644 index 00000000..0f5176b3 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/workflow/support/steps/build/WaitForBuildStepTest.java @@ -0,0 +1,66 @@ +package org.jenkinsci.plugins.workflow.support.steps.build; + +import hudson.model.Descriptor; +import hudson.model.FreeStyleProject; +import hudson.model.Result; +import hudson.model.Run; +import hudson.tasks.Builder; +import hudson.util.DescribableList; +import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.BuildWatcher; +import org.jvnet.hudson.test.FailureBuilder; +import org.jvnet.hudson.test.SleepBuilder; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.LoggerRule; + +import static org.junit.Assert.assertEquals; + +public class WaitForBuildStepTest { + + @ClassRule public static BuildWatcher buildWatcher = new BuildWatcher(); + @Rule public JenkinsRule j = new JenkinsRule(); + @Rule public LoggerRule logging = new LoggerRule(); + + @Test public void waitForBuild() throws Exception { + FreeStyleProject ds = j.createFreeStyleProject("ds"); + DescribableList> buildersList = ds.getBuildersList(); + buildersList.add(new SleepBuilder(500)); + buildersList.add(new FailureBuilder()); + WorkflowJob us = j.jenkins.createProject(WorkflowJob.class, "us"); + us.setDefinition(new CpsFlowDefinition( + "def ds = build job: 'ds', waitForStart: true\n" + + "def dsRunId = \"${ds.getFullProjectName()}#${ds.getNumber()}\"\n" + + "def completeDs = waitForBuild runId: dsRunId\n" + + "echo \"'ds' completed with status ${completeDs.getResult()}\"", true)); + j.assertLogContains("'ds' completed with status FAILURE", j.buildAndAssertSuccess(us)); + } + + @Test public void waitForBuildPropagte() throws Exception { + FreeStyleProject ds = j.createFreeStyleProject("ds"); + DescribableList> buildersList = ds.getBuildersList(); + buildersList.add(new SleepBuilder(500)); + buildersList.add(new FailureBuilder()); + WorkflowJob us = j.jenkins.createProject(WorkflowJob.class, "us"); + us.setDefinition(new CpsFlowDefinition( + "def ds = build job: 'ds', waitForStart: true\n" + + "def dsRunId = \"${ds.getFullProjectName()}#${ds.getNumber()}\"\n" + + "waitForBuild runId: dsRunId, propagate: true", true)); + j.assertLogContains("completed with status FAILURE", j.buildAndAssertStatus(Result.FAILURE, us)); + } + + @SuppressWarnings("rawtypes") + @Test public void waitForBuildAlreadyComplete() throws Exception { + FreeStyleProject ds = j.createFreeStyleProject("ds"); + ds.getBuildersList().add(new FailureBuilder()); + Run ds1 = ds.scheduleBuild2(0).waitForStart(); + assertEquals(1, ds1.getNumber()); + j.waitForCompletion(ds1); + WorkflowJob us = j.jenkins.createProject(WorkflowJob.class, "us"); + us.setDefinition(new CpsFlowDefinition("waitForBuild runId: 'ds#1'", true)); + j.assertLogContains("is already complete", j.buildAndAssertSuccess(us)); + } +}