├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── cd.yaml │ └── jenkins-security-scan.yml ├── .gitignore ├── .mvn ├── extensions.xml └── maven.config ├── CHANGELOG.md ├── Jenkinsfile ├── README.md ├── pom.xml └── src ├── main ├── java │ └── org │ │ └── jenkinsci │ │ └── plugins │ │ └── workflow │ │ └── support │ │ └── steps │ │ └── build │ │ ├── BuildQueueListener.java │ │ ├── BuildTriggerAction.java │ │ ├── BuildTriggerCancelledCause.java │ │ ├── BuildTriggerListener.java │ │ ├── BuildTriggerStep.java │ │ ├── BuildTriggerStepExecution.java │ │ ├── BuildUpstreamCause.java │ │ ├── BuildUpstreamNodeAction.java │ │ ├── DownstreamBuildAction.java │ │ ├── DownstreamFailureCause.java │ │ ├── WaitForBuildAction.java │ │ ├── WaitForBuildListener.java │ │ ├── WaitForBuildStep.java │ │ └── WaitForBuildStepExecution.java └── resources │ ├── index.jelly │ └── org │ └── jenkinsci │ └── plugins │ └── workflow │ └── support │ └── steps │ └── build │ ├── BuildTriggerStep │ ├── DescriptorImpl │ │ └── parameters.groovy │ ├── config.jelly │ ├── configLoad.js │ ├── help-job.html │ ├── help-parameters.html │ ├── help-propagate.html │ ├── help-quietPeriod.html │ ├── help-wait.groovy │ ├── help-wait.html │ ├── help-waitForStart.html │ └── help.html │ ├── DownstreamFailureCause │ └── summary.jelly │ ├── Messages.properties │ └── WaitForBuildStep │ ├── config.jelly │ ├── help-propagate.html │ ├── help-runId.html │ └── help.html └── test ├── java └── org │ └── jenkinsci │ └── plugins │ └── workflow │ └── support │ └── steps │ └── build │ ├── BuildTriggerStepConfigTest.java │ ├── BuildTriggerStepRestartTest.java │ ├── BuildTriggerStepTest.java │ └── WaitForBuildStepTest.java └── resources └── org └── jenkinsci └── plugins └── workflow └── support └── steps └── build └── BuildTriggerStepTest └── storedForm.zip /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @jenkinsci/pipeline-build-step-plugin-developers 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: maven 4 | directory: / 5 | schedule: 6 | interval: monthly 7 | - package-ecosystem: github-actions 8 | directory: / 9 | schedule: 10 | interval: monthly 11 | -------------------------------------------------------------------------------- /.github/workflows/cd.yaml: -------------------------------------------------------------------------------- 1 | # Note: additional setup is required, see https://www.jenkins.io/redirect/continuous-delivery-of-plugins 2 | 3 | name: cd 4 | on: 5 | workflow_dispatch: 6 | check_run: 7 | types: 8 | - completed 9 | 10 | jobs: 11 | maven-cd: 12 | uses: jenkins-infra/github-reusable-workflows/.github/workflows/maven-cd.yml@v1 13 | secrets: 14 | MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} 15 | MAVEN_TOKEN: ${{ secrets.MAVEN_TOKEN }} 16 | -------------------------------------------------------------------------------- /.github/workflows/jenkins-security-scan.yml: -------------------------------------------------------------------------------- 1 | name: Jenkins Security Scan 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | types: [ opened, synchronize, reopened ] 9 | workflow_dispatch: 10 | 11 | permissions: 12 | security-events: write 13 | contents: read 14 | actions: read 15 | 16 | jobs: 17 | security-scan: 18 | uses: jenkins-infra/jenkins-security-scan/.github/workflows/jenkins-security-scan.yaml@v2 19 | with: 20 | java-cache: 'maven' # Optionally enable use of a build dependency cache. Specify 'maven' or 'gradle' as appropriate. 21 | # java-version: 21 # Optionally specify what version of Java to set up for the build, or remove to use a recent default. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | work 3 | .project 4 | .classpath 5 | .settings 6 | .idea/ 7 | *.iml 8 | -------------------------------------------------------------------------------- /.mvn/extensions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | io.jenkins.tools.incrementals 4 | git-changelist-maven-extension 5 | 1.8 6 | 7 | 8 | -------------------------------------------------------------------------------- /.mvn/maven.config: -------------------------------------------------------------------------------- 1 | -Pconsume-incrementals 2 | -Pmight-produce-incrementals 3 | -Dchangelist.format=%d.v%s 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Changelog 2 | 3 | ## Starting from 2.14 release notes are available as github release format https://github.com/jenkinsci/pipeline-build-step-plugin/releases 4 | 5 | ### 2.13 6 | 7 | Release date: 2020-08-04 8 | 9 | - Fix: Make password parameters work with the `build` step in Jenkins 2.236 and newer ([JENKINS-62305](https://issues.jenkins-ci.org/browse/JENKINS-62305)) 10 | - Fix: Make the Parameters page of builds triggered by the `build` step always display parameters in the order they are defined on the job for that build ([JENKINS-62483](https://issues.jenkins-ci.org/browse/JENKINS-62483)) 11 | - Documentation: [Add guidelines recommending credentials parameters over password parameters when passing secrets using the `build` step](https://plugins.jenkins.io/pipeline-build-step/) 12 | - Internal: Update minimum required Jenkins version to 2.176.4, update parent POM, and update dependencies ([PR 46](https://github.com/jenkinsci/pipeline-build-step-plugin/pull/46)) 13 | 14 | ### 2.12 15 | 16 | Release date: 2020-03-17 17 | 18 | - Fix: Do not log parameter conversion warnings for parameters from Active Choices Plugin or Extended Choice Parameter Plugin. ([JENKINS-60779](https://issues.jenkins-ci.org/browse/JENKINS-60779)) 19 | 20 | ### 2.11 21 | 22 | Release date: 2020-01-03 23 | 24 | - Fix: Mark that the `FlowInterruptedException` thrown by the `build` step when the downstream build fails while using `propagate: true` should not be treated as a build interruption. Part of the fix for [JENKINS-60354](https://issues.jenkins-ci.org/browse/JENKINS-60354). Update Pipeline: Basic Steps Plugin to 2.19 or newer along with this update for the full fix. ([PR 39](https://github.com/jenkinsci/pipeline-build-step-plugin/pull/39)) 25 | - Internal: Update parent POM. ([PR 40](https://github.com/jenkinsci/pipeline-build-step-plugin/pull/40)) 26 | 27 | ### 2.10 28 | 29 | Release date: 2019-11-26 30 | 31 | - Fix: When using `propagate: true` (the default), the result of the downstream job is now used as the result of the `build` step. Previously, when the downstream job was not successful, the result of the build step was `FAILURE` no matter the actual result (`UNSTABLE`, `ABORTED`, etc.). [JENKINS-49073](https://issues.jenkins-ci.org/browse/JENKINS-49073). 32 | 33 | **Note**: As a result of this change, you are advised to also update Pipeline: Groovy Plugin to version 2.77 (or newer), so that the result of parallel steps when not using `failFast: true` is the worst result of all branches, rather than the result of the first completed branch. The distinction between these behaviors is more likely to be encountered now that the build step can have results other than `SUCCESS` and `FAILURE`. 34 | - Improvement: When the type of a parameter passed to the `build` step does not match the type of the same parameter on the downstream job, the passed parameter will now be automatically converted to the correct type in some cases (such as when passing a string parameter when the downstream job expects a password parameter). ([JENKINS-60216](https://issues.jenkins-ci.org/browse/JENKINS-60216)) 35 | - Improvement: Document the default value of the `propagate` option for the `build` step. ([PR 25](https://github.com/jenkinsci/pipeline-build-step-plugin/pull/25)) 36 | - Internal: Incrementalify the plugin, simplify code using new methods from recent versions of Jenkins core, replace usages of deprecated APIs, add additional tests, and improve existing tests. ([PR 27](https://github.com/jenkinsci/pipeline-build-step-plugin/pull/27), [PR 28](https://github.com/jenkinsci/pipeline-build-step-plugin/pull/28), [PR 29](https://github.com/jenkinsci/pipeline-build-step-plugin/pull/29), [PR 30](https://github.com/jenkinsci/pipeline-build-step-plugin/pull/30), [PR 31](https://github.com/jenkinsci/pipeline-build-step-plugin/pull/31), [PR 32](https://github.com/jenkinsci/pipeline-build-step-plugin/pull/32), [PR 33](https://github.com/jenkinsci/pipeline-build-step-plugin/pull/33)) 37 | 38 | ### 2.9 39 | 40 | Release date: 2019-04-15 41 | 42 | - [JENKINS-52038](https://issues.jenkins-ci.org/browse/JENKINS-52038) - 43 | Reject invalid values for choice parameters. 44 | 45 | ### 2.8 46 | 47 | Release date: 2019-03-18 48 | 49 | - Internal: Update dependencies and fix resulting test failures so 50 | that the plugin's tests pass successfully when run using the PCT 51 | ([PR 20](https://github.com/jenkinsci/pipeline-build-step-plugin/pull/20), 52 | [PR 22](https://github.com/jenkinsci/pipeline-build-step-plugin/pull/22)) 53 | 54 | ### 2.7 55 | 56 | Release date: 2018-01-24 57 | 58 | - [JENKINS-48632](https://issues.jenkins-ci.org/browse/JENKINS-48632) - 59 | ensure descriptions are included on downstream parameters. 60 | - [JENKINS-38339](https://issues.jenkins-ci.org/browse/JENKINS-38339) - 61 | Link downstream builds to the `FlowNode` that triggered them. 62 | 63 | ### 2.6 64 | 65 | Release date: 2017-11-06 66 | 67 | - [JENKINS-46934](https://issues.jenkins-ci.org/browse/JENKINS-46934) - 68 | Prevent possible deadlock when killing jobs using `build` step. 69 | 70 | ### 2.5.1 71 | 72 | Release date: 2017-07-10 73 | 74 | - [Fix security 75 | issue](https://jenkins.io/security/advisory/2017-07-10/) 76 | 77 | ### 2.5 78 | 79 | Release date: 2017-04-06 80 | 81 | - [JENKINS-38887](https://issues.jenkins-ci.org/browse/JENKINS-38887) `build` 82 | can now be used to trigger indexing of multibranch projects, rather 83 | than building regular jobs. 84 | 85 | ### 2.4 86 | 87 | Release date: 2016-11-21 88 | 89 | - In certain cases, interrupting a build running in a `build` step 90 | might not break you out of a loop. 91 | - Making sure interrupting a build running in a `build` step does 92 | something, even if Jenkins is unsure of the status of this step. 93 | - [JENKINS-39454](https://issues.jenkins-ci.org/browse/JENKINS-39454) 94 | Work around a core race condition that could result in hanging 95 | `build` steps when many are being run concurrently. 96 | 97 | ### 2.3 98 | 99 | Release date: 2016-09-23 100 | 101 | - [JENKINS-38114](https://issues.jenkins-ci.org/browse/JENKINS-38114) 102 | Unified help between the `currentBuild` global variable and the 103 | return value of `build` into the [Pipeline Supporting APIs 104 | Plugin](https://plugins.jenkins.io/workflow-support). 105 | - [JENKINS-37484](https://issues.jenkins-ci.org/browse/JENKINS-37484) 106 | Documentation fix: `FAILURE`, not `FAILED`, is the status name. 107 | 108 | ### 2.2 109 | 110 | Release date: 2016-07-11 111 | 112 | - Documentation for 113 | [JENKINS-30412](https://issues.jenkins-ci.org/browse/JENKINS-30412). 114 | - [JENKINS-31842](https://issues.jenkins-ci.org/browse/JENKINS-31842) 115 | Display status of a running `build` step in thread dumps. 116 | 117 | ### 2.1 118 | 119 | Release date: 2016-05-31 120 | 121 | - [JENKINS-28673](https://issues.jenkins-ci.org/browse/JENKINS-28673) 122 | `IllegalStateException` printed to log after deleting a downstream 123 | build. 124 | 125 | ### 2.0 126 | 127 | Release date: 2016-04-05 128 | 129 | - First release under per-plugin versioning scheme. See [1.x 130 | changelog](https://github.com/jenkinsci/workflow-plugin/blob/82e7defa37c05c5f004f1ba01c93df61ea7868a5/CHANGES.md) 131 | for earlier releases. 132 | - Includes the `build` step formerly in [Pipeline Supporting APIs 133 | Plugin](https://plugins.jenkins.io/workflow-support). 134 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | /* 2 | See the documentation for more options: 3 | 4 | https://github.com/jenkins-infra/pipeline-library/ 5 | 6 | */ 7 | buildPlugin( 8 | useContainerAgent: true, // Set to `false` if you need to use Docker for containerized tests 9 | configurations: [ 10 | [platform: 'linux', jdk: 21], 11 | [platform: 'windows', jdk: 17], 12 | ]) 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pipeline: Build Step Plugin 2 | 3 | [![Jenkins Plugin](https://img.shields.io/jenkins/plugin/v/pipeline-build-step)](https://plugins.jenkins.io/pipeline-build-step) 4 | [![Changelog](https://img.shields.io/github/v/tag/jenkinsci/pipeline-build-step-plugin?label=changelog)](https://github.com/jenkinsci/pipeline-build-step-plugin/blob/master/CHANGELOG.md) 5 | [![Jenkins Plugin Installs](https://img.shields.io/jenkins/plugin/i/pipeline-build-step?color=blue)](https://plugins.jenkins.io/pipeline-build-step) 6 | 7 | ## Introduction 8 | 9 | Adds the Pipeline `build` step, which triggers builds of other jobs. 10 | Use the [_"Pipeline Syntax" Snippet Generator_](https://jenkins.io/redirect/pipeline-snippet-generator) to get a detailed example for your build step. 11 | The Pipeline Syntax Snippet Generator helps the user generate steps for Jenkins pipeline. 12 | 13 | ### Passing secret values to other jobs 14 | 15 | The recommended approach to pass secret values using the `build` step is to use credentials parameters: 16 | 17 | ```groovy 18 | build(job: 'foo', parameters: [credentials(name: 'parameter-name', value: 'credentials-id')]) 19 | ``` 20 | 21 | See [the user guide for the Credentials Plugin](https://plugins.jenkins.io/credentials/) for a general overview of how credentials work in Jenkins and how they can be configured, and [the documentation for the Credentials Binding Plugin](https://plugins.jenkins.io/credentials-binding/) for an overview of how to access and use credentials from a Pipeline. 22 | 23 | The `build` step also supports passing password parameters, but this is not recommended. 24 | The plaintext secret may be persisted as part of the Pipeline's internal state, and it will not be automatically masked if it appears in the build log. 25 | Here is an example for reference: 26 | 27 | ```groovy 28 | build(job: 'foo', parameters: [password(name: 'parameter-name', value: 'secret-value')]) 29 | ``` 30 | 31 | ## Version History 32 | 33 | See [the changelog](CHANGELOG.md). 34 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | org.jenkins-ci.plugins 6 | plugin 7 | 5.9 8 | 9 | 10 | org.jenkins-ci.plugins 11 | pipeline-build-step 12 | ${changelist} 13 | hpi 14 | Pipeline: Build Step 15 | https://github.com/jenkinsci/${project.artifactId}-plugin 16 | 17 | 18 | MIT License 19 | https://opensource.org/licenses/MIT 20 | 21 | 22 | 23 | scm:git:https://github.com/${gitHubRepo}.git 24 | scm:git:git@github.com:${gitHubRepo}.git 25 | https://github.com/${gitHubRepo} 26 | ${scmTag} 27 | 28 | 29 | 30 | repo.jenkins-ci.org 31 | https://repo.jenkins-ci.org/public/ 32 | 33 | 34 | 35 | 36 | repo.jenkins-ci.org 37 | https://repo.jenkins-ci.org/public/ 38 | 39 | 40 | 41 | 999999-SNAPSHOT 42 | 43 | 2.479 44 | ${jenkins.baseline}.1 45 | jenkinsci/${project.artifactId}-plugin 46 | 47 | 48 | 49 | 50 | io.jenkins.tools.bom 51 | bom-${jenkins.baseline}.x 52 | 4023.va_eeb_b_4e45f07 53 | import 54 | pom 55 | 56 | 57 | 58 | 59 | 60 | org.jenkins-ci.plugins.workflow 61 | workflow-step-api 62 | 63 | 64 | org.jenkins-ci.plugins.workflow 65 | workflow-api 66 | 67 | 68 | org.jenkins-ci.plugins.workflow 69 | workflow-support 70 | 71 | 72 | org.jenkins-ci.plugins 73 | script-security 74 | 75 | 76 | org.jenkins-ci.plugins 77 | structs 78 | 79 | 80 | org.jenkins-ci.plugins.workflow 81 | workflow-step-api 82 | tests 83 | test 84 | 85 | 86 | org.jenkins-ci.plugins.workflow 87 | workflow-support 88 | tests 89 | test 90 | 91 | 92 | org.jenkins-ci.plugins.workflow 93 | workflow-cps 94 | test 95 | 96 | 97 | org.jenkins-ci.plugins.workflow 98 | workflow-cps 99 | tests 100 | test 101 | 102 | 103 | org.jenkins-ci.plugins.workflow 104 | workflow-job 105 | test 106 | 107 | 108 | org.jenkins-ci.plugins.workflow 109 | workflow-basic-steps 110 | test 111 | 112 | 113 | org.jenkins-ci.plugins 114 | branch-api 115 | test 116 | 117 | 118 | org.jenkins-ci.plugins 119 | credentials 120 | test 121 | 122 | 123 | org.jenkins-ci.plugins 124 | scm-api 125 | test 126 | 127 | 128 | org.jenkins-ci.plugins 129 | scm-api 130 | tests 131 | test 132 | 133 | 134 | org.jenkins-ci.plugins.workflow 135 | workflow-scm-step 136 | test 137 | 138 | 139 | org.jenkins-ci.plugins.workflow 140 | workflow-multibranch 141 | test 142 | 143 | 144 | org.jenkins-ci.plugins 145 | cloudbees-folder 146 | test 147 | 148 | 149 | org.jenkins-ci.plugins 150 | git 151 | test 152 | 153 | 154 | org.awaitility 155 | awaitility 156 | 4.3.0 157 | test 158 | 159 | 160 | 161 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/workflow/support/steps/build/BuildQueueListener.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.workflow.support.steps.build; 2 | 3 | import hudson.AbortException; 4 | import hudson.Extension; 5 | import hudson.model.Queue; 6 | import hudson.model.queue.QueueListener; 7 | 8 | /** 9 | * @author Vivek Pandey 10 | */ 11 | @Extension 12 | public class BuildQueueListener extends QueueListener { 13 | @Override 14 | public void onLeft(Queue.LeftItem li) { 15 | if(li.isCancelled()){ 16 | for (BuildTriggerAction.Trigger trigger : BuildTriggerAction.triggersFor(li)) { 17 | trigger.context.onFailure(new AbortException("Build of " + li.task.getFullDisplayName() + " was cancelled")); 18 | } 19 | } 20 | } 21 | 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/workflow/support/steps/build/BuildTriggerAction.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.workflow.support.steps.build; 2 | 3 | import edu.umd.cs.findbugs.annotations.CheckForNull; 4 | import hudson.model.Action; 5 | import hudson.model.Actionable; 6 | import hudson.model.InvisibleAction; 7 | import hudson.model.Queue; 8 | import hudson.model.queue.FoldableAction; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | import java.util.logging.Level; 12 | import java.util.logging.Logger; 13 | import org.jenkinsci.plugins.workflow.steps.StepContext; 14 | 15 | @SuppressWarnings("SynchronizeOnNonFinalField") 16 | class BuildTriggerAction extends InvisibleAction implements FoldableAction { 17 | 18 | private static final Logger LOGGER = Logger.getLogger(BuildTriggerAction.class.getName()); 19 | 20 | @Deprecated 21 | private StepContext context; 22 | 23 | @Deprecated 24 | private Boolean propagate; 25 | 26 | /** Record of one upstream build step. */ 27 | static class Trigger { 28 | 29 | final StepContext context; 30 | 31 | final boolean propagate; 32 | final boolean waitForStart; 33 | 34 | /** Record of cancellation cause passed to {@link BuildTriggerStepExecution#stop}, if any. */ 35 | @CheckForNull 36 | Throwable interruption; 37 | 38 | Trigger(StepContext context, boolean propagate, boolean waitForStart) { 39 | this.context = context; 40 | this.propagate = propagate; 41 | this.waitForStart = waitForStart; 42 | } 43 | 44 | } 45 | 46 | private /* final */ List triggers; 47 | 48 | BuildTriggerAction(StepContext context, boolean propagate, boolean waitForStart) { 49 | triggers = new ArrayList<>(); 50 | triggers.add(new Trigger(context, propagate, waitForStart)); 51 | } 52 | 53 | private Object readResolve() { 54 | if (triggers == null) { 55 | triggers = new ArrayList<>(); 56 | triggers.add(new Trigger(context, propagate != null ? propagate : /* old serialized record */ true, false)); 57 | context = null; 58 | propagate = null; 59 | } 60 | return this; 61 | } 62 | 63 | static Iterable triggersFor(Actionable actionable) { 64 | List triggers = new ArrayList<>(); 65 | for (BuildTriggerAction action : actionable.getActions(BuildTriggerAction.class)) { 66 | synchronized (action.triggers) { 67 | triggers.addAll(action.triggers); 68 | } 69 | } 70 | return triggers; 71 | } 72 | 73 | @Override public void foldIntoExisting(Queue.Item item, Queue.Task owner, List otherActions) { 74 | // there may be >1 upstream builds (or other unrelated causes) for a single downstream build 75 | BuildTriggerAction existing = item.getAction(BuildTriggerAction.class); 76 | if (existing == null) { 77 | item.addAction(this); 78 | } else { 79 | synchronized (existing.triggers) { 80 | existing.triggers.addAll(triggers); 81 | } 82 | } 83 | LOGGER.log(Level.FINE, "coalescing actions for {0}", item); 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/workflow/support/steps/build/BuildTriggerCancelledCause.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.workflow.support.steps.build; 2 | 3 | import jenkins.model.CauseOfInterruption; 4 | 5 | /** 6 | * Indicates that a build is cancelled because the workflow that requested it is aborted. 7 | * 8 | * TODO: real summary.jelly 9 | * @author Kohsuke Kawaguchi 10 | */ 11 | public class BuildTriggerCancelledCause extends CauseOfInterruption { 12 | 13 | private static final long serialVersionUID = 1; 14 | 15 | private final Throwable cause; 16 | // TODO: capture ModelObject (such as WorkflowRun) that caused this cancellation 17 | 18 | public BuildTriggerCancelledCause(Throwable cause) { 19 | this.cause = cause; 20 | } 21 | 22 | @Override 23 | public String getShortDescription() { 24 | return "Calling Pipeline was cancelled"; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/workflow/support/steps/build/BuildTriggerListener.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.Cause; 7 | import hudson.model.Result; 8 | import hudson.model.Run; 9 | import hudson.model.TaskListener; 10 | import hudson.model.listeners.RunListener; 11 | import java.io.IOException; 12 | import java.util.logging.Level; 13 | import java.util.logging.Logger; 14 | import jenkins.util.Timer; 15 | import org.jenkinsci.plugins.workflow.actions.WarningAction; 16 | import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner; 17 | import org.jenkinsci.plugins.workflow.graph.FlowNode; 18 | import org.jenkinsci.plugins.workflow.steps.FlowInterruptedException; 19 | import org.jenkinsci.plugins.workflow.steps.StepContext; 20 | 21 | @Extension 22 | public class BuildTriggerListener extends RunListener>{ 23 | 24 | private static final Logger LOGGER = Logger.getLogger(BuildTriggerListener.class.getName()); 25 | 26 | @Override 27 | public void onStarted(Run run, TaskListener listener) { 28 | for (BuildTriggerAction.Trigger trigger : BuildTriggerAction.triggersFor(run)) { 29 | StepContext stepContext = trigger.context; 30 | if (stepContext != null && stepContext.isReady()) { 31 | LOGGER.log(Level.FINE, "started building {0} from #{1} in {2}", new Object[] {run, run.getQueueId(), stepContext}); 32 | try { 33 | TaskListener taskListener = stepContext.get(TaskListener.class); 34 | // encodeTo(Run) calls getDisplayName, which does not include the project name. 35 | taskListener.getLogger().println("Starting building: " + ModelHyperlinkNote.encodeTo("/" + run.getUrl(), run.getFullDisplayName())); 36 | if (trigger.waitForStart) { 37 | stepContext.onSuccess(new RunWrapper(run, false)); 38 | } 39 | } catch (Exception e) { 40 | LOGGER.log(Level.WARNING, null, e); 41 | } 42 | } else { 43 | LOGGER.log(Level.FINE, "{0} unavailable in {1}", new Object[] {stepContext, run}); 44 | } 45 | } 46 | Timer.get().submit(() -> updateDownstreamBuildAction(run)); 47 | } 48 | 49 | @Override 50 | public void onFinalized(Run run) { 51 | for (BuildTriggerAction.Trigger trigger : BuildTriggerAction.triggersFor(run)) { 52 | if (!trigger.waitForStart) { 53 | StepContext stepContext = trigger.context; 54 | LOGGER.log(Level.FINE, "completing {0} for {1}", new Object[] {run, stepContext}); 55 | Result result = run.getResult(); 56 | if (result == null) { /* probably impossible */ 57 | result = Result.FAILURE; 58 | } 59 | 60 | try { 61 | stepContext.get(TaskListener.class).getLogger().println("Build " + ModelHyperlinkNote.encodeTo("/" + run.getUrl(), run.getFullDisplayName()) + " completed: " + result.toString()); 62 | if (trigger.propagate && result != Result.SUCCESS) { 63 | stepContext.get(FlowNode.class).addOrReplaceAction(new WarningAction(result)); 64 | } 65 | } catch (Exception e) { 66 | LOGGER.log(Level.WARNING, null, e); 67 | } 68 | 69 | if (!trigger.propagate || result == Result.SUCCESS) { 70 | if (trigger.interruption == null) { 71 | stepContext.onSuccess(new RunWrapper(run, false)); 72 | } else { 73 | stepContext.onFailure(trigger.interruption); 74 | } 75 | } else { 76 | stepContext.onFailure(new FlowInterruptedException(result, false, new DownstreamFailureCause(run))); 77 | } 78 | } 79 | } 80 | run.removeActions(BuildTriggerAction.class); 81 | } 82 | 83 | @Override 84 | public void onDeleted(final Run run) { 85 | for (final BuildTriggerAction.Trigger trigger : BuildTriggerAction.triggersFor(run)) { 86 | Timer.get().submit(() -> trigger.context.onFailure(new AbortException(run.getFullDisplayName() + " was deleted"))); 87 | } 88 | } 89 | 90 | private void updateDownstreamBuildAction(Run downstream) { 91 | for (Cause cause : downstream.getCauses()) { 92 | if (cause instanceof BuildUpstreamCause) { 93 | BuildUpstreamCause buildUpstreamCause = (BuildUpstreamCause) cause; 94 | Run upstream = buildUpstreamCause.getUpstreamRun(); 95 | if (upstream instanceof FlowExecutionOwner.Executable) { 96 | String flowNodeId = buildUpstreamCause.getNodeId(); 97 | DownstreamBuildAction.getOrCreate(upstream, flowNodeId, downstream.getParent()).setBuild(downstream); 98 | try { 99 | upstream.save(); 100 | } catch (IOException e) { 101 | LOGGER.log(Level.FINE, e, () -> "Unable to update DownstreamBuildAction for " + upstream + " node " + buildUpstreamCause.getNodeId()); 102 | } 103 | } 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/workflow/support/steps/build/BuildTriggerStep.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.workflow.support.steps.build; 2 | 3 | import edu.umd.cs.findbugs.annotations.NonNull; 4 | import edu.umd.cs.findbugs.annotations.Nullable; 5 | import hudson.Extension; 6 | import hudson.Functions; 7 | import hudson.Util; 8 | import hudson.model.AutoCompletionCandidates; 9 | import hudson.model.Describable; 10 | import hudson.model.Item; 11 | import hudson.model.ItemGroup; 12 | import hudson.model.ItemVisitor; 13 | import hudson.model.Job; 14 | import hudson.model.ParameterDefinition; 15 | import hudson.model.ParameterValue; 16 | import hudson.model.ParametersDefinitionProperty; 17 | import hudson.model.PasswordParameterDefinition; 18 | import hudson.model.PasswordParameterValue; 19 | import hudson.model.Queue; 20 | import hudson.model.Run; 21 | import hudson.model.TaskListener; 22 | import hudson.util.FormValidation; 23 | import hudson.util.Secret; 24 | import java.util.ArrayList; 25 | import java.util.Collections; 26 | import java.util.HashMap; 27 | import java.util.HashSet; 28 | import java.util.List; 29 | import java.util.Map; 30 | import java.util.Set; 31 | import java.util.function.Function; 32 | import java.util.stream.Collectors; 33 | import jenkins.model.Jenkins; 34 | import net.sf.json.JSONArray; 35 | import net.sf.json.JSONObject; 36 | import org.apache.commons.lang.StringEscapeUtils; 37 | import org.apache.commons.lang.StringUtils; 38 | import org.jenkinsci.plugins.structs.describable.CustomDescribableModel; 39 | import org.jenkinsci.plugins.structs.describable.DescribableModel; 40 | import org.jenkinsci.plugins.structs.describable.UninstantiatedDescribable; 41 | import org.jenkinsci.plugins.workflow.graph.FlowNode; 42 | import org.jenkinsci.plugins.workflow.steps.Step; 43 | import org.jenkinsci.plugins.workflow.steps.StepContext; 44 | import org.jenkinsci.plugins.workflow.steps.StepDescriptor; 45 | import org.jenkinsci.plugins.workflow.steps.StepExecution; 46 | import org.jenkinsci.plugins.workflow.util.StaplerReferer; 47 | import org.kohsuke.accmod.Restricted; 48 | import org.kohsuke.accmod.restrictions.DoNotUse; 49 | import org.kohsuke.stapler.AncestorInPath; 50 | import org.kohsuke.stapler.DataBoundConstructor; 51 | import org.kohsuke.stapler.DataBoundSetter; 52 | import org.kohsuke.stapler.QueryParameter; 53 | import org.kohsuke.stapler.StaplerRequest2; 54 | 55 | public class BuildTriggerStep extends Step { 56 | 57 | private final String job; 58 | private List parameters; 59 | private boolean wait = true; 60 | private boolean waitForStart = false; 61 | private boolean propagate = true; 62 | private Integer quietPeriod; 63 | 64 | @DataBoundConstructor 65 | public BuildTriggerStep(String job) { 66 | this.job = job; 67 | } 68 | 69 | public String getJob() { 70 | return job; 71 | } 72 | 73 | public List getParameters() { 74 | return parameters; 75 | } 76 | 77 | @DataBoundSetter public void setParameters(List parameters) { 78 | this.parameters = parameters; 79 | } 80 | 81 | public boolean getWait() { 82 | return wait; 83 | } 84 | 85 | @DataBoundSetter public void setWait(boolean wait) { 86 | this.wait = wait; 87 | } 88 | 89 | public boolean getWaitForStart() { 90 | return waitForStart; 91 | } 92 | 93 | @DataBoundSetter public void setWaitForStart(boolean waitForStart) { 94 | this.waitForStart = waitForStart; 95 | } 96 | 97 | public Integer getQuietPeriod() { 98 | return quietPeriod; 99 | } 100 | 101 | @DataBoundSetter public void setQuietPeriod(Integer quietPeriod) { 102 | this.quietPeriod = quietPeriod; 103 | } 104 | 105 | public boolean isPropagate() { 106 | return propagate; 107 | } 108 | 109 | @DataBoundSetter public void setPropagate(boolean propagate) { 110 | this.propagate = propagate; 111 | } 112 | 113 | @Override 114 | public StepExecution start(StepContext context) { 115 | return new BuildTriggerStepExecution(this, context); 116 | } 117 | 118 | @Extension 119 | public static class DescriptorImpl extends StepDescriptor implements CustomDescribableModel { 120 | 121 | // Note: This is necessary because the JSON format of the parameters produced by config.jelly when 122 | // using the snippet generator does not match what would be neccessary for databinding to work automatically. 123 | // Only called via the snippet generator. 124 | @Override public Step newInstance(@Nullable StaplerRequest2 req, @NonNull JSONObject formData) throws FormException { 125 | BuildTriggerStep step = (BuildTriggerStep) super.newInstance(req, formData); 126 | // Cf. ParametersDefinitionProperty._doBuild: 127 | Object parameter = formData.get("parameter"); 128 | JSONArray params = parameter != null ? JSONArray.fromObject(parameter) : null; 129 | if (params != null) { 130 | Job context = StaplerReferer.findItemFromRequest(Job.class); 131 | Job job = Jenkins.get().getItem(step.getJob(), context, Job.class); 132 | if (job != null) { 133 | ParametersDefinitionProperty pdp = job.getProperty(ParametersDefinitionProperty.class); 134 | if (pdp != null) { 135 | List values = new ArrayList<>(); 136 | for (Object o : params) { 137 | JSONObject jo = (JSONObject) o; 138 | String name = jo.getString("name"); 139 | ParameterDefinition d = pdp.getParameterDefinition(name); 140 | if (d == null) { 141 | throw new IllegalArgumentException("No such parameter definition: " + name); 142 | } 143 | ParameterValue parameterValue; 144 | if (d instanceof PasswordParameterDefinition) { 145 | parameterValue = req.bindJSON(PasswordParameterValue.class, jo); 146 | parameterValue.setDescription(d.getDescription()); 147 | } else { 148 | parameterValue = d.createValue(req, jo); 149 | } 150 | if (parameterValue != null) { 151 | values.add(parameterValue); 152 | } else { 153 | throw new IllegalArgumentException("Cannot retrieve the parameter value: " + name); 154 | } 155 | } 156 | step.setParameters(values); 157 | } 158 | } 159 | } 160 | return step; 161 | } 162 | 163 | /** 164 | * Compatibility hack for JENKINS-62305. Only affects runtime behavior of the step, not the snippet generator. 165 | * Ideally, password parameters would not be used at all with this step, but there was no documentation or 166 | * runtime warnings for this usage previously and so it is relatively common. 167 | */ 168 | @NonNull 169 | @Override 170 | public Map customInstantiate(@NonNull Map map) { 171 | if (DescribableModel.of(PasswordParameterValue.class).getParameter("value").getErasedType() != Secret.class) { 172 | return map; 173 | } 174 | return copyMapReplacingEntry(map, "parameters", List.class, parameters -> parameters.stream() 175 | .map(parameter -> { 176 | if (parameter instanceof UninstantiatedDescribable) { 177 | UninstantiatedDescribable ud = (UninstantiatedDescribable) parameter; 178 | if (ud.getSymbol().equals("password")) { 179 | Map newArguments = copyMapReplacingEntry(ud.getArguments(), "value", String.class, Secret::fromString); 180 | return ud.withArguments(newArguments); 181 | } 182 | } 183 | return parameter; 184 | }) 185 | .collect(Collectors.toList()) 186 | ); 187 | } 188 | 189 | @NonNull 190 | @Override 191 | public UninstantiatedDescribable customUninstantiate(@NonNull UninstantiatedDescribable step) { 192 | Map newStepArgs = copyMapReplacingEntry(step.getArguments(), "parameters", List.class, parameters -> parameters.stream() 193 | .map(parameter -> { 194 | if (parameter instanceof UninstantiatedDescribable) { 195 | UninstantiatedDescribable ud = (UninstantiatedDescribable) parameter; 196 | if (ud.getSymbol().equals("password")) { 197 | Map newParamArgs = copyMapReplacingEntry(ud.getArguments(), "value", Secret.class, Secret::getPlainText); 198 | return ud.withArguments(newParamArgs); 199 | } 200 | } 201 | return parameter; 202 | }) 203 | .collect(Collectors.toList()) 204 | ); 205 | return step.withArguments(newStepArgs); 206 | } 207 | 208 | /** 209 | * Copy a map, replacing the entry with the specified key if it matches the specified type. 210 | */ 211 | private static Map copyMapReplacingEntry(Map map, String keyToReplace, Class requiredValueType, Function replacer) { 212 | Map newMap = new HashMap<>(); 213 | for (Map.Entry entry : map.entrySet()) { 214 | if (entry.getKey().equals(keyToReplace) && requiredValueType.isInstance(entry.getValue())) { 215 | newMap.put(entry.getKey(), replacer.apply(requiredValueType.cast(entry.getValue()))); 216 | } else { 217 | newMap.put(entry.getKey(), entry.getValue()); 218 | } 219 | } 220 | return newMap; 221 | } 222 | 223 | @Override 224 | public Set> getRequiredContext() { 225 | Set> context = new HashSet<>(); 226 | Collections.addAll(context, FlowNode.class, Run.class, TaskListener.class); 227 | return Collections.unmodifiableSet(context); 228 | } 229 | 230 | @Override 231 | public String getFunctionName() { 232 | return "build"; 233 | } 234 | 235 | @NonNull 236 | @Override 237 | public String getDisplayName() { 238 | return "Build a job"; 239 | } 240 | 241 | public AutoCompletionCandidates doAutoCompleteJob(@AncestorInPath ItemGroup container, @QueryParameter final String value) { 242 | // TODO remove code copy&pasted from AutoCompletionCandidates.ofJobNames when it supports testing outside Item bound 243 | final AutoCompletionCandidates candidates = new AutoCompletionCandidates(); 244 | class Visitor extends ItemVisitor { 245 | String prefix; 246 | 247 | Visitor(String prefix) { 248 | this.prefix = prefix; 249 | } 250 | 251 | @Override 252 | public void onItem(Item i) { 253 | String n = contextualNameOf(i); 254 | if ((n.startsWith(value) || value.startsWith(n)) 255 | // 'foobar' is a valid candidate if the current value is 'foo'. 256 | // Also, we need to visit 'foo' if the current value is 'foo/bar' 257 | && (value.length() > n.length() || !n.substring(value.length()).contains("/")) 258 | // but 'foobar/zot' isn't if the current value is 'foo' 259 | // we'll first show 'foobar' and then wait for the user to type '/' to show the rest 260 | && i.hasPermission(Item.READ) 261 | // and read permission required 262 | ) { 263 | if (i instanceof Queue.Task && n.startsWith(value)) 264 | candidates.add(n); 265 | 266 | // recurse 267 | String oldPrefix = prefix; 268 | prefix = n; 269 | super.onItem(i); 270 | prefix = oldPrefix; 271 | } 272 | } 273 | 274 | private String contextualNameOf(Item i) { 275 | if (prefix.endsWith("/") || prefix.length() == 0) 276 | return prefix + i.getName(); 277 | else 278 | return prefix + '/' + i.getName(); 279 | } 280 | } 281 | 282 | if (container == null || container == Jenkins.getInstanceOrNull()) { 283 | new Visitor("").onItemGroup(Jenkins.getInstanceOrNull()); 284 | } else { 285 | new Visitor("").onItemGroup(container); 286 | if (value.startsWith("/")) 287 | new Visitor("/").onItemGroup(Jenkins.getInstanceOrNull()); 288 | 289 | for (StringBuilder p = new StringBuilder("../"); value.startsWith(p.toString()); p .append("../")) { 290 | container = ((Item) container).getParent(); 291 | new Visitor(p.toString()).onItemGroup(container); 292 | } 293 | } 294 | return candidates; 295 | // END of copy&paste 296 | } 297 | 298 | @Restricted(DoNotUse.class) // for use from config.jelly 299 | public String getContext() { 300 | Job job = StaplerReferer.findItemFromRequest(Job.class); 301 | return job != null ? job.getFullName() : null; 302 | } 303 | 304 | @Restricted(DoNotUse.class) // for use from config.jelly 305 | public String getContextEncoded() { 306 | final String context = getContext(); 307 | //Functions.jsStringEscape() is also an alternative though the one below escapes more stuff 308 | return context != null ? StringEscapeUtils.escapeJavaScript(context) : null; 309 | } 310 | 311 | public FormValidation doCheckPropagate(@QueryParameter boolean value, @QueryParameter boolean wait) { 312 | if (!value && !wait) { 313 | return FormValidation.warningWithMarkup(Messages.BuildTriggerStep_explicitly_disabling_both_propagate_and_wait()); 314 | } 315 | return FormValidation.ok(); 316 | } 317 | 318 | public FormValidation doCheckWait(@AncestorInPath ItemGroup context, @QueryParameter boolean value, @QueryParameter String job) { 319 | if (!value) { 320 | return FormValidation.ok(); 321 | } 322 | Item item = Jenkins.get().getItem(job, context, Item.class); 323 | if (item == null) { 324 | return FormValidation.ok(); 325 | } 326 | if (item instanceof Job) { 327 | return FormValidation.ok(); 328 | } 329 | return FormValidation.error(Messages.BuildTriggerStep_no_wait_for_non_jobs()); 330 | } 331 | 332 | public FormValidation doCheckJob(@AncestorInPath ItemGroup context, @QueryParameter String value) { 333 | if (StringUtils.isBlank(value)) { 334 | return FormValidation.warning(Messages.BuildTriggerStep_no_job_configured()); 335 | } 336 | Item item = Jenkins.get().getItem(value, context, Item.class); 337 | if (item == null) { 338 | return FormValidation.error(Messages.BuildTriggerStep_cannot_find(value)); 339 | } 340 | if (item instanceof Queue.Task) { 341 | return FormValidation.ok(); 342 | } 343 | if (item instanceof Describable) { 344 | return FormValidation.error(Messages.BuildTriggerStep_unsupported(((Describable)item).getDescriptor().getDisplayName())); 345 | } 346 | return FormValidation.error(Messages.BuildTriggerStep_unsupported(item.getClass().getName())); 347 | } 348 | 349 | } 350 | } 351 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/workflow/support/steps/build/BuildTriggerStepExecution.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.workflow.support.steps.build; 2 | 3 | import com.google.common.collect.ImmutableSet; 4 | import com.google.common.collect.Lists; 5 | import edu.umd.cs.findbugs.annotations.CheckForNull; 6 | import edu.umd.cs.findbugs.annotations.NonNull; 7 | import hudson.AbortException; 8 | import hudson.Util; 9 | import hudson.console.ModelHyperlinkNote; 10 | import hudson.model.Action; 11 | import hudson.model.CauseAction; 12 | import hudson.model.ChoiceParameterDefinition; 13 | import hudson.model.Computer; 14 | import hudson.model.Describable; 15 | import hudson.model.Executor; 16 | import hudson.model.Item; 17 | import hudson.model.Job; 18 | import hudson.model.ParameterDefinition; 19 | import hudson.model.ParameterValue; 20 | import hudson.model.ParametersAction; 21 | import hudson.model.ParametersDefinitionProperty; 22 | import hudson.model.Queue; 23 | import hudson.model.Result; 24 | import hudson.model.Run; 25 | import hudson.model.SimpleParameterDefinition; 26 | import hudson.model.StringParameterDefinition; 27 | import hudson.model.StringParameterValue; 28 | import hudson.model.TaskListener; 29 | import hudson.model.queue.ScheduleResult; 30 | import jenkins.model.Jenkins; 31 | import jenkins.model.ParameterizedJobMixIn; 32 | import org.jenkinsci.plugins.workflow.actions.LabelAction; 33 | import org.jenkinsci.plugins.workflow.graph.FlowNode; 34 | import org.jenkinsci.plugins.workflow.steps.AbstractStepExecutionImpl; 35 | import org.jenkinsci.plugins.workflow.steps.StepContext; 36 | 37 | import java.io.IOException; 38 | import java.lang.reflect.InvocationTargetException; 39 | import java.lang.reflect.Method; 40 | import java.util.ArrayList; 41 | import java.util.LinkedHashMap; 42 | import java.util.List; 43 | import java.util.Map; 44 | import java.util.Set; 45 | import java.util.logging.Level; 46 | import java.util.logging.Logger; 47 | 48 | public class BuildTriggerStepExecution extends AbstractStepExecutionImpl { 49 | 50 | private static final Logger LOGGER = Logger.getLogger(BuildTriggerStepExecution.class.getName()); 51 | private static final Set CHOICE_PARAMETER_DEFINITION_LIKE_CLASSES = ImmutableSet.of( 52 | "jp.ikedam.jenkins.plugins.extensible_choice_parameter.ExtensibleChoiceParameterDefinition", 53 | // The names are misleading, but these classes are all parameter definitions, not parameters. 54 | "org.biouno.unochoice.CascadeChoiceParameter", 55 | "org.biouno.unochoice.ChoiceParameter", 56 | "org.biouno.unochoice.DynamicReferenceParameter"); 57 | 58 | private final transient BuildTriggerStep step; 59 | 60 | public BuildTriggerStepExecution(BuildTriggerStep step, @NonNull StepContext context) { 61 | super(context); 62 | this.step = step; 63 | } 64 | 65 | @SuppressWarnings({"unchecked", "rawtypes"}) // cannot get from ParameterizedJob back to ParameterizedJobMixIn trivially 66 | @Override 67 | public boolean start() throws Exception { 68 | String job = step.getJob(); 69 | Run upstream = getContext().get(Run.class); 70 | Item item = Jenkins.get().getItem(job, upstream.getParent(), Item.class); 71 | if (item == null) { 72 | throw new AbortException("No item named " + job + " found"); 73 | } 74 | item.checkPermission(Item.BUILD); 75 | if ((step.getWait() || step.getWaitForStart()) && !(item instanceof Job)) { 76 | // TODO find some way of allowing ComputedFolders to hook into the listener code 77 | throw new AbortException("Waiting for non-job items is not supported"); 78 | } 79 | 80 | FlowNode node = getContext().get(FlowNode.class); 81 | DownstreamBuildAction.getOrCreate(upstream, node.getId(), item); 82 | 83 | List actions = new ArrayList<>(); 84 | actions.add(new CauseAction(new BuildUpstreamCause(getContext().get(FlowNode.class), upstream))); 85 | actions.add(new BuildUpstreamNodeAction(node, upstream)); 86 | 87 | if (item instanceof ParameterizedJobMixIn.ParameterizedJob) { 88 | final ParameterizedJobMixIn.ParameterizedJob project = (ParameterizedJobMixIn.ParameterizedJob) item; 89 | getContext().get(TaskListener.class).getLogger().println("Scheduling project: " + ModelHyperlinkNote.encodeTo(project)); 90 | 91 | if (!step.getWait() || step.getWaitForStart()) { 92 | node.addAction(new LabelAction(Messages.BuildTriggerStepExecution_scheduling(project.getFullDisplayName()))); 93 | } else { 94 | node.addAction(new LabelAction(Messages.BuildTriggerStepExecution_building_(project.getFullDisplayName()))); 95 | } 96 | 97 | if (step.getWait() || step.getWaitForStart()) { 98 | StepContext context = getContext(); 99 | actions.add(new BuildTriggerAction(context, step.isPropagate(), step.getWaitForStart())); 100 | LOGGER.log(Level.FINER, "scheduling a build of {0} from {1}", new Object[]{project, context}); 101 | } 102 | 103 | List parameters = step.getParameters(); 104 | if (parameters != null) { 105 | parameters = completeDefaultParameters(parameters, (Job) project); 106 | actions.add(new ParametersAction(parameters)); 107 | } 108 | int quietPeriod = step.getQuietPeriod() != null ? step.getQuietPeriod() : -1; 109 | Queue.Item queueItem = 110 | ParameterizedJobMixIn.scheduleBuild2( 111 | (Job) project, quietPeriod, actions.toArray(new Action[0])); 112 | if (queueItem == null || queueItem.getFuture() == null) { 113 | throw new AbortException("Failed to trigger build of " + project.getFullName()); 114 | } 115 | } else if (item instanceof Queue.Task){ 116 | if (step.getParameters() != null && !step.getParameters().isEmpty()) { 117 | throw new AbortException("Item type does not support parameters"); 118 | } 119 | Queue.Task task = (Queue.Task) item; 120 | getContext().get(TaskListener.class).getLogger().println("Scheduling item: " + ModelHyperlinkNote.encodeTo(item)); 121 | if (!step.getWait() || step.getWaitForStart()) { 122 | node.addAction(new LabelAction(Messages.BuildTriggerStepExecution_scheduling(task.getFullDisplayName()))); 123 | } else { 124 | node.addAction(new LabelAction(Messages.BuildTriggerStepExecution_building_(task.getFullDisplayName()))); 125 | } 126 | 127 | if (step.getWait() || step.getWaitForStart()) { 128 | StepContext context = getContext(); 129 | actions.add(new BuildTriggerAction(context, step.isPropagate(), step.getWaitForStart())); 130 | LOGGER.log(Level.FINER, "scheduling a build of {0} from {1}", new Object[]{task, context}); 131 | } 132 | 133 | Integer quietPeriod = step.getQuietPeriod(); 134 | if (quietPeriod == null) { 135 | try { 136 | Method getQuietPeriod = task.getClass().getMethod("getQuietPeriod"); 137 | if (getQuietPeriod.getReturnType().equals(int.class)) { 138 | quietPeriod = (Integer) getQuietPeriod.invoke(task); 139 | } 140 | } catch (NoSuchMethodException e) { 141 | // ignore, best effort only 142 | } catch (IllegalAccessError | IllegalArgumentException | InvocationTargetException e) { 143 | LOGGER.log(Level.WARNING, "Could not determine quiet period of " + item.getFullName(), e); 144 | } 145 | } 146 | if (quietPeriod == null) { 147 | quietPeriod = Jenkins.get().getQuietPeriod(); 148 | } 149 | ScheduleResult scheduleResult = Jenkins.get().getQueue().schedule2(task, quietPeriod,actions); 150 | if (scheduleResult.isRefused()) { 151 | throw new AbortException("Failed to trigger build of " + item.getFullName()); 152 | } 153 | } else { 154 | throw new AbortException("The item named " + job + " is a " 155 | + (item instanceof Describable 156 | ? ((Describable) item).getDescriptor().getDisplayName() 157 | : item.getClass().getName()) 158 | + " which is not something that can be built"); 159 | } 160 | if (step.getWait() || step.getWaitForStart()) { 161 | return false; 162 | } else { 163 | getContext().onSuccess(null); 164 | return true; 165 | } 166 | } 167 | 168 | private List completeDefaultParameters(List parameters, Job project) throws IOException, InterruptedException { 169 | Map allParameters = new LinkedHashMap<>(); 170 | for (ParameterValue pv : parameters) { 171 | allParameters.put(pv.getName(), pv); 172 | } 173 | if (project != null) { 174 | ParametersDefinitionProperty pdp = project.getProperty(ParametersDefinitionProperty.class); 175 | if (pdp != null) { 176 | for (ParameterDefinition pDef : pdp.getParameterDefinitions()) { 177 | if (!allParameters.containsKey(pDef.getName())) { 178 | ParameterValue defaultP = pDef.getDefaultParameterValue(); 179 | if (defaultP != null) { 180 | allParameters.put(defaultP.getName(), defaultP); 181 | } 182 | } else { 183 | String description = Util.fixNull(pDef.getDescription()); 184 | if (pDef instanceof SimpleParameterDefinition && !(pDef instanceof StringParameterDefinition) && !(pDef instanceof ChoiceParameterDefinition)) { 185 | // c.f. https://github.com/jenkinsci/parameterized-trigger-plugin/blob/633587c4b0ae027175c738b3a2f46554a672f330/src/main/java/hudson/plugins/parameterizedtrigger/ProjectSpecificParameterValuesActionTransform.java 186 | ParameterValue pv = allParameters.get(pDef.getName()); 187 | if (pv instanceof StringParameterValue) { 188 | String pDefDisplayName = pDef.getDescriptor().getDisplayName(); 189 | // For classes with semantics similar to ChoiceParameterDefinition, a type mismatch for 190 | // the parameter versus the definition is expected, so we want to do the conversion, but 191 | // not log a warning. 192 | if (!CHOICE_PARAMETER_DEFINITION_LIKE_CLASSES.contains(pDef.getClass().getName())) { 193 | getContext().get(TaskListener.class).getLogger().printf("The parameter '%s' did not have the type expected by %s. Converting to %s.%n", pv.getName(), ModelHyperlinkNote.encodeTo(project), pDefDisplayName); 194 | description = Messages.BuildTriggerStepExecution_convertedParameterDescription(description, pDefDisplayName, getContext().get(Run.class).toString()); 195 | } 196 | ParameterValue convertedValue = ((SimpleParameterDefinition) pDef).createValue((String) pv.getValue()); 197 | allParameters.put(pDef.getName(), convertedValue); 198 | } 199 | } 200 | ParameterValue pv = allParameters.get(pDef.getName()); 201 | if (!pDef.isValid(pv)) { 202 | throw new AbortException("Invalid parameter value: " + pv); 203 | } 204 | 205 | // TODO: Should we try to detect some unconvertible cases and fail here instead of allowing it? 206 | // For example, someone passing BooleanParameterValue for a PasswordParameterDefinition? 207 | 208 | // Get the description of specified parameters here. UI submission of parameters uses formatted description. 209 | allParameters.get(pDef.getName()).setDescription(description); 210 | } 211 | } 212 | } 213 | } 214 | return Lists.newArrayList(allParameters.values()); 215 | } 216 | 217 | @Override 218 | public void stop(@NonNull Throwable cause) throws Exception { 219 | StepContext context = getContext(); 220 | Jenkins jenkins = Jenkins.getInstanceOrNull(); 221 | if (jenkins == null) { 222 | context.onFailure(cause); 223 | return; 224 | } 225 | 226 | boolean interrupted = false; 227 | 228 | Queue q = jenkins.getQueue(); 229 | // if the build is still in the queue, abort it. 230 | // BuildQueueListener will report the failure, so this method shouldn't call getContext().onFailure() 231 | for (Queue.Item i : q.getItems()) { 232 | for (BuildTriggerAction.Trigger trigger : BuildTriggerAction.triggersFor(i)) { 233 | if (trigger.context.equals(context)) { 234 | // Note that it is a little questionable to cancel the queue item in case it has other causes, 235 | // but in the common case that this is the only cause, it is most intuitive to do so. 236 | // The same applies to aborting the actual build once started. 237 | q.cancel(i); 238 | interrupted = true; 239 | } 240 | } 241 | } 242 | 243 | // if there's any in-progress build already, abort that. 244 | // when the build is actually aborted, BuildTriggerListener will take notice and report the failure, 245 | // so this method shouldn't call getContext().onFailure() 246 | for (Computer c : jenkins.getComputers()) { 247 | for (Executor e : c.getExecutors()) { 248 | interrupted |= maybeInterrupt(e, cause, context); 249 | } 250 | for (Executor e : c.getOneOffExecutors()) { 251 | interrupted |= maybeInterrupt(e, cause, context); 252 | } 253 | } 254 | 255 | if (!interrupted) { 256 | super.stop(cause); 257 | } 258 | } 259 | private static boolean maybeInterrupt(Executor e, Throwable cause, StepContext context) { 260 | boolean interrupted = false; 261 | Queue.Executable exec = e.getCurrentExecutable(); 262 | if (exec instanceof Run) { 263 | for (BuildTriggerAction.Trigger trigger : BuildTriggerAction.triggersFor((Run) exec)) { 264 | if (trigger.context.equals(context)) { 265 | e.interrupt(Result.ABORTED, new BuildTriggerCancelledCause(cause)); 266 | trigger.interruption = cause; 267 | try { 268 | ((Run) exec).save(); 269 | } catch (IOException x) { 270 | LOGGER.log(Level.WARNING, "failed to save interrupt cause on " + exec, x); 271 | } 272 | interrupted = true; 273 | } 274 | } 275 | } 276 | return interrupted; 277 | } 278 | 279 | @Override public String getStatus() { 280 | for (Queue.Item i : Queue.getInstance().getItems()) { 281 | for (BuildTriggerAction.Trigger trigger : BuildTriggerAction.triggersFor(i)) { 282 | if (trigger.context.equals(getContext())) { 283 | return "waiting to schedule " + i.task.getFullDisplayName() + "; blocked: " + i.getWhy(); 284 | } 285 | } 286 | } 287 | for (Computer c : Jenkins.get().getComputers()) { 288 | for (Executor e : c.getExecutors()) { 289 | String r = running(e); 290 | if (r != null) { 291 | return r; 292 | } 293 | } 294 | for (Executor e : c.getOneOffExecutors()) { 295 | String r = running(e); 296 | if (r != null) { 297 | return r; 298 | } 299 | } 300 | } 301 | // TODO QueueTaskFuture does not allow us to record the queue item ID 302 | return "unsure what happened to downstream build"; 303 | } 304 | private @CheckForNull String running(@NonNull Executor e) { 305 | Queue.Executable exec = e.getCurrentExecutable(); 306 | if (exec instanceof Run) { 307 | Run run = (Run) exec; 308 | for (BuildTriggerAction.Trigger trigger : BuildTriggerAction.triggersFor(run)) { 309 | if (trigger.context.equals(getContext())) { 310 | return "running " + run; 311 | } 312 | } 313 | } 314 | return null; 315 | } 316 | 317 | private static final long serialVersionUID = 1L; 318 | 319 | } 320 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/workflow/support/steps/build/BuildUpstreamCause.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.workflow.support.steps.build; 2 | 3 | import hudson.model.Cause; 4 | import hudson.model.Run; 5 | import java.util.Objects; 6 | import org.jenkinsci.plugins.workflow.graph.FlowNode; 7 | 8 | /** 9 | * Points back to the triggering {@link FlowNode}. 10 | * 11 | * @see DownstreamBuildAction 12 | */ 13 | public class BuildUpstreamCause extends Cause.UpstreamCause { 14 | private final String nodeId; 15 | 16 | public BuildUpstreamCause(FlowNode node, Run invokingRun) { 17 | super(invokingRun); 18 | this.nodeId = node.getId(); 19 | } 20 | 21 | public String getNodeId() { 22 | return nodeId; 23 | } 24 | 25 | @Override 26 | public boolean equals(Object o) { 27 | if (this == o) return true; 28 | if (o == null || getClass() != o.getClass()) return false; 29 | if (!super.equals(o)) return false; 30 | BuildUpstreamCause that = (BuildUpstreamCause) o; 31 | return Objects.equals(nodeId, that.nodeId); 32 | } 33 | 34 | @Override 35 | public int hashCode() { 36 | return Objects.hash(super.hashCode(), nodeId); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/workflow/support/steps/build/BuildUpstreamNodeAction.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.workflow.support.steps.build; 2 | 3 | import hudson.model.InvisibleAction; 4 | import hudson.model.Run; 5 | import org.jenkinsci.plugins.workflow.graph.FlowNode; 6 | 7 | import java.util.Objects; 8 | 9 | /** 10 | * Attached to newly-created builds in order to point back to the triggering FlowNode. 11 | * 12 | * @see DownstreamBuildAction 13 | * @deprecated Use {@link BuildUpstreamCause} instead 14 | */ 15 | @Deprecated 16 | public class BuildUpstreamNodeAction extends InvisibleAction { 17 | 18 | private final String upstreamNodeId; 19 | private final String upstreamRunId; 20 | 21 | public BuildUpstreamNodeAction(FlowNode node, Run invokingRun) { 22 | this.upstreamNodeId = node.getId(); 23 | this.upstreamRunId = invokingRun.getExternalizableId(); 24 | } 25 | 26 | public String getUpstreamNodeId() { 27 | return upstreamNodeId; 28 | } 29 | 30 | public String getUpstreamRunId() { 31 | return upstreamRunId; 32 | } 33 | 34 | @Override 35 | public boolean equals(Object o) { 36 | if (this == o) { 37 | return true; 38 | } 39 | if (o == null || getClass() != o.getClass()) { 40 | return false; 41 | } 42 | BuildUpstreamNodeAction that = (BuildUpstreamNodeAction) o; 43 | return Objects.equals(upstreamNodeId, that.upstreamNodeId) && 44 | Objects.equals(upstreamRunId, that.upstreamRunId); 45 | } 46 | 47 | @Override 48 | public int hashCode() { 49 | return Objects.hash(upstreamNodeId, upstreamRunId); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/workflow/support/steps/build/DownstreamBuildAction.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.workflow.support.steps.build; 2 | 3 | import edu.umd.cs.findbugs.annotations.CheckForNull; 4 | import edu.umd.cs.findbugs.annotations.NonNull; 5 | import hudson.model.InvisibleAction; 6 | import hudson.model.Item; 7 | import hudson.model.ItemGroup; 8 | import hudson.model.Run; 9 | import java.util.ArrayList; 10 | import java.util.Collections; 11 | import java.util.List; 12 | import org.jenkinsci.plugins.workflow.graph.FlowNode; 13 | import org.springframework.security.access.AccessDeniedException; 14 | 15 | /** 16 | * Tracks downstream builds triggered by the {@code build} step, as well as the {@link FlowNode#getId} of the step. 17 | * 18 | * @see BuildUpstreamCause 19 | */ 20 | public final class DownstreamBuildAction extends InvisibleAction { 21 | private final List downstreamBuilds = new ArrayList<>(); 22 | 23 | public static @NonNull DownstreamBuild getOrCreate(@NonNull Run run, @NonNull String flowNodeId, @NonNull Item job) { 24 | DownstreamBuildAction downstreamBuildAction; 25 | synchronized (DownstreamBuildAction.class) { 26 | downstreamBuildAction = run.getAction(DownstreamBuildAction.class); 27 | if (downstreamBuildAction == null) { 28 | downstreamBuildAction = new DownstreamBuildAction(); 29 | run.addAction(downstreamBuildAction); 30 | } 31 | } 32 | return downstreamBuildAction.getOrAddDownstreamBuild(flowNodeId, job); 33 | } 34 | 35 | public synchronized @NonNull List 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> 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;