getDownstreamBuilds() {
36 | return Collections.unmodifiableList(new ArrayList<>(downstreamBuilds));
37 | }
38 |
39 | private synchronized @NonNull DownstreamBuild getOrAddDownstreamBuild(@NonNull String flowNodeId, @NonNull Item job) {
40 | for (DownstreamBuild build : downstreamBuilds) {
41 | if (build.getFlowNodeId().equals(flowNodeId)) {
42 | return build;
43 | }
44 | }
45 | var build = new DownstreamBuild(flowNodeId, job);
46 | downstreamBuilds.add(build);
47 | return build;
48 | }
49 |
50 | public static final class DownstreamBuild {
51 | private final String flowNodeId;
52 | private final String jobFullName;
53 | private Integer buildNumber;
54 |
55 | DownstreamBuild(String flowNodeId, @NonNull Item job) {
56 | this.flowNodeId = flowNodeId;
57 | this.jobFullName = job.getFullName();
58 | }
59 |
60 | public @NonNull String getFlowNodeId() {
61 | return flowNodeId;
62 | }
63 |
64 | public @NonNull String getJobFullName() {
65 | return jobFullName;
66 | }
67 |
68 | /**
69 | * Get the build number of the downstream build, or {@code null} if the downstream build has not yet started or the queue item was cancelled.
70 | */
71 | public @CheckForNull Integer getBuildNumber() {
72 | return buildNumber;
73 | }
74 |
75 | /**
76 | * Load the downstream build, if it has started and still exists.
77 | * Loading builds indiscriminately will affect controller performance, so use this carefully. If you only need
78 | * to know whether the build started at one point, use {@link #getBuildNumber}.
79 | * @throws AccessDeniedException as per {@link ItemGroup#getItem}
80 | */
81 | public @CheckForNull Run, ?> getBuild() throws AccessDeniedException {
82 | if (buildNumber == null) {
83 | return null;
84 | }
85 | return Run.fromExternalizableId(jobFullName + '#' + buildNumber);
86 | }
87 |
88 | void setBuild(@NonNull Run, ?> run) {
89 | this.buildNumber = run.getNumber();
90 | }
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/workflow/support/steps/build/DownstreamFailureCause.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright 2019 CloudBees, Inc.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | */
24 |
25 | package org.jenkinsci.plugins.workflow.support.steps.build;
26 |
27 | import edu.umd.cs.findbugs.annotations.CheckForNull;
28 | import hudson.console.ModelHyperlinkNote;
29 | import hudson.model.Run;
30 | import hudson.model.TaskListener;
31 | import jenkins.model.CauseOfInterruption;
32 |
33 | /**
34 | * Indicates that an upstream build failed because of a downstream build’s status.
35 | */
36 | public final class DownstreamFailureCause extends CauseOfInterruption {
37 |
38 | private static final long serialVersionUID = 1;
39 |
40 | private final String id;
41 |
42 | DownstreamFailureCause(Run, ?> downstream) {
43 | id = downstream.getExternalizableId();
44 | }
45 |
46 | public @CheckForNull Run, ?> getDownstreamBuild() {
47 | return Run.fromExternalizableId(id);
48 | }
49 |
50 | @Override public void print(TaskListener listener) {
51 | String description;
52 | Run, ?> downstream = getDownstreamBuild();
53 | if (downstream != null) {
54 | // encodeTo(Run) calls getDisplayName, which does not include the project name.
55 | description = ModelHyperlinkNote.encodeTo("/" + downstream.getUrl(), downstream.getFullDisplayName()) + " completed with status " + downstream.getResult() + " (propagate: false to ignore)";
56 | } else {
57 | description = "Downstream build was not stable (propagate: false to ignore)";
58 | }
59 | listener.getLogger().println(description);
60 | }
61 |
62 | @Override public String getShortDescription() {
63 | Run, ?> downstream = getDownstreamBuild();
64 | if (downstream != null) {
65 | return downstream.getFullDisplayName() + " completed with status " + downstream.getResult() + " (propagate: false to ignore)";
66 | } else {
67 | return "Downstream build was not stable (propagate: false to ignore)";
68 | }
69 | }
70 |
71 | }
72 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/workflow/support/steps/build/WaitForBuildAction.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.workflow.support.steps.build;
2 |
3 | import hudson.model.InvisibleAction;
4 | import org.jenkinsci.plugins.workflow.steps.StepContext;
5 |
6 | public class WaitForBuildAction extends InvisibleAction {
7 |
8 | final StepContext context;
9 | final boolean propagate;
10 |
11 | WaitForBuildAction(StepContext context, boolean propagate) {
12 | this.context = context;
13 | this.propagate = propagate;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/workflow/support/steps/build/WaitForBuildListener.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.workflow.support.steps.build;
2 |
3 | import hudson.AbortException;
4 | import hudson.Extension;
5 | import hudson.console.ModelHyperlinkNote;
6 | import hudson.model.Result;
7 | import hudson.model.Run;
8 | import hudson.model.TaskListener;
9 | import hudson.model.listeners.RunListener;
10 | import jenkins.util.Timer;
11 |
12 | import java.util.logging.Level;
13 | import java.util.logging.Logger;
14 | import org.jenkinsci.plugins.workflow.actions.WarningAction;
15 | import org.jenkinsci.plugins.workflow.graph.FlowNode;
16 | import org.jenkinsci.plugins.workflow.steps.FlowInterruptedException;
17 | import org.jenkinsci.plugins.workflow.steps.StepContext;
18 |
19 | @Extension
20 | public class WaitForBuildListener extends RunListener> {
21 |
22 | private static final Logger LOGGER = Logger.getLogger(WaitForBuildListener.class.getName());
23 |
24 | @Override
25 | public void onFinalized(Run,?> run) {
26 | for (WaitForBuildAction action : run.getActions(WaitForBuildAction.class)) {
27 | StepContext context = action.context;
28 | LOGGER.log(Level.FINE, "completing {0} for {1}", new Object[] {run, context});
29 |
30 | Result result = run.getResult();
31 | if (result == null) { /* probably impossible */
32 | result = Result.FAILURE;
33 | }
34 | try {
35 | context.get(TaskListener.class).getLogger().println("Build " + ModelHyperlinkNote.encodeTo("/" + run.getUrl(), run.getFullDisplayName()) + " completed: " + result.toString());
36 | if (action.propagate && result.isWorseThan(Result.SUCCESS)) {
37 | context.get(FlowNode.class).addOrReplaceAction(new WarningAction(result));
38 | }
39 | } catch (Exception e) {
40 | LOGGER.log(Level.WARNING, null, e);
41 | }
42 |
43 | if (!action.propagate || result == Result.SUCCESS) {
44 | context.onSuccess(new RunWrapper(run, false));
45 | } else {
46 | context.onFailure(new FlowInterruptedException(result, false, new DownstreamFailureCause(run)));
47 | }
48 | }
49 | run.removeActions(WaitForBuildAction.class);
50 | }
51 |
52 | @Override
53 | public void onDeleted(final Run,?> run) {
54 | for (WaitForBuildAction action : run.getActions(WaitForBuildAction.class)) {
55 | Timer.get().submit(() -> action.context.onFailure(new AbortException(run.getFullDisplayName() + " was deleted")));
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/workflow/support/steps/build/WaitForBuildStep.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.workflow.support.steps.build;
2 |
3 | import edu.umd.cs.findbugs.annotations.NonNull;
4 | import hudson.Extension;
5 | import hudson.model.ItemGroup;
6 | import hudson.model.Run;
7 | import hudson.model.TaskListener;
8 | import hudson.util.FormValidation;
9 | import java.util.Collections;
10 | import java.util.HashSet;
11 | import java.util.Set;
12 | import org.apache.commons.lang.StringUtils;
13 | import org.jenkinsci.plugins.workflow.graph.FlowNode;
14 | import org.jenkinsci.plugins.workflow.steps.Step;
15 | import org.jenkinsci.plugins.workflow.steps.StepContext;
16 | import org.jenkinsci.plugins.workflow.steps.StepDescriptor;
17 | import org.jenkinsci.plugins.workflow.steps.StepExecution;
18 | import org.kohsuke.stapler.AncestorInPath;
19 | import org.kohsuke.stapler.DataBoundConstructor;
20 | import org.kohsuke.stapler.DataBoundSetter;
21 | import org.kohsuke.stapler.QueryParameter;
22 |
23 | public class WaitForBuildStep extends Step {
24 |
25 | private final String runId;
26 | private boolean propagate = false;
27 | private boolean propagateAbort = false;
28 |
29 | @DataBoundConstructor
30 | public WaitForBuildStep(String runId) {
31 | this.runId = runId;
32 | }
33 |
34 | public String getRunId() {
35 | return runId;
36 | }
37 |
38 | public boolean isPropagate() {
39 | return propagate;
40 | }
41 |
42 | @DataBoundSetter public void setPropagate(boolean propagate) {
43 | this.propagate = propagate;
44 | }
45 |
46 | public boolean isPropagateAbort() {
47 | return propagateAbort;
48 | }
49 |
50 | @DataBoundSetter public void setPropagateAbort(boolean propagateAbort) {
51 | this.propagateAbort = propagateAbort;
52 | }
53 |
54 | @Override
55 | public StepExecution start(StepContext context) throws Exception {
56 | return new WaitForBuildStepExecution(this, context);
57 | }
58 |
59 | @Extension
60 | public static class DescriptorImpl extends StepDescriptor {
61 |
62 | @Override
63 | public String getFunctionName() {
64 | return "waitForBuild";
65 | }
66 |
67 | @NonNull
68 | @Override
69 | public String getDisplayName() {
70 | return "Wait for build to complete";
71 | }
72 |
73 | @Override
74 | public Set extends Class>> getRequiredContext() {
75 | Set> context = new HashSet<>();
76 | Collections.addAll(context, FlowNode.class, Run.class, TaskListener.class);
77 | return Collections.unmodifiableSet(context);
78 | }
79 | }
80 |
81 | @SuppressWarnings("rawtypes")
82 | public FormValidation doCheckRunId(@AncestorInPath ItemGroup> context, @QueryParameter String value) {
83 | if (StringUtils.isBlank(value)) {
84 | return FormValidation.warning(Messages.WaitForBuildStep_no_run_configured());
85 | }
86 | Run run = Run.fromExternalizableId(value);
87 | if (run == null) {
88 | return FormValidation.error(Messages.WaitForBuildStep_cannot_find(value));
89 | }
90 | return FormValidation.ok();
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/workflow/support/steps/build/WaitForBuildStepExecution.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.workflow.support.steps.build;
2 |
3 | import edu.umd.cs.findbugs.annotations.NonNull;
4 | import hudson.AbortException;
5 | import hudson.console.ModelHyperlinkNote;
6 | import hudson.model.Computer;
7 | import hudson.model.Executor;
8 | import hudson.model.Queue;
9 | import hudson.model.Result;
10 | import hudson.model.Run;
11 | import hudson.model.TaskListener;
12 | import jenkins.model.Jenkins;
13 |
14 | import org.jenkinsci.plugins.workflow.actions.LabelAction;
15 | import org.jenkinsci.plugins.workflow.graph.FlowNode;
16 | import org.jenkinsci.plugins.workflow.steps.AbstractStepExecutionImpl;
17 | import org.jenkinsci.plugins.workflow.steps.FlowInterruptedException;
18 | import org.jenkinsci.plugins.workflow.steps.StepContext;
19 |
20 | import java.io.IOException;
21 | import java.util.logging.Level;
22 | import java.util.logging.Logger;
23 |
24 | public class WaitForBuildStepExecution extends AbstractStepExecutionImpl {
25 |
26 | private static final long serialVersionUID = 1L;
27 |
28 | private static final Logger LOGGER = Logger.getLogger(WaitForBuildStepExecution.class.getName());
29 |
30 | private final transient WaitForBuildStep step;
31 |
32 | public WaitForBuildStepExecution(WaitForBuildStep step, @NonNull StepContext context) {
33 | super(context);
34 | this.step = step;
35 | }
36 |
37 | @SuppressWarnings("rawtypes")
38 | @Override
39 | public boolean start() throws Exception {
40 | Run run = Run.fromExternalizableId(step.getRunId());
41 | if (run == null) {
42 | throw new AbortException("No build exists with runId " + step.getRunId());
43 | }
44 |
45 | FlowNode node = getContext().get(FlowNode.class);
46 | node.addAction(new LabelAction(Messages.WaitForBuildStepExecution_waitfor(step.getRunId())));
47 |
48 | String runHyperLink = ModelHyperlinkNote.encodeTo("/" + run.getUrl(), run.getFullDisplayName());
49 | TaskListener taskListener = getContext().get(TaskListener.class);
50 | if (run.isBuilding()) {
51 | run.addAction(new WaitForBuildAction(getContext(), step.isPropagate()));
52 | taskListener.getLogger().println("Waiting for " + runHyperLink + " to complete");
53 | return false;
54 | } else {
55 | Result result = run.getResult();
56 | if (result == null) {
57 | taskListener.getLogger().println("Warning: " + runHyperLink + " already completed but getResult() returned null. Treating the result of this build as a failure");
58 | result = Result.FAILURE;
59 | }
60 | else {
61 | taskListener.getLogger().println(runHyperLink + " already completed: " + result.toString());
62 | }
63 |
64 | StepContext context = getContext();
65 | if (!step.isPropagate() || result == Result.SUCCESS) {
66 | context.onSuccess(new RunWrapper(run, false));
67 | } else {
68 | context.onFailure(new FlowInterruptedException(result, false, new DownstreamFailureCause(run)));
69 | }
70 | return true;
71 | }
72 | }
73 |
74 | @Override
75 | public void stop(@NonNull Throwable cause) throws Exception {
76 | StepContext context = getContext();
77 | Jenkins jenkins = Jenkins.getInstanceOrNull();
78 | if (jenkins == null) {
79 | context.onFailure(cause);
80 | return;
81 | }
82 |
83 | boolean interrupted = false;
84 |
85 | if (step.isPropagateAbort()) {
86 | // if there's any in-progress build already, abort that.
87 | // when the build is actually aborted, WaitForBuildListener will take notice and report the failure,
88 | // so this method shouldn't call getContext().onFailure()
89 | for (Computer c : jenkins.getComputers()) {
90 | for (Executor e : c.getAllExecutors()) {
91 | interrupted |= maybeInterrupt(e, cause, context);
92 | }
93 | }
94 | }
95 |
96 | if(!interrupted) {
97 | super.stop(cause);
98 | }
99 | }
100 |
101 | private static boolean maybeInterrupt(Executor e, Throwable cause, StepContext context) throws IOException, InterruptedException {
102 | boolean interrupted = false;
103 | Queue.Executable exec = e.getCurrentExecutable();
104 | if (exec instanceof Run) {
105 | Run, ?> downstream = (Run, ?>) exec;
106 | for(WaitForBuildAction waitForBuildAction : downstream.getActions(WaitForBuildAction.class)) {
107 | if (waitForBuildAction.context.equals(context)) {
108 | e.interrupt(Result.ABORTED, new BuildTriggerCancelledCause(cause));
109 | try {
110 | downstream.save();
111 | } catch (IOException x) {
112 | LOGGER.log(Level.WARNING, "failed to save interrupt cause on " + exec, x);
113 | }
114 | interrupted = true;
115 | }
116 | }
117 | }
118 | return interrupted;
119 | }
120 |
121 | }
122 |
--------------------------------------------------------------------------------
/src/main/resources/index.jelly:
--------------------------------------------------------------------------------
1 |
2 |
25 |
26 |
27 |
28 | Adds the Pipeline step build
to trigger builds of other jobs.
29 |
30 |
--------------------------------------------------------------------------------
/src/main/resources/org/jenkinsci/plugins/workflow/support/steps/build/BuildTriggerStep/DescriptorImpl/parameters.groovy:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.workflow.support.steps.build.BuildTriggerStep;
2 | def st = namespace('jelly:stapler')
3 | def l = namespace('/lib/layout')
4 | l.ajax {
5 | def jobName = request2.getParameter('job')
6 | if (jobName != null) {
7 | // Cf. BuildTriggerStepExecution:
8 | def contextName = request2.getParameter('context')
9 | def context = contextName != null ? app.getItemByFullName(contextName) : null
10 | def job = app.getItem(jobName, (hudson.model.Item) context, hudson.model.Item)
11 | if (job instanceof jenkins.model.ParameterizedJobMixIn.ParameterizedJob) {
12 | def pdp = job.getProperty(hudson.model.ParametersDefinitionProperty)
13 | if (pdp != null) {
14 | // Cf. ParametersDefinitionProperty/index.jelly:
15 | table(width: '100%', class: 'parameters') {
16 | for (parameterDefinition in pdp.parameterDefinitions) {
17 | tbody {
18 | set("escapeEntryTitleAndDescription", true);
19 | // TODO JENKINS-26578 does not work for CredentialsParameterDefinition: pulldown is not populated because select.js is never loaded;