Skip to content

Commit

Permalink
Merge pull request #121 from abayer/jenkins-42168
Browse files Browse the repository at this point in the history
[FIXED JENKINS-42168] Add validateDeclarativePipeline(path) step
  • Loading branch information
abayer committed Feb 23, 2017
2 parents c40dd71 + 0c59b3a commit d0bdbf7
Show file tree
Hide file tree
Showing 7 changed files with 307 additions and 4 deletions.
@@ -0,0 +1,124 @@
/*
* The MIT License
*
* Copyright (c) 2017, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.jenkinsci.plugins.pipeline.modeldefinition.steps;

import com.google.inject.Inject;
import hudson.Extension;
import hudson.FilePath;
import hudson.model.TaskListener;
import org.apache.commons.lang.StringUtils;
import org.jenkinsci.plugins.pipeline.modeldefinition.ast.ModelASTPipelineDef;
import org.jenkinsci.plugins.pipeline.modeldefinition.parser.Converter;
import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl;
import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl;
import org.jenkinsci.plugins.workflow.steps.AbstractSynchronousNonBlockingStepExecution;
import org.jenkinsci.plugins.workflow.steps.StepContextParameter;
import org.kohsuke.stapler.DataBoundConstructor;

import java.io.Serializable;

/**
* Step that will validate a string containing a Declarative Pipeline.
*
* @author Andrew Bayer
*/
public final class ValidateDeclarativePipelineStep extends AbstractStepImpl implements Serializable {

private static final long serialVersionUID = 1L;

private final String path;

@DataBoundConstructor
public ValidateDeclarativePipelineStep(String path) {
this.path = path;
}

public String getPath() {
return path;
}

@Extension
public static final class DescriptorImpl extends AbstractStepDescriptorImpl {

public DescriptorImpl() {
super(ValidateDeclarativePipelineStepExecution.class);
}

@Override public String getFunctionName() {
return "validateDeclarativePipeline";
}

@Override public String getDisplayName() {
return "Validate a file containing a Declarative Pipeline";
}
}

public static final class ValidateDeclarativePipelineStepExecution extends AbstractSynchronousNonBlockingStepExecution<Boolean> {

@Inject
private transient ValidateDeclarativePipelineStep step;

@StepContextParameter
private transient FilePath cwd;

@StepContextParameter
private transient TaskListener listener;

@Override
public Boolean run() throws Exception {
if (StringUtils.isEmpty(step.getPath())) {
listener.getLogger().println("No Declarative Pipeline file specified.");
return false;
} else {
FilePath f = cwd.child(step.getPath());
if (!f.exists() || f.isDirectory()) {
listener.getLogger().println("Declarative Pipeline file '" + step.getPath() + "' does not exist.");
return false;
} else {
String text = f.readToString();
if (StringUtils.isEmpty(text)) {
listener.getLogger().println("Declarative Pipeline file '" + step.getPath() + "' is empty.");
return false;
} else {
try {
ModelASTPipelineDef pipelineDef = Converter.scriptToPipelineDef(text);
if (pipelineDef != null) {
listener.getLogger().println("Declarative Pipeline file '" + step.getPath() + "' is valid.");
return true;
} else {
listener.getLogger().println("Declarative Pipeline file '" + step.getPath() + "' does not contain the 'pipeline' step.");
return false;
}
} catch (Exception e) {
listener.getLogger().println("Error(s) validating Declarative Pipeline file '" + step.getPath() + "' - " + e.toString());
return false;
}
}
}
}
}

private static final long serialVersionUID = 1L;
}
}
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ The MIT License
~
~ Copyright (c) 2017, CloudBees, Inc.
~
~ Permission is hereby granted, free of charge, to any person obtaining a copy
~ of this software and associated documentation files (the "Software"), to deal
~ in the Software without restriction, including without limitation the rights
~ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
~ copies of the Software, and to permit persons to whom the Software is
~ furnished to do so, subject to the following conditions:
~
~ The above copyright notice and this permission notice shall be included in
~ all copies or substantial portions of the Software.
~
~ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
~ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
~ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
~ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
~ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
~ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
~ THE SOFTWARE.
-->

<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
<f:entry field="path" title="File path in workspace">
<f:textbox/>
</f:entry>
</j:jelly>
@@ -0,0 +1,27 @@
<!--
~ The MIT License
~
~ Copyright (c) 2017, CloudBees, Inc.
~
~ Permission is hereby granted, free of charge, to any person obtaining a copy
~ of this software and associated documentation files (the "Software"), to deal
~ in the Software without restriction, including without limitation the rights
~ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
~ copies of the Software, and to permit persons to whom the Software is
~ furnished to do so, subject to the following conditions:
~
~ The above copyright notice and this permission notice shall be included in
~ all copies or substantial portions of the Software.
~
~ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
~ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
~ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
~ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
~ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
~ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
~ THE SOFTWARE.
-->

<div>
Relative (<code>/</code>-separated) path to file within a workspace to validate as a Declarative Pipeline.
</div>
@@ -0,0 +1,28 @@
<!--
~ The MIT License
~
~ Copyright (c) 2017, CloudBees, Inc.
~
~ Permission is hereby granted, free of charge, to any person obtaining a copy
~ of this software and associated documentation files (the "Software"), to deal
~ in the Software without restriction, including without limitation the rights
~ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
~ copies of the Software, and to permit persons to whom the Software is
~ furnished to do so, subject to the following conditions:
~
~ The above copyright notice and this permission notice shall be included in
~ all copies or substantial portions of the Software.
~
~ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
~ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
~ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
~ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
~ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
~ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
~ THE SOFTWARE.
-->

<div>
Checks if the given file (as relative path to current directory) contains a valid Declarative Pipeline.
Returns <code>true | false</code>.
</div>
Expand Up @@ -288,16 +288,24 @@ protected void prepRepoWithJenkinsfile(String subDir, String pipelineName) throw
}

protected void prepRepoWithJenkinsfileAndOtherFiles(String pipelineName, String... otherFiles) throws Exception {
Map<String,String> otherMap = new HashMap<>();
for (String otherFile : otherFiles) {
otherMap.put(otherFile, otherFile);
}
prepRepoWithJenkinsfileAndOtherFiles(pipelineName, otherMap);
}

protected void prepRepoWithJenkinsfileAndOtherFiles(String pipelineName, Map<String,String> otherFiles) throws Exception {
sampleRepo.init();
sampleRepo.write("Jenkinsfile",
pipelineSourceFromResources(pipelineName));
sampleRepo.git("add", "Jenkinsfile");


for (String otherFile : otherFiles) {
for (Map.Entry<String,String> otherFile : otherFiles.entrySet()) {
if (otherFile != null) {
sampleRepo.write(otherFile, fileContentsFromResources(otherFile));
sampleRepo.git("add", otherFile);
sampleRepo.write(otherFile.getValue(), fileContentsFromResources(otherFile.getKey()));
sampleRepo.git("add", otherFile.getValue());
}
}

Expand Down Expand Up @@ -429,6 +437,7 @@ public class ExpectationsBuilder {
private Result result = Result.SUCCESS;
private final String resourceParent;
private String resource;
private Map<String,String> otherResources;
private List<String> logContains;
private List<String> logNotContains;
private WorkflowRun run;
Expand All @@ -447,6 +456,12 @@ private ExpectationsBuilder(Result result, String resourceParent, String resourc
this.resourceParent = resourceParent;
this.resource = resource;
buildMatchers = new ArrayList<>();
otherResources = new HashMap<>();
}

public ExpectationsBuilder otherResource(String resource, String filename) {
this.otherResources.put(resource, filename);
return this;
}

public ExpectationsBuilder inLogInOrder(String... msgsInOrder) {
Expand Down Expand Up @@ -516,7 +531,11 @@ public WorkflowRun go() throws Exception {

if (run == null) {
if (runFromRepo) {
prepRepoWithJenkinsfile(resourceFullName);
if (otherResources.isEmpty()) {
prepRepoWithJenkinsfile(resourceFullName);
} else {
prepRepoWithJenkinsfileAndOtherFiles(resourceFullName, otherResources);
}
run = getAndStartBuild(folder);
} else {
run = getAndStartNonRepoBuild(folder, resourceFullName);
Expand Down
@@ -0,0 +1,44 @@
package org.jenkinsci.plugins.pipeline.modeldefinition.steps;

import org.jenkinsci.plugins.pipeline.modeldefinition.AbstractModelDefTest;
import org.junit.Test;

public class ValidateDeclarativePipelineStepTest extends AbstractModelDefTest {

@Test
public void passes() throws Exception {
expect("validateDeclarativePipelineStep")
.otherResource("simplePipeline.groovy", "testFile.groovy")
.logContains("Declarative Pipeline file 'testFile.groovy' is valid.",
"validation result - true")
.go();
}

@Test
public void noFile() throws Exception {
expect("validateDeclarativePipelineStep")
.logContains("Declarative Pipeline file 'testFile.groovy' does not exist.",
"validation result - false")
.go();
}

@Test
public void noPipelineStep() throws Exception {
expect("validateDeclarativePipelineStep")
.otherResource("validateDeclarativePipelineStep.groovy", "testFile.groovy")
.logContains("Declarative Pipeline file 'testFile.groovy' does not contain the 'pipeline' step.",
"validation result - false")
.go();
}

@Test
public void validationErrors() throws Exception {
expect("validateDeclarativePipelineStep")
.otherResource("errors/emptyEnvironment.groovy", "testFile.groovy")
.logContains("Error(s) validating Declarative Pipeline file 'testFile.groovy' - org.codehaus.groovy.control.MultipleCompilationErrorsException",
"WorkflowScript: 26: No variables specified for environment @ line 26, column 5.",
"validation result - false")
.go();
}

}
@@ -0,0 +1,30 @@
/*
* The MIT License
*
* Copyright (c) 2016, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

node {
checkout scm

def valid = validateDeclarativePipeline("testFile.groovy")
echo "validation result - ${valid}"
}

0 comments on commit d0bdbf7

Please sign in to comment.