├── .github
├── CODEOWNERS
├── dependabot.yml
└── release-drafter.yml
├── .gitignore
├── Jenkinsfile
├── LICENSE
├── README.md
├── examples
├── dynamic
│ └── Jenkinsfile
└── script
│ └── Jenkinsfile
├── images
└── config-driven-pipeline-project-recognizer.png
├── pom.xml
└── src
├── main
├── java
│ └── org
│ │ └── jenkinsci
│ │ └── plugins
│ │ └── workflow
│ │ └── multibranch
│ │ └── template
│ │ ├── ConfigDrivenWorkflowBranchProjectFactory.java
│ │ ├── ConfigDrivenWorkflowMultiBranchProjectFactory.java
│ │ ├── ConfigFileEnvironmentContributingAction.java
│ │ ├── ConfigFileSCMBinder.java
│ │ └── finder
│ │ ├── ConfigurationValueFinder.java
│ │ └── SupportedSCMFinder.java
└── resources
│ ├── index.jelly
│ └── org
│ └── jenkinsci
│ └── plugins
│ └── workflow
│ └── multibranch
│ └── template
│ ├── ConfigDrivenWorkflowBranchProjectFactory
│ ├── config.jelly
│ ├── getting-started.jelly
│ └── help-scriptPath.html
│ ├── ConfigDrivenWorkflowMultiBranchProjectFactory
│ ├── config.jelly
│ ├── getting-started.jelly
│ └── help-scriptPath.html
│ ├── ConfigFileSCMBinder
│ └── config.jelly
│ └── Messages.properties
└── test
└── java
└── org
└── jenkinsci
└── plugins
└── workflow
└── multibranch
└── template
└── finder
└── ConfigurationValueFinderTest.java
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @justinharringa @afalko
2 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: maven
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | open-pull-requests-limit: 10
8 |
--------------------------------------------------------------------------------
/.github/release-drafter.yml:
--------------------------------------------------------------------------------
1 | _extends: .github
2 | tag-template: config-driven-pipeline-$NEXT_MINOR_VERSION
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | target
2 | work
3 | .idea
4 | *.iml
5 |
--------------------------------------------------------------------------------
/Jenkinsfile:
--------------------------------------------------------------------------------
1 | buildPlugin()
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Justin Harringa
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 all
13 | 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 THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Config-Driven Pipeline Plugin
2 | [](LICENSE)
3 | [](https://plugins.jenkins.io/config-driven-pipeline)
4 | [](https://github.com/jenkinsci/config-driven-pipeline-plugin/releases/latest)
5 | [](https://plugins.jenkins.io/config-driven-pipeline)
6 |
7 | ## Purpose
8 | Would you like to share `Jenkinsfile` without copy-pasting in git (or other SCMs) but
9 | would also like to be able to have some variance in your `Jenkinsfile` (e.g. configuration
10 | values such as email address, different unit test scripts, etc...)?
11 |
12 | If so, this is the main driver of this plugin. We desired a central git-driven repository
13 | of trusted `Jenkinsfile` templates which are inherently visible, can be contributed to, but
14 | also allow us the ability to centrally roll out updates and improvements to hundreds of
15 | pipelines at a time.
16 |
17 | This plugin will select a Jenkinsfile based on config in the repository. What this means is
18 | that you can configure a whole GitHub Organization to use a Jenkinsfile repository and
19 | different repos can run different Jenkinsfiles in the central Jenkinsfile repo based on
20 | versioned configuration in the repo (no messing around with job configuration and it's all
21 | under version control). You simply point your
22 | [pipeline_template](https://github.com/jenkinsci/config-driven-pipeline-plugin#pipeline_template)
23 | to the path in the repo. This means you can switch between templates for different branches
24 | and test out new templates in PRs without having to muck around with job config.
25 |
26 | ## Setup
27 | This plugin provides you with a new Project Recognizer that you can use with any
28 | [Multibranch Pipeline type](https://jenkins.io/doc/book/pipeline/multibranch/#creating-a-multibranch-pipeline)
29 | such as a GitHub Organization or Multibranch Pipeline.
30 |
31 | 
32 |
33 | You'll simply set the `Config File Path` to a location where you expect the config file to reside in
34 | the repositories (traditionally at the root of the repo).
35 |
36 | ### pipeline_template
37 | The `pipeline_template` configuration key is reserved for finding the `Jenkinsfile` template you'd
38 | like to use out of the centralized `Jenkinsfile` repo.
39 |
40 | ### Config File Format - Most Any!
41 | The plugin itself is only going to search for a `pipeline_template` key/value in your Yaml, JSON,
42 | Java property file (and likely some others). This logic is in the `ConfigurationValueFinder` and
43 | we'd be happy to entertain additions to expand compatibility. We recommend using the
44 | [Pipeline Utility Steps Plugin](https://plugins.jenkins.io/pipeline-utility-steps) to parse your
45 | config but you're free to implement and validate this however you'd like in your `Jenkinsfile`
46 | templates.
47 |
48 | ## Config File Contents Available for Parsing!
49 | The plugin places the contents of the config file in the `PIPELINE_CONFIG` environment variable so
50 | that you don't have to read the file again.
51 |
52 | ## Why would I use this when I can use ${OTHER_SOLUTION}?
53 | ### Shared Libraries
54 | [Shared libraries](https://jenkins.io/doc/book/pipeline/shared-libraries/) are fantastic and
55 | are a great way to be able to make your pipeline code testable. However, it was nice to compose
56 | overall stages declaratively in Jenkinsfile and simply let each repo pass in configurable values
57 | such as the unit test command, Docker container to run under, etc...
58 |
59 | ### Buildpacks
60 | [Buildpacks](https://buildpacks.io/) are also awesome! However, sometimes there aren't quite the
61 | right buildpacks for your needs (and you could also use them within this :smile:).
62 |
63 | ## Upcoming Additions To This Repo
64 | * Example project configured via [Job DSL Plugin](https://plugins.jenkins.io/job-dsl)
65 | * More configuration information
66 | * Other shinies
67 | * FAQs?
68 |
--------------------------------------------------------------------------------
/examples/dynamic/Jenkinsfile:
--------------------------------------------------------------------------------
1 | def config = readYaml text: "${env.PIPELINE_CONFIG}"
2 | pipeline {
3 | agent {
4 | label "${config.agent_label}"
5 | }
6 | stages {
7 | stage("run run ANYTHING") {
8 | steps {
9 | echo "What to run: ${config.run_me}"
10 | sh "${config.run_me}"
11 | }
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/examples/script/Jenkinsfile:
--------------------------------------------------------------------------------
1 | def config = readYaml text: "${env.PIPELINE_CONFIG}"
2 | pipeline {
3 | agent any
4 | stages {
5 | stage("run run.sh") {
6 | steps {
7 | echo "Script to run: ${config.script_to_run}"
8 | sh "./${config.script_to_run}"
9 | }
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/images/config-driven-pipeline-project-recognizer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jenkinsci/config-driven-pipeline-plugin/62d19d6eddd45e6fc234eaba36ef8da17da190bb/images/config-driven-pipeline-project-recognizer.png
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 | configContents
to an environment variable called PIPELINE_CONFIG for a run with this
11 | * Action
12 | */
13 | @Extension
14 | public class ConfigFileEnvironmentContributingAction extends InvisibleAction implements EnvironmentContributingAction {
15 |
16 | public static final String PIPELINE_CONFIG = "PIPELINE_CONFIG";
17 | private String configContents;
18 |
19 | public ConfigFileEnvironmentContributingAction() {
20 | // @Extension annotated classes must have a public no-argument constructor
21 | super();
22 | }
23 |
24 | /**
25 | * Init with the configContents
which should be added to PIPELINE_CONFIG
26 | * @param configContents
27 | */
28 | public ConfigFileEnvironmentContributingAction(String configContents) {
29 | this.configContents = configContents;
30 | }
31 |
32 | @Override
33 | public void buildEnvironment(@Nonnull Run, ?> run, @Nonnull EnvVars env) {
34 | env.put(PIPELINE_CONFIG, configContents);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/workflow/multibranch/template/ConfigFileSCMBinder.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.template;
26 |
27 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
28 | import hudson.AbortException;
29 | import hudson.Extension;
30 | import hudson.FilePath;
31 | import hudson.Functions;
32 | import hudson.model.*;
33 | import hudson.scm.SCM;
34 | import hudson.slaves.WorkspaceList;
35 | import jenkins.branch.Branch;
36 | import jenkins.model.Jenkins;
37 | import jenkins.scm.api.*;
38 | import jenkins.scm.api.mixin.ChangeRequestSCMHead;
39 | import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
40 | import org.jenkinsci.plugins.workflow.flow.FlowDefinition;
41 | import org.jenkinsci.plugins.workflow.flow.FlowDefinitionDescriptor;
42 | import org.jenkinsci.plugins.workflow.flow.FlowExecution;
43 | import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner;
44 | import org.jenkinsci.plugins.workflow.job.WorkflowJob;
45 | import org.jenkinsci.plugins.workflow.job.WorkflowRun;
46 | import org.jenkinsci.plugins.workflow.multibranch.BranchJobProperty;
47 | import org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject;
48 | import org.jenkinsci.plugins.workflow.multibranch.template.finder.ConfigurationValueFinder;
49 | import org.jenkinsci.plugins.workflow.steps.scm.GenericSCMStep;
50 | import org.jenkinsci.plugins.workflow.steps.scm.SCMStep;
51 | import org.kohsuke.stapler.DataBoundConstructor;
52 |
53 | import java.io.FileNotFoundException;
54 | import java.io.IOException;
55 | import java.util.List;
56 |
57 | import static org.jenkinsci.plugins.workflow.multibranch.template.ConfigDrivenWorkflowBranchProjectFactory.PIPELINE_TEMPLATE;
58 | import static org.jenkinsci.plugins.workflow.multibranch.template.ConfigDrivenWorkflowBranchProjectFactory.USER_DEFINITION_PATH;
59 |
60 | /**
61 | * Checks out the desired version of {@link ConfigDrivenWorkflowBranchProjectFactory#USER_DEFINITION_PATH}.
62 | */
63 | class ConfigFileSCMBinder extends FlowDefinition {
64 |
65 | private String scriptPath;
66 | private SCM jenkinsFileScm;
67 |
68 | public Object readResolve() {
69 | if (this.scriptPath == null) {
70 | this.scriptPath = USER_DEFINITION_PATH;
71 | }
72 | return this;
73 | }
74 |
75 | @DataBoundConstructor public ConfigFileSCMBinder(String scriptPath, SCM jenkinsFileScm) {
76 | this.scriptPath = scriptPath;
77 | this.jenkinsFileScm = jenkinsFileScm;
78 | }
79 |
80 | public static final String INAPPROPRIATE_CONTEXT = "inappropriate context";
81 |
82 | @SuppressFBWarnings(value = "RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE",
83 | justification = "SCMFileSystem.of can return a null but spotbugs does not understand")
84 | @Override public FlowExecution create(FlowExecutionOwner handle, TaskListener listener, List extends Action> actions) throws Exception {
85 | Queue.Executable exec = handle.getExecutable();
86 | if (!(exec instanceof WorkflowRun)) {
87 | throw new IllegalStateException(INAPPROPRIATE_CONTEXT);
88 | }
89 | WorkflowRun build = (WorkflowRun) exec;
90 | WorkflowJob job = build.getParent();
91 | BranchJobProperty property = job.getProperty(BranchJobProperty.class);
92 | if (property == null) {
93 | throw new IllegalStateException(INAPPROPRIATE_CONTEXT);
94 | }
95 | Branch branch = property.getBranch();
96 | ItemGroup> parent = job.getParent();
97 | if (!(parent instanceof WorkflowMultiBranchProject)) {
98 | throw new IllegalStateException(INAPPROPRIATE_CONTEXT);
99 | }
100 | SCMSource scmSource = ((WorkflowMultiBranchProject) parent).getSCMSource(branch.getSourceId());
101 | if (scmSource == null) {
102 | throw new IllegalStateException(branch.getSourceId() + " not found");
103 | }
104 | SCMHead head = branch.getHead();
105 | SCMRevision tip = scmSource.fetch(head, listener);
106 | String script = null;
107 | String configContents;
108 | if (tip != null) {
109 | // TODO are we getting an extra "Could not update commit status." from here?
110 | build.addAction(new SCMRevisionAction(scmSource, tip));
111 | if (head instanceof ChangeRequestSCMHead) {
112 | // TODO evaluate if there's a better way to snag pull requests
113 | // Keep in mind that the pull request could be:
114 | // a) coming from the same repo (easy)
115 | // b) coming from a fork (why we defaulted to this...)
116 | SCM scm = scmSource.build(head, tip);
117 | listener.getLogger().println("Checking out " + scm.getKey() + " to read " + scriptPath);
118 | FilePath dir;
119 | Node node = Jenkins.getInstanceOrNull();
120 | if (node == null) {
121 | throw new IOException("Unable to communicate with Jenkins node");
122 | }
123 | FilePath baseWorkspace = node.getWorkspaceFor(build.getParent());
124 | if (baseWorkspace == null) {
125 | throw new IOException(node.getDisplayName() + " may be offline");
126 | }
127 | dir = baseWorkspace.withSuffix(
128 | System.getProperty(WorkspaceList.class.getName(), "@") + "script");
129 | Computer computer = node.toComputer();
130 | if (computer == null) {
131 | throw new IOException(node.getDisplayName() + " may be offline");
132 | }
133 | SCMStep delegate = new GenericSCMStep(scm);
134 | delegate.setPoll(true);
135 | delegate.setChangelog(true);
136 | try (WorkspaceList.Lease lease = computer.getWorkspaceList().acquire(dir)) {
137 | delegate.checkout(build, dir, listener, node.createLauncher(listener));
138 | FilePath scriptFile = dir.child(scriptPath);
139 | if (!scriptFile.absolutize().getRemote().replace('\\', '/').startsWith(dir.absolutize().getRemote().replace('\\', '/') + '/')) { // TODO JENKINS-26838
140 | throw new IOException(scriptFile + " is not inside " + dir);
141 | }
142 | if (!scriptFile.exists()) {
143 | throw new AbortException(scriptFile + " not found");
144 | }
145 | configContents = scriptFile.readToString();
146 | }
147 | } else {
148 | try (SCMFileSystem fs = SCMFileSystem.of(scmSource, head, tip)) {
149 | if (fs != null) { // JENKINS-33273
150 | try {
151 | configContents = fs.child(scriptPath).contentAsString();
152 | listener.getLogger().println("Obtained " + scriptPath);
153 | } catch (IOException | InterruptedException x) {
154 | throw new AbortException(String.format("Could not do lightweight checkout, %n%s",
155 | Functions.printThrowable(x).trim()));
156 | }
157 |
158 | } else {
159 | // TODO: Evaluate if this is necessary...
160 | // PRs were getting here because they're not trusted revisions but we're checking
161 | // for PRs above... Are there other possible scenarios or should we just bail?
162 | throw new AbortException("Could not do a lightweight checkout and retrieve an SCMFileSystem");
163 | }
164 | }
165 | }
166 | if (configContents == null) {
167 | String pipelineTemplateNotFound =
168 | String.format("Could not find a value for %s in %s", PIPELINE_TEMPLATE, scriptPath);
169 | throw new AbortException(pipelineTemplateNotFound);
170 | } else {
171 | String jenkinsfilePathString =
172 | ConfigurationValueFinder.findFirstConfigurationValue(configContents,
173 | ConfigDrivenWorkflowBranchProjectFactory.PIPELINE_TEMPLATE);
174 |
175 | build.addAction(new ConfigFileEnvironmentContributingAction(configContents));
176 |
177 | try (SCMFileSystem scriptFileSystem = SCMFileSystem.of(job, jenkinsFileScm)) {
178 | if (scriptFileSystem != null) {
179 | script = scriptFileSystem.child(jenkinsfilePathString).contentAsString();
180 | listener.getLogger().println("Obtained " + jenkinsfilePathString);
181 |
182 | }
183 |
184 | } catch (FileNotFoundException exception) {
185 | throw new AbortException(String.format("Could not find file %s", jenkinsfilePathString));
186 | }
187 | }
188 |
189 | if (script != null) {
190 | return new CpsFlowDefinition(script, true).create(handle, listener, actions);
191 | }
192 | }
193 | // TODO evaluate if there's something else we should be looking at for no `tip` or `script`
194 | throw new AbortException("Unable to properly load a script file.");
195 | }
196 |
197 | @Extension
198 | public static class DescriptorImpl extends FlowDefinitionDescriptor {
199 |
200 | @Override public String getDisplayName() {
201 | return "Pipeline script from " + Messages.ProjectRecognizer_DisplayName();
202 | }
203 |
204 | }
205 |
206 | /** 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. */
207 | @Extension
208 | public static class HideMeElsewhere extends DescriptorVisibilityFilter {
209 |
210 | @Override public boolean filter(Object context, Descriptor descriptor) {
211 | if (descriptor instanceof DescriptorImpl) {
212 | return context instanceof WorkflowJob && ((WorkflowJob) context).getParent() instanceof WorkflowMultiBranchProject;
213 | }
214 | return true;
215 | }
216 |
217 | }
218 |
219 | }
220 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/workflow/multibranch/template/finder/ConfigurationValueFinder.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.workflow.multibranch.template.finder;
2 |
3 | /**
4 | * Finds a key's value from a string in many config formats (e.g. properties, yaml, json)
5 | */
6 | public class ConfigurationValueFinder {
7 |
8 | private ConfigurationValueFinder() {}
9 |
10 | /**
11 | * This method will find the first given {@code keyToFind} in {@code configurationContents} and then
12 | * return the value of it assuming that the key/value is represented in the following formats:
13 | *
.yourconfig.yml
) in branches of the repository.
27 | This file should contain a valid
28 | Jenkins Pipeline script. See also:
29 | Creating Multibranch Projects.
30 | Jenkinsfile
if left empty.
5 | (just use checkout scm
to retrieve sources from the same location as is configured here)
6 | .yourconfig.yml
) in branches of the repository.
27 | This file should contain a valid
28 | Jenkins Pipeline script. See also:
29 | Creating Multibranch Projects.
30 | Jenkinsfile
if left empty.
5 | (just use checkout scm
to retrieve sources from the same location as is configured here)
6 |