├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── cd.yaml │ └── jenkins-security-scan.yml ├── .gitignore ├── .mvn ├── extensions.xml └── maven.config ├── Jenkinsfile ├── README.md ├── pom.xml └── src ├── main ├── java │ └── org │ │ └── jenkinsci │ │ └── plugins │ │ └── workflow │ │ └── multibranch │ │ ├── AbstractWorkflowBranchProjectFactory.java │ │ ├── AbstractWorkflowMultiBranchProjectFactory.java │ │ ├── BranchJobProperty.java │ │ ├── DurabilityHintBranchProperty.java │ │ ├── JobPropertyStep.java │ │ ├── JobPropertyTrackerAction.java │ │ ├── ReadTrustedStep.java │ │ ├── ResolveScmStep.java │ │ ├── SCMBinder.java │ │ ├── SCMVar.java │ │ ├── WorkflowBranchProjectFactory.java │ │ ├── WorkflowMultiBranchProject.java │ │ └── WorkflowMultiBranchProjectFactory.java ├── resources │ ├── index.jelly │ └── org │ │ └── jenkinsci │ │ └── plugins │ │ └── workflow │ │ └── multibranch │ │ ├── DurabilityHintBranchProperty │ │ ├── config.jelly │ │ └── help.html │ │ ├── JobPropertyStep │ │ ├── config.jelly │ │ └── help.html │ │ ├── Messages.properties │ │ ├── Messages_fr.properties │ │ ├── Messages_zh_CN.properties │ │ ├── ReadTrustedStep │ │ ├── config.jelly │ │ ├── help-path.html │ │ └── help.html │ │ ├── ResolveScmStep │ │ ├── config.jelly │ │ ├── help-ignoreErrors.html │ │ ├── help-source.html │ │ ├── help-targets.html │ │ └── help.html │ │ ├── SCMBinder │ │ └── config.jelly │ │ ├── SCMVar │ │ └── help.jelly │ │ ├── WorkflowBranchProjectFactory │ │ ├── config.jelly │ │ ├── getting-started-links.jelly │ │ ├── getting-started.jelly │ │ └── help-scriptPath.html │ │ ├── WorkflowMultiBranchProject │ │ └── newInstanceDetail.jelly │ │ └── WorkflowMultiBranchProjectFactory │ │ ├── config.jelly │ │ ├── getting-started-links.jelly │ │ ├── getting-started.jelly │ │ └── help-scriptPath.html └── webapp │ └── images │ └── pipelinemultibranchproject.svg └── test ├── java └── org │ └── jenkinsci │ └── plugins │ └── workflow │ └── multibranch │ ├── DurabilityHintBranchPropertyWorkflowTest.java │ ├── GitDirectorySCMNavigator.java │ ├── JobPropertyStepTest.java │ ├── NoTriggerBranchPropertyWorkflowTest.java │ ├── ReadTrustedStepTest.java │ ├── RepairBranchPropertyTest.java │ ├── ReplayActionTest.java │ ├── ResolveScmStepTest.java │ ├── SCMBinderTest.java │ ├── SCMVarTest.java │ ├── WorkflowBranchProjectFactoryTest.java │ ├── WorkflowMultiBranchProjectFactoryTest.java │ └── WorkflowMultiBranchProjectTest.java └── resources └── org └── jenkinsci └── plugins └── workflow └── multibranch ├── GitDirectorySCMNavigator └── config.jelly ├── JobPropertyStepTest └── trackerPropertyUpgrade.zip ├── RepairBranchPropertyTest └── removedPropertyAtStartup.zip └── WorkflowMultiBranchProjectTest └── OldSCM └── config.jelly /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @jenkinsci/workflow-multibranch-plugin-developers 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 2 | 3 | version: 2 4 | updates: 5 | - package-ecosystem: "maven" 6 | directory: "/" 7 | schedule: 8 | interval: "weekly" 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | interval: "weekly" 13 | -------------------------------------------------------------------------------- /.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 | 17 | -------------------------------------------------------------------------------- /.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 | workflow-multibranch.iml 4 | .idea -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | buildPlugin( 2 | useContainerAgent: true, 3 | configurations: [ 4 | [platform: 'linux', jdk: 21], 5 | [platform: 'windows', jdk: 17], 6 | ]) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pipeline: Multibranch 2 | 3 | ## Introduction 4 | 5 | Enhances Pipeline plugin to handle branches better by automatically 6 | grouping builds from different branches. 7 | 8 | Automatically creates a new Jenkins job whenever a new branch is pushed 9 | to a source code repository. 10 | Other plugins can define various branch types, e.g. a Git branch, a 11 | Subversion branch, a GitHub Pull Request etc. 12 | 13 | See this blog post for more 14 | info: 15 | 16 | ## Notes 17 | 18 | To determine the branch being built - use the environment variable 19 | `BRANCH_NAME` - e.g. `${env.BRANCH_NAME}` 20 | 21 | ## Version History 22 | 23 | For version 2.23 and beyond, see the [GitHub releases](https://github.com/jenkinsci/workflow-multibranch-plugin/releases) list. 24 | For older versions, see the [archive](https://github.com/jenkinsci/workflow-multibranch-plugin/blob/2e067658d86895c4c22005c4022cca53f65f98c1/CHANGELOG.md). 25 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 25 | 26 | 27 | 4.0.0 28 | 29 | org.jenkins-ci.plugins 30 | plugin 31 | 5.9 32 | 33 | 34 | org.jenkins-ci.plugins.workflow 35 | workflow-multibranch 36 | ${changelist} 37 | hpi 38 | Pipeline: Multibranch 39 | https://github.com/jenkinsci/${project.artifactId}-plugin 40 | 41 | 42 | MIT License 43 | https://opensource.org/licenses/MIT 44 | 45 | 46 | 47 | scm:git:https://github.com/${gitHubRepo}.git 48 | scm:git:git@github.com:${gitHubRepo}.git 49 | https://github.com/${gitHubRepo} 50 | ${scmTag} 51 | 52 | 53 | 54 | repo.jenkins-ci.org 55 | https://repo.jenkins-ci.org/public/ 56 | 57 | 58 | 59 | 60 | repo.jenkins-ci.org 61 | https://repo.jenkins-ci.org/public/ 62 | 63 | 64 | 65 | 999999-SNAPSHOT 66 | 2.479 67 | ${jenkins.baseline}.1 68 | false 69 | jenkinsci/${project.artifactId}-plugin 70 | 71 | 72 | 73 | 74 | io.jenkins.tools.bom 75 | bom-${jenkins.baseline}.x 76 | 4023.va_eeb_b_4e45f07 77 | import 78 | pom 79 | 80 | 81 | 82 | 83 | 84 | org.jenkins-ci.plugins.workflow 85 | workflow-step-api 86 | 87 | 88 | org.jenkins-ci.plugins.workflow 89 | workflow-job 90 | 91 | 92 | org.jenkins-ci.plugins.workflow 93 | workflow-support 94 | 95 | 96 | org.jenkins-ci.plugins.workflow 97 | workflow-cps 98 | 99 | 100 | org.jenkins-ci.plugins 101 | scm-api 102 | 103 | 104 | org.jenkins-ci.plugins 105 | branch-api 106 | 107 | 108 | org.jenkins-ci.plugins 109 | cloudbees-folder 110 | 111 | 112 | org.jenkins-ci.plugins.workflow 113 | workflow-api 114 | 115 | 116 | org.jenkins-ci.plugins.workflow 117 | workflow-scm-step 118 | 119 | 120 | org.jenkins-ci.plugins 121 | script-security 122 | 123 | 124 | io.jenkins.plugins 125 | ionicons-api 126 | 127 | 128 | org.jenkins-ci.plugins 129 | scm-api 130 | tests 131 | test 132 | 133 | 134 | org.jenkins-ci.plugins.workflow 135 | workflow-basic-steps 136 | test 137 | 138 | 139 | org.jenkins-ci.plugins.workflow 140 | workflow-durable-task-step 141 | test 142 | 143 | 144 | io.jenkins.plugins 145 | pipeline-groovy-lib 146 | test 147 | 148 | 149 | org.jenkins-ci.plugins.workflow 150 | workflow-step-api 151 | tests 152 | test 153 | 154 | 155 | org.jenkins-ci.plugins.workflow 156 | workflow-support 157 | tests 158 | test 159 | 160 | 161 | org.jenkins-ci.plugins.workflow 162 | workflow-cps 163 | tests 164 | test 165 | 166 | 167 | org.jenkins-ci.plugins 168 | git 169 | test 170 | 171 | 172 | org.jenkins-ci.plugins 173 | git 174 | tests 175 | test 176 | 177 | 178 | org.jenkins-ci.plugins.workflow 179 | workflow-job 180 | tests 181 | test 182 | 183 | 184 | org.jenkins-ci.plugins 185 | matrix-auth 186 | test 187 | 188 | 189 | org.jenkins-ci.plugins 190 | credentials 191 | test 192 | 193 | 194 | org.jenkins-ci.plugins 195 | mailer 196 | test 197 | 198 | 199 | org.jenkins-ci.plugins 200 | junit 201 | test 202 | 203 | 204 | 205 | 206 | 207 | org.jenkins-ci.tools 208 | maven-hpi-plugin 209 | 210 | 211 | FINE 212 | 213 | 214 | 215 | 216 | 217 | 218 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/workflow/multibranch/AbstractWorkflowBranchProjectFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2016 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.multibranch; 26 | 27 | import edu.umd.cs.findbugs.annotations.NonNull; 28 | import hudson.BulkChange; 29 | import hudson.model.Item; 30 | import java.io.IOException; 31 | import java.util.logging.Level; 32 | import java.util.logging.Logger; 33 | import jenkins.branch.Branch; 34 | import jenkins.branch.BranchProjectFactory; 35 | import jenkins.branch.BranchProjectFactoryDescriptor; 36 | import jenkins.branch.MultiBranchProject; 37 | import jenkins.scm.api.SCMSource; 38 | import jenkins.scm.api.SCMSourceCriteria; 39 | import org.jenkinsci.plugins.workflow.flow.FlowDefinition; 40 | import org.jenkinsci.plugins.workflow.job.WorkflowJob; 41 | import org.jenkinsci.plugins.workflow.job.WorkflowRun; 42 | 43 | /** 44 | * A selectable project factory for multibranch Pipelines. 45 | */ 46 | public abstract class AbstractWorkflowBranchProjectFactory extends BranchProjectFactory { 47 | 48 | private static final Logger LOGGER = Logger.getLogger(AbstractWorkflowBranchProjectFactory.class.getName()); 49 | 50 | protected abstract FlowDefinition createDefinition(); 51 | 52 | protected abstract SCMSourceCriteria getSCMSourceCriteria(SCMSource source); 53 | 54 | @Override public WorkflowJob newInstance(Branch branch) { 55 | WorkflowJob job = new WorkflowJob(getOwner(), branch.getEncodedName()); 56 | setBranch(job, branch); 57 | return job; 58 | } 59 | 60 | @NonNull 61 | @Override public Branch getBranch(@NonNull WorkflowJob project) { 62 | return project.getProperty(BranchJobProperty.class).getBranch(); 63 | } 64 | 65 | @NonNull 66 | @Override public WorkflowJob setBranch(@NonNull WorkflowJob project, @NonNull Branch branch) { 67 | try (BulkChange bc = new BulkChange(project)) { 68 | project.setDefinition(createDefinition()); 69 | BranchJobProperty property = project.getProperty(BranchJobProperty.class); 70 | if (property == null) { 71 | project.addProperty(new BranchJobProperty(branch)); 72 | } else { // TODO may add equality check if https://github.com/jenkinsci/branch-api-plugin/pull/36 or equivalent is implemented 73 | property.setBranch(branch); 74 | project.save(); 75 | } 76 | bc.commit(); 77 | } catch (IOException x) { 78 | LOGGER.log(Level.WARNING, null, x); 79 | } 80 | return project; 81 | } 82 | 83 | @Override public boolean isProject(Item item) { 84 | return item instanceof WorkflowJob && ((WorkflowJob) item).getProperty(BranchJobProperty.class) != null; 85 | } 86 | 87 | protected static abstract class AbstractWorkflowBranchProjectFactoryDescriptor extends BranchProjectFactoryDescriptor { 88 | 89 | @SuppressWarnings("rawtypes") 90 | @Override public boolean isApplicable(Class clazz) { 91 | return WorkflowMultiBranchProject.class.isAssignableFrom(clazz); 92 | } 93 | 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/workflow/multibranch/AbstractWorkflowMultiBranchProjectFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2016 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.multibranch; 26 | 27 | import edu.umd.cs.findbugs.annotations.NonNull; 28 | import hudson.Extension; 29 | import hudson.model.Action; 30 | import hudson.model.Item; 31 | import hudson.model.ItemGroup; 32 | import hudson.model.TaskListener; 33 | import java.io.IOException; 34 | import java.util.Collection; 35 | import java.util.Collections; 36 | import java.util.Map; 37 | import jenkins.branch.MultiBranchProject; 38 | import jenkins.branch.MultiBranchProjectFactory; 39 | import jenkins.branch.OrganizationFolder; 40 | import jenkins.model.TransientActionFactory; 41 | import org.jenkinsci.plugins.workflow.cps.Snippetizer; 42 | 43 | /** 44 | * Defines organization folders by some {@link AbstractWorkflowBranchProjectFactory}. 45 | */ 46 | public abstract class AbstractWorkflowMultiBranchProjectFactory extends MultiBranchProjectFactory.BySCMSourceCriteria { 47 | 48 | @NonNull 49 | @Override protected final WorkflowMultiBranchProject doCreateProject(@NonNull ItemGroup parent, @NonNull String name, @NonNull Map attributes) throws IOException, InterruptedException { 50 | WorkflowMultiBranchProject project = new WorkflowMultiBranchProject(parent, name); 51 | customize(project); 52 | return project; 53 | } 54 | 55 | @Override public final void updateExistingProject(@NonNull MultiBranchProject project, @NonNull Map attributes, @NonNull TaskListener listener) throws IOException, InterruptedException { 56 | if (project instanceof WorkflowMultiBranchProject) { 57 | customize((WorkflowMultiBranchProject) project); 58 | } // otherwise got recognized by something else before, oh well 59 | } 60 | 61 | protected void customize(WorkflowMultiBranchProject project) throws IOException, InterruptedException {} 62 | 63 | @Extension public static class PerFolderAdder extends TransientActionFactory { 64 | 65 | @Override public Class type() { 66 | return OrganizationFolder.class; 67 | } 68 | 69 | @NonNull 70 | @Override public Collection createFor(@NonNull OrganizationFolder target) { 71 | if (target.getProjectFactories().get(AbstractWorkflowMultiBranchProjectFactory.class) != null && target.hasPermission(Item.EXTENDED_READ)) { 72 | return Collections.singleton(new Snippetizer.LocalAction()); 73 | } else { 74 | return Collections.emptySet(); 75 | } 76 | } 77 | 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/workflow/multibranch/BranchJobProperty.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2015 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.multibranch; 26 | 27 | import edu.umd.cs.findbugs.annotations.NonNull; 28 | import hudson.Extension; 29 | import hudson.model.AbstractItem; 30 | import hudson.model.Item; 31 | import hudson.model.JobPropertyDescriptor; 32 | import hudson.security.ACL; 33 | import hudson.security.Permission; 34 | import hudson.util.AlternativeUiTextProvider; 35 | import jenkins.branch.Branch; 36 | import org.springframework.security.core.Authentication; 37 | import org.jenkinsci.plugins.workflow.job.WorkflowJob; 38 | import org.jenkinsci.plugins.workflow.job.WorkflowJobProperty; 39 | import org.kohsuke.stapler.export.Exported; 40 | import org.kohsuke.stapler.export.ExportedBean; 41 | 42 | /** 43 | * Marker for jobs based on a specific branch. 44 | */ 45 | @ExportedBean 46 | public class BranchJobProperty extends WorkflowJobProperty { 47 | 48 | private @NonNull Branch branch; 49 | 50 | BranchJobProperty(@NonNull Branch branch) { 51 | this.branch = branch; 52 | } 53 | 54 | @Exported 55 | public synchronized @NonNull Branch getBranch() { 56 | return branch; 57 | } 58 | 59 | synchronized void setBranch(@NonNull Branch branch) { 60 | branch.getClass(); 61 | this.branch = branch; 62 | } 63 | 64 | @NonNull 65 | @Override public ACL decorateACL(@NonNull final ACL acl) { 66 | return new ACL() { 67 | @Override public boolean hasPermission2(@NonNull Authentication a, @NonNull Permission permission) { 68 | // This project is managed by its parent and may not be directly configured or deleted by users. 69 | // Note that Item.EXTENDED_READ may still be granted, so you can still see Snippet Generator, etc. 70 | if (ACL.SYSTEM2.equals(a)) { 71 | return true; // e.g., ComputedFolder.updateChildren 72 | } else if (permission == Item.CONFIGURE) { 73 | return false; 74 | } else if (permission == Item.DELETE && !(branch instanceof Branch.Dead)) { 75 | // allow early manual clean-up of dead branches 76 | return false; 77 | } else { 78 | return acl.hasPermission2(a, permission); 79 | } 80 | } 81 | }; 82 | } 83 | 84 | @Override public Boolean isBuildable() { 85 | if (branch instanceof Branch.Dead) { 86 | return false; 87 | } 88 | return null; 89 | } 90 | 91 | @Extension public static class DescriptorImpl extends JobPropertyDescriptor { 92 | 93 | @NonNull 94 | @Override public String getDisplayName() { 95 | return "Based on branch"; 96 | } 97 | 98 | } 99 | 100 | @Extension 101 | public static class AlternativeUiTextProviderImpl extends AlternativeUiTextProvider { 102 | 103 | @Override 104 | public String getText(Message text, T context) { 105 | if (text == AbstractItem.PRONOUN && context instanceof WorkflowJob) { 106 | WorkflowJob job = (WorkflowJob) context; 107 | BranchJobProperty property = job.getProperty(BranchJobProperty.class); 108 | if (property != null) { 109 | return property.getBranch().getHead().getPronoun(); 110 | } 111 | } 112 | return null; 113 | } 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/workflow/multibranch/DurabilityHintBranchProperty.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.workflow.multibranch; 2 | 3 | import edu.umd.cs.findbugs.annotations.CheckForNull; 4 | import edu.umd.cs.findbugs.annotations.NonNull; 5 | import hudson.Extension; 6 | import hudson.model.Item; 7 | import hudson.model.Job; 8 | import hudson.model.Run; 9 | import jenkins.branch.BranchProperty; 10 | import jenkins.branch.BranchPropertyDescriptor; 11 | import jenkins.branch.BranchPropertyStrategy; 12 | import jenkins.branch.JobDecorator; 13 | import jenkins.branch.MultiBranchProject; 14 | import jenkins.scm.api.SCMSource; 15 | import org.jenkinsci.plugins.workflow.flow.DurabilityHintProvider; 16 | import org.jenkinsci.plugins.workflow.flow.FlowDurabilityHint; 17 | import org.jenkinsci.plugins.workflow.flow.GlobalDefaultFlowDurabilityLevel; 18 | import org.jenkinsci.plugins.workflow.job.WorkflowJob; 19 | import org.jenkinsci.plugins.workflow.job.properties.DurabilityHintJobProperty; 20 | import org.jenkinsci.Symbol; 21 | import org.kohsuke.accmod.Restricted; 22 | import org.kohsuke.accmod.restrictions.NoExternalUse; 23 | import org.kohsuke.stapler.DataBoundConstructor; 24 | 25 | import java.util.Optional; 26 | 27 | /** 28 | * Branch property so we can define per-branch durability policies, i.e. so feature branches aren't built durably but master is. 29 | * Also lets us set the durability level before the pipeline has run (a step ahead of the "properties" step). 30 | * 31 | * This implementation is designed so that each build will freshly evaluate the {@link FlowDurabilityHint} provided by {@link BranchPropertyStrategy} 32 | * thus sidestepping issues with failing to update along with the BranchPropertyStrategy (JENKINS-48826). 33 | * 34 | * @author Sam Van Oort 35 | */ 36 | @Restricted(NoExternalUse.class) 37 | public class DurabilityHintBranchProperty extends BranchProperty { 38 | 39 | private final FlowDurabilityHint hint; 40 | 41 | public FlowDurabilityHint getHint() { 42 | return hint; 43 | } 44 | 45 | @DataBoundConstructor 46 | public DurabilityHintBranchProperty(@NonNull FlowDurabilityHint hint) { 47 | this.hint = hint; 48 | } 49 | 50 | /** No-op impl because we only care about the actual BranchProperty attached. */ 51 | @Override 52 | public final

, B extends Run> JobDecorator jobDecorator(Class

clazz) { 53 | return null; 54 | } 55 | 56 | @Symbol("durabilityHint") 57 | @Extension 58 | public static class DescriptorImpl extends BranchPropertyDescriptor implements DurabilityHintProvider { 59 | @NonNull 60 | @Override 61 | public String getDisplayName() { 62 | return "Pipeline branch speed/durability override"; 63 | } 64 | 65 | public FlowDurabilityHint[] getDurabilityHintValues() { 66 | return FlowDurabilityHint.values(); 67 | } 68 | 69 | public static FlowDurabilityHint getDefaultDurabilityHint() { 70 | return GlobalDefaultFlowDurabilityLevel.getDefaultDurabilityHint(); 71 | } 72 | 73 | /** Lower ordinal than {@link DurabilityHintJobProperty} so those can override. */ 74 | @Override 75 | public int ordinal() { 76 | return 200; 77 | } 78 | 79 | /** 80 | * Dynamically fetch the property with each build, because the {@link BranchPropertyStrategy} does not re-evaluate. 81 | * @see JENKINS-48826 82 | */ 83 | @CheckForNull 84 | @Override 85 | public FlowDurabilityHint suggestFor(@NonNull Item x) { 86 | // BranchJobProperty *should* be present if it's a child of a MultiBranchProject but we double-check for safety 87 | if (x instanceof WorkflowJob && x.getParent() instanceof MultiBranchProject && ((WorkflowJob)x).getProperty(BranchJobProperty.class) != null) { 88 | MultiBranchProject mp = (MultiBranchProject) x.getParent(); 89 | WorkflowJob job = (WorkflowJob)x; 90 | BranchJobProperty bjp = job.getProperty(BranchJobProperty.class); 91 | 92 | String sourceId = bjp.getBranch().getSourceId(); 93 | if (sourceId != null) { 94 | SCMSource source = mp.getSCMSource(sourceId); 95 | if (source != null) { 96 | BranchPropertyStrategy bps = mp.getBranchPropertyStrategy(source); 97 | if (bps != null) { 98 | Optional props = bps.getPropertiesFor(bjp.getBranch().getHead()).stream().filter( 99 | bp -> bp instanceof DurabilityHintBranchProperty 100 | ).findFirst(); 101 | if (props.isPresent()) { 102 | return ((DurabilityHintBranchProperty)props.get()).getHint(); 103 | } 104 | } 105 | } 106 | } 107 | 108 | } 109 | return null; 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/workflow/multibranch/JobPropertyStep.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2015 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.multibranch; 26 | 27 | import edu.umd.cs.findbugs.annotations.NonNull; 28 | import hudson.AbortException; 29 | import hudson.BulkChange; 30 | import hudson.Extension; 31 | import hudson.ExtensionList; 32 | import hudson.model.Descriptor; 33 | import hudson.model.DescriptorVisibilityFilter; 34 | import hudson.model.Job; 35 | import hudson.model.JobProperty; 36 | import hudson.model.JobPropertyDescriptor; 37 | import hudson.model.Run; 38 | 39 | import java.io.IOException; 40 | import java.util.ArrayList; 41 | import java.util.Collection; 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 | import jenkins.branch.BuildRetentionBranchProperty; 49 | import jenkins.branch.RateLimitBranchProperty; 50 | import jenkins.model.Jenkins; 51 | import net.sf.json.JSONObject; 52 | import org.jenkinsci.plugins.workflow.flow.FlowExecution; 53 | import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner; 54 | import org.jenkinsci.plugins.workflow.graphanalysis.DepthFirstScanner; 55 | import org.jenkinsci.plugins.workflow.graphanalysis.NodeStepTypePredicate; 56 | import org.jenkinsci.plugins.workflow.job.WorkflowJob; 57 | import org.jenkinsci.plugins.workflow.steps.Step; 58 | import org.jenkinsci.plugins.workflow.steps.StepContext; 59 | import org.jenkinsci.plugins.workflow.steps.StepDescriptor; 60 | import org.jenkinsci.plugins.workflow.steps.StepExecution; 61 | import org.jenkinsci.plugins.workflow.steps.SynchronousStepExecution; 62 | import org.kohsuke.accmod.Restricted; 63 | import org.kohsuke.accmod.restrictions.DoNotUse; 64 | import org.kohsuke.stapler.DataBoundConstructor; 65 | import org.kohsuke.stapler.StaplerRequest2; 66 | 67 | /** 68 | * Resets the properties of the current job. 69 | */ 70 | @SuppressWarnings({"unchecked", "rawtypes"}) // TODO JENKINS-26535: cannot bind List> 71 | public class JobPropertyStep extends Step { 72 | 73 | private final List properties; 74 | 75 | @DataBoundConstructor public JobPropertyStep(List properties) { 76 | this.properties = properties; 77 | } 78 | 79 | public List getProperties() { 80 | return properties; 81 | } 82 | 83 | private static final Logger LOGGER = Logger.getLogger(JobPropertyStep.class.getName()); 84 | 85 | public Map getPropertiesMap() { 86 | return Descriptor.toMap((List) properties); 87 | } 88 | 89 | @Override public StepExecution start(StepContext context) throws Exception { 90 | return new Execution(this, context); 91 | } 92 | 93 | public static class Execution extends SynchronousStepExecution { 94 | 95 | private transient final JobPropertyStep step; 96 | 97 | Execution(JobPropertyStep step, StepContext context) { 98 | super(context); 99 | this.step = step; 100 | } 101 | 102 | @SuppressWarnings("unchecked") // untypable 103 | @Override protected Void run() throws Exception { 104 | Run build = getContext().get(Run.class); 105 | Job job = build.getParent(); 106 | 107 | JobPropertyTrackerAction previousAction = job.getAction(JobPropertyTrackerAction.class); 108 | boolean previousHadStep = false; 109 | if (previousAction == null) { 110 | Run previousRun = build.getPreviousCompletedBuild(); 111 | if (previousRun instanceof FlowExecutionOwner.Executable) { 112 | // If the job doesn't have the tracker action but does have a previous completed build,, check to 113 | // see if it ran the properties step. This is to deal with first run after this change is added. 114 | FlowExecutionOwner owner = ((FlowExecutionOwner.Executable) previousRun).asFlowExecutionOwner(); 115 | 116 | if (owner != null) { 117 | try { 118 | FlowExecution execution = owner.get(); 119 | if (execution != null) { 120 | previousHadStep = new DepthFirstScanner().findFirstMatch(execution, 121 | new NodeStepTypePredicate(step.getDescriptor())) != null; 122 | } 123 | } catch (IOException ex) { 124 | // May happen legitimately due to owner.get() throwing IOException when previous execution was nulled 125 | LOGGER.log(Level.FINE, "Could not search for JobPropertyStep execution: previous run either had null execution due to legitimate error and shows as not-yet-started, or threw other IOException", ex); 126 | } 127 | } 128 | } 129 | } 130 | 131 | for (JobProperty prop : step.properties) { 132 | if (!prop.getDescriptor().isApplicable(job.getClass())) { 133 | throw new AbortException("cannot apply " + prop.getDescriptor().getId() + " to a " + job.getClass().getSimpleName()); 134 | } 135 | } 136 | BulkChange bc = new BulkChange(job); 137 | try { 138 | for (JobProperty prop : job.getAllProperties()) { 139 | if (prop instanceof BranchJobProperty) { 140 | // To be safe and avoid breaking everything if there's a corner case, we're explicitly ignoring 141 | // BranchJobProperty to make sure it gets preserved. 142 | continue; 143 | } 144 | // If we have a record of JobPropertys defined via the properties step in the previous run, only 145 | // remove those properties. 146 | if (previousAction != null) { 147 | if (previousAction.getJobPropertyDescriptors().contains(prop.getDescriptor().getId())) { 148 | job.removeProperty(prop); 149 | } 150 | } else if (previousHadStep) { 151 | // If the previous run did not have the tracker action but *did* run the properties step, use 152 | // legacy behavior and remove everything. 153 | job.removeProperty(prop); 154 | } 155 | } 156 | for (JobProperty prop : step.properties) { 157 | job.addProperty(prop); 158 | } 159 | bc.commit(); 160 | job.replaceAction(new JobPropertyTrackerAction(step.properties)); 161 | } finally { 162 | bc.abort(); 163 | } 164 | return null; 165 | } 166 | 167 | private static final long serialVersionUID = 1L; 168 | 169 | } 170 | 171 | @Extension public static class DescriptorImpl extends StepDescriptor { 172 | 173 | @Override public String getFunctionName() { 174 | return "properties"; 175 | } 176 | 177 | @NonNull 178 | @Override public String getDisplayName() { 179 | return "Set job properties"; 180 | } 181 | 182 | @Override public Set> getRequiredContext() { 183 | return Set.of(Run.class); 184 | } 185 | 186 | @Override public Step newInstance(@NonNull StaplerRequest2 req, @NonNull JSONObject formData) throws FormException { 187 | if (req == null) { // should not happen 188 | return super.newInstance((StaplerRequest2) null, formData); 189 | } 190 | // A modified version of RequestImpl.TypePair.convertJSON. 191 | // Works around the fact that Stapler does not call back into Descriptor.newInstance for nested objects (JENKINS-31458); 192 | // and propertiesMap virtual field name; and null values for unselected properties. 193 | List properties = new ArrayList<>(); 194 | ClassLoader cl = req.getStapler().getWebApp().getClassLoader(); 195 | @SuppressWarnings("unchecked") Set> entrySet = formData.getJSONObject("propertiesMap").entrySet(); 196 | for (Map.Entry e : entrySet) { 197 | if (e.getValue() instanceof JSONObject) { 198 | String className = e.getKey().replace('-', '.'); // decode JSON-safe class name escaping 199 | Class itemType; 200 | try { 201 | itemType = cl.loadClass(className).asSubclass(JobProperty.class); 202 | } catch (ClassNotFoundException x) { 203 | throw new FormException(x, "propertiesMap"); 204 | } 205 | JobPropertyDescriptor d = (JobPropertyDescriptor) Jenkins.get().getDescriptorOrDie(itemType); 206 | JSONObject more = (JSONObject) e.getValue(); 207 | JobProperty property = d.newInstance(req, more); 208 | if (property != null) { 209 | properties.add(property); 210 | } 211 | } 212 | } 213 | return new JobPropertyStep(properties); 214 | } 215 | 216 | @Restricted(DoNotUse.class) // f:repeatableHeteroProperty 217 | public Collection> getPropertyDescriptors() { 218 | List> result = new ArrayList<>(); 219 | for (JobPropertyDescriptor p : ExtensionList.lookup(JobPropertyDescriptor.class)) { 220 | if (p.isApplicable(WorkflowJob.class)) { 221 | result.add(p); 222 | } 223 | } 224 | return result; 225 | } 226 | 227 | } 228 | 229 | @Extension public static class HideSuperfluousBranchProperties extends DescriptorVisibilityFilter { 230 | 231 | @Override public boolean filter(Object context, @NonNull Descriptor descriptor) { 232 | if (context instanceof WorkflowMultiBranchProject && (descriptor.clazz == RateLimitBranchProperty.class || descriptor.clazz == BuildRetentionBranchProperty.class)) { 233 | // These are both adequately handled by declarative job properties. 234 | return false; 235 | } 236 | return true; 237 | } 238 | 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/workflow/multibranch/JobPropertyTrackerAction.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.workflow.multibranch; 2 | 3 | import edu.umd.cs.findbugs.annotations.NonNull; 4 | import hudson.model.Descriptor; 5 | import hudson.model.InvisibleAction; 6 | import hudson.model.JobProperty; 7 | 8 | import java.util.Collections; 9 | import java.util.HashSet; 10 | import java.util.List; 11 | import java.util.Set; 12 | 13 | /** 14 | * Invisible action used for tracking what {@link JobProperty}s were defined in the Jenkinsfile in the last run of a 15 | * job. 16 | */ 17 | public class JobPropertyTrackerAction extends InvisibleAction { 18 | /** 19 | * Uses {@link Descriptor#getId()} to identify the {@link JobProperty}s. 20 | */ 21 | private final Set jobPropertyDescriptors = new HashSet<>(); 22 | 23 | public JobPropertyTrackerAction(@NonNull List jobProperties) { 24 | for (JobProperty j : jobProperties) { 25 | jobPropertyDescriptors.add(j.getDescriptor().getId()); 26 | } 27 | } 28 | 29 | public Set getJobPropertyDescriptors() { 30 | return Collections.unmodifiableSet(jobPropertyDescriptors); 31 | } 32 | 33 | @Override 34 | public String toString() { 35 | return "JobPropertyTrackerAction[jobPropertyDescriptors:" + jobPropertyDescriptors + "]"; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/workflow/multibranch/ReadTrustedStep.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2016 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.multibranch; 26 | 27 | import edu.umd.cs.findbugs.annotations.NonNull; 28 | import hudson.AbortException; 29 | import hudson.Extension; 30 | import hudson.FilePath; 31 | import hudson.Functions; 32 | import hudson.model.Computer; 33 | import hudson.model.ItemGroup; 34 | import hudson.model.Job; 35 | import hudson.model.Node; 36 | import hudson.model.Run; 37 | import hudson.model.TaskListener; 38 | import hudson.model.TopLevelItem; 39 | import hudson.scm.SCM; 40 | import hudson.slaves.WorkspaceList; 41 | 42 | import java.io.File; 43 | import java.io.IOException; 44 | import java.util.Set; 45 | import jenkins.branch.Branch; 46 | import jenkins.model.Jenkins; 47 | import jenkins.scm.api.SCMFileSystem; 48 | import jenkins.scm.api.SCMHead; 49 | import jenkins.scm.api.SCMRevision; 50 | import jenkins.scm.api.SCMRevisionAction; 51 | import jenkins.scm.api.SCMSource; 52 | import jenkins.security.HMACConfidentialKey; 53 | import org.jenkinsci.plugins.workflow.cps.CpsScmFlowDefinition; 54 | import org.jenkinsci.plugins.workflow.cps.steps.LoadStepExecution; 55 | import org.jenkinsci.plugins.workflow.flow.FlowDefinition; 56 | import org.jenkinsci.plugins.workflow.job.WorkflowJob; 57 | import org.jenkinsci.plugins.workflow.steps.Step; 58 | import org.jenkinsci.plugins.workflow.steps.StepContext; 59 | import org.jenkinsci.plugins.workflow.steps.StepDescriptor; 60 | import org.jenkinsci.plugins.workflow.steps.StepExecution; 61 | import org.jenkinsci.plugins.workflow.steps.SynchronousNonBlockingStepExecution; 62 | import org.jenkinsci.plugins.workflow.steps.scm.GenericSCMStep; 63 | import org.jenkinsci.plugins.workflow.steps.scm.SCMStep; 64 | import org.kohsuke.stapler.DataBoundConstructor; 65 | 66 | /** 67 | * Replacement for {@code readFile} which reads from the SCM using {@link SCMSource#getTrustedRevision}. 68 | * Refuses to load a file which has been modified in an untrusted revision. 69 | * If run multiple times, always loads from the same revision. 70 | * May be used in combination with {@code evaluate} to delegate to more Pipeline Groovy, as a substitute for {@link SCMBinder}, 71 | * at least until {@link LoadStepExecution} has been split into an abstract part that a {@code loadTrusted} step could extend. 72 | */ 73 | public class ReadTrustedStep extends Step { 74 | 75 | // Intentionally using the same key as CpsScmFlowDefinition. 76 | private static final HMACConfidentialKey CHECKOUT_DIR_KEY = new HMACConfidentialKey(CpsScmFlowDefinition.class, "filePathWithSuffix", 32); 77 | 78 | private final String path; 79 | // TODO encoding 80 | 81 | @DataBoundConstructor public ReadTrustedStep(String path) { 82 | this.path = path; 83 | } 84 | 85 | public String getPath() { 86 | return path; 87 | } 88 | 89 | @Override public StepExecution start(StepContext context) throws Exception { 90 | return new Execution(this, context); 91 | } 92 | 93 | public static class Execution extends SynchronousNonBlockingStepExecution { 94 | 95 | private transient final ReadTrustedStep step; 96 | 97 | Execution(ReadTrustedStep step, StepContext context) { 98 | super(context); 99 | this.step = step; 100 | } 101 | 102 | @Override protected String run() throws Exception { 103 | Run build = getContext().get(Run.class); 104 | TaskListener listener = getContext().get(TaskListener.class); 105 | Job job = build.getParent(); 106 | // Portions adapted from SCMBinder, SCMVar, and CpsScmFlowDefinition: 107 | SCM standaloneSCM = null; 108 | BranchJobProperty property = job.getProperty(BranchJobProperty.class); 109 | if (property == null) { 110 | boolean ok = false; 111 | if (job instanceof WorkflowJob) { 112 | FlowDefinition defn = ((WorkflowJob) job).getDefinition(); 113 | if (defn instanceof CpsScmFlowDefinition) { 114 | // JENKINS-31386: retrofit to work with standalone projects, without doing any trust checks. 115 | standaloneSCM = ((CpsScmFlowDefinition) defn).getScm(); 116 | try (SCMFileSystem fs = SCMBinder.USE_HEAVYWEIGHT_CHECKOUT ? null : SCMFileSystem.of(job, standaloneSCM)) { 117 | if (fs != null) { // JENKINS-33273 118 | try { 119 | String text = fs.child(step.path).contentAsString(); 120 | listener.getLogger().println("Obtained " + step.path + " from " + standaloneSCM.getKey()); 121 | return text; 122 | } catch (IOException | InterruptedException x) { 123 | listener.error("Could not do lightweight checkout, falling back to heavyweight").println(Functions.printThrowable(x).trim()); 124 | } 125 | } else if (!SCMBinder.USE_HEAVYWEIGHT_CHECKOUT) { 126 | listener.getLogger().println("No lightweight checkout support in this SCM configuration"); 127 | } 128 | } 129 | ok = true; 130 | } 131 | } 132 | if (!ok) { // wrong definition or job type 133 | throw new AbortException("‘readTrusted’ is only available when using “" + 134 | Jenkins.get().getDescriptorByType(WorkflowMultiBranchProject.DescriptorImpl.class).getDisplayName() + 135 | "” or “" + Jenkins.get().getDescriptorByType(CpsScmFlowDefinition.DescriptorImpl.class).getDisplayName() + "”"); 136 | } 137 | } 138 | Node node = Jenkins.get(); 139 | FilePath baseWorkspace; 140 | if (job instanceof TopLevelItem) { 141 | baseWorkspace = node.getWorkspaceFor((TopLevelItem) job); 142 | if (baseWorkspace == null) { 143 | throw new AbortException(node.getDisplayName() + " may be offline"); 144 | } 145 | } else { // should not happen, but just in case: 146 | throw new IllegalStateException(job + " was not top level"); 147 | } 148 | Computer computer = node.toComputer(); 149 | if (computer == null) { 150 | throw new IOException(node.getDisplayName() + " may be offline"); 151 | } 152 | if (standaloneSCM != null) { 153 | FilePath dir = getFilePathWithSuffix(baseWorkspace, standaloneSCM); 154 | FilePath file = dir.child(step.path); 155 | try (WorkspaceList.Lease lease = computer.getWorkspaceList().acquire(dir)) { 156 | dir.withSuffix("-scm-key.txt").write(standaloneSCM.getKey(), "UTF-8"); 157 | SCMStep delegate = new GenericSCMStep(standaloneSCM); 158 | delegate.setPoll(true); 159 | delegate.setChangelog(true); 160 | delegate.checkout(build, dir, listener, node.createLauncher(listener)); 161 | if (!isDescendant(file, dir)) { 162 | throw new AbortException(file + " references a file that is not inside " + dir); 163 | } else if (!file.exists()) { 164 | throw new AbortException(file + " not found"); 165 | } 166 | return file.readToString(); 167 | } 168 | } 169 | Branch branch = property.getBranch(); 170 | ItemGroup parent = job.getParent(); 171 | if (!(parent instanceof WorkflowMultiBranchProject)) { 172 | throw new IllegalStateException("inappropriate context"); 173 | } 174 | SCMSource scmSource = ((WorkflowMultiBranchProject) parent).getSCMSource(branch.getSourceId()); 175 | if (scmSource == null) { 176 | throw new IllegalStateException(branch.getSourceId() + " not found"); 177 | } 178 | SCMHead head = branch.getHead(); 179 | SCMRevision tip; 180 | SCMRevisionAction action = build.getAction(SCMRevisionAction.class); 181 | if (action != null) { 182 | tip = action.getRevision(); 183 | } else { 184 | tip = scmSource.fetch(head, listener); 185 | if (tip == null) { 186 | throw new AbortException("Could not determine exact tip revision of " + branch.getName()); 187 | } 188 | build.addAction(new SCMRevisionAction(scmSource, tip)); 189 | } 190 | SCMRevision trusted = scmSource.getTrustedRevision(tip, listener); 191 | boolean trustCheck = !tip.equals(trusted); 192 | String untrustedFile = null; 193 | String content; 194 | try (SCMFileSystem tipFS = trustCheck && !SCMBinder.USE_HEAVYWEIGHT_CHECKOUT ? SCMFileSystem.of(scmSource, head, tip) : null; 195 | SCMFileSystem trustedFS = SCMBinder.USE_HEAVYWEIGHT_CHECKOUT ? null : SCMFileSystem.of(scmSource, head, trusted)) { 196 | if (trustedFS != null && (!trustCheck || tipFS != null)) { 197 | if (trustCheck) { 198 | untrustedFile = tipFS.child(step.path).contentAsString(); 199 | } 200 | content = trustedFS.child(step.path).contentAsString(); 201 | listener.getLogger().println("Obtained " + step.path + " from " + trusted); 202 | } else { 203 | listener.getLogger().println("Checking out " + head.getName() + " to read " + step.path); 204 | SCM trustedScm = scmSource.build(head, trusted); 205 | FilePath dir = getFilePathWithSuffix(baseWorkspace, trustedScm); 206 | FilePath file = dir.child(step.path); 207 | try (WorkspaceList.Lease lease = computer.getWorkspaceList().acquire(dir)) { 208 | dir.withSuffix("-scm-key.txt").write(trustedScm.getKey(), "UTF-8"); 209 | if (trustCheck) { 210 | SCMStep delegate = new GenericSCMStep(scmSource.build(head, tip)); 211 | delegate.setPoll(false); 212 | delegate.setChangelog(false); 213 | delegate.checkout(build, dir, listener, node.createLauncher(listener)); 214 | if (!isDescendant(file, dir)) { 215 | throw new AbortException(file + " references a file that is not inside " + dir); 216 | } else if (!file.exists()) { 217 | throw new AbortException(file + " not found"); 218 | } 219 | untrustedFile = file.readToString(); 220 | } 221 | SCMStep delegate = new GenericSCMStep(trustedScm); 222 | delegate.setPoll(true); 223 | delegate.setChangelog(true); 224 | delegate.checkout(build, dir, listener, node.createLauncher(listener)); 225 | if (!isDescendant(file, dir)) { 226 | throw new AbortException(file + " references a file that is not inside " + dir); 227 | } else if (!file.exists()) { 228 | throw new AbortException(file + " not found"); 229 | } 230 | content = file.readToString(); 231 | } 232 | } 233 | } 234 | if (trustCheck && !untrustedFile.equals(content)) { 235 | throw new AbortException(Messages.ReadTrustedStep__has_been_modified_in_an_untrusted_revis(step.path)); 236 | } 237 | return content; 238 | } 239 | 240 | private FilePath getFilePathWithSuffix(FilePath baseWorkspace, SCM scm) { 241 | return baseWorkspace.withSuffix(getFilePathSuffix() + "script").child(CHECKOUT_DIR_KEY.mac(scm.getKey())); 242 | } 243 | 244 | private String getFilePathSuffix() { 245 | return System.getProperty(WorkspaceList.class.getName(), "@"); 246 | } 247 | 248 | /** 249 | * Checks whether a given child path is a descendent of a given parent path using {@link File#getCanonicalFile}. 250 | * 251 | * If the child path does not exist, this method will canonicalize path elements such as {@code /../} and 252 | * {@code /./} before comparing it to the parent path, and it will not throw an exception. If the child path 253 | * does exist, symlinks will be resolved before checking whether the child is a descendant of the parent path. 254 | */ 255 | private static boolean isDescendant(FilePath child, FilePath parent) throws IOException, InterruptedException { 256 | if (child.isRemote() || parent.isRemote()) { 257 | throw new IllegalStateException(); 258 | } 259 | return new File(child.getRemote()).getCanonicalFile().toPath().startsWith(new File(parent.getRemote()).getCanonicalPath()); 260 | } 261 | 262 | private static final long serialVersionUID = 1L; 263 | 264 | } 265 | 266 | @Extension public static class DescriptorImpl extends StepDescriptor { 267 | 268 | @Override public String getFunctionName() { 269 | return "readTrusted"; 270 | } 271 | 272 | @NonNull 273 | @Override public String getDisplayName() { 274 | return "Read trusted file from SCM"; 275 | } 276 | 277 | @Override public Set> getRequiredContext() { 278 | return Set.of(Run.class, TaskListener.class); 279 | } 280 | 281 | } 282 | 283 | } 284 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/workflow/multibranch/ResolveScmStep.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2017 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 | 26 | package org.jenkinsci.plugins.workflow.multibranch; 27 | 28 | import edu.umd.cs.findbugs.annotations.CheckForNull; 29 | import edu.umd.cs.findbugs.annotations.NonNull; 30 | import hudson.AbortException; 31 | import hudson.Extension; 32 | import hudson.model.Descriptor; 33 | import hudson.model.TaskListener; 34 | import hudson.scm.SCM; 35 | import hudson.util.FormValidation; 36 | import java.io.PrintStream; 37 | import java.util.ArrayList; 38 | import java.util.Collections; 39 | import java.util.LinkedHashMap; 40 | import java.util.List; 41 | import java.util.Map; 42 | import java.util.Set; 43 | import jenkins.scm.api.SCMHead; 44 | import jenkins.scm.api.SCMHeadObserver; 45 | import jenkins.scm.api.SCMRevision; 46 | import jenkins.scm.api.SCMSource; 47 | import net.sf.json.JSONArray; 48 | import net.sf.json.JSONObject; 49 | import org.apache.commons.lang.StringUtils; 50 | import org.jenkinsci.plugins.workflow.steps.Step; 51 | import org.jenkinsci.plugins.workflow.steps.StepContext; 52 | import org.jenkinsci.plugins.workflow.steps.StepDescriptor; 53 | import org.jenkinsci.plugins.workflow.steps.StepExecution; 54 | import org.jenkinsci.plugins.workflow.steps.SynchronousStepExecution; 55 | import org.kohsuke.stapler.DataBoundConstructor; 56 | import org.kohsuke.stapler.DataBoundSetter; 57 | import org.kohsuke.stapler.QueryParameter; 58 | import org.kohsuke.stapler.StaplerRequest2; 59 | 60 | /** 61 | * Resolves an {@link SCM} from a {@link SCMSource} using a priority list of target branch names. 62 | * 63 | * @since 2.10 64 | */ 65 | public class ResolveScmStep extends Step { 66 | 67 | /** 68 | * The {@link SCMSource} 69 | */ 70 | @NonNull 71 | private final SCMSource source; 72 | 73 | /** 74 | * The {@link SCMSource} 75 | */ 76 | @NonNull 77 | private final List targets; 78 | 79 | /** 80 | * If {@code true} then {@code null} will be returned in the event that none of the target branch names can be 81 | * resolved. 82 | */ 83 | private boolean ignoreErrors; 84 | 85 | /** 86 | * Constructor. 87 | * 88 | * @param source The {@link SCMSource} 89 | * @param targets The {@link SCMSource} 90 | */ 91 | @DataBoundConstructor 92 | public ResolveScmStep(@NonNull SCMSource source, @NonNull List targets) { 93 | this.source = source; 94 | this.targets = new ArrayList<>(targets); 95 | } 96 | 97 | /** 98 | * Gets the {@link SCMSource} to resolve from. 99 | * 100 | * @return the {@link SCMSource} to resolve from. 101 | */ 102 | @NonNull 103 | public SCMSource getSource() { 104 | return source; 105 | } 106 | 107 | /** 108 | * Gets the {@link SCMHead} names to try and resolve. 109 | * 110 | * @return the {@link SCMHead} names to try and resolve. 111 | */ 112 | @NonNull 113 | public List getTargets() { 114 | return Collections.unmodifiableList(targets); 115 | } 116 | 117 | /** 118 | * Returns {@code true} if and only if errors will be ignored. 119 | * 120 | * @return {@code true} if and only if errors will be ignored. 121 | */ 122 | public boolean isIgnoreErrors() { 123 | return ignoreErrors; 124 | } 125 | 126 | /** 127 | * Sets the error handling behaviour. 128 | * 129 | * @param ignoreErrors {@code true} if and only if errors will be ignored. 130 | */ 131 | @DataBoundSetter 132 | public void setIgnoreErrors(boolean ignoreErrors) { 133 | this.ignoreErrors = ignoreErrors; 134 | } 135 | 136 | /** 137 | * {@inheritDoc} 138 | */ 139 | @Override 140 | public StepExecution start(StepContext context) throws Exception { 141 | return new Execution(context, this); 142 | } 143 | 144 | /** 145 | * {@inheritDoc} 146 | */ 147 | @Override 148 | public String toString() { 149 | return "ResolveScmStep{" + 150 | "source=" + source + 151 | ", targets=" + targets + 152 | ", ignoreErrors=" + ignoreErrors + 153 | '}'; 154 | } 155 | 156 | /** 157 | * Our {@link Descriptor}. 158 | */ 159 | @Extension 160 | public static class DescriptorImpl extends StepDescriptor { 161 | 162 | /** 163 | * {@inheritDoc} 164 | */ 165 | @Override 166 | public Set> getRequiredContext() { 167 | return Collections.singleton(TaskListener.class); 168 | } 169 | 170 | /** 171 | * {@inheritDoc} 172 | */ 173 | @Override 174 | public String getFunctionName() { 175 | return "resolveScm"; 176 | } 177 | 178 | /** 179 | * {@inheritDoc} 180 | */ 181 | @NonNull 182 | @Override 183 | public String getDisplayName() { 184 | return "Resolves an SCM from an SCM Source and a list of candidate target branch names"; 185 | } 186 | 187 | @Override 188 | public Step newInstance(@CheckForNull StaplerRequest2 req, @NonNull JSONObject formData) 189 | throws FormException { 190 | assert req != null : "see contract for method, it's never null but has to claim it could be"; 191 | // roll our own because we want the groovy api to be easier than the jelly form binding would have us 192 | JSONObject src = formData.getJSONObject("source"); 193 | src.put("id", "_"); 194 | SCMSource source = req.bindJSON(SCMSource.class, src); 195 | List targets = new ArrayList<>(); 196 | // TODO JENKINS-27901 use standard control when available 197 | Object t = formData.get("targets"); 198 | if (t instanceof JSONObject) { 199 | JSONObject o = (JSONObject) t; 200 | targets.add(o.getString("target")); 201 | } else if (t instanceof JSONArray) { 202 | JSONArray a = (JSONArray) t; 203 | for (int i = 0; i < a.size(); i++) { 204 | JSONObject o = a.getJSONObject(i); 205 | targets.add(o.getString("target")); 206 | } 207 | } 208 | ResolveScmStep step = new ResolveScmStep(source, targets); 209 | if (formData.optBoolean("ignoreErrors", false)) { 210 | step.setIgnoreErrors(true); 211 | } 212 | return step; 213 | } 214 | 215 | public FormValidation doCheckTarget(@QueryParameter String value) { 216 | if (StringUtils.isNotBlank(value)) { 217 | return FormValidation.ok(); 218 | } 219 | return FormValidation.error("You must supply a target branch name to resolve"); 220 | } 221 | } 222 | 223 | /** 224 | * Our {@link StepExecution}. 225 | */ 226 | public static class Execution extends SynchronousStepExecution { 227 | 228 | /** 229 | * Ensure consistent serialization. 230 | */ 231 | private static final long serialVersionUID = 1L; 232 | 233 | /** 234 | * The {@link SCMSource} 235 | */ 236 | @NonNull 237 | private transient final SCMSource source; 238 | 239 | /** 240 | * The {@link SCMSource} 241 | */ 242 | @NonNull 243 | private final List targets; 244 | 245 | /** 246 | * If {@code true} then {@code null} will be returned in the event that none of the target branch names can be 247 | * resolved. 248 | */ 249 | private boolean ignoreErrors; 250 | 251 | /** 252 | * Our constructor. 253 | * 254 | * @param context the context. 255 | * @param step the step. 256 | */ 257 | Execution(StepContext context, ResolveScmStep step) { 258 | super(context); 259 | this.source = step.getSource(); 260 | this.targets = new ArrayList<>(step.getTargets()); 261 | this.ignoreErrors = step.isIgnoreErrors(); 262 | } 263 | 264 | /** 265 | * {@inheritDoc} 266 | */ 267 | @Override 268 | protected SCM run() throws Exception { 269 | StepContext context = getContext(); 270 | TaskListener listener = context.get(TaskListener.class); 271 | assert listener != null; 272 | PrintStream out = listener.getLogger(); 273 | out.printf("Checking for first existing branch from %s...%n", targets); 274 | SCMRevision fetch = source.fetch(new ObserverImpl(targets), listener).result(); 275 | if (fetch == null) { 276 | if (ignoreErrors) { 277 | out.println("Could not find any matching branch"); 278 | return null; 279 | } 280 | throw new AbortException("Could not find any matching branch"); 281 | } 282 | out.printf("Found %s at revision %s%n", fetch.getHead().getName(), fetch); 283 | return source.build(fetch.getHead(), fetch); 284 | } 285 | 286 | } 287 | 288 | /** 289 | * An observer that collects the {@link SCMRevision} of a named {@link SCMHead} from a list of priority 290 | * candidates and stops observing when the preferred candidate is found. 291 | */ 292 | private static class ObserverImpl extends SCMHeadObserver { 293 | /** 294 | * The heads we are looking for 295 | */ 296 | private final Map revision = new LinkedHashMap<>(); 297 | 298 | /** 299 | * Constructor. 300 | * 301 | * @param heads the {@link SCMHead#getName()} to get the {@link SCMRevision} of. 302 | */ 303 | public ObserverImpl(@NonNull List heads) { 304 | heads.getClass(); // fail fast if null 305 | for (String head : heads) { 306 | if (StringUtils.isNotBlank(head)) { 307 | revision.put(head, null); 308 | } 309 | } 310 | } 311 | 312 | /** 313 | * Returns the result. 314 | * 315 | * @return the result. 316 | */ 317 | @CheckForNull 318 | public SCMRevision result() { 319 | for (SCMRevision r : revision.values()) { 320 | if (r != null) { 321 | return r; 322 | } 323 | } 324 | return null; 325 | } 326 | 327 | /** 328 | * {@inheritDoc} 329 | */ 330 | @Override 331 | public void observe(@NonNull SCMHead head, @NonNull SCMRevision revision) { 332 | if (this.revision.containsKey(head.getName())) { 333 | this.revision.put(head.getName(), revision); 334 | } 335 | } 336 | 337 | /** 338 | * {@inheritDoc} 339 | */ 340 | @Override 341 | public boolean isObserving() { 342 | return revision.values().iterator().next() == null; 343 | } 344 | 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/workflow/multibranch/SCMBinder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2015 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.multibranch; 26 | 27 | import edu.umd.cs.findbugs.annotations.NonNull; 28 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 29 | import hudson.Extension; 30 | import hudson.Functions; 31 | import hudson.MarkupText; 32 | import hudson.console.ConsoleAnnotationDescriptor; 33 | import hudson.console.ConsoleAnnotator; 34 | import hudson.console.ConsoleNote; 35 | import hudson.model.Action; 36 | import hudson.model.Descriptor; 37 | import hudson.model.DescriptorVisibilityFilter; 38 | import hudson.model.ItemGroup; 39 | import hudson.model.Queue; 40 | import hudson.model.TaskListener; 41 | import hudson.scm.SCM; 42 | import java.io.IOException; 43 | import java.util.List; 44 | import jenkins.branch.Branch; 45 | import jenkins.scm.api.SCMFileSystem; 46 | import jenkins.scm.api.SCMHead; 47 | import jenkins.scm.api.SCMRevision; 48 | import jenkins.scm.api.SCMRevisionAction; 49 | import jenkins.scm.api.SCMSource; 50 | import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; 51 | import org.jenkinsci.plugins.workflow.cps.CpsScmFlowDefinition; 52 | import org.jenkinsci.plugins.workflow.flow.FlowDefinition; 53 | import org.jenkinsci.plugins.workflow.flow.FlowDefinitionDescriptor; 54 | import org.jenkinsci.plugins.workflow.flow.FlowExecution; 55 | import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner; 56 | import org.jenkinsci.plugins.workflow.job.WorkflowJob; 57 | import org.jenkinsci.plugins.workflow.job.WorkflowRun; 58 | import org.kohsuke.accmod.Restricted; 59 | import org.kohsuke.accmod.restrictions.NoExternalUse; 60 | 61 | /** 62 | * Checks out the desired version of the script referred to by scriptPath. 63 | */ 64 | @Restricted(NoExternalUse.class) // just tests 65 | public class SCMBinder extends FlowDefinition { 66 | 67 | /** Kill switch for JENKINS-33273 in case of problems. */ 68 | @SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "Just for scripting.") 69 | public static boolean USE_HEAVYWEIGHT_CHECKOUT = Boolean.getBoolean(SCMBinder.class.getName() + ".USE_HEAVYWEIGHT_CHECKOUT"); // TODO 2.4+ use SystemProperties 70 | private String scriptPath = WorkflowBranchProjectFactory.SCRIPT; 71 | 72 | public Object readResolve() { 73 | if (this.scriptPath == null) { 74 | this.scriptPath = WorkflowBranchProjectFactory.SCRIPT; 75 | } 76 | return this; 77 | } 78 | 79 | public SCMBinder(String scriptPath) { 80 | this.scriptPath = scriptPath; 81 | } 82 | 83 | @Override public FlowExecution create(FlowExecutionOwner handle, TaskListener listener, List actions) throws Exception { 84 | Queue.Executable exec = handle.getExecutable(); 85 | if (!(exec instanceof WorkflowRun)) { 86 | throw new IllegalStateException("inappropriate context"); 87 | } 88 | WorkflowRun build = (WorkflowRun) exec; 89 | WorkflowJob job = build.getParent(); 90 | BranchJobProperty property = job.getProperty(BranchJobProperty.class); 91 | if (property == null) { 92 | throw new IllegalStateException("inappropriate context"); 93 | } 94 | Branch branch = property.getBranch(); 95 | ItemGroup parent = job.getParent(); 96 | if (!(parent instanceof WorkflowMultiBranchProject)) { 97 | throw new IllegalStateException("inappropriate context"); 98 | } 99 | SCMSource scmSource = ((WorkflowMultiBranchProject) parent).getSCMSource(branch.getSourceId()); 100 | if (scmSource == null) { 101 | throw new IllegalStateException(branch.getSourceId() + " not found"); 102 | } 103 | SCMHead head = branch.getHead(); 104 | SCMRevision tip = scmSource.fetch(head, listener); 105 | SCM scm; 106 | if (tip != null) { 107 | build.addAction(new SCMRevisionAction(scmSource, tip)); 108 | SCMRevision rev = scmSource.getTrustedRevision(tip, listener); 109 | try (SCMFileSystem fs = USE_HEAVYWEIGHT_CHECKOUT ? null : SCMFileSystem.of(scmSource, head, rev)) { 110 | if (fs != null) { // JENKINS-33273 111 | String script = null; 112 | try { 113 | script = fs.child(scriptPath).contentAsString(); 114 | listener.getLogger().println("Obtained " + scriptPath + " from " + rev); 115 | } catch (IOException | InterruptedException x) { 116 | listener.error("Could not do lightweight checkout, falling back to heavyweight").println(Functions.printThrowable(x).trim()); 117 | } 118 | if (script != null) { 119 | if (!rev.equals(tip)) { 120 | // Print a warning in builds where an untrusted contributor has tried to edit Jenkinsfile. 121 | // If we fail to check this (e.g., due to heavyweight checkout), a warning will still be printed to the log 122 | // by the SCM, but that is less apparent. 123 | SCMFileSystem tipFS = SCMFileSystem.of(scmSource, head, tip); 124 | if (tipFS != null) { 125 | String tipScript = null; 126 | try { 127 | tipScript = tipFS.child(scriptPath).contentAsString(); 128 | } catch (IOException | InterruptedException x) { 129 | listener.error("Could not compare lightweight checkout of trusted revision").println(Functions.printThrowable(x).trim()); 130 | } 131 | if (tipScript != null && !script.equals(tipScript)) { 132 | listener.annotate(new WarningNote()); 133 | listener.getLogger().println(Messages.ReadTrustedStep__has_been_modified_in_an_untrusted_revis(scriptPath)); 134 | // TODO JENKINS-45970 consider aborting instead, at least optionally 135 | } 136 | } 137 | } 138 | return new CpsFlowDefinition(script, true).create(handle, listener, actions); 139 | } 140 | } 141 | } 142 | scm = scmSource.build(head, rev); 143 | } else { 144 | listener.error("Could not determine exact tip revision of " + branch.getName() + "; falling back to nondeterministic checkout"); 145 | // Build might fail later anyway, but reason should become clear: for example, branch was deleted before indexing could run. 146 | scm = branch.getScm(); 147 | } 148 | return new CpsScmFlowDefinition(scm, scriptPath).create(handle, listener, actions); 149 | } 150 | 151 | @Extension public static class DescriptorImpl extends FlowDefinitionDescriptor { 152 | 153 | @NonNull 154 | @Override public String getDisplayName() { 155 | return "Pipeline from multibranch configuration"; 156 | } 157 | 158 | } 159 | 160 | /** Want to display this in the r/o configuration for a branch project, but not offer it on standalone jobs or in any other context. */ 161 | @Extension public static class HideMeElsewhere extends DescriptorVisibilityFilter { 162 | 163 | @SuppressWarnings("rawtypes") 164 | @Override public boolean filter(Object context, @NonNull Descriptor descriptor) { 165 | if (descriptor instanceof DescriptorImpl) { 166 | return context instanceof WorkflowJob && ((WorkflowJob) context).getParent() instanceof WorkflowMultiBranchProject; 167 | } 168 | return true; 169 | } 170 | 171 | } 172 | 173 | // TODO seems there is no general-purpose ConsoleNote which simply wraps markup in specified HTML 174 | @SuppressWarnings("rawtypes") 175 | public static class WarningNote extends ConsoleNote { 176 | 177 | @Override public ConsoleAnnotator annotate(Object context, MarkupText text, int charPos) { 178 | text.addMarkup(0, text.length(), "", ""); 179 | return null; 180 | } 181 | 182 | @Extension public static final class DescriptorImpl extends ConsoleAnnotationDescriptor { 183 | @NonNull 184 | @Override public String getDisplayName() { 185 | return "Multibranch warnings"; 186 | } 187 | } 188 | 189 | } 190 | 191 | } 192 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/workflow/multibranch/SCMVar.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2015 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.multibranch; 26 | 27 | import com.thoughtworks.xstream.converters.Converter; 28 | import edu.umd.cs.findbugs.annotations.NonNull; 29 | import hudson.AbortException; 30 | import hudson.Extension; 31 | import hudson.model.ItemGroup; 32 | import hudson.model.Job; 33 | import hudson.model.Run; 34 | import hudson.model.TaskListener; 35 | import hudson.scm.SCM; 36 | import hudson.util.DescribableList; 37 | import java.io.Serializable; 38 | import jenkins.branch.Branch; 39 | import jenkins.model.Jenkins; 40 | import jenkins.scm.api.SCMHead; 41 | import jenkins.scm.api.SCMRevision; 42 | import jenkins.scm.api.SCMRevisionAction; 43 | import jenkins.scm.api.SCMSource; 44 | import org.jenkinsci.plugins.workflow.cps.CpsScmFlowDefinition; 45 | import org.jenkinsci.plugins.workflow.cps.CpsScript; 46 | import org.jenkinsci.plugins.workflow.cps.GlobalVariable; 47 | import org.jenkinsci.plugins.workflow.flow.FlowDefinition; 48 | import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner; 49 | import org.jenkinsci.plugins.workflow.job.WorkflowJob; 50 | import org.jenkinsci.plugins.workflow.job.WorkflowRun; 51 | import org.jenkinsci.plugins.workflow.pickles.Pickle; 52 | import org.jenkinsci.plugins.workflow.support.pickles.SingleTypedPickleFactory; 53 | import org.jenkinsci.plugins.workflow.support.pickles.XStreamPickle; 54 | 55 | /** 56 | * Adds an {@code scm} global variable to the script. 57 | * This makes it possible to run {@code checkout scm} to get your project sources in the right branch. 58 | */ 59 | @Extension public class SCMVar extends GlobalVariable { 60 | 61 | @NonNull 62 | @Override public String getName() { 63 | return "scm"; 64 | } 65 | 66 | @NonNull 67 | @Override public SCM getValue(@NonNull CpsScript script) throws Exception { 68 | Run build = script.$build(); 69 | // TODO some code overlap with SCMBinder.create, but not obvious how to factor out common parts 70 | if (!(build instanceof WorkflowRun)) { 71 | throw new AbortException("‘checkout scm’ is not available outside a Pipeline build"); 72 | } 73 | Job job = build.getParent(); 74 | BranchJobProperty property = job.getProperty(BranchJobProperty.class); 75 | if (property == null) { 76 | if (job instanceof WorkflowJob) { 77 | FlowDefinition defn = ((WorkflowJob) job).getDefinition(); 78 | if (defn instanceof CpsScmFlowDefinition) { 79 | // JENKINS-31386: retrofit to work with standalone projects, minus the exact revision support. 80 | return ((CpsScmFlowDefinition) defn).getScm(); 81 | } 82 | } 83 | throw new AbortException("‘checkout scm’ is only available when using “" + 84 | Jenkins.get().getDescriptorByType(WorkflowMultiBranchProject.DescriptorImpl.class).getDisplayName() + 85 | "” or “" + Jenkins.get().getDescriptorByType(CpsScmFlowDefinition.DescriptorImpl.class).getDisplayName() + "”"); 86 | } 87 | Branch branch = property.getBranch(); 88 | ItemGroup parent = job.getParent(); 89 | if (!(parent instanceof WorkflowMultiBranchProject)) { 90 | throw new IllegalStateException("inappropriate context"); 91 | } 92 | SCMSource scmSource = ((WorkflowMultiBranchProject) parent).getSCMSource(branch.getSourceId()); 93 | if (scmSource == null) { 94 | throw new IllegalStateException(branch.getSourceId() + " not found"); 95 | } 96 | SCMRevision tip; 97 | SCMRevisionAction revisionAction = build.getAction(SCMRevisionAction.class); 98 | if (revisionAction != null) { 99 | tip = revisionAction.getRevision(); 100 | } else { 101 | SCMHead head = branch.getHead(); 102 | FlowExecutionOwner owner = ((WorkflowRun) build).asFlowExecutionOwner(); 103 | TaskListener listener = owner.getListener(); 104 | tip = scmSource.fetch(head, listener); 105 | if (tip == null) { 106 | throw new AbortException("Could not determine exact tip revision of " + branch.getName()); 107 | } 108 | revisionAction = new SCMRevisionAction(scmSource, tip); 109 | build.addAction(revisionAction); 110 | } 111 | return scmSource.build(branch.getHead(), tip); 112 | } 113 | 114 | /** 115 | * Ensures that {@code scm} is saved in its XML representation. 116 | * Necessary for {@code GitSCM} which is marked {@link Serializable} 117 | * yet includes a {@link DescribableList} which relies on a custom {@link Converter}. 118 | * Note that a script which merely calls {@code checkout scm}, even after a restart, does not rely on this; 119 | * but one which saves {@code scm} somewhere and uses it later would. 120 | */ 121 | @Extension public static class Pickler extends SingleTypedPickleFactory { 122 | 123 | @NonNull 124 | @Override protected Pickle pickle(@NonNull SCM scm) { 125 | return new XStreamPickle(scm); 126 | } 127 | 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/workflow/multibranch/WorkflowBranchProjectFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2015 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.multibranch; 26 | 27 | import edu.umd.cs.findbugs.annotations.NonNull; 28 | import hudson.Extension; 29 | import hudson.model.TaskListener; 30 | import java.io.IOException; 31 | import jenkins.scm.api.SCMProbeStat; 32 | import jenkins.scm.api.SCMSource; 33 | import jenkins.scm.api.SCMSourceCriteria; 34 | import org.apache.commons.lang.StringUtils; 35 | import org.jenkinsci.plugins.workflow.flow.FlowDefinition; 36 | import org.kohsuke.stapler.DataBoundConstructor; 37 | import org.kohsuke.stapler.DataBoundSetter; 38 | 39 | /** 40 | * Recognizes and builds {@code Jenkinsfile}. 41 | */ 42 | public class WorkflowBranchProjectFactory extends AbstractWorkflowBranchProjectFactory { 43 | static final String SCRIPT = "Jenkinsfile"; 44 | private String scriptPath = SCRIPT; 45 | 46 | public Object readResolve() { 47 | if (this.scriptPath == null) { 48 | this.scriptPath = WorkflowBranchProjectFactory.SCRIPT; 49 | } 50 | return this; 51 | } 52 | 53 | @DataBoundConstructor public WorkflowBranchProjectFactory() { } 54 | 55 | @DataBoundSetter 56 | public void setScriptPath(String scriptPath) { 57 | if (StringUtils.isEmpty(scriptPath)) { 58 | this.scriptPath = SCRIPT; 59 | } else { 60 | this.scriptPath = scriptPath; 61 | } 62 | } 63 | 64 | public String getScriptPath(){ 65 | return scriptPath; 66 | } 67 | 68 | @Override protected FlowDefinition createDefinition() { 69 | return new SCMBinder(scriptPath); 70 | } 71 | 72 | @Override protected SCMSourceCriteria getSCMSourceCriteria(SCMSource source) { 73 | return new SCMSourceCriteria() { 74 | @Override public boolean isHead(@NonNull SCMSourceCriteria.Probe probe, @NonNull TaskListener listener) throws IOException { 75 | SCMProbeStat stat = probe.stat(scriptPath); 76 | switch (stat.getType()) { 77 | case NONEXISTENT: 78 | if (stat.getAlternativePath() != null) { 79 | listener.getLogger().format(" ‘%s’ not found (but found ‘%s’, search is case sensitive)%n", scriptPath, stat.getAlternativePath()); 80 | } else { 81 | listener.getLogger().format(" ‘%s’ not found%n", scriptPath); 82 | } 83 | return false; 84 | case DIRECTORY: 85 | listener.getLogger().format(" ‘%s’ found but is a directory not a file%n", scriptPath); 86 | return false; 87 | default: 88 | listener.getLogger().format(" ‘%s’ found%n", scriptPath); 89 | return true; 90 | 91 | } 92 | } 93 | 94 | @Override 95 | public int hashCode() { 96 | return getClass().hashCode(); 97 | } 98 | 99 | @Override 100 | public boolean equals(Object obj) { 101 | return getClass().isInstance(obj); 102 | } 103 | }; 104 | } 105 | 106 | @Extension public static class DescriptorImpl extends AbstractWorkflowBranchProjectFactoryDescriptor { 107 | 108 | @NonNull 109 | @Override public String getDisplayName() { 110 | return "by " + SCRIPT; 111 | } 112 | 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/workflow/multibranch/WorkflowMultiBranchProject.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2015 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.multibranch; 26 | 27 | import edu.umd.cs.findbugs.annotations.NonNull; 28 | import hudson.Extension; 29 | import hudson.model.Action; 30 | import hudson.model.Descriptor; 31 | import hudson.model.Item; 32 | import hudson.model.ItemGroup; 33 | import hudson.model.TopLevelItem; 34 | import hudson.scm.SCMDescriptor; 35 | 36 | import java.io.IOException; 37 | import java.util.Collection; 38 | import java.util.Collections; 39 | import java.util.logging.Level; 40 | import java.util.logging.Logger; 41 | 42 | import jenkins.branch.Branch; 43 | import jenkins.branch.BranchProjectFactory; 44 | import jenkins.branch.BranchPropertyStrategy; 45 | import jenkins.branch.BranchSource; 46 | import jenkins.branch.MultiBranchProject; 47 | import jenkins.branch.MultiBranchProjectDescriptor; 48 | import jenkins.model.TransientActionFactory; 49 | import jenkins.scm.api.SCMHead; 50 | import jenkins.scm.api.SCMRevisionAction; 51 | import jenkins.scm.api.SCMSource; 52 | import jenkins.scm.api.SCMSourceCriteria; 53 | import org.apache.commons.lang.StringUtils; 54 | import org.jenkins.ui.icon.Icon; 55 | import org.jenkins.ui.icon.IconSet; 56 | import org.jenkins.ui.icon.IconSpec; 57 | import org.jenkinsci.Symbol; 58 | import org.jenkinsci.plugins.workflow.cps.Snippetizer; 59 | import org.jenkinsci.plugins.workflow.job.WorkflowJob; 60 | import org.jenkinsci.plugins.workflow.job.WorkflowRun; 61 | 62 | /** 63 | * Representation of a set of workflows keyed off of source branches. 64 | */ 65 | @SuppressWarnings({"unchecked", "rawtypes"}) // core’s fault 66 | public class WorkflowMultiBranchProject extends MultiBranchProject { 67 | 68 | private static final Logger LOGGER = Logger.getLogger(WorkflowMultiBranchProject.class.getName()); 69 | 70 | public WorkflowMultiBranchProject(ItemGroup parent, String name) { 71 | super(parent, name); 72 | } 73 | 74 | @NonNull 75 | @Override protected BranchProjectFactory newProjectFactory() { 76 | return new WorkflowBranchProjectFactory(); 77 | } 78 | 79 | @Override public SCMSourceCriteria getSCMSourceCriteria(@NonNull SCMSource source) { 80 | return ((AbstractWorkflowBranchProjectFactory) getProjectFactory()).getSCMSourceCriteria(source); 81 | } 82 | 83 | @Override 84 | public void onLoad(ItemGroup parent, String name) throws IOException { 85 | super.onLoad(parent, name); 86 | for (WorkflowJob job : items.values()) { 87 | if (job.getProperty(BranchJobProperty.class) == null) { 88 | //JENKINS-55116 It is highly unlikely that this property shouldn't be there 89 | //we only somehow lost the BranchJobProperty so we take the penalty to load a build or two 90 | //to see if this is what we think it is, and maybe desperately try to patch it. 91 | LOGGER.log(Level.WARNING, String.format("[JENKINS-55116] Found potential broken branch job property on %s, attempting to patch, you'll need to run a full rescan asap.", job.getFullName())); 92 | WorkflowRun build = job.getLastBuild(); 93 | for (int i = 0; i < 3; i++) { 94 | if (build != null) { 95 | SCMRevisionAction action = build.getAction(SCMRevisionAction.class); 96 | if (action != null) { 97 | BranchJobProperty p = reconstructBranchJobProperty(job, action); 98 | if (p != null) { 99 | try { 100 | job.addProperty(p); 101 | LOGGER.log(Level.WARNING, String.format("[JENKINS-55116] Reconstructed branch job property on %s from %s, you'll need to run a full rescan asap.", job.getFullName(), build.getNumber())); 102 | break; 103 | } catch (IOException e) { 104 | LOGGER.log(Level.SEVERE, String.format("[JENKINS-55116] Failed storing reconstructed branch job property on %s from %s", job.getFullName(), build.getNumber()), e); 105 | } 106 | } 107 | } 108 | build = build.getPreviousBuild(); 109 | } 110 | } 111 | } 112 | } 113 | } 114 | 115 | private BranchJobProperty reconstructBranchJobProperty(@NonNull WorkflowJob job, @NonNull SCMRevisionAction action) { 116 | String sourceId = action.getSourceId(); 117 | if (StringUtils.isEmpty(sourceId)) { 118 | return null; 119 | } 120 | SCMHead head = action.getRevision().getHead(); 121 | BranchSource source = null; 122 | for (BranchSource s : ((WorkflowMultiBranchProject) job.getParent()).getSources()) { 123 | if (sourceId.equals(s.getSource().getId())) { 124 | source = s; 125 | break; 126 | } 127 | } 128 | if (source == null) { 129 | return null; 130 | } 131 | 132 | final BranchPropertyStrategy strategy = source.getStrategy(); 133 | return new BranchJobProperty(new Branch(sourceId, head, source.getSource().build(head), 134 | strategy != null ? strategy.getPropertiesFor(head) : Collections.emptyList())); 135 | } 136 | 137 | @Extension @Symbol({"multibranch"}) 138 | public static class DescriptorImpl extends MultiBranchProjectDescriptor implements IconSpec { 139 | 140 | @NonNull 141 | @Override public String getDisplayName() { 142 | return Messages.WorkflowMultiBranchProject_DisplayName(); 143 | } 144 | 145 | @NonNull 146 | @Override public String getDescription() { 147 | return Messages.WorkflowMultiBranchProject_Description(); 148 | } 149 | 150 | @Override public String getIconFilePathPattern() { 151 | return "plugin/workflow-multibranch/images/pipelinemultibranchproject.svg"; 152 | } 153 | 154 | @Override 155 | public String getIconClassName() { 156 | return "symbol-git-branch-outline plugin-ionicons-api"; 157 | } 158 | 159 | @Override public TopLevelItem newInstance(ItemGroup parent, String name) { 160 | return new WorkflowMultiBranchProject(parent, name); 161 | } 162 | 163 | @Override public boolean isApplicable(Descriptor descriptor) { 164 | if (descriptor instanceof SCMDescriptor) { 165 | SCMDescriptor d = (SCMDescriptor) descriptor; 166 | // TODO would prefer to have SCMDescriptor.isApplicable(Class) 167 | try { 168 | if (!d.isApplicable(new WorkflowJob(null, null))) { 169 | return false; 170 | } 171 | } catch (RuntimeException x) { 172 | LOGGER.log(Level.FINE, "SCMDescriptor.isApplicable hack failed", x); 173 | } 174 | } 175 | return super.isApplicable(descriptor); 176 | } 177 | 178 | static { 179 | IconSet.icons.addIcon( 180 | new Icon("icon-pipeline-multibranch-project icon-sm", 181 | "plugin/workflow-multibranch/images/pipelinemultibranchproject.svg", 182 | Icon.ICON_SMALL_STYLE)); 183 | IconSet.icons.addIcon( 184 | new Icon("icon-pipeline-multibranch-project icon-md", 185 | "plugin/workflow-multibranch/images/pipelinemultibranchproject.svg", 186 | Icon.ICON_MEDIUM_STYLE)); 187 | IconSet.icons.addIcon( 188 | new Icon("icon-pipeline-multibranch-project icon-lg", 189 | "plugin/workflow-multibranch/images/pipelinemultibranchproject.svg", 190 | Icon.ICON_LARGE_STYLE)); 191 | IconSet.icons.addIcon( 192 | new Icon("icon-pipeline-multibranch-project icon-xlg", 193 | "plugin/workflow-multibranch/images/pipelinemultibranchproject.svg", 194 | Icon.ICON_XLARGE_STYLE)); 195 | } 196 | } 197 | 198 | @Extension public static class PerFolderAdder extends TransientActionFactory { 199 | 200 | @Override public Class type() { 201 | return WorkflowMultiBranchProject.class; 202 | } 203 | 204 | @NonNull 205 | @Override public Collection createFor(@NonNull WorkflowMultiBranchProject target) { 206 | if (target.hasPermission(Item.EXTENDED_READ)) { 207 | return Collections.singleton(new Snippetizer.LocalAction()); 208 | } else { 209 | return Collections.emptySet(); 210 | } 211 | } 212 | 213 | } 214 | 215 | } 216 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/workflow/multibranch/WorkflowMultiBranchProjectFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2015 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.multibranch; 26 | 27 | import edu.umd.cs.findbugs.annotations.NonNull; 28 | import hudson.Extension; 29 | import jenkins.branch.MultiBranchProjectFactory; 30 | import jenkins.branch.MultiBranchProjectFactoryDescriptor; 31 | import jenkins.scm.api.SCMSource; 32 | import jenkins.scm.api.SCMSourceCriteria; 33 | import org.apache.commons.lang.StringUtils; 34 | import org.kohsuke.stapler.DataBoundConstructor; 35 | import org.kohsuke.stapler.DataBoundSetter; 36 | import java.io.IOException; 37 | 38 | /** 39 | * Defines organization folders by {@link WorkflowBranchProjectFactory}. 40 | */ 41 | public class WorkflowMultiBranchProjectFactory extends AbstractWorkflowMultiBranchProjectFactory { 42 | private String scriptPath = WorkflowBranchProjectFactory.SCRIPT; 43 | 44 | public Object readResolve() { 45 | if (this.scriptPath == null) { 46 | this.scriptPath = WorkflowBranchProjectFactory.SCRIPT; 47 | } 48 | return this; 49 | } 50 | 51 | @DataBoundSetter 52 | public void setScriptPath(String scriptPath) { 53 | if (StringUtils.isEmpty(scriptPath)) { 54 | this.scriptPath = WorkflowBranchProjectFactory.SCRIPT; 55 | } else { 56 | this.scriptPath = scriptPath; 57 | } 58 | } 59 | 60 | public String getScriptPath() { return scriptPath; } 61 | 62 | @DataBoundConstructor public WorkflowMultiBranchProjectFactory() { } 63 | 64 | @Override protected SCMSourceCriteria getSCMSourceCriteria(@NonNull SCMSource source) { 65 | return newProjectFactory().getSCMSourceCriteria(source); 66 | } 67 | 68 | private AbstractWorkflowBranchProjectFactory newProjectFactory() { 69 | WorkflowBranchProjectFactory workflowBranchProjectFactory = new WorkflowBranchProjectFactory(); 70 | workflowBranchProjectFactory.setScriptPath(scriptPath); 71 | return workflowBranchProjectFactory; 72 | } 73 | 74 | @Extension public static class DescriptorImpl extends MultiBranchProjectFactoryDescriptor { 75 | 76 | @Override public MultiBranchProjectFactory newInstance() { 77 | return new WorkflowMultiBranchProjectFactory(); 78 | } 79 | 80 | @NonNull 81 | @Override public String getDisplayName() { 82 | return "Pipeline " + WorkflowBranchProjectFactory.SCRIPT; 83 | } 84 | 85 | } 86 | 87 | @Override 88 | protected void customize(WorkflowMultiBranchProject project) throws IOException, InterruptedException { 89 | project.setProjectFactory(newProjectFactory()); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/resources/index.jelly: -------------------------------------------------------------------------------- 1 | 2 | 25 | 26 | 27 |

28 | Enhances Pipeline plugin to handle branches better by automatically grouping builds from different branches. 29 |
30 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/workflow/multibranch/DurabilityHintBranchProperty/config.jelly: -------------------------------------------------------------------------------- 1 | 24 | 25 | 26 | 27 | 28 | 35 | 36 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/workflow/multibranch/DurabilityHintBranchProperty/help.html: -------------------------------------------------------------------------------- 1 | 24 |
25 |

26 | This setting allows users to change the default durability mode for running Pipelines. In most cases this is a 27 | trade-off between performance and the ability for running pipelines to resume after unplanned Jenkins outages. 28 | 29 |

What does this do? 30 |

    31 |
  • Previously, running pipelines wrote data constantly, so that they could resume even if Jenkins fails. 32 | This setting gives the user the ability to adjust this write behavior.
  • 33 |
  • Higher-performance/lower-durability modes write data to disk much less often for running pipelines.
  • 34 |
  • Writing data less often can massively reduce build times for Pipelines with many steps or complex logic. 35 | For pipelines which spend most of their time waiting for a shell/batch script to run, the difference will be less visible.
  • 36 |
  • Running pipelines with lower durability settings may lose data if they do not finish and Jenkins is not shut down gracefully:
  • 37 |
      38 |
    • A "graceful" shutdown is where Jenkins goes through a full shutdown process, such as visiting http://[jenkins-server]/exit
    • 39 |
    • A "dirty" shutdown, such as using kill -9 to terminate the Jenkins process, may prevent incomplete pipelines from persisting data
    • 40 |
    41 |
  • Pipelines that cannot persist data may not be able to resume or displayed in Blue Ocean/Stage View/etc.
  • 42 |
  • Pipelines will generally write log data regardless of durability settings.
  • 43 |
  • Some modes use an "atomic write" option - this helps ensure that pipeline build files aren't overwritten or left partially written if something fails.
  • 44 |
  • Atomic writes may place more stress on filesystems, so especially with networked storage it may be faster not to use them.
  • 45 |
46 |

47 |

48 | Note: defaults may be set globally under Manage Jenkins > Configure System. 49 |

50 |
-------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/workflow/multibranch/JobPropertyStep/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/workflow/multibranch/JobPropertyStep/help.html: -------------------------------------------------------------------------------- 1 |
2 | Updates the properties of the job which runs this step. 3 | Mainly useful from multibranch Pipelines, so that Jenkinsfile itself can encode what would otherwise be static job configuration. 4 | Existing properties set through the Jenkins UI for non-multibranch Pipelines will be preserved. 5 |
6 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/workflow/multibranch/Messages.properties: -------------------------------------------------------------------------------- 1 | ReadTrustedStep._has_been_modified_in_an_untrusted_revis=\u2018{0}\u2019 has been modified in an untrusted revision 2 | WorkflowMultiBranchProject.DisplayName=Multibranch Pipeline 3 | WorkflowMultiBranchProject.Description=Creates a set of Pipeline projects according to detected branches in one SCM repository. 4 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/workflow/multibranch/Messages_fr.properties: -------------------------------------------------------------------------------- 1 | ReadTrustedStep._has_been_modified_in_an_untrusted_revis=\u2018{0}\u2019 a \u00e9t\u00e9 modifi\u00e9 dans une r\u00e9vision non fiable 2 | WorkflowMultiBranchProject.DisplayName=Pipeline Multibranches 3 | WorkflowMultiBranchProject.Description=Cr\u00e9e un ensemble de projets Pipeline en se basant sur les branches d\u00e9tect\u00e9es dans le d\u00e9p\u00f4t d'un gestionnaire de code source. 4 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/workflow/multibranch/Messages_zh_CN.properties: -------------------------------------------------------------------------------- 1 | # The MIT License 2 | # 3 | # Copyright (c) 2018, suren 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | ReadTrustedStep._has_been_modified_in_an_untrusted_revis=\u2018{0}\u2019 \u5df2\u7ecf\u5728\u672a\u6388\u4fe1\u7684\u7248\u672c\u4e2d\u88ab\u4fee\u6539 24 | WorkflowMultiBranchProject.DisplayName=\u591a\u5206\u652f\u6d41\u6c34\u7ebf 25 | WorkflowMultiBranchProject.Description=\u6839\u636e\u4e00\u4e2aSCM\u4ed3\u5e93\u4e2d\u68c0\u6d4b\u5230\u7684\u5206\u652f\u521b\u5efa\u4e00\u7cfb\u5217\u6d41\u6c34\u7ebf\u3002 26 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/workflow/multibranch/ReadTrustedStep/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/workflow/multibranch/ReadTrustedStep/help-path.html: -------------------------------------------------------------------------------- 1 |
2 | Relative (slash-separated) path to the file from the SCM root. 3 | Thus readTrusted 'subdir/file' is similar to node {checkout scm; readFile 'subdir/file'}. 4 |
5 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/workflow/multibranch/ReadTrustedStep/help.html: -------------------------------------------------------------------------------- 1 |
2 | From a multibranch Pipeline project, reads a file from the associated SCM and returns its contents. 3 | Unlike the readFile step, no workspace is required. 4 | If the associated branch is not trusted, yet the file has been modified from its trusted version, an error is thrown. 5 | Thus this step is useful for loading scripts or other files which might otherwise be used to run malicious commands. 6 | Like checkout scm, as a convenience it may also be used from a standalone project configured with Pipeline from SCM, 7 | in which case there is no security aspect. 8 |
9 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/workflow/multibranch/ResolveScmStep/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 25 | 26 | 27 | 28 | 29 | 30 | 32 | 33 | 34 | 37 | 40 | 41 |
35 | 36 | 38 | 39 |
42 |
43 |
44 | 45 | 46 | 47 |
48 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/workflow/multibranch/ResolveScmStep/help-ignoreErrors.html: -------------------------------------------------------------------------------- 1 | 24 |
25 | When selected, the step will return null in the event that no matching branch can be resolved. 26 |
27 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/workflow/multibranch/ResolveScmStep/help-source.html: -------------------------------------------------------------------------------- 1 | 24 |
25 | The source repository from which to resolve the target branches. 26 |
27 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/workflow/multibranch/ResolveScmStep/help-targets.html: -------------------------------------------------------------------------------- 1 | 24 |
25 | The branch names to try and resolve from the source, in order of preference. 26 |
27 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/workflow/multibranch/ResolveScmStep/help.html: -------------------------------------------------------------------------------- 1 | 24 |
25 | When you have a multi-branch pipeline that checks out other sibling repositories, you may want to check out the 26 | matching branch from that sibling repository (assuming it exists) but fall back to the main branch if there is no 27 | matching branch. 28 |

29 | This step lets you create a branch in the primary repository and then when you need downstream / upstream changes in 30 | the sibling repository you can just create a matching branch and it will be resolved automatically. 31 | For example: 32 |

// checkout the main source
33 | dir('main'){
34 |     // this will checkout the source repository that is driving the multi-branch pipeline
35 |     checkout scm
36 | }
37 | // now checkout the tests
38 | dir('tests'){
39 |     // this will check if there is a branch with the same name as the current branch in
40 |     // https://example.com/example.git and use that for the checkout, but if there is no
41 |     // branch with the same name it will fall back to the master branch
42 |     checkout resolveScm(source: git('https://example.com/example.git'), targets: [BRANCH_NAME,'master']
43 | }
44 | // rest of pipeline
45 | 
46 |

47 | The return value is the resolved SCM instance (or null if ignoring errors). 48 | Where the SCM implementation supports it, the SCM instance will be pinned to the current head revision of 49 | the resolved branch. This can be useful if, for example, you want to check out the resolved branch on 50 | multiple nodes because all the nodes will get the same revision. 51 |

52 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/workflow/multibranch/SCMBinder/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/workflow/multibranch/SCMVar/help.jelly: -------------------------------------------------------------------------------- 1 | 2 | 25 | 26 | 27 | 28 | Represents the SCM configuration in a multibranch project build. 29 | Use checkout scm to check out sources matching Jenkinsfile. 30 |
You may also use this in a standalone project configured with Pipeline from SCM, 31 | though in that case the checkout will just be of the latest revision in the branch, 32 | possibly newer than the revision from which the Pipeline was loaded. 33 |
34 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/workflow/multibranch/WorkflowBranchProjectFactory/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/workflow/multibranch/WorkflowBranchProjectFactory/getting-started-links.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 |
  • 4 | 5 | Creating a Jenkins Pipeline 6 | 7 | 8 | 9 | 10 |
  • 11 |
  • 12 | 13 | Creating Multibranch Projects 14 | 15 | 16 | 17 | 18 |
  • 19 |
    20 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/workflow/multibranch/WorkflowBranchProjectFactory/getting-started.jelly: -------------------------------------------------------------------------------- 1 | 24 | 25 | 26 | Pipeline Branch projects support building branches within a repository containing a pipeline script. By default it uses 27 | a file named Jenkinsfile in the root directory. 28 | 29 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/workflow/multibranch/WorkflowBranchProjectFactory/help-scriptPath.html: -------------------------------------------------------------------------------- 1 |
    2 | Relative location within the checkout of your Pipeline script. 3 | Note that it will always be run inside a Groovy sandbox. 4 | Default is Jenkinsfile if left empty. 5 | (just use checkout scm to retrieve sources from the same location as is configured here) 6 |
    7 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/workflow/multibranch/WorkflowMultiBranchProject/newInstanceDetail.jelly: -------------------------------------------------------------------------------- 1 | 2 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/workflow/multibranch/WorkflowMultiBranchProjectFactory/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/workflow/multibranch/WorkflowMultiBranchProjectFactory/getting-started-links.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 |
  • 4 | 5 | Creating a Jenkins Pipeline 6 | 7 | 8 | 9 | 10 |
  • 11 |
  • 12 | 13 | Creating Multibranch Projects 14 | 15 | 16 | 17 | 18 |
  • 19 |
    20 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/workflow/multibranch/WorkflowMultiBranchProjectFactory/getting-started.jelly: -------------------------------------------------------------------------------- 1 | 24 | 25 | 26 | Pipeline Multibranch projects recognize and build repositories with a file named Jenkinsfile in branches of the repository. 27 | 28 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/workflow/multibranch/WorkflowMultiBranchProjectFactory/help-scriptPath.html: -------------------------------------------------------------------------------- 1 |
    2 | Relative location within the checkout of your Pipeline script. 3 | Note that it will always be run inside a Groovy sandbox. 4 | Default is Jenkinsfile if left empty. 5 | (just use checkout scm to retrieve sources from the same location as is configured here) 6 |
    7 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/workflow/multibranch/DurabilityHintBranchPropertyWorkflowTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2016 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.multibranch; 26 | 27 | import hudson.model.Result; 28 | import jenkins.branch.BranchProperty; 29 | import jenkins.branch.BranchSource; 30 | import jenkins.branch.DefaultBranchPropertyStrategy; 31 | import jenkins.branch.NoTriggerBranchProperty; 32 | import jenkins.branch.NoTriggerOrganizationFolderProperty; 33 | import jenkins.plugins.git.GitSCMSource; 34 | import jenkins.plugins.git.GitSampleRepoRule; 35 | import org.jenkinsci.plugins.workflow.flow.DurabilityHintProvider; 36 | import org.jenkinsci.plugins.workflow.flow.FlowDurabilityHint; 37 | import org.jenkinsci.plugins.workflow.flow.GlobalDefaultFlowDurabilityLevel; 38 | import org.jenkinsci.plugins.workflow.job.WorkflowJob; 39 | import org.jenkinsci.plugins.workflow.job.WorkflowRun; 40 | import org.jenkinsci.plugins.workflow.job.properties.DurabilityHintJobProperty; 41 | import org.junit.Assert; 42 | import org.junit.Rule; 43 | import org.junit.Test; 44 | import org.jvnet.hudson.test.Issue; 45 | import org.jvnet.hudson.test.JenkinsRule; 46 | 47 | import static org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProjectTest.scheduleAndFindBranchProject; 48 | 49 | /** 50 | * Integration test for {@link NoTriggerBranchProperty} and {@link NoTriggerOrganizationFolderProperty}. 51 | */ 52 | @Issue("JENKINS-32396") 53 | public class DurabilityHintBranchPropertyWorkflowTest { 54 | 55 | @Rule public JenkinsRule r = new JenkinsRule(); 56 | @Rule public GitSampleRepoRule sampleRepo = new GitSampleRepoRule(); 57 | 58 | @Test 59 | public void configRoundtrip() throws Exception { 60 | WorkflowMultiBranchProject mp = r.jenkins.createProject(WorkflowMultiBranchProject.class, "p"); 61 | BranchSource bs = new BranchSource(new GitSCMSource(null, sampleRepo.toString(), "", "*", "", false)); 62 | mp.getSourcesList().add(bs); 63 | bs.setStrategy(new DefaultBranchPropertyStrategy(new BranchProperty[]{new DurabilityHintBranchProperty(FlowDurabilityHint.SURVIVABLE_NONATOMIC)})); 64 | r.configRoundtrip(mp); 65 | DefaultBranchPropertyStrategy strat = (DefaultBranchPropertyStrategy) mp.getBranchPropertyStrategy(mp.getSCMSources().get(0)); 66 | DurabilityHintBranchProperty prop = null; 67 | for (BranchProperty bp : strat.getProps()) { 68 | if (bp instanceof DurabilityHintBranchProperty) { 69 | prop = (DurabilityHintBranchProperty)bp; 70 | break; 71 | } 72 | } 73 | Assert.assertNotNull(prop); 74 | Assert.assertEquals(FlowDurabilityHint.SURVIVABLE_NONATOMIC, prop.getHint()); 75 | } 76 | 77 | @Test public void durabilityHintByPropertyStep() throws Exception { 78 | sampleRepo.init(); 79 | sampleRepo.write("Jenkinsfile", 80 | "properties([durabilityHint('" + FlowDurabilityHint.SURVIVABLE_NONATOMIC.getName()+"')])\n"+ 81 | "echo 'whynot'"); 82 | sampleRepo.git("add", "Jenkinsfile"); 83 | sampleRepo.git("commit", "--all", "--message=flow"); 84 | 85 | 86 | WorkflowMultiBranchProject mp = r.jenkins.createProject(WorkflowMultiBranchProject.class, "p"); 87 | mp.getSourcesList().add(new BranchSource(new GitSCMSource(null, sampleRepo.toString(), "", "*", "", false))); 88 | WorkflowJob p = scheduleAndFindBranchProject(mp, "master"); 89 | r.waitUntilNoActivity(); 90 | 91 | WorkflowRun b1 = p.getLastBuild(); 92 | Assert.assertEquals(Result.SUCCESS, b1.getResult()); 93 | DurabilityHintJobProperty prop = p.getProperty(DurabilityHintJobProperty.class); 94 | Assert.assertEquals(FlowDurabilityHint.SURVIVABLE_NONATOMIC, prop.getHint()); 95 | } 96 | 97 | @Test 98 | @Issue("JENKINS-48826") 99 | public void durabilityHintByBranchProperty() throws Exception { 100 | sampleRepo.init(); 101 | sampleRepo.write("Jenkinsfile", 102 | "echo 'whynot'"); 103 | sampleRepo.git("add", "Jenkinsfile"); 104 | sampleRepo.git("commit", "--all", "--message=flow"); 105 | 106 | 107 | WorkflowMultiBranchProject mp = r.jenkins.createProject(WorkflowMultiBranchProject.class, "p"); 108 | BranchSource bs = new BranchSource(new GitSCMSource(null, sampleRepo.toString(), "", "*", "", false)); 109 | mp.getSourcesList().add(bs); 110 | bs.setStrategy(new DefaultBranchPropertyStrategy(new BranchProperty[]{new DurabilityHintBranchProperty(FlowDurabilityHint.SURVIVABLE_NONATOMIC)})); 111 | WorkflowJob p = scheduleAndFindBranchProject(mp, "master"); 112 | r.waitUntilNoActivity(); 113 | 114 | Assert.assertEquals(FlowDurabilityHint.SURVIVABLE_NONATOMIC, DurabilityHintProvider.suggestedFor(p)); 115 | WorkflowRun b1 = p.getLastBuild(); 116 | Assert.assertEquals(Result.SUCCESS, b1.getResult()); 117 | 118 | // Ensure when we remove the property, branches see that on the next build 119 | bs.setStrategy(new DefaultBranchPropertyStrategy(new BranchProperty[]{})); 120 | p = scheduleAndFindBranchProject(mp, "master"); 121 | r.waitUntilNoActivity(); 122 | 123 | Assert.assertEquals(GlobalDefaultFlowDurabilityLevel.getDefaultDurabilityHint(), DurabilityHintProvider.suggestedFor(mp.getItems().iterator().next())); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/workflow/multibranch/GitDirectorySCMNavigator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2015 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.multibranch; 26 | 27 | import edu.umd.cs.findbugs.annotations.NonNull; 28 | import hudson.Extension; 29 | import hudson.Launcher; 30 | import hudson.Proc; 31 | import hudson.model.TaskListener; 32 | import java.io.ByteArrayInputStream; 33 | import java.io.ByteArrayOutputStream; 34 | import java.io.File; 35 | import java.io.IOException; 36 | import java.util.regex.Matcher; 37 | import java.util.regex.Pattern; 38 | import jenkins.plugins.git.GitSCMSource; 39 | import jenkins.scm.api.SCMNavigator; 40 | import jenkins.scm.api.SCMNavigatorDescriptor; 41 | import jenkins.scm.api.SCMSourceObserver; 42 | import org.apache.commons.io.IOUtils; 43 | import static org.junit.Assert.assertFalse; 44 | 45 | import org.jenkinsci.Symbol; 46 | import org.kohsuke.stapler.DataBoundConstructor; 47 | 48 | /** 49 | * Sample provider which scans a directory for Git checkouts. 50 | */ 51 | public class GitDirectorySCMNavigator extends SCMNavigator { 52 | 53 | private final String directory; 54 | 55 | @DataBoundConstructor public GitDirectorySCMNavigator(String directory) { 56 | this.directory = directory; 57 | } 58 | 59 | public String getDirectory() { 60 | return directory; 61 | } 62 | 63 | @NonNull 64 | @Override 65 | protected String id() { 66 | return directory; 67 | } 68 | 69 | @Override public void visitSources(SCMSourceObserver observer) throws IOException, InterruptedException { 70 | TaskListener listener = observer.getListener(); 71 | File[] kids = new File(directory).listFiles(); 72 | if (kids == null) { 73 | listener.error(directory + " does not exist"); 74 | return; 75 | } 76 | for (File kid : kids) { 77 | if (!observer.isObserving()) { 78 | return; 79 | } 80 | if (!new File(kid, ".git").isDirectory()) { 81 | listener.getLogger().format("Ignoring %s since it does not look like a Git checkout%n", kid); 82 | continue; 83 | } 84 | listener.getLogger().format("Considering: %s%n", kid); 85 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 86 | Proc proc = new Launcher.LocalLauncher(listener).launch().pwd(kid).cmds("git", "remote", "-v").stdout(baos).start(); 87 | if (proc.join() != 0) { 88 | listener.error("git-remote failed"); 89 | continue; 90 | } 91 | String origin = kid.getAbsolutePath(); // fallback 92 | for (String line : IOUtils.readLines(new ByteArrayInputStream(baos.toByteArray()))) { 93 | Matcher m = ORIGIN.matcher(line); 94 | if (m.matches()) { 95 | origin = m.group(1); 96 | listener.getLogger().format("Found origin URL: %s%n", origin); 97 | assertFalse(origin.startsWith("git@")); 98 | break; 99 | } 100 | } 101 | SCMSourceObserver.ProjectObserver projectObserver = observer.observe(kid.getName()); 102 | projectObserver.addSource(new GitSCMSource(getId() + "::" + kid.getName(), origin, "", "*", "", false)); 103 | projectObserver.complete(); 104 | } 105 | } 106 | private static final Pattern ORIGIN = Pattern.compile("origin\t(.+) [(]fetch[)]"); 107 | 108 | @Extension @Symbol("gitDirectory") 109 | public static class DescriptorImpl extends SCMNavigatorDescriptor { 110 | 111 | @NonNull 112 | @Override public String getDisplayName() { 113 | return "Directory of Git checkouts"; 114 | } 115 | 116 | @Override public SCMNavigator newInstance(String name) { 117 | return null; 118 | } 119 | 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/workflow/multibranch/NoTriggerBranchPropertyWorkflowTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2016 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.multibranch; 26 | 27 | import hudson.model.queue.QueueTaskFuture; 28 | import java.util.Collections; 29 | import jenkins.branch.BranchProperty; 30 | import jenkins.branch.BranchSource; 31 | import jenkins.branch.NamedExceptionsBranchPropertyStrategy; 32 | import jenkins.branch.NoTriggerBranchProperty; 33 | import jenkins.branch.NoTriggerOrganizationFolderProperty; 34 | import jenkins.branch.OrganizationFolder; 35 | import jenkins.plugins.git.GitSCMSource; 36 | import jenkins.plugins.git.GitSampleRepoRule; 37 | import jenkins.scm.impl.SingleSCMNavigator; 38 | import org.jenkinsci.plugins.workflow.job.WorkflowJob; 39 | import org.jenkinsci.plugins.workflow.job.WorkflowRun; 40 | import org.junit.Test; 41 | import static org.junit.Assert.*; 42 | import org.junit.Rule; 43 | import org.jvnet.hudson.test.Issue; 44 | import org.jvnet.hudson.test.JenkinsRule; 45 | 46 | /** 47 | * Integration test for {@link NoTriggerBranchProperty} and {@link NoTriggerOrganizationFolderProperty}. 48 | */ 49 | @Issue("JENKINS-32396") 50 | public class NoTriggerBranchPropertyWorkflowTest { 51 | 52 | @Rule public JenkinsRule r = new JenkinsRule(); 53 | @Rule public GitSampleRepoRule sampleRepo = new GitSampleRepoRule(); 54 | 55 | @Issue("JENKINS-30206") 56 | @Test public void singleRepo() throws Exception { 57 | round1(); 58 | WorkflowMultiBranchProject p = r.jenkins.createProject(WorkflowMultiBranchProject.class, "p"); 59 | BranchSource branchSource = new BranchSource(new GitSCMSource("source-id", sampleRepo.toString(), "", "*", "", false)); 60 | branchSource.setStrategy(new NamedExceptionsBranchPropertyStrategy(new BranchProperty[0], new NamedExceptionsBranchPropertyStrategy.Named[] { 61 | new NamedExceptionsBranchPropertyStrategy.Named("release*", new BranchProperty[] {new NoTriggerBranchProperty()}) 62 | })); 63 | p.getSourcesList().add(branchSource); 64 | // Should be initial builds of master & newfeature but not release. 65 | WorkflowJob master = WorkflowMultiBranchProjectTest.scheduleAndFindBranchProject(p, "master"); 66 | r.waitUntilNoActivity(); 67 | assertEquals(2, master.getNextBuildNumber()); 68 | WorkflowJob release = WorkflowMultiBranchProjectTest.scheduleAndFindBranchProject(p, "release"); 69 | assertEquals(1, release.getNextBuildNumber()); 70 | WorkflowJob newfeature = WorkflowMultiBranchProjectTest.scheduleAndFindBranchProject(p, "newfeature"); 71 | assertEquals(2, newfeature.getNextBuildNumber()); 72 | round2(); 73 | WorkflowMultiBranchProjectTest.showIndexing(p); 74 | // Should be second builds of master & newfeature but not release. 75 | assertEquals(3, master.getNextBuildNumber()); 76 | assertEquals(1, release.getNextBuildNumber()); 77 | assertEquals(3, newfeature.getNextBuildNumber()); 78 | // Should be able to manually build release. 79 | QueueTaskFuture releaseBuild = release.scheduleBuild2(0); 80 | assertNotNull(releaseBuild); 81 | assertEquals(1, releaseBuild.get().getNumber()); 82 | assertEquals(2, release.getNextBuildNumber()); 83 | // Updating configuration should take effect for next time: new builds of newfeature & release but not master. 84 | branchSource = new BranchSource(new GitSCMSource("source-id", sampleRepo.toString(), "", "*", "", false)); 85 | branchSource.setStrategy(new NamedExceptionsBranchPropertyStrategy(new BranchProperty[0], new NamedExceptionsBranchPropertyStrategy.Named[] { 86 | new NamedExceptionsBranchPropertyStrategy.Named("master", new BranchProperty[] {new NoTriggerBranchProperty()}) 87 | })); 88 | p.getSourcesList().clear(); 89 | p.getSourcesList().add(branchSource); 90 | round3(); 91 | WorkflowMultiBranchProjectTest.showIndexing(p); 92 | assertEquals(3, master.getNextBuildNumber()); 93 | assertEquals(3, release.getNextBuildNumber()); 94 | assertEquals(4, newfeature.getNextBuildNumber()); 95 | } 96 | 97 | @Test public void organizationFolder() throws Exception { 98 | round1(); 99 | OrganizationFolder top = r.jenkins.createProject(OrganizationFolder.class, "top"); 100 | top.getProperties().add(new NoTriggerOrganizationFolderProperty("(?!release.*).*")); 101 | top.getNavigators().add(new SingleSCMNavigator("p", Collections.singletonList(new GitSCMSource("source-id", sampleRepo.toString(), "", "*", "", false)))); 102 | top.scheduleBuild2(0).getFuture().get(); 103 | r.waitUntilNoActivity(); 104 | top.getComputation().writeWholeLogTo(System.out); 105 | WorkflowMultiBranchProject p = r.jenkins.getItemByFullName("top/p", WorkflowMultiBranchProject.class); 106 | assertNotNull(p); 107 | WorkflowJob master = WorkflowMultiBranchProjectTest.scheduleAndFindBranchProject(p, "master"); 108 | r.waitUntilNoActivity(); 109 | assertEquals(2, master.getNextBuildNumber()); 110 | WorkflowJob release = WorkflowMultiBranchProjectTest.scheduleAndFindBranchProject(p, "release"); 111 | assertEquals(1, release.getNextBuildNumber()); 112 | WorkflowJob newfeature = WorkflowMultiBranchProjectTest.scheduleAndFindBranchProject(p, "newfeature"); 113 | assertEquals(2, newfeature.getNextBuildNumber()); 114 | round2(); 115 | WorkflowMultiBranchProjectTest.showIndexing(p); 116 | assertEquals(3, master.getNextBuildNumber()); 117 | assertEquals(1, release.getNextBuildNumber()); 118 | assertEquals(3, newfeature.getNextBuildNumber()); 119 | QueueTaskFuture releaseBuild = release.scheduleBuild2(0); 120 | assertNotNull(releaseBuild); 121 | assertEquals(1, releaseBuild.get().getNumber()); 122 | assertEquals(2, release.getNextBuildNumber()); 123 | top.getProperties().replace(new NoTriggerOrganizationFolderProperty("(?!master$).*")); 124 | round3(); 125 | WorkflowMultiBranchProjectTest.showIndexing(p); 126 | assertEquals(3, master.getNextBuildNumber()); 127 | assertEquals(3, release.getNextBuildNumber()); 128 | assertEquals(4, newfeature.getNextBuildNumber()); 129 | } 130 | 131 | private void round1() throws Exception { 132 | sampleRepo.init(); 133 | sampleRepo.write("Jenkinsfile", ""); 134 | sampleRepo.git("add", "Jenkinsfile"); 135 | sampleRepo.git("commit", "--message=init"); 136 | sampleRepo.git("checkout", "-b", "newfeature"); 137 | sampleRepo.write("Jenkinsfile", "// newfeature"); 138 | sampleRepo.git("commit", "--all", "--message=newfeature"); 139 | sampleRepo.git("checkout", "-b", "release", "master"); 140 | sampleRepo.write("Jenkinsfile", "// release"); 141 | sampleRepo.git("commit", "--all", "--message=release"); 142 | } 143 | 144 | private void round2() throws Exception { 145 | sampleRepo.git("checkout", "master"); 146 | sampleRepo.write("Jenkinsfile", "// more"); 147 | sampleRepo.git("commit", "--all", "--message=master-2"); 148 | sampleRepo.git("checkout", "newfeature"); 149 | sampleRepo.write("Jenkinsfile", "// more"); 150 | sampleRepo.git("commit", "--all", "--message=newfeature-2"); 151 | sampleRepo.git("checkout", "release"); 152 | sampleRepo.write("Jenkinsfile", "// more"); 153 | sampleRepo.git("commit", "--all", "--message=release-2"); 154 | sampleRepo.notifyCommit(r); 155 | } 156 | 157 | private void round3() throws Exception { 158 | sampleRepo.git("checkout", "master"); 159 | sampleRepo.write("Jenkinsfile", "// yet more"); 160 | sampleRepo.git("commit", "--all", "--message=master-3"); 161 | sampleRepo.git("checkout", "newfeature"); 162 | sampleRepo.write("Jenkinsfile", "// yet more"); 163 | sampleRepo.git("commit", "--all", "--message=newfeature-3"); 164 | sampleRepo.git("checkout", "release"); 165 | sampleRepo.write("Jenkinsfile", "// yet more"); 166 | sampleRepo.git("commit", "--all", "--message=release-3"); 167 | sampleRepo.notifyCommit(r); 168 | } 169 | 170 | } 171 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/workflow/multibranch/RepairBranchPropertyTest.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 | package org.jenkinsci.plugins.workflow.multibranch; 25 | 26 | import com.cloudbees.hudson.plugins.folder.computed.FolderComputation; 27 | import hudson.model.Actionable; 28 | import hudson.model.Cause; 29 | import hudson.model.Job; 30 | import hudson.model.Queue; 31 | import hudson.model.Result; 32 | import hudson.model.TopLevelItem; 33 | import jenkins.branch.MultiBranchProject; 34 | import jenkins.branch.OrganizationFolder; 35 | import jenkins.scm.api.SCMRevisionAction; 36 | import jenkins.scm.impl.mock.MockSCMController; 37 | import jenkins.scm.impl.mock.MockSCMDiscoverBranches; 38 | import jenkins.scm.impl.mock.MockSCMNavigator; 39 | import org.jenkinsci.plugins.workflow.job.WorkflowJob; 40 | import org.junit.Before; 41 | import org.junit.Rule; 42 | import org.junit.Test; 43 | import org.jvnet.hudson.test.Issue; 44 | import org.jvnet.hudson.test.JenkinsRule; 45 | import org.jvnet.hudson.test.recipes.LocalData; 46 | 47 | import java.io.IOException; 48 | 49 | import static junit.framework.TestCase.assertEquals; 50 | import static junit.framework.TestCase.assertNotNull; 51 | import static junit.framework.TestCase.assertNull; 52 | import static junit.framework.TestCase.assertTrue; 53 | 54 | public class RepairBranchPropertyTest { 55 | 56 | @Rule 57 | public JenkinsRule j = new JenkinsRule(); 58 | private MockSCMController controller; 59 | 60 | @Before 61 | public void setUp() throws IOException { 62 | setup(MockSCMController.create()); 63 | } 64 | 65 | void setup(MockSCMController co) throws IOException { 66 | controller = co; 67 | controller.createRepository("repo"); 68 | controller.createBranch("repo", "master"); 69 | controller.addFile("repo", "master", "First!", WorkflowBranchProjectFactory.SCRIPT, 70 | "echo 'hello'".getBytes()); 71 | } 72 | 73 | @Test @Issue("JENKINS-55116") 74 | public void removedProperty() throws Exception { 75 | OrganizationFolder org = j.createProject(OrganizationFolder.class, "org"); 76 | org.getNavigators().add(new MockSCMNavigator(controller.getId(), new MockSCMDiscoverBranches())); 77 | org.save(); 78 | org.scheduleBuild(new Cause.UserIdCause("anonymous")); 79 | j.waitUntilNoActivity(); 80 | MultiBranchProject repo = org.getItem("repo"); 81 | assertNotNull(repo); 82 | Job master = repo.getItem("master"); 83 | assertNotNull(master); 84 | assertNotNull(master.getProperty(BranchJobProperty.class)); 85 | assertNotNull(master.getLastBuild()); 86 | master.removeProperty(BranchJobProperty.class); 87 | //removeProperty calls save 88 | j.jenkins.reload(); 89 | 90 | org = j.jenkins.getItem("org", j.jenkins, OrganizationFolder.class); 91 | assertNotNull(org); 92 | repo = org.getItem("repo"); 93 | assertNotNull(repo); 94 | master = repo.getItem("master"); 95 | assertNotNull(master); 96 | 97 | assertNotNull(master.getProperty(BranchJobProperty.class)); 98 | assertTrue(repo.getProjectFactory().isProject(master)); 99 | assertTrue(repo.getPrimaryView().contains((TopLevelItem)master)); 100 | } 101 | 102 | @Test @Issue("JENKINS-55116") 103 | public void removedPropertyLastBuildCorrupt() throws Exception { 104 | OrganizationFolder org = j.createProject(OrganizationFolder.class, "org"); 105 | org.getNavigators().add(new MockSCMNavigator(controller.getId(), new MockSCMDiscoverBranches())); 106 | org.save(); 107 | org.scheduleBuild(new Cause.UserIdCause("anonymous")); 108 | j.waitUntilNoActivity(); 109 | MultiBranchProject repo = org.getItem("repo"); 110 | assertNotNull(repo); 111 | Job master = repo.getItem("master"); 112 | assertNotNull(master); 113 | assertNotNull(master.getProperty(BranchJobProperty.class)); 114 | assertNotNull(master.getLastBuild()); 115 | 116 | controller.addFile("repo", "master", "Second", "README.txt", "Hello".getBytes()); 117 | repo.scheduleBuild(); 118 | j.waitUntilNoActivity(); 119 | assertEquals(2, master.getBuilds().size()); 120 | final Actionable lastBuild = master.getLastBuild(); 121 | lastBuild.removeAction(lastBuild.getAction(SCMRevisionAction.class)); 122 | assertNull(lastBuild.getAction(SCMRevisionAction.class)); 123 | 124 | master.removeProperty(BranchJobProperty.class); 125 | //removeProperty calls save 126 | j.jenkins.reload(); 127 | org = j.jenkins.getItem("org", j.jenkins, OrganizationFolder.class); 128 | assertNotNull(org); 129 | repo = org.getItem("repo"); 130 | assertNotNull(repo); 131 | master = repo.getItem("master"); 132 | assertNotNull(master); 133 | 134 | assertNotNull(master.getProperty(BranchJobProperty.class)); 135 | assertTrue(repo.getProjectFactory().isProject(master)); 136 | assertTrue(repo.getPrimaryView().contains((TopLevelItem)master)); 137 | } 138 | 139 | @Test @LocalData @Issue("JENKINS-55116") 140 | public void removedPropertyAtStartup() throws Exception { 141 | MockSCMController cont = MockSCMController.recreate("9ea2ef21-aa07-4973-a942-6c4c4c7851d1"); 142 | setup(cont); 143 | OrganizationFolder org = j.jenkins.getItem("org", j.jenkins, OrganizationFolder.class); 144 | assertNotNull(org); 145 | MultiBranchProject repo = org.getItem("repo"); 146 | assertNotNull(repo); 147 | WorkflowJob master = (WorkflowJob)repo.getItem("master"); 148 | assertNotNull(master); 149 | assertNotNull(master.getProperty(BranchJobProperty.class)); 150 | assertTrue(((WorkflowMultiBranchProject)master.getParent()).getProjectFactory().isProject(master)); 151 | assertTrue(repo.getPrimaryView().contains(master)); 152 | 153 | //Can it be scanned successfully afterwards 154 | final Queue.Item item = repo.scheduleBuild2(0); 155 | assertNotNull(item); 156 | final Queue.Executable executable = item.getFuture().get(); 157 | assertNotNull(executable); 158 | FolderComputation computation = (FolderComputation) executable; 159 | computation.writeWholeLogTo(System.out); 160 | assertEquals(Result.SUCCESS, computation.getResult()); 161 | //Since the new controller has new "commits" a build of master should have been scheduled 162 | j.waitUntilNoActivity(); 163 | assertEquals(2, master.getBuilds().size()); 164 | j.assertBuildStatusSuccess(master.getBuildByNumber(2)); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/workflow/multibranch/ReplayActionTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2016 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.multibranch; 26 | 27 | import hudson.model.Item; 28 | import hudson.model.User; 29 | import hudson.security.ACL; 30 | import hudson.security.ACLContext; 31 | import java.io.File; 32 | import java.util.Collections; 33 | import jenkins.branch.BranchProperty; 34 | import jenkins.branch.BranchSource; 35 | import jenkins.branch.DefaultBranchPropertyStrategy; 36 | import jenkins.branch.MultiBranchProject; 37 | import jenkins.branch.OrganizationFolder; 38 | import jenkins.model.Jenkins; 39 | import jenkins.plugins.git.GitSCMSource; 40 | import jenkins.plugins.git.GitSampleRepoRule; 41 | import jenkins.plugins.git.GitStep; 42 | import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; 43 | import org.jenkinsci.plugins.workflow.cps.CpsScmFlowDefinition; 44 | import org.jenkinsci.plugins.workflow.cps.replay.ReplayAction; 45 | import org.jenkinsci.plugins.workflow.job.WorkflowJob; 46 | import org.jenkinsci.plugins.workflow.job.WorkflowRun; 47 | import org.junit.Test; 48 | import static org.junit.Assert.*; 49 | import org.junit.ClassRule; 50 | import org.junit.Rule; 51 | import org.junit.rules.TemporaryFolder; 52 | import org.jvnet.hudson.test.BuildWatcher; 53 | import org.jvnet.hudson.test.JenkinsRule; 54 | import org.jvnet.hudson.test.MockAuthorizationStrategy; 55 | 56 | public class ReplayActionTest { 57 | 58 | @ClassRule public static BuildWatcher buildWatcher = new BuildWatcher(); 59 | @Rule public JenkinsRule r = new JenkinsRule(); 60 | @Rule public GitSampleRepoRule sampleRepo = new GitSampleRepoRule(); 61 | @Rule public TemporaryFolder tmp = new TemporaryFolder(); 62 | 63 | @Test public void scriptFromSCM() throws Exception { 64 | sampleRepo.init(); 65 | sampleRepo.write("Jenkinsfile", "node {checkout scm; echo \"loaded ${readFile 'file'}\"}"); 66 | sampleRepo.git("add", "Jenkinsfile"); 67 | sampleRepo.write("file", "initial content"); 68 | sampleRepo.git("add", "file"); 69 | sampleRepo.git("commit", "--message=init"); 70 | WorkflowJob p = r.jenkins.createProject(WorkflowJob.class, "p"); 71 | p.setDefinition(new CpsScmFlowDefinition(new GitStep(sampleRepo.toString()).createSCM(), "Jenkinsfile")); 72 | WorkflowRun b = r.assertBuildStatusSuccess(p.scheduleBuild2(0)); 73 | r.assertLogContains("loaded initial content", b); 74 | // Changing contents of a file in the repo. Since this is a standalone project, scm currently “floats” to the branch head. 75 | sampleRepo.write("file", "subsequent content"); 76 | sampleRepo.git("add", "file"); 77 | sampleRepo.git("commit", "--message=next"); 78 | // Replaying with a modified main script; checkout scm will get branch head. 79 | b = (WorkflowRun) b.getAction(ReplayAction.class).run("node {checkout scm; echo \"this time loaded ${readFile 'file'}\"}", Collections.emptyMap()).get(); 80 | assertEquals(2, b.number); 81 | r.assertLogContains("this time loaded subsequent content", b); 82 | } 83 | 84 | @Test public void multibranch() throws Exception { 85 | sampleRepo.init(); 86 | sampleRepo.write("Jenkinsfile", "node {checkout scm; echo readFile('file')}"); 87 | sampleRepo.write("file", "initial content"); 88 | sampleRepo.git("add", "Jenkinsfile"); 89 | sampleRepo.git("commit", "--all", "--message=init"); 90 | WorkflowMultiBranchProject mp = r.jenkins.createProject(WorkflowMultiBranchProject.class, "p"); 91 | mp.getSourcesList().add(new BranchSource(new GitSCMSource(null, sampleRepo.toString(), "", "*", "", false), new DefaultBranchPropertyStrategy(new BranchProperty[0]))); 92 | WorkflowJob p = WorkflowMultiBranchProjectTest.scheduleAndFindBranchProject(mp, "master"); 93 | r.waitUntilNoActivity(); 94 | WorkflowRun b1 = p.getLastBuild(); 95 | assertNotNull(b1); 96 | assertEquals(1, b1.getNumber()); 97 | r.assertLogContains("initial content", b1); 98 | sampleRepo.write("file", "subsequent content"); 99 | sampleRepo.git("add", "file"); 100 | sampleRepo.git("commit", "--message=next"); 101 | // Replaying main script with some upcasing. 102 | WorkflowRun b2 = (WorkflowRun) b1.getAction(ReplayAction.class).run("node {checkout scm; echo readFile('file').toUpperCase()}", Collections.emptyMap()).get(); 103 | assertEquals(2, b2.number); 104 | // For a multibranch project, we expect checkout scm to retrieve the same repository revision as the (original) Jenkinsfile. 105 | r.assertLogContains("INITIAL CONTENT", b2); 106 | } 107 | 108 | @Test public void permissions() throws Exception { 109 | File clones = tmp.newFolder(); 110 | sampleRepo.init(); 111 | sampleRepo.write("Jenkinsfile", ""); 112 | sampleRepo.git("add", "Jenkinsfile"); 113 | sampleRepo.git("commit", "--all", "--message=init"); 114 | sampleRepo.git("clone", ".", new File(clones, "one").getAbsolutePath()); 115 | // Set up a secured instance with an organization folder. 116 | // Developers have varying permissions set at the topmost (configurable) level. 117 | r.jenkins.setSecurityRealm(r.createDummySecurityRealm()); 118 | OrganizationFolder top = r.jenkins.createProject(OrganizationFolder.class, "top"); 119 | r.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy(). 120 | grant(Jenkins.ADMINISTER).everywhere().to("admin"). 121 | grant(Jenkins.READ).everywhere().to("dev1", "dev2", "dev3"). 122 | grant(Item.CONFIGURE).onFolders(top).to("dev1"). // implies REPLAY 123 | grant(ReplayAction.REPLAY).onFolders(top).to("dev2"). 124 | grant(Item.BUILD).onFolders(top).to("dev3")); // does not imply REPLAY 125 | top.getNavigators().add(new GitDirectorySCMNavigator(clones.getAbsolutePath())); 126 | top.scheduleBuild2(0).getFuture().get(); 127 | top.getComputation().writeWholeLogTo(System.out); 128 | assertEquals(1, top.getItems().size()); 129 | MultiBranchProject one = top.getItem("one"); 130 | r.waitUntilNoActivity(); 131 | WorkflowJob p = WorkflowMultiBranchProjectTest.findBranchProject((WorkflowMultiBranchProject) one, "master"); 132 | WorkflowRun b1 = p.getLastBuild(); 133 | assertEquals(1, b1.getNumber()); 134 | // Multibranch projects are always sandboxed, so any dev with REPLAY (or CONFIGURE) can replay. 135 | assertTrue(canReplay(b1, "admin")); 136 | // Note that while dev1 cannot actually configure the WorkflowJob (it is read-only; no one can), 137 | // the implication CONFIGURE → REPLAY is done at a lower level than this suppression of CONFIGURE. 138 | assertTrue(canReplay(b1, "dev1")); 139 | assertTrue(canReplay(b1, "dev2")); 140 | assertFalse(canReplay(b1, "dev3")); 141 | // For whole-script-approval standalone projects, you need RUN_SCRIPTS to replay. 142 | p = r.jenkins.createProject(WorkflowJob.class, "p"); 143 | p.setDefinition(new CpsFlowDefinition("", false)); 144 | b1 = p.scheduleBuild2(0).get(); 145 | assertTrue(canReplay(b1, "admin")); 146 | assertFalse("not sandboxed, so only safe for admins", canReplay(b1, "dev1")); 147 | assertFalse(canReplay(b1, "dev2")); 148 | assertFalse(canReplay(b1, "dev3")); 149 | } 150 | private static boolean canReplay(WorkflowRun b, String user) { 151 | final ReplayAction a = b.getAction(ReplayAction.class); 152 | try (ACLContext context = ACL.as(User.getById(user, true))) { 153 | return a.isEnabled(); 154 | } 155 | } 156 | 157 | } 158 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/workflow/multibranch/ResolveScmStepTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2017 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 | 26 | package org.jenkinsci.plugins.workflow.multibranch; 27 | 28 | import hudson.model.TopLevelItem; 29 | import jenkins.scm.impl.mock.MockSCMController; 30 | import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; 31 | import org.jenkinsci.plugins.workflow.job.WorkflowJob; 32 | import org.junit.Before; 33 | import org.junit.ClassRule; 34 | import org.junit.Test; 35 | import org.jvnet.hudson.test.JenkinsRule; 36 | 37 | public class ResolveScmStepTest { 38 | 39 | @ClassRule 40 | public static JenkinsRule j = new JenkinsRule(); 41 | 42 | @Before 43 | public void cleanOutAllItems() throws Exception { 44 | for (TopLevelItem i : j.getInstance().getItems()) { 45 | i.delete(); 46 | } 47 | } 48 | 49 | @Test 50 | public void given_existingHeadName_when_invoked_then_existingHeadNameReturned() throws Exception { 51 | try (MockSCMController c = MockSCMController.create()) { 52 | c.createRepository("repo"); 53 | c.createBranch("repo", "foo"); 54 | c.addFile("repo", "foo", "Add file", "new-file.txt", "content".getBytes()); 55 | WorkflowJob job = j.jenkins.createProject(WorkflowJob.class, "workflow"); 56 | job.setDefinition(new CpsFlowDefinition("node {\n" 57 | + " def tests = resolveScm source: mockScm(controllerId:'" 58 | + c.getId() 59 | + "', repository:'repo', traits: [discoverBranches()]), " 60 | + "targets:['foo']\n" 61 | + " checkout tests\n" 62 | + " if (!fileExists('new-file.txt')) { error 'wrong branch checked out' }\n" 63 | + "}", true)); 64 | j.buildAndAssertSuccess(job); 65 | } 66 | } 67 | 68 | @Test 69 | public void given_nonExistingHeadName_when_invokedIgnoringErrors_then_nullReturned() throws Exception { 70 | try (MockSCMController c = MockSCMController.create()) { 71 | c.createRepository("repo"); 72 | c.createBranch("repo", "foo"); 73 | WorkflowJob job = j.jenkins.createProject(WorkflowJob.class, "workflow"); 74 | job.setDefinition(new CpsFlowDefinition("node {\n" 75 | + " def tests = resolveScm source: mockScm(controllerId:'" 76 | + c.getId() 77 | + "', repository:'repo', traits: [discoverBranches()]), " 78 | + "targets:['bar'], ignoreErrors: true\n" 79 | + " if (tests != null) { error \"resolved as ${tests}\"}\n" 80 | + "}", true)); 81 | j.buildAndAssertSuccess(job); 82 | } 83 | } 84 | 85 | @Test 86 | public void given_nonExistingHeadName_when_invoked_then_abortThrown() throws Exception { 87 | try (MockSCMController c = MockSCMController.create()) { 88 | c.createRepository("repo"); 89 | c.createBranch("repo", "foo"); 90 | WorkflowJob job = j.jenkins.createProject(WorkflowJob.class, "workflow"); 91 | job.setDefinition(new CpsFlowDefinition("node {\n" 92 | + " def ok = true\n" 93 | + " try {\n" 94 | + " def tests = resolveScm source: mockScm(controllerId:'" 95 | + c.getId() 96 | + "', repository:'repo', traits: [discoverBranches()]), " 97 | + "targets:['bar']\n" 98 | + " ok = false\n" 99 | + " } catch (e) {}\n" 100 | + " if (!ok) { error 'abort not thrown' }\n" 101 | + "}", true)); 102 | j.buildAndAssertSuccess(job); 103 | } 104 | } 105 | 106 | @Test 107 | public void given_nonExistingHeadName_when_invokedWithDefault_then_defaultReturned() throws Exception { 108 | try (MockSCMController c = MockSCMController.create()) { 109 | c.createRepository("repo"); 110 | c.createBranch("repo", "manchu"); 111 | c.addFile("repo", "manchu", "Add file", "new-file.txt", "content".getBytes()); 112 | WorkflowJob job = j.jenkins.createProject(WorkflowJob.class, "workflow"); 113 | job.setDefinition(new CpsFlowDefinition("node {\n" 114 | + " def tests = resolveScm source: mockScm(controllerId:'" 115 | + c.getId() 116 | + "', repository:'repo', traits: [discoverBranches()]), " 117 | + "targets:['bar', 'manchu']\n" 118 | + " checkout tests\n" 119 | + " if (!fileExists('new-file.txt')) { error 'wrong branch checked out' }\n" 120 | + "}", true)); 121 | j.buildAndAssertSuccess(job); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/workflow/multibranch/SCMBinderTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2015 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.multibranch; 26 | 27 | import com.cloudbees.hudson.plugins.folder.computed.DefaultOrphanedItemStrategy; 28 | import edu.umd.cs.findbugs.annotations.NonNull; 29 | import hudson.Util; 30 | import hudson.model.Item; 31 | import hudson.model.Result; 32 | import hudson.model.TaskListener; 33 | import hudson.model.User; 34 | import hudson.plugins.git.util.BuildData; 35 | import hudson.scm.ChangeLogSet; 36 | import java.io.File; 37 | import java.io.IOException; 38 | import java.util.HashSet; 39 | import java.util.Iterator; 40 | import java.util.List; 41 | import java.util.Set; 42 | import java.util.TreeSet; 43 | import jenkins.branch.BranchSource; 44 | import jenkins.model.Jenkins; 45 | import jenkins.plugins.git.GitBranchSCMRevision; 46 | import jenkins.plugins.git.GitSCMSource; 47 | import jenkins.plugins.git.GitSampleRepoRule; 48 | import jenkins.scm.api.SCMHead; 49 | import jenkins.scm.api.SCMRevision; 50 | import jenkins.scm.api.SCMRevisionAction; 51 | import jenkins.scm.api.SCMSourceDescriptor; 52 | import static org.hamcrest.Matchers.*; 53 | 54 | import org.springframework.security.core.Authentication; 55 | import org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval; 56 | import org.jenkinsci.plugins.workflow.job.WorkflowJob; 57 | import org.jenkinsci.plugins.workflow.job.WorkflowRun; 58 | import org.jenkinsci.plugins.workflow.test.steps.SemaphoreStep; 59 | import org.junit.Test; 60 | import static org.junit.Assert.*; 61 | import org.junit.ClassRule; 62 | import org.junit.Rule; 63 | import org.jvnet.hudson.test.BuildWatcher; 64 | import org.jvnet.hudson.test.Issue; 65 | import org.jvnet.hudson.test.JenkinsRule; 66 | 67 | public class SCMBinderTest { 68 | 69 | @ClassRule public static BuildWatcher buildWatcher = new BuildWatcher(); 70 | @Rule public JenkinsRule r = new JenkinsRule(); 71 | @Rule public GitSampleRepoRule sampleGitRepo = new GitSampleRepoRule(); 72 | 73 | @Test public void exactRevisionGit() throws Exception { 74 | sampleGitRepo.init(); 75 | ScriptApproval sa = ScriptApproval.get(); 76 | sa.approveSignature("staticField hudson.model.Items XSTREAM2"); 77 | sa.approveSignature("method com.thoughtworks.xstream.XStream toXML java.lang.Object"); 78 | sampleGitRepo.write("Jenkinsfile", "echo hudson.model.Items.XSTREAM2.toXML(scm); semaphore 'wait'; node {checkout scm; echo readFile('file')}"); 79 | sampleGitRepo.write("file", "initial content"); 80 | sampleGitRepo.git("add", "Jenkinsfile"); 81 | sampleGitRepo.git("commit", "--all", "--message=flow"); 82 | WorkflowMultiBranchProject mp = r.jenkins.createProject(WorkflowMultiBranchProject.class, "p"); 83 | mp.getSourcesList().add(new BranchSource(new GitSCMSource(null, sampleGitRepo.toString(), "", "*", "", false))); 84 | WorkflowJob p = WorkflowMultiBranchProjectTest.scheduleAndFindBranchProject(mp, "master"); 85 | SemaphoreStep.waitForStart("wait/1", null); 86 | WorkflowRun b1 = p.getLastBuild(); 87 | assertNotNull(b1); 88 | assertEquals(1, b1.getNumber()); 89 | assertRevisionAction(b1); 90 | r.assertLogContains("Obtained Jenkinsfile from ", b1); 91 | sampleGitRepo.write("Jenkinsfile", "node {checkout scm; echo readFile('file').toUpperCase()}"); 92 | sampleGitRepo.write("file", "subsequent content"); 93 | sampleGitRepo.git("commit", "--all", "--message=tweaked"); 94 | SemaphoreStep.success("wait/1", null); 95 | sampleGitRepo.notifyCommit(r); 96 | WorkflowRun b2 = p.getLastBuild(); 97 | assertEquals(2, b2.getNumber()); 98 | r.assertLogContains("initial content", r.waitForCompletion(b1)); 99 | r.assertLogContains("SUBSEQUENT CONTENT", b2); 100 | assertRevisionAction(b2); 101 | WorkflowMultiBranchProjectTest.showIndexing(mp); 102 | List> changeSets = b2.getChangeSets(); 103 | assertEquals(1, changeSets.size()); 104 | ChangeLogSet changeSet = changeSets.get(0); 105 | assertEquals(b2, changeSet.getRun()); 106 | assertEquals("git", changeSet.getKind()); 107 | Iterator iterator = changeSet.iterator(); 108 | assertTrue(iterator.hasNext()); 109 | ChangeLogSet.Entry entry = iterator.next(); 110 | assertEquals("tweaked", entry.getMsg()); 111 | assertEquals("[Jenkinsfile, file]", new TreeSet<>(entry.getAffectedPaths()).toString()); 112 | assertFalse(iterator.hasNext()); 113 | } 114 | 115 | public static void assertRevisionAction(WorkflowRun build) { 116 | SCMRevisionAction revisionAction = build.getAction(SCMRevisionAction.class); 117 | assertNotNull(revisionAction); 118 | SCMRevision revision = revisionAction.getRevision(); 119 | assertEquals(GitBranchSCMRevision.class, revision.getClass()); 120 | Set expected = new HashSet<>(); 121 | List buildDataActions = build.getActions(BuildData.class); 122 | if (!buildDataActions.isEmpty()) { // i.e., we have run at least one checkout step, or done a heavyweight checkout to get a single file 123 | for (BuildData data : buildDataActions) { 124 | expected.add(data.lastBuild.marked.getSha1().getName()); 125 | } 126 | assertThat(expected, hasItem(((GitBranchSCMRevision) revision).getHash())); 127 | } 128 | } 129 | 130 | @Test public void deletedJenkinsfile() throws Exception { 131 | sampleGitRepo.init(); 132 | sampleGitRepo.write("Jenkinsfile", "node { echo 'Hello World' }"); 133 | sampleGitRepo.git("add", "Jenkinsfile"); 134 | sampleGitRepo.git("commit", "--all", "--message=flow"); 135 | WorkflowMultiBranchProject mp = r.jenkins.createProject(WorkflowMultiBranchProject.class, "p"); 136 | mp.getSourcesList().add(new BranchSource(new GitSCMSource(null, sampleGitRepo.toString(), "", "*", "", false))); 137 | WorkflowJob p = WorkflowMultiBranchProjectTest.scheduleAndFindBranchProject(mp, "master"); 138 | assertEquals(1, mp.getItems().size()); 139 | r.waitUntilNoActivity(); 140 | WorkflowRun b1 = p.getLastBuild(); 141 | assertEquals(1, b1.getNumber()); 142 | sampleGitRepo.git("rm", "Jenkinsfile"); 143 | sampleGitRepo.git("commit", "--all", "--message=remove"); 144 | WorkflowRun b2 = r.assertBuildStatus(Result.FAILURE, p.scheduleBuild2(0).get()); 145 | r.assertLogContains("Jenkinsfile not found", b2); 146 | } 147 | 148 | @Issue("JENKINS-40521") 149 | @Test public void deletedBranch() throws Exception { 150 | r.jenkins.setSecurityRealm(r.createDummySecurityRealm()); 151 | sampleGitRepo.init(); 152 | // TODO GitSCMSource offers no way to set a GitSCMExtension such as CleanBeforeCheckout; work around with deleteDir 153 | // (without cleaning, b2 will succeed since the workspace will still have a cached origin/feature ref) 154 | sampleGitRepo.write("Jenkinsfile", "node {deleteDir(); checkout scm; echo 'Hello World'}"); 155 | sampleGitRepo.git("add", "Jenkinsfile"); 156 | sampleGitRepo.git("commit", "--all", "--message=flow"); 157 | sampleGitRepo.git("checkout", "-b", "feature"); 158 | sampleGitRepo.write("somefile", "stuff"); 159 | sampleGitRepo.git("add", "somefile"); 160 | sampleGitRepo.git("commit", "--all", "--message=tweaked"); 161 | WorkflowMultiBranchProject mp = r.jenkins.createProject(WorkflowMultiBranchProject.class, "p"); 162 | mp.getSourcesList().add(new BranchSource(new GitSCMSource(null, sampleGitRepo.toString(), "", "*", "", false))); 163 | mp.setOrphanedItemStrategy(new DefaultOrphanedItemStrategy(false, "", "")); 164 | WorkflowJob p = WorkflowMultiBranchProjectTest.scheduleAndFindBranchProject(mp, "feature"); 165 | assertEquals(2, mp.getItems().size()); 166 | r.waitUntilNoActivity(); 167 | WorkflowRun b1 = p.getLastBuild(); 168 | assertEquals(1, b1.getNumber()); 169 | Authentication auth = User.getById("dev", true).impersonate2(); 170 | assertFalse(p.getACL().hasPermission2(auth, Item.DELETE)); 171 | assertTrue(p.isBuildable()); 172 | sampleGitRepo.git("checkout", "master"); 173 | sampleGitRepo.git("branch", "-D", "feature"); 174 | { // TODO AbstractGitSCMSource.retrieve(SCMHead, TaskListener) is incorrect: after fetching remote refs into the cache, 175 | // the origin/feature ref remains locally even though it has been deleted upstream, since only the other overload prunes stale remotes: 176 | Util.deleteRecursive(new File(r.jenkins.getRootDir(), "caches")); 177 | } 178 | WorkflowRun b2 = r.assertBuildStatus(Result.FAILURE, p.scheduleBuild2(0).get()); 179 | r.assertLogContains("nondeterministic checkout", b2); // SCMBinder 180 | r.assertLogContains("Could not determine exact tip revision of feature", b2); // SCMVar 181 | mp.scheduleBuild2(0).getFuture().get(); 182 | WorkflowMultiBranchProjectTest.showIndexing(mp); 183 | assertEquals(2, mp.getItems().size()); 184 | assertTrue(p.getACL().hasPermission2(auth, Item.DELETE)); 185 | assertFalse(p.isBuildable()); 186 | mp.setOrphanedItemStrategy(new DefaultOrphanedItemStrategy(true, "", "0")); 187 | mp.scheduleBuild2(0).getFuture().get(); 188 | WorkflowMultiBranchProjectTest.showIndexing(mp); 189 | assertEquals(1, mp.getItems().size()); 190 | } 191 | 192 | @Test public void untrustedRevisions() throws Exception { 193 | sampleGitRepo.init(); 194 | sampleGitRepo.write("Jenkinsfile", "node {checkout scm; echo readFile('file')}"); 195 | sampleGitRepo.write("file", "initial content"); 196 | sampleGitRepo.git("add", "Jenkinsfile"); 197 | sampleGitRepo.git("commit", "--all", "--message=flow"); 198 | WorkflowMultiBranchProject mp = r.jenkins.createProject(WorkflowMultiBranchProject.class, "p"); 199 | mp.getSourcesList().add(new BranchSource(new WarySource(null, sampleGitRepo.toString(), "", "*", "", false))); 200 | WorkflowJob p = WorkflowMultiBranchProjectTest.scheduleAndFindBranchProject(mp, "master"); 201 | r.waitUntilNoActivity(); 202 | WorkflowRun b = p.getLastBuild(); 203 | assertNotNull(b); 204 | assertEquals(1, b.getNumber()); 205 | assertRevisionAction(b); 206 | r.assertBuildStatusSuccess(b); 207 | r.assertLogContains("initial content", b); 208 | String branch = "some-other-branch-from-Norway"; 209 | sampleGitRepo.git("checkout", "-b", branch); 210 | sampleGitRepo.write("Jenkinsfile", "error 'ALL YOUR BUILD STEPS ARE BELONG TO US'"); 211 | sampleGitRepo.write("file", "subsequent content"); 212 | sampleGitRepo.git("commit", "--all", "--message=big evil laugh"); 213 | p = WorkflowMultiBranchProjectTest.scheduleAndFindBranchProject(mp, branch); 214 | r.waitUntilNoActivity(); 215 | b = p.getLastBuild(); 216 | assertNotNull(b); 217 | assertEquals(1, b.getNumber()); 218 | assertRevisionAction(b); 219 | r.assertBuildStatusSuccess(b); 220 | r.assertLogContains(Messages.ReadTrustedStep__has_been_modified_in_an_untrusted_revis("Jenkinsfile"), b); 221 | r.assertLogContains("subsequent content", b); 222 | r.assertLogContains("not trusting", b); 223 | } 224 | public static class WarySource extends GitSCMSource { 225 | public WarySource(String id, String remote, String credentialsId, String includes, String excludes, boolean ignoreOnPushNotifications) { 226 | super(id, remote, credentialsId, includes, excludes, ignoreOnPushNotifications); 227 | } 228 | @Override public SCMRevision getTrustedRevision(@NonNull SCMRevision revision, @NonNull TaskListener listener) throws IOException, InterruptedException { 229 | String branch = revision.getHead().getName(); 230 | if (branch.equals("master")) { 231 | return revision; 232 | } else { 233 | listener.getLogger().println("not trusting " + branch); 234 | return fetch(new SCMHead("master"), listener); 235 | } 236 | } 237 | @Override public SCMSourceDescriptor getDescriptor() { 238 | return Jenkins.get().getDescriptorByType(GitSCMSource.DescriptorImpl.class); 239 | } 240 | } 241 | 242 | } 243 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/workflow/multibranch/SCMVarTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2015 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.multibranch; 26 | 27 | import hudson.FilePath; 28 | import hudson.model.Run; 29 | import hudson.model.TaskListener; 30 | import java.io.File; 31 | import java.nio.file.Files; 32 | import java.util.Arrays; 33 | import jenkins.branch.BranchProperty; 34 | import jenkins.branch.BranchSource; 35 | import jenkins.branch.DefaultBranchPropertyStrategy; 36 | import jenkins.model.Jenkins; 37 | import jenkins.plugins.git.GitSCMSource; 38 | import jenkins.plugins.git.GitSampleRepoRule; 39 | import jenkins.plugins.git.GitStep; 40 | import org.apache.commons.io.FileUtils; 41 | import org.jenkinsci.plugins.workflow.cps.CpsScmFlowDefinition; 42 | import org.jenkinsci.plugins.workflow.job.WorkflowJob; 43 | import org.jenkinsci.plugins.workflow.job.WorkflowRun; 44 | import org.jenkinsci.plugins.workflow.libs.GlobalLibraries; 45 | import org.jenkinsci.plugins.workflow.libs.LibraryConfiguration; 46 | import org.jenkinsci.plugins.workflow.libs.LibraryRetriever; 47 | import org.jenkinsci.plugins.workflow.test.steps.SemaphoreStep; 48 | import org.junit.Test; 49 | import static org.junit.Assert.*; 50 | import org.junit.ClassRule; 51 | import org.junit.Rule; 52 | import org.jvnet.hudson.test.BuildWatcher; 53 | import org.jvnet.hudson.test.Issue; 54 | import org.jvnet.hudson.test.JenkinsSessionRule; 55 | 56 | public class SCMVarTest { 57 | 58 | @ClassRule public static BuildWatcher buildWatcher = new BuildWatcher(); 59 | @Rule public JenkinsSessionRule story = new JenkinsSessionRule(); 60 | @Rule public GitSampleRepoRule sampleGitRepo = new GitSampleRepoRule(); 61 | 62 | @Test public void scmPickle() throws Throwable { 63 | story.then(j -> { 64 | sampleGitRepo.init(); 65 | sampleGitRepo.write("Jenkinsfile", "def _scm = scm; semaphore 'wait'; node {checkout _scm; echo readFile('file')}"); 66 | sampleGitRepo.write("file", "initial content"); 67 | sampleGitRepo.git("add", "Jenkinsfile"); 68 | sampleGitRepo.git("commit", "--all", "--message=flow"); 69 | WorkflowMultiBranchProject mp = j.jenkins.createProject(WorkflowMultiBranchProject.class, "p"); 70 | mp.getSourcesList().add(new BranchSource(new GitSCMSource(null, sampleGitRepo.toString(), "", "*", "", false), new DefaultBranchPropertyStrategy(new BranchProperty[0]))); 71 | WorkflowJob p = WorkflowMultiBranchProjectTest.scheduleAndFindBranchProject(mp, "master"); 72 | SemaphoreStep.waitForStart("wait/1", null); 73 | WorkflowRun b1 = p.getLastBuild(); 74 | assertNotNull(b1); 75 | }); 76 | story.then(j -> { 77 | SemaphoreStep.success("wait/1", null); 78 | WorkflowJob p = j.jenkins.getItemByFullName("p/master", WorkflowJob.class); 79 | assertNotNull(p); 80 | WorkflowRun b1 = p.getLastBuild(); 81 | assertNotNull(b1); 82 | assertEquals(1, b1.getNumber()); 83 | j.assertLogContains("initial content", j.waitForCompletion(b1)); 84 | SCMBinderTest.assertRevisionAction(b1); 85 | }); 86 | } 87 | 88 | @Issue("JENKINS-30222") 89 | @Test public void globalVariable() throws Throwable { 90 | story.then(j -> { 91 | // Set up a standardJob definition: 92 | File lib = new File(Jenkins.get().getRootDir(), "somelib"); 93 | LibraryConfiguration cfg = new LibraryConfiguration("somelib", new LocalRetriever(lib)); 94 | cfg.setImplicit(true); 95 | cfg.setDefaultVersion("fixed"); 96 | GlobalLibraries.get().setLibraries(Arrays.asList(cfg)); 97 | File vars = new File(lib, "vars"); 98 | Files.createDirectories(vars.toPath()); 99 | FileUtils.writeStringToFile(new File(vars, "standardJob.groovy"), 100 | "def call(body) {\n" + 101 | " def config = [:]\n" + 102 | " body.resolveStrategy = Closure.DELEGATE_FIRST\n" + 103 | " body.delegate = config\n" + 104 | " body()\n" + 105 | " node {\n" + 106 | " checkout scm\n" + 107 | " echo \"loaded ${readFile config.file}\"\n" + 108 | " }\n" + 109 | "}\n"); 110 | // Then a project using it: 111 | sampleGitRepo.init(); 112 | sampleGitRepo.write("Jenkinsfile", "standardJob {file = 'resource'}"); 113 | sampleGitRepo.write("resource", "resource content"); 114 | sampleGitRepo.git("add", "Jenkinsfile"); 115 | sampleGitRepo.git("add", "resource"); 116 | sampleGitRepo.git("commit", "--all", "--message=flow"); 117 | // And run: 118 | WorkflowMultiBranchProject mp = j.jenkins.createProject(WorkflowMultiBranchProject.class, "p"); 119 | mp.getSourcesList().add(new BranchSource(new GitSCMSource(null, sampleGitRepo.toString(), "", "*", "", false), new DefaultBranchPropertyStrategy(new BranchProperty[0]))); 120 | WorkflowJob p = WorkflowMultiBranchProjectTest.scheduleAndFindBranchProject(mp, "master"); 121 | WorkflowRun b = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); 122 | j.assertLogContains("loaded resource content", b); 123 | }); 124 | } 125 | 126 | // TODO copied from GrapeTest along with body of libroot(); could make sense as a *-tests.jar utility 127 | private static final class LocalRetriever extends LibraryRetriever { 128 | private final File lib; 129 | LocalRetriever(File lib) { 130 | this.lib = lib; 131 | } 132 | @Override public void retrieve(String name, String version, boolean changelog, FilePath target, Run run, TaskListener listener) throws Exception { 133 | new FilePath(lib).copyRecursiveTo(target); 134 | } 135 | @Override public void retrieve(String name, String version, FilePath target, Run run, TaskListener listener) throws Exception { 136 | retrieve(name, version, false, target, run, listener); 137 | } 138 | } 139 | 140 | @Issue("JENKINS-31386") 141 | @Test public void standaloneProject() throws Throwable { 142 | story.then(j -> { 143 | sampleGitRepo.init(); 144 | sampleGitRepo.write("Jenkinsfile", "node {checkout scm; echo readFile('file')}"); 145 | sampleGitRepo.write("file", "some content"); 146 | sampleGitRepo.git("add", "Jenkinsfile"); 147 | sampleGitRepo.git("commit", "--all", "--message=flow"); 148 | WorkflowJob p = j.jenkins.createProject(WorkflowJob.class, "p"); 149 | p.setDefinition(new CpsScmFlowDefinition(new GitStep(sampleGitRepo.toString()).createSCM(), "Jenkinsfile")); 150 | WorkflowRun b = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); 151 | j.assertLogContains("some content", b); 152 | }); 153 | } 154 | 155 | } 156 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/workflow/multibranch/WorkflowBranchProjectFactoryTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2015 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.multibranch; 26 | 27 | import java.io.File; 28 | import java.util.Collections; 29 | import jenkins.branch.BranchSource; 30 | import jenkins.plugins.git.GitSCMSource; 31 | import jenkins.plugins.git.GitSampleRepoRule; 32 | import jenkins.plugins.git.traits.BranchDiscoveryTrait; 33 | import org.jenkinsci.plugins.workflow.job.WorkflowJob; 34 | import org.jenkinsci.plugins.workflow.job.WorkflowRun; 35 | import static org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProjectTest.scheduleAndFindBranchProject; 36 | import org.junit.Test; 37 | import static org.junit.Assert.*; 38 | import org.junit.ClassRule; 39 | import org.junit.Rule; 40 | import org.jvnet.hudson.test.BuildWatcher; 41 | import org.jvnet.hudson.test.Issue; 42 | import org.jvnet.hudson.test.JenkinsRule; 43 | import org.jvnet.hudson.test.JenkinsSessionRule; 44 | 45 | public class WorkflowBranchProjectFactoryTest { 46 | 47 | @ClassRule public static BuildWatcher buildWatcher = new BuildWatcher(); 48 | @Rule public JenkinsSessionRule story = new JenkinsSessionRule(); 49 | @Rule public GitSampleRepoRule sampleRepo = new GitSampleRepoRule(); 50 | 51 | @Issue("JENKINS-30744") 52 | @Test public void slashyBranches() throws Throwable { 53 | story.then(j -> { 54 | sampleRepo.init(); 55 | sampleRepo.git("checkout", "-b", "dev/main"); 56 | String script = 57 | "echo \"branch=${env.BRANCH_NAME}\"\n" + 58 | "node {\n" + 59 | " checkout scm\n" + 60 | " echo \"workspace=${pwd().replaceFirst('.+dev', 'dev')}\"\n" + 61 | "}"; 62 | sampleRepo.write("Jenkinsfile", script); 63 | sampleRepo.git("add", "Jenkinsfile"); 64 | sampleRepo.git("commit", "--all", "--message=flow"); 65 | WorkflowMultiBranchProject mp = j.jenkins.createProject(WorkflowMultiBranchProject.class, "p"); 66 | GitSCMSource source = new GitSCMSource(sampleRepo.toString()); 67 | source.setTraits(Collections.singletonList(new BranchDiscoveryTrait())); 68 | mp.getSourcesList().add(new BranchSource(source)); 69 | WorkflowJob p = scheduleAndFindBranchProject(mp, "dev%2Fmain"); 70 | assertEquals(1, mp.getItems().size()); 71 | j.waitUntilNoActivity(); 72 | WorkflowRun b1 = p.getLastBuild(); 73 | assertEquals(1, b1.getNumber()); 74 | j.assertLogContains("branch=dev/main", b1); 75 | j.assertLogContains("workspace=dev_main", b1); 76 | verifyProject(j, p); 77 | sampleRepo.write("Jenkinsfile", script.replace("branch=", "Branch=")); 78 | }); 79 | story.then(j -> { 80 | WorkflowJob p = j.jenkins.getItemByFullName("p/dev%2Fmain", WorkflowJob.class); 81 | assertNotNull(p); 82 | sampleRepo.git("commit", "--all", "--message=Flow"); 83 | sampleRepo.notifyCommit(j); 84 | WorkflowRun b2 = p.getLastBuild(); 85 | assertEquals(2, b2.getNumber()); 86 | j.assertLogContains("Branch=dev/main", b2); 87 | j.assertLogContains("workspace=dev_main", b2); 88 | verifyProject(j, p); 89 | }); 90 | } 91 | private static void verifyProject(JenkinsRule j, WorkflowJob p) throws Exception { 92 | assertEquals("dev%2Fmain", p.getName()); 93 | assertEquals("dev/main", p.getDisplayName()); 94 | assertEquals("p/dev%2Fmain", p.getFullName()); 95 | assertEquals("p » dev/main", p.getFullDisplayName()); 96 | j.createWebClient().getPage(p); 97 | assertEquals(new File(new File(p.getParent().getRootDir(), "branches"), "dev-main.k31kdj"), p.getRootDir()); 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/workflow/multibranch/WorkflowMultiBranchProjectFactoryTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2015 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.multibranch; 26 | 27 | import hudson.model.Item; 28 | import hudson.model.User; 29 | import hudson.model.View; 30 | import hudson.security.ACL; 31 | import hudson.security.FullControlOnceLoggedInAuthorizationStrategy; 32 | import java.io.File; 33 | import java.util.List; 34 | import jenkins.branch.MultiBranchProject; 35 | import jenkins.branch.OrganizationFolder; 36 | import jenkins.plugins.git.GitSampleRepoRule; 37 | import jenkins.scm.api.SCMSource; 38 | import org.springframework.security.core.Authentication; 39 | import static org.hamcrest.Matchers.*; 40 | import org.jenkinsci.plugins.workflow.job.WorkflowJob; 41 | import org.jenkinsci.plugins.workflow.job.WorkflowRun; 42 | import static org.junit.Assert.*; 43 | import org.junit.ClassRule; 44 | import org.junit.Rule; 45 | import org.junit.Test; 46 | import org.junit.rules.TemporaryFolder; 47 | import org.jvnet.hudson.test.BuildWatcher; 48 | import org.jvnet.hudson.test.Issue; 49 | import org.jvnet.hudson.test.JenkinsRule; 50 | 51 | public class WorkflowMultiBranchProjectFactoryTest { 52 | 53 | @ClassRule public static BuildWatcher buildWatcher = new BuildWatcher(); 54 | @Rule public JenkinsRule r = new JenkinsRule(); 55 | @Rule public GitSampleRepoRule sampleRepo1 = new GitSampleRepoRule(); 56 | @Rule public GitSampleRepoRule sampleRepo2 = new GitSampleRepoRule(); 57 | @Rule public GitSampleRepoRule sampleRepo3 = new GitSampleRepoRule(); 58 | @Rule public TemporaryFolder tmp = new TemporaryFolder(); 59 | 60 | @Test public void smokes() throws Exception { 61 | File clones = tmp.newFolder(); 62 | sampleRepo1.init(); 63 | sampleRepo1.write(WorkflowBranchProjectFactory.SCRIPT, "echo 'ran one'"); 64 | sampleRepo1.git("add", WorkflowBranchProjectFactory.SCRIPT); 65 | sampleRepo1.git("commit", "--all", "--message=flow"); 66 | sampleRepo1.git("clone", ".", new File(clones, "one").getAbsolutePath()); 67 | sampleRepo3.init(); // but do not write SCRIPT, so should be ignored 68 | sampleRepo3.git("clone", ".", new File(clones, "three").getAbsolutePath()); 69 | r.jenkins.setSecurityRealm(r.createDummySecurityRealm()); 70 | r.jenkins.setAuthorizationStrategy(new FullControlOnceLoggedInAuthorizationStrategy()); 71 | OrganizationFolder top = r.jenkins.createProject(OrganizationFolder.class, "top"); 72 | top.getNavigators().add(new GitDirectorySCMNavigator(clones.getAbsolutePath())); 73 | // Make sure we created one multibranch projects: 74 | top.scheduleBuild2(0).getFuture().get(); 75 | top.getComputation().writeWholeLogTo(System.out); 76 | assertEquals(1, top.getItems().size()); 77 | MultiBranchProject one = top.getItem("one"); 78 | assertThat(one, is(instanceOf(WorkflowMultiBranchProject.class))); 79 | // Check that it has Git configured: 80 | List sources = one.getSCMSources(); 81 | assertEquals(1, sources.size()); 82 | assertEquals("GitSCMSource", sources.get(0).getClass().getSimpleName()); 83 | // Verify permissions: 84 | Authentication admin = User.getById("admin", true).impersonate2(); 85 | ACL acl = one.getACL(); 86 | assertTrue(acl.hasPermission2(ACL.SYSTEM2, Item.CONFIGURE)); 87 | assertTrue(acl.hasPermission2(ACL.SYSTEM2, Item.DELETE)); 88 | assertFalse(acl.hasPermission2(admin, Item.CONFIGURE)); 89 | assertFalse(acl.hasPermission2(admin, View.CONFIGURE)); 90 | assertFalse(acl.hasPermission2(admin, View.CREATE)); 91 | assertFalse(acl.hasPermission2(admin, View.DELETE)); 92 | assertFalse(acl.hasPermission2(admin, Item.DELETE)); 93 | assertTrue(acl.hasPermission2(admin, Item.EXTENDED_READ)); 94 | assertTrue(acl.hasPermission2(admin, Item.READ)); 95 | assertTrue(acl.hasPermission2(admin, View.READ)); 96 | // Check that the master branch project works: 97 | r.waitUntilNoActivity(); 98 | WorkflowJob p = WorkflowMultiBranchProjectTest.findBranchProject((WorkflowMultiBranchProject) one, "master"); 99 | WorkflowRun b1 = p.getLastBuild(); 100 | assertEquals(1, b1.getNumber()); 101 | r.assertLogContains("ran one", b1); 102 | // Then add a second checkout and reindex: 103 | sampleRepo2.init(); 104 | sampleRepo2.write(WorkflowBranchProjectFactory.SCRIPT, "echo 'ran two'"); 105 | sampleRepo2.git("add", WorkflowBranchProjectFactory.SCRIPT); 106 | sampleRepo2.git("commit", "--all", "--message=flow"); 107 | sampleRepo2.git("clone", ".", new File(clones, "two").getAbsolutePath()); 108 | top.scheduleBuild2(0).getFuture().get(); 109 | top.getComputation().writeWholeLogTo(System.out); 110 | assertEquals(2, top.getItems().size()); 111 | // Same for another one: 112 | MultiBranchProject two = top.getItem("two"); 113 | assertThat(two, is(instanceOf(WorkflowMultiBranchProject.class))); 114 | r.waitUntilNoActivity(); 115 | p = WorkflowMultiBranchProjectTest.findBranchProject((WorkflowMultiBranchProject) two, "master"); 116 | b1 = p.getLastBuild(); 117 | assertEquals(1, b1.getNumber()); 118 | r.assertLogContains("ran two", b1); 119 | // JENKINS-34246: also delete Jenkinsfile 120 | sampleRepo2.git("rm", WorkflowBranchProjectFactory.SCRIPT); 121 | sampleRepo2.git("commit", "--message=noflow"); 122 | top.scheduleBuild2(0).getFuture().get(); 123 | top.getComputation().writeWholeLogTo(System.out); 124 | assertEquals(1, top.getItems().size()); 125 | } 126 | 127 | @Issue("JENKINS-34561") 128 | @Test public void configuredScriptName() throws Exception { 129 | String alternativeJenkinsFileName = "alternative_Jenkinsfile_name.groovy"; 130 | 131 | File clones = tmp.newFolder(); 132 | sampleRepo1.init(); 133 | sampleRepo1.write(WorkflowBranchProjectFactory.SCRIPT, 134 | "echo 'echo from " + WorkflowBranchProjectFactory.SCRIPT + "'"); 135 | sampleRepo1.git("add", WorkflowBranchProjectFactory.SCRIPT); 136 | sampleRepo1.git("commit", "--all", "--message=flow"); 137 | String repoWithJenkinsfile = "repo_with_jenkinsfile"; 138 | sampleRepo1.git("clone", ".", new File(clones, repoWithJenkinsfile).getAbsolutePath()); 139 | 140 | r.jenkins.setSecurityRealm(r.createDummySecurityRealm()); 141 | r.jenkins.setAuthorizationStrategy(new FullControlOnceLoggedInAuthorizationStrategy()); 142 | OrganizationFolder top = r.jenkins.createProject(OrganizationFolder.class, "top"); 143 | top.getNavigators().add(new GitDirectorySCMNavigator(clones.getAbsolutePath())); 144 | top.scheduleBuild2(0).getFuture().get(); 145 | top.getComputation().writeWholeLogTo(System.out); 146 | assertEquals(1, top.getItems().size()); 147 | 148 | // Make sure we created one multibranch project: 149 | top.scheduleBuild2(0).getFuture().get(); 150 | top.getComputation().writeWholeLogTo(System.out); 151 | assertEquals(1, top.getItems().size()); 152 | MultiBranchProject projectFromJenkinsfile = top.getItem(repoWithJenkinsfile); 153 | assertThat(projectFromJenkinsfile, is(instanceOf(WorkflowMultiBranchProject.class))); 154 | 155 | // Check that the 'Jenkinsfile' project works: 156 | r.waitUntilNoActivity(); 157 | WorkflowJob p = WorkflowMultiBranchProjectTest.findBranchProject((WorkflowMultiBranchProject) projectFromJenkinsfile, "master"); 158 | WorkflowRun b1 = p.getLastBuild(); 159 | assertEquals(1, b1.getNumber()); 160 | r.assertLogContains("echo from Jenkinsfile", b1); 161 | 162 | // add second Project Recognizer with alternative Jenkinsfile name to organization folder 163 | WorkflowMultiBranchProjectFactory workflowMultiBranchProjectFactory = new WorkflowMultiBranchProjectFactory(); 164 | workflowMultiBranchProjectFactory.setScriptPath(alternativeJenkinsFileName); 165 | top.getProjectFactories().add(workflowMultiBranchProjectFactory); 166 | 167 | // Then add a second checkout and reindex: 168 | sampleRepo2.init(); 169 | sampleRepo2.write(alternativeJenkinsFileName, 170 | "echo 'echo from " + alternativeJenkinsFileName + "'"); 171 | sampleRepo2.git("add", alternativeJenkinsFileName); 172 | sampleRepo2.git("commit", "--all", "--message=flow"); 173 | String repoWithAlternativeJenkinsfile = "repo_with_alternative_jenkinsfile"; 174 | sampleRepo2.git("clone", ".", new File(clones, repoWithAlternativeJenkinsfile).getAbsolutePath()); 175 | 176 | // Make sure we created two multibranch projects: 177 | top.scheduleBuild2(0).getFuture().get(); 178 | top.getComputation().writeWholeLogTo(System.out); 179 | assertEquals(2, top.getItems().size()); 180 | 181 | // Check that the 'alternative_Jenkinsfile_name' project works: 182 | MultiBranchProject projectFromAlternativeJenkinsFile = top.getItem(repoWithAlternativeJenkinsfile); 183 | assertThat(projectFromAlternativeJenkinsFile, is(instanceOf(WorkflowMultiBranchProject.class))); 184 | r.waitUntilNoActivity(); 185 | p = WorkflowMultiBranchProjectTest.findBranchProject((WorkflowMultiBranchProject) projectFromAlternativeJenkinsFile, "master"); 186 | b1 = p.getLastBuild(); 187 | assertEquals(1, b1.getNumber()); 188 | r.assertLogContains("echo from alternative_Jenkinsfile_name", b1); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/workflow/multibranch/GitDirectorySCMNavigator/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/workflow/multibranch/JobPropertyStepTest/trackerPropertyUpgrade.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/workflow-multibranch-plugin/bb688f609ee91a5a2334caa75063d368cde173c5/src/test/resources/org/jenkinsci/plugins/workflow/multibranch/JobPropertyStepTest/trackerPropertyUpgrade.zip -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/workflow/multibranch/RepairBranchPropertyTest/removedPropertyAtStartup.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/workflow-multibranch-plugin/bb688f609ee91a5a2334caa75063d368cde173c5/src/test/resources/org/jenkinsci/plugins/workflow/multibranch/RepairBranchPropertyTest/removedPropertyAtStartup.zip -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/workflow/multibranch/WorkflowMultiBranchProjectTest/OldSCM/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | --------------------------------------------------------------------------------