Skip to content

Commit

Permalink
Implement waitForBuild step (#106)
Browse files Browse the repository at this point in the history
* Implement waitForBuild step

* Add a 500ms sleep to 'ds' test job to make tests less flaky
  • Loading branch information
stuartrowe committed Mar 17, 2023
1 parent d08f550 commit a823138
Show file tree
Hide file tree
Showing 10 changed files with 270 additions and 0 deletions.
@@ -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;
}
}
@@ -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<Run<?,?>> {

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);
}
}
@@ -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<? extends Class<?>> getRequiredContext() {
Set<Class<?>> 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();
}
}
@@ -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;
}
}

}
Expand Up @@ -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
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>

<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form" xmlns:st="jelly:stapler">
<f:entry field="runId" title="Build to wait on">
<f:textbox/>
</f:entry>
<f:entry field="propagate">
<f:checkbox default="false" title="Propagate errors"/>
</f:entry>
</j:jelly>
@@ -0,0 +1,6 @@
<p>
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 <code>result</code> property of the return value as needed.
</p>
@@ -0,0 +1,3 @@
<p>
The externalizableId of the build to wait on.
</p>
@@ -0,0 +1,8 @@
<div>
<p>
Wait on a build to complete.
</p>
<p>
Use the <a href="https://www.jenkins.io/redirect/pipeline-snippet-generator">Pipeline Snippet Generator</a> to generate a sample pipeline script for the waitforBuild step.
</p>
</div>
@@ -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<Builder, Descriptor<Builder>> 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<Builder, Descriptor<Builder>> 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));
}
}

0 comments on commit a823138

Please sign in to comment.