From 5710ac48ec71fc441ad3a7ccc8b3da5c43d52543 Mon Sep 17 00:00:00 2001 From: Marcus Holl Date: Thu, 21 Mar 2019 16:21:55 +0100 Subject: [PATCH 01/21] Introduce step helper --- test/groovy/util/StepHelper.groovy | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 test/groovy/util/StepHelper.groovy diff --git a/test/groovy/util/StepHelper.groovy b/test/groovy/util/StepHelper.groovy new file mode 100644 index 000000000..59fb311e5 --- /dev/null +++ b/test/groovy/util/StepHelper.groovy @@ -0,0 +1,15 @@ +package util; + +import java.util.List; + +import groovy.io.FileType + +public class StepHelper { + + private static getSteps() { + List steps = [] + new File('vars').traverse(type: FileType.FILES, maxDepth: 0) + { if(it.getName().endsWith('.groovy')) steps << (it =~ /vars[\\\/](.*)\.groovy/)[0][1] } + return steps + } +} From a42e727da20e617b357de4e209cf5076a86b1762 Mon Sep 17 00:00:00 2001 From: Marcus Holl Date: Tue, 19 Mar 2019 12:40:58 +0100 Subject: [PATCH 02/21] Create step specific plugin lists: Helper class for tracking step calls Before the test we remmber which test is currently running. During the test we collect all the calls to steps. Beside that we persist the names of all steps within this shared lib itself. After the test(s) we write a corresponding json file. In fact we write the file after each test, which is too often. But since we don't know which test is the last test we can't do better. The resulting file can be used later on for resolving the plugins contributing the particular steps. With that we are able to create a list of required plugins for each step. --- test/groovy/util/StepTracker.groovy | 69 +++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 test/groovy/util/StepTracker.groovy diff --git a/test/groovy/util/StepTracker.groovy b/test/groovy/util/StepTracker.groovy new file mode 100644 index 000000000..748ad6532 --- /dev/null +++ b/test/groovy/util/StepTracker.groovy @@ -0,0 +1,69 @@ +package util + +import static com.lesfurets.jenkins.unit.MethodSignature.method +import static util.StepHelper.getSteps + +import org.codehaus.groovy.runtime.MetaClassHelper +import com.lesfurets.jenkins.unit.MethodSignature +import com.lesfurets.jenkins.unit.PipelineTestHelper +import groovy.json.JsonBuilder + +class StepTracker { + + /* + * Contains the piper steps as key (derived from the test name, so this is blurry since it might + * contains also other cases than only piper step name) and the observed calls in a collection. + */ + static Map piperStepCallMapping = [:] + static Set piperSteps = StepHelper.getSteps() + + static Set calls + + static { + initialize() + } + + final static void initialize() { + + PipelineTestHelper.metaClass.getAllowedMethodEntry = { + + // We need to be careful here, in case we switch to another + // version of the Les Furets framework we have to check if + // this here still works. + + String name, Object[] args -> + + Class[] paramTypes = MetaClassHelper.castArgumentsToClassArray(args) + MethodSignature signature = method(name, paramTypes) + def intercepted = allowedMethodCallbacks.find { k, v -> k == signature } + + if(intercepted != null) + StepTracker.add(name) + + return intercepted + } + } + + static void before(String stepName) { + + if(piperStepCallMapping[stepName] == null) + piperStepCallMapping[stepName] = (Set)[] + calls = piperStepCallMapping[stepName] + } + + static void after() { + calls = null + write() + } + static void add (String call) { + calls.add(call) + } + + static private void write() { + Map root = [ + piperSteps: piperSteps, + calls: piperStepCallMapping.sort() + ] + new File('target/trackedCalls.json').write(new JsonBuilder(root).toPrettyString()) + } +} From 5ad99599ab0e3868bdb126be1d14b4b9944cfaee Mon Sep 17 00:00:00 2001 From: Marcus Holl Date: Wed, 27 Mar 2019 15:44:06 +0100 Subject: [PATCH 03/21] Make use of the step tracker --- test/groovy/util/LibraryLoadingTestExecutionListener.groovy | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/groovy/util/LibraryLoadingTestExecutionListener.groovy b/test/groovy/util/LibraryLoadingTestExecutionListener.groovy index cc2e58675..85b8d0c7c 100644 --- a/test/groovy/util/LibraryLoadingTestExecutionListener.groovy +++ b/test/groovy/util/LibraryLoadingTestExecutionListener.groovy @@ -90,6 +90,7 @@ class LibraryLoadingTestExecutionListener extends AbstractTestExecutionListener @Override void beforeTestClass(TestContext testContext) throws Exception { super.beforeTestClass(testContext) + StepTracker.before(testContext.testClass.getSimpleName()) def helper = LibraryLoadingTestExecutionListener.getSingletonInstance() registerDefaultAllowedMethods(helper) LibraryLoadingTestExecutionListener.START_CLASS_TRACKING = true @@ -98,6 +99,7 @@ class LibraryLoadingTestExecutionListener extends AbstractTestExecutionListener @Override void afterTestClass(TestContext testContext) throws Exception { super.afterTestClass(testContext) + StepTracker.after() PipelineTestHelper helper = LibraryLoadingTestExecutionListener.getSingletonInstance() helper.clearAllowedMethodCallbacks(LibraryLoadingTestExecutionListener.TRACKED_ON_CLASS) LibraryLoadingTestExecutionListener.TRACKED_ON_CLASS.clear() @@ -123,6 +125,7 @@ class LibraryLoadingTestExecutionListener extends AbstractTestExecutionListener void beforeTestMethod(TestContext testContext) throws Exception { super.beforeTestMethod(testContext) def testInstance = testContext.getTestInstance() + StepTracker.before(testInstance.getClass().getSimpleName()) testInstance.binding.setVariable('currentBuild', [result: 'SUCCESS', currentResult: 'SUCCESS']) PipelineTestHelper helper = LibraryLoadingTestExecutionListener.getSingletonInstance() LibraryLoadingTestExecutionListener.START_METHOD_TRACKING = true @@ -132,6 +135,7 @@ class LibraryLoadingTestExecutionListener extends AbstractTestExecutionListener void afterTestMethod(TestContext testContext) throws Exception { super.afterTestMethod(testContext) def testInstance = testContext.getTestInstance() + StepTracker.after() PipelineTestHelper helper = LibraryLoadingTestExecutionListener.getSingletonInstance() helper.clearCallStack() @@ -192,6 +196,7 @@ class LibraryLoadingTestExecutionListener extends AbstractTestExecutionListener static class PipelineTestHelperHook { def helper = new PipelineTestHelper() { + def clearAllowedMethodCallbacks(Collection c = []) { List itemsToRemove = [] c.each { From c52d1ceb0d00c926b85bb12c5739a31a739d2b8d Mon Sep 17 00:00:00 2001 From: Christopher Fenner Date: Tue, 14 May 2019 10:59:52 +0200 Subject: [PATCH 04/21] add Sonar docs link (#704) --- documentation/mkdocs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/documentation/mkdocs.yml b/documentation/mkdocs.yml index 211bf7c1e..241174973 100644 --- a/documentation/mkdocs.yml +++ b/documentation/mkdocs.yml @@ -37,6 +37,7 @@ nav: - setupCommonPipelineEnvironment: steps/setupCommonPipelineEnvironment.md - slackSendNotification: steps/slackSendNotification.md - snykExecute: steps/snykExecute.md + - sonarExecuteScan: steps/sonarExecuteScan.md - testsPublishResults: steps/testsPublishResults.md - transportRequestCreate: steps/transportRequestCreate.md - transportRequestRelease: steps/transportRequestRelease.md From 7a7fd3ebabc79288681ebf882bd31333b2e97215 Mon Sep 17 00:00:00 2001 From: Marcus Holl Date: Fri, 10 May 2019 12:44:58 +0200 Subject: [PATCH 05/21] Provide a deep config copy from ConfigHelper.use() The config map prepared by ConfigHelper is a mix from several configuration levels. The lowest config level (DefaultValueCache) is shared between several ConfigHelper invocations. In case a Map or Collection which is inherited from the DefaultValueCache level gets modified, this is also visible for all subsequent steps. This causes trouble and situation which are hard to debug. With this change here each invocation of ConfigHelper.use() provides a deep defensive copy. With that we can ensure that there is no configuration update from one step to another. --- src/com/sap/piper/ConfigurationHelper.groovy | 4 ++- src/com/sap/piper/MapUtils.groovy | 34 +++++++++++++++++++ test/groovy/com/sap/piper/MapUtilsTest.groovy | 31 +++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/src/com/sap/piper/ConfigurationHelper.groovy b/src/com/sap/piper/ConfigurationHelper.groovy index 7d733adeb..7bbfaa5f7 100644 --- a/src/com/sap/piper/ConfigurationHelper.groovy +++ b/src/com/sap/piper/ConfigurationHelper.groovy @@ -127,9 +127,11 @@ class ConfigurationHelper implements Serializable { handleValidationFailures() MapUtils.traverse(config, { v -> (v instanceof GString) ? v.toString() : v }) if(config.verbose) step.echo "[${name}] Configuration: ${config}" - return config + return MapUtils.deepCopy(config) } + + /* private */ def getConfigPropertyNested(key) { return getConfigPropertyNested(config, key) } diff --git a/src/com/sap/piper/MapUtils.groovy b/src/com/sap/piper/MapUtils.groovy index 784a281e7..5f8123b9a 100644 --- a/src/com/sap/piper/MapUtils.groovy +++ b/src/com/sap/piper/MapUtils.groovy @@ -62,4 +62,38 @@ class MapUtils implements Serializable { } m.putAll(updates) } + + static deepCopy(Map original) { + Map copy = [:] + for (def e : original.entrySet()) { + if(e.value == null) { + copy.put(e.key, e.value) + } else { + copy.put(e.key, deepCopy(e.value)) + } + } + copy + } + + static deepCopy(Set original) { + Set copy = [] + for(def e : original) + copy << deepCopy(e) + copy + } + + static deepCopy(List original) { + List copy = [] + for(def e : original) + copy << deepCopy(e) + copy + } + + /* + * In fact not a copy, but a catch all for everything not matching + * with the other signatures + */ + static deepCopy(def original) { + original + } } diff --git a/test/groovy/com/sap/piper/MapUtilsTest.groovy b/test/groovy/com/sap/piper/MapUtilsTest.groovy index 61a06aaf8..6c63ccd14 100644 --- a/test/groovy/com/sap/piper/MapUtilsTest.groovy +++ b/test/groovy/com/sap/piper/MapUtilsTest.groovy @@ -50,4 +50,35 @@ class MapUtilsTest { MapUtils.traverse(m, { s -> (s.startsWith('x')) ? "replaced" : s}) assert m == [a: 'replaced', m: [b: 'replaced', c: 'otherString']] } + + @Test + void testDeepCopy() { + + List l = ['a', 'b', 'c'] + + def original = [ + list: l, + set: (Set)['1', '2'], + nextLevel: [ + list: ['x', 'y'], + duplicate: l, + set: (Set)[9, 8, 7] + ] + ] + + def copy = MapUtils.deepCopy(original) + + assert ! copy.is(original) + assert ! copy.list.is(original.list) + assert ! copy.set.is(original.set) + assert ! copy.nextLevel.list.is(original.nextLevel.list) + assert ! copy.nextLevel.set.is(original.nextLevel.set) + assert ! copy.nextLevel.duplicate.is(original.nextLevel.duplicate) + + // Within the original identical list is used twice, but the + // assuption is that there are different lists in the copy. + assert ! copy.nextLevel.duplicate.is(copy.list) + + assert copy == original + } } From 0431c290630d1818bb7a8eeef8dfb8e3398289b4 Mon Sep 17 00:00:00 2001 From: Marcus Holl Date: Tue, 14 May 2019 12:56:47 +0200 Subject: [PATCH 06/21] Fix exception list for step neoDeploy (#697) due to wrong indentation in the md file the excpetions are not rendered as expected. The reasons why an exception occures is on the same level like the exception, but should have only level more. --- documentation/docs/steps/neoDeploy.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/documentation/docs/steps/neoDeploy.md b/documentation/docs/steps/neoDeploy.md index 4fe0f3609..3fb095615 100644 --- a/documentation/docs/steps/neoDeploy.md +++ b/documentation/docs/steps/neoDeploy.md @@ -25,15 +25,15 @@ none ## Exceptions * `Exception`: - * If `source` is not provided. - * If `propertiesFile` is not provided (when using `'WAR_PROPERTIESFILE'` deployment mode). - * If `application` is not provided (when using `'WAR_PARAMS'` deployment mode). - * If `runtime` is not provided (when using `'WAR_PARAMS'` deployment mode). - * If `runtimeVersion` is not provided (when using `'WAR_PARAMS'` deployment mode). + * If `source` is not provided. + * If `propertiesFile` is not provided (when using `'WAR_PROPERTIESFILE'` deployment mode). + * If `application` is not provided (when using `'WAR_PARAMS'` deployment mode). + * If `runtime` is not provided (when using `'WAR_PARAMS'` deployment mode). + * If `runtimeVersion` is not provided (when using `'WAR_PARAMS'` deployment mode). * `AbortException`: - * If neo-java-web-sdk is not installed, or `neoHome`is wrong. + * If neo-java-web-sdk is not installed, or `neoHome`is wrong. * `CredentialNotFoundException`: - * If the credentials cannot be resolved. + * If the credentials cannot be resolved. ## Example From 07f73dab716ebef595b859cf9ee8c356d74d47b0 Mon Sep 17 00:00:00 2001 From: Sven Merk Date: Tue, 14 May 2019 16:30:13 +0200 Subject: [PATCH 07/21] Compatibility to Piper PR support --- vars/commonPipelineEnvironment.groovy | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vars/commonPipelineEnvironment.groovy b/vars/commonPipelineEnvironment.groovy index 92de32807..98d9db24b 100644 --- a/vars/commonPipelineEnvironment.groovy +++ b/vars/commonPipelineEnvironment.groovy @@ -12,6 +12,7 @@ class commonPipelineEnvironment implements Serializable { //stores the gitCommitId as well as additional git information for the build during pipeline run String gitCommitId + String gitCommitMessage String gitSshUrl String gitHttpsUrl String gitBranch @@ -46,6 +47,7 @@ class commonPipelineEnvironment implements Serializable { configuration = [:] gitCommitId = null + gitCommitMessage = null gitSshUrl = null gitHttpsUrl = null gitBranch = null From 9d43f7abb35acdb12c431f7e50843713d7e61925 Mon Sep 17 00:00:00 2001 From: Marcus Holl Date: Thu, 9 May 2019 15:19:17 +0200 Subject: [PATCH 08/21] [fix] do not return a wrong config value if only a part of the path could be resolved. --- documentation/bin/createDocu.groovy | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/documentation/bin/createDocu.groovy b/documentation/bin/createDocu.groovy index 83bce877d..1c52b6696 100644 --- a/documentation/bin/createDocu.groovy +++ b/documentation/bin/createDocu.groovy @@ -375,11 +375,12 @@ class Helper { return mappings } - static getValue(Map config, def pPath) { - def p =config[pPath.head()] + static getValue(Map config, List pPath) { + def p = config[pPath.head()] if(pPath.size() == 1) return p // there is no tail if(p in Map) getValue(p, pPath.tail()) - else return p + return null // there is a remaining path which could not be resolved. + // the value we are looking for does not exist. } static resolveDocuRelevantSteps(GroovyScriptEngine gse, File stepsDir) { @@ -640,7 +641,7 @@ def handleStep(stepName, prepareDefaultValuesStep, gse, customDefaults) { it -> - def defaultValue = Helper.getValue(defaultConfig, it.split('/')) + def defaultValue = Helper.getValue(defaultConfig, it.tokenize('/')) def parameterProperties = [ defaultValue: defaultValue, @@ -675,7 +676,7 @@ def handleStep(stepName, prepareDefaultValuesStep, gse, customDefaults) { [ dependentParameterKey: dependentParameterKey, key: possibleValue, - value: Helper.getValue(defaultConfig.get(possibleValue), k.split('/')) + value: Helper.getValue(defaultConfig.get(possibleValue), k.tokenize('/')) ] } } From a0649aa63f52d027820f25e9ad88b45faf88587e Mon Sep 17 00:00:00 2001 From: Marcus Holl Date: Wed, 15 May 2019 14:21:19 +0200 Subject: [PATCH 09/21] Explain what 'deepCopy' means --- src/com/sap/piper/MapUtils.groovy | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/com/sap/piper/MapUtils.groovy b/src/com/sap/piper/MapUtils.groovy index 5f8123b9a..d3cd32956 100644 --- a/src/com/sap/piper/MapUtils.groovy +++ b/src/com/sap/piper/MapUtils.groovy @@ -63,6 +63,13 @@ class MapUtils implements Serializable { m.putAll(updates) } + /* + * Provides a new map with the same content like the original map. + * Nested Collections and Maps are copied. Values with are not + * Collections/Maps are not copied/cloned. + * <paranoia>&/ltThe keys are also not copied/cloned, even if they are + * Maps or Collections;paranoia> + */ static deepCopy(Map original) { Map copy = [:] for (def e : original.entrySet()) { @@ -75,14 +82,14 @@ class MapUtils implements Serializable { copy } - static deepCopy(Set original) { + /* private */ static deepCopy(Set original) { Set copy = [] for(def e : original) copy << deepCopy(e) copy } - static deepCopy(List original) { + /* private */ static deepCopy(List original) { List copy = [] for(def e : original) copy << deepCopy(e) @@ -93,7 +100,7 @@ class MapUtils implements Serializable { * In fact not a copy, but a catch all for everything not matching * with the other signatures */ - static deepCopy(def original) { + /* private */ static deepCopy(def original) { original } } From 2ad52708fa5fc58c2c9a80f42481efbfbd5d9bed Mon Sep 17 00:00:00 2001 From: Marcus Holl Date: Wed, 15 May 2019 14:46:46 +0200 Subject: [PATCH 10/21] fix: null is rendered when no default value is available instead of the empty string --- documentation/bin/createDocu.groovy | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/documentation/bin/createDocu.groovy b/documentation/bin/createDocu.groovy index 1c52b6696..bf9ab06f7 100644 --- a/documentation/bin/createDocu.groovy +++ b/documentation/bin/createDocu.groovy @@ -21,7 +21,8 @@ class TemplateHelper { def props = parameters.get(it) - def defaultValue = isComplexDefault(props.defaultValue) ? renderComplexDefaultValue(props.defaultValue) : "`${props.defaultValue}`" + def defaultValue = isComplexDefault(props.defaultValue) ? renderComplexDefaultValue(props.defaultValue) : + props.defaultValue ? "`${props.defaultValue}`" : '' t += "| `${it}` | ${props.mandatory ?: props.required ? 'yes' : 'no'} | ${defaultValue} | ${props.value ?: ''} |\n" } From ee3f820e5f458eee6977198fdd551fe282d30ab3 Mon Sep 17 00:00:00 2001 From: Marcus Holl Date: Wed, 15 May 2019 15:20:14 +0200 Subject: [PATCH 11/21] explict null check in order to render also false Co-Authored-By: Christopher Fenner --- documentation/bin/createDocu.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/bin/createDocu.groovy b/documentation/bin/createDocu.groovy index bf9ab06f7..69d0e97b3 100644 --- a/documentation/bin/createDocu.groovy +++ b/documentation/bin/createDocu.groovy @@ -22,7 +22,7 @@ class TemplateHelper { def props = parameters.get(it) def defaultValue = isComplexDefault(props.defaultValue) ? renderComplexDefaultValue(props.defaultValue) : - props.defaultValue ? "`${props.defaultValue}`" : '' + props.defaultValue != null ? "`${props.defaultValue}`" : '' t += "| `${it}` | ${props.mandatory ?: props.required ? 'yes' : 'no'} | ${defaultValue} | ${props.value ?: ''} |\n" } From abdfa7b20173e2716bae8544cbabdf5c27ef8868 Mon Sep 17 00:00:00 2001 From: Marcus Holl Date: Tue, 14 May 2019 16:45:02 +0200 Subject: [PATCH 12/21] [refactoring] Increase readabiliy: inline rules without references --- test/groovy/NeoDeployTest.groovy | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/test/groovy/NeoDeployTest.groovy b/test/groovy/NeoDeployTest.groovy index d4b30a67f..0c9ca9143 100644 --- a/test/groovy/NeoDeployTest.groovy +++ b/test/groovy/NeoDeployTest.groovy @@ -35,9 +35,6 @@ class NeoDeployTest extends BasePiperTest { private JenkinsLoggingRule loggingRule = new JenkinsLoggingRule(this) private JenkinsShellCallRule shellRule = new JenkinsShellCallRule(this) private JenkinsStepRule stepRule = new JenkinsStepRule(this) - private JenkinsLockRule lockRule = new JenkinsLockRule(this) - private JenkinsFileExistsRule fileExistsRule = new JenkinsFileExistsRule(this, ['warArchive.war', 'archive.mtar', 'war.properties']) - @Rule public RuleChain ruleChain = Rules @@ -51,9 +48,9 @@ class NeoDeployTest extends BasePiperTest { .withCredentials('myCredentialsId', 'anonymous', '********') .withCredentials('CI_CREDENTIALS_ID', 'defaultUser', '********')) .around(stepRule) - .around(lockRule) + .around(new JenkinsLockRule(this)) .around(new JenkinsWithEnvRule(this)) - .around(fileExistsRule) + .around(new JenkinsFileExistsRule(this, ['warArchive.war', 'archive.mtar', 'war.properties'])) private static warArchiveName = 'warArchive.war' From 9db487d189c6e61768bbf33a5cce078844a1c8bb Mon Sep 17 00:00:00 2001 From: Christopher Fenner Date: Wed, 15 May 2019 20:52:45 +0200 Subject: [PATCH 13/21] move method to MapUtils (#709) * move method to MapUtils * use MapUtils * fix indent * Update src/com/sap/piper/MapUtils.groovy * Update MapUtils.groovy * remove obsolete test case * fix typo --- documentation/bin/createDocu.groovy | 14 ++--- src/com/sap/piper/MapUtils.groovy | 11 ++++ test/groovy/com/sap/piper/MapUtilsTest.groovy | 12 +++++ .../PiperInitRunStageConfigurationTest.groovy | 51 +++++-------------- vars/piperInitRunStageConfiguration.groovy | 24 +++------ 5 files changed, 47 insertions(+), 65 deletions(-) diff --git a/documentation/bin/createDocu.groovy b/documentation/bin/createDocu.groovy index 1c52b6696..3532c394e 100644 --- a/documentation/bin/createDocu.groovy +++ b/documentation/bin/createDocu.groovy @@ -6,6 +6,8 @@ import com.sap.piper.GenerateDocumentation import java.util.regex.Matcher import groovy.text.StreamingTemplateEngine +import com.sap.piper.MapUtils + // // Collects helper functions for rendering the documentation // @@ -375,14 +377,6 @@ class Helper { return mappings } - static getValue(Map config, List pPath) { - def p = config[pPath.head()] - if(pPath.size() == 1) return p // there is no tail - if(p in Map) getValue(p, pPath.tail()) - return null // there is a remaining path which could not be resolved. - // the value we are looking for does not exist. - } - static resolveDocuRelevantSteps(GroovyScriptEngine gse, File stepsDir) { def docuRelevantSteps = [] @@ -641,7 +635,7 @@ def handleStep(stepName, prepareDefaultValuesStep, gse, customDefaults) { it -> - def defaultValue = Helper.getValue(defaultConfig, it.tokenize('/')) + def defaultValue = MapUtils.getByPath(defaultConfig, it) def parameterProperties = [ defaultValue: defaultValue, @@ -676,7 +670,7 @@ def handleStep(stepName, prepareDefaultValuesStep, gse, customDefaults) { [ dependentParameterKey: dependentParameterKey, key: possibleValue, - value: Helper.getValue(defaultConfig.get(possibleValue), k.tokenize('/')) + value: MapUtils.getByPath(defaultConfig.get(possibleValue), k) ] } } diff --git a/src/com/sap/piper/MapUtils.groovy b/src/com/sap/piper/MapUtils.groovy index d3cd32956..657d82215 100644 --- a/src/com/sap/piper/MapUtils.groovy +++ b/src/com/sap/piper/MapUtils.groovy @@ -63,6 +63,17 @@ class MapUtils implements Serializable { m.putAll(updates) } + static private def getByPath(Map m, def key) { + List path = key in CharSequence ? key.tokenize('/') : key + + def value = m.get(path.head()) + + if (path.size() == 1) return value + if (value in Map) return getByPath(value, path.tail()) + + return null + } + /* * Provides a new map with the same content like the original map. * Nested Collections and Maps are copied. Values with are not diff --git a/test/groovy/com/sap/piper/MapUtilsTest.groovy b/test/groovy/com/sap/piper/MapUtilsTest.groovy index 6c63ccd14..a7b0415ef 100644 --- a/test/groovy/com/sap/piper/MapUtilsTest.groovy +++ b/test/groovy/com/sap/piper/MapUtilsTest.groovy @@ -3,6 +3,9 @@ package com.sap.piper import org.junit.Assert import org.junit.Test +import static org.hamcrest.Matchers.is +import static org.junit.Assert.assertThat + class MapUtilsTest { @Test @@ -51,6 +54,15 @@ class MapUtilsTest { assert m == [a: 'replaced', m: [b: 'replaced', c: 'otherString']] } + @Test + void testGetByPath() { + Map m = [trees: [oak: 5, beech :1], flowers:[rose: 23]] + + assertThat(MapUtils.getByPath(m, 'flowers'), is([rose: 23])) + assertThat(MapUtils.getByPath(m, 'trees/oak'), is(5)) + assertThat(MapUtils.getByPath(m, 'trees/palm'), is(null)) + } + @Test void testDeepCopy() { diff --git a/test/groovy/templates/PiperInitRunStageConfigurationTest.groovy b/test/groovy/templates/PiperInitRunStageConfigurationTest.groovy index c6d2263a1..b12ff1aae 100644 --- a/test/groovy/templates/PiperInitRunStageConfigurationTest.groovy +++ b/test/groovy/templates/PiperInitRunStageConfigurationTest.groovy @@ -99,7 +99,7 @@ stages: stepConditions: firstStep: config: testGeneral - testStage2: + testStage2: stepConditions: secondStep: config: testStage @@ -107,7 +107,7 @@ stages: stepConditions: thirdStep: config: testStep - + ''' } else { return ''' @@ -155,23 +155,23 @@ stages: testStage1: stepConditions: firstStep: - config: + config: testGeneral: - myValx - - myVal1 - testStage2: + - myVal1 + testStage2: stepConditions: secondStep: - config: - testStage: + config: + testStage: - maValXyz testStage3: stepConditions: thirdStep: - config: + config: testStep: - myVal3 - + ''' } else { return ''' @@ -218,18 +218,18 @@ stages: testStage1: stepConditions: firstStep: - configKeys: + configKeys: - myKey1_1 - - myKey1_2 - testStage2: + - myKey1_2 + testStage2: stepConditions: secondStep: - configKeys: + configKeys: - myKey2_1 testStage3: stepConditions: thirdStep: - configKeys: + configKeys: - myKey3_1 ''' } else { @@ -451,27 +451,4 @@ steps: {} assertThat(nullScript.commonPipelineEnvironment.configuration.runStage.Acceptance, is(true)) } - - @Test - void testGetConfigValue() { - - def config = [ - invalidKey: 'invalidValue', - stringKey: 'stringValue', - listKey: [ - 'listValue1', - 'listValue2' - ], - nested: [ - key: 'nestedValue' - ] - ] - - assertThat(jsr.step.piperInitRunStageConfiguration.getConfigValue(config, 'stringKey'), is('stringValue')) - assertThat(jsr.step.piperInitRunStageConfiguration.getConfigValue(config, 'listKey'), is(['listValue1','listValue2'])) - assertThat(jsr.step.piperInitRunStageConfiguration.getConfigValue(config, 'nested/key'), is('nestedValue')) - assertThat(jsr.step.piperInitRunStageConfiguration.getConfigValue(config, 'invalidKey/key'), is(nullValue())) - - //assertThat(jsr.step.piperInitRunStageConfiguration.getConfigValue(config, 'nested/key'), is('nestedValue')) - } } diff --git a/vars/piperInitRunStageConfiguration.groovy b/vars/piperInitRunStageConfiguration.groovy index c89197c28..6fcf51786 100644 --- a/vars/piperInitRunStageConfiguration.groovy +++ b/vars/piperInitRunStageConfiguration.groovy @@ -3,6 +3,7 @@ import com.sap.piper.ConfigurationLoader import static com.sap.piper.Prerequisites.checkScript import com.sap.piper.ConfigurationHelper +import com.sap.piper.MapUtils import groovy.transform.Field @Field String STEP_NAME = getClass().getName() @@ -65,27 +66,27 @@ void call(Map parameters = [:]) { case 'config': if (condition.getValue() instanceof Map) { condition.getValue().each {configCondition -> - if (getConfigValue(stepConfig, configCondition.getKey()) in configCondition.getValue()) { + if (MapUtils.getByPath(stepConfig, configCondition.getKey()) in configCondition.getValue()) { stepActive = true } } - } else if (getConfigValue(stepConfig, condition.getValue())) { + } else if (MapUtils.getByPath(stepConfig, condition.getValue())) { stepActive = true } break case 'configKeys': if (condition.getValue() instanceof List) { condition.getValue().each {configKey -> - if (getConfigValue(stepConfig, configKey)) { + if (MapUtils.getByPath(stepConfig, configKey)) { stepActive = true } } - } else if (getConfigValue(stepConfig, condition.getValue())) { + } else if (MapUtils.getByPath(stepConfig, condition.getValue())) { stepActive = true } break case 'filePatternFromConfig': - def conditionValue = getConfigValue(stepConfig, condition.getValue()) + def conditionValue = MapUtils.getByPath(stepConfig, condition.getValue()) if (conditionValue && findFiles(glob: conditionValue)) { stepActive = true } @@ -110,16 +111,3 @@ void call(Map parameters = [:]) { echo "[${STEP_NAME}] Debug - Run Step Configuration: ${script.commonPipelineEnvironment.configuration.runStep}" } } - -private def getConfigValue(Map stepConfig, def configKey) { - if (stepConfig == null) return null - - List configPath = configKey instanceof String ? configKey.tokenize('/') : configKey - - def configValue = stepConfig[configPath.head()] - - if (configPath.size() == 1) return configValue - if (configValue in Map) return getConfigValue(configValue, configPath.tail()) - - return null -} From ca33a86ee65b415c16c47c914998862f3e6b71bf Mon Sep 17 00:00:00 2001 From: Marcus Holl Date: Fri, 12 Apr 2019 15:32:30 +0200 Subject: [PATCH 14/21] Switch to named parameters when invoking docu generation --- documentation/bin/createDocu.groovy | 34 +++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/documentation/bin/createDocu.groovy b/documentation/bin/createDocu.groovy index 44c66f2c7..9388debd1 100644 --- a/documentation/bin/createDocu.groovy +++ b/documentation/bin/createDocu.groovy @@ -381,21 +381,37 @@ steps = [] // // assign parameters -if(args.length >= 1) - stepsDir = new File(args[0]) + +def cli = new CliBuilder( + usage: 'groovy createDocu []', + header: 'Options:', + footer: 'Copyright: SAP SE') + +cli.with { + s longOpt: 'stepsDir', args: 1, argName: 'dir', 'The directory containing the steps. Defaults to \'vars\'.' + d longOpt: 'docuDir', args: 1, argName: 'dir', 'The directory containing the docu stubs. Defaults to \'documentation/docs/steps\'.' + h longOpt: 'help', 'Prints this help.' +} + +def options = cli.parse(args) + +if(options.h) { + System.err << "Printing help.\n" + cli.usage() + return +} + +if(options.s) + stepsDir = new File(Helper.projectRoot, options.s) stepsDir = stepsDir ?: new File(Helper.projectRoot, "vars") -if(args.length >= 2) - stepsDocuDir = new File(args[1]) +if(options.d) + stepsDocuDir = new File(Helper.projectRoot, options.d) stepsDocuDir = stepsDocuDir ?: new File(Helper.projectRoot, "documentation/docs/steps") - -if(args.length >= 3) - steps = (args as List).drop(2) // the first two entries are stepsDir and docuDir - // the other parts are considered as step names - +steps.addAll(options.arguments()) // assign parameters // From 6d5fdd41bfd82f525442b30810d6e5139bd1e44a Mon Sep 17 00:00:00 2001 From: Christopher Fenner Date: Thu, 16 May 2019 10:46:13 +0200 Subject: [PATCH 15/21] fix code climate issues (#712) * fix code climate issues * fix code climate issues * Update GitUtils.groovy --- README.md | 3 +- .../docs/scenarios/ui5-sap-cp/Readme.md | 6 --- documentation/docs/steps/influxWriteData.md | 4 +- documentation/docs/steps/npmExecute.md | 1 - .../docs/steps/seleniumExecuteTests.md | 2 +- .../docs/steps/transportRequestRelease.md | 3 -- pom.xml | 8 ++-- src/com/sap/piper/GitUtils.groovy | 34 ++++++++------- src/com/sap/piper/cm/ChangeManagement.groovy | 32 +++++++------- vars/durationMeasure.groovy | 1 - vars/pipelineExecute.groovy | 22 ++++++---- vars/transportRequestCreate.groovy | 43 ++++++++++--------- vars/transportRequestRelease.groovy | 2 +- 13 files changed, 79 insertions(+), 82 deletions(-) diff --git a/README.md b/README.md index af3a99339..fc413e63d 100644 --- a/README.md +++ b/README.md @@ -74,8 +74,7 @@ To setup the shared library, you need to perform the following steps: 1. Scroll down to section *Global Pipeline Libraries* and add a new Library by clicking the *Add* button. 1. set *Library Name* to `piper-lib-os` - 1. set *Default Version* to the branch or tag you want to consume (e.g. - `master` or `v0.1`) + 1. set *Default Version* to the branch or tag you want to consume (e.g. `master` or `v0.1`) 1. set *Retrieval Method* to `Modern SCM` 1. set *Source Code Management* to `Git` 1. set *Project Repository* to `https://github.com/SAP/jenkins-library` diff --git a/documentation/docs/scenarios/ui5-sap-cp/Readme.md b/documentation/docs/scenarios/ui5-sap-cp/Readme.md index 113a3dbe1..0ad0201f1 100644 --- a/documentation/docs/scenarios/ui5-sap-cp/Readme.md +++ b/documentation/docs/scenarios/ui5-sap-cp/Readme.md @@ -11,12 +11,10 @@ Build an application based on SAPUI5 or SAP Fiori with Jenkins and deploy the bu * You have installed Node.js including node and npm. See [Node.js](https://nodejs.org/en/download/). * You have installed the SAP Cloud Platform Neo Environment SDK. See [SAP Development Tools](https://tools.hana.ondemand.com/#cloud). - ### Project Prerequisites This scenario requires additional files in your project and in the execution environment on your Jenkins instance. - On the project level, provide and adjust the following template: | File Name | Description | Position | @@ -26,12 +24,10 @@ On the project level, provide and adjust the following template: | [`package.json`](https://github.com/SAP/jenkins-library/blob/master/documentation/docs/scenarios/ui5-sap-cp/files/package.json) | This file lists the required development dependencies for the build. | Add the content of the `package.json` file to your existing `package.json` file. | | [`Gruntfile.js`](https://github.com/SAP/jenkins-library/blob/master/documentation/docs/scenarios/ui5-sap-cp/files/Gruntfile.js) | This file controls the grunt build. By default the tasks `clean`, `build`, and `lint` are executed. | Place the `Gruntfile.js` in the root directory of your project. | - ## Context This scenario combines various different steps to create a complete pipeline. - In this scenario, we want to show how to build an application based on SAPUI5 or SAP Fiori by using the multi-target application (MTA) concept and how to deploy the build result into an SAP Cloud Platform account in the Neo environment. This document comprises the [mtaBuild](https://sap.github.io/jenkins-library/steps/mtaBuild/) and the [neoDeploy](https://sap.github.io/jenkins-library/steps/neoDeploy/) steps. ![This pipeline in Jenkins Blue Ocean](images/pipeline.jpg) @@ -73,7 +69,6 @@ steps: | `buildTarget` | The target platform to which the mtar can be deployed. Possible values are: `CF`, `NEO`, `XSA` | | `mtaJarLocation` | The location of the multi-target application archive builder jar file, including file name and extension. | - #### Configuration for the Deployment to SAP Cloud Platform | Parameter | Description | @@ -83,7 +78,6 @@ steps: | `host` | The SAP Cloud Platform host to deploy to. | | `neoHome` | The path to the `neo-java-web-sdk` tool that is used for the deployment. | - ### Parameters For the detailed description of the relevant parameters, see: diff --git a/documentation/docs/steps/influxWriteData.md b/documentation/docs/steps/influxWriteData.md index e51eeb7ff..3842e4fca 100644 --- a/documentation/docs/steps/influxWriteData.md +++ b/documentation/docs/steps/influxWriteData.md @@ -15,8 +15,8 @@ Very basic setup can be done like that (with user "admin" and password "adminPwd For more advanced setup please reach out to the respective documentation: -- https://hub.docker.com/_/influxdb/ (and https://github.com/docker-library/docs/tree/master/influxdb) -- https://hub.docker.com/r/grafana/grafana/ (and https://github.com/grafana/grafana-docker) +- InfluxDB ([Docker Hub](https://hub.docker.com/_/influxdb/) [GitHub](https://github.com/docker-library/docs/tree/master/influxdb)) +- Grafana ([Docker Hub](https://hub.docker.com/r/grafana/grafana/) [GitHub](https://github.com/grafana/grafana-docker)) After you have started your InfluxDB docker you need to create a database: diff --git a/documentation/docs/steps/npmExecute.md b/documentation/docs/steps/npmExecute.md index ba9384961..45d58ffb7 100644 --- a/documentation/docs/steps/npmExecute.md +++ b/documentation/docs/steps/npmExecute.md @@ -1,7 +1,6 @@ # ${docGenStepName} - ## ${docGenDescription} ## ${docGenParameters} diff --git a/documentation/docs/steps/seleniumExecuteTests.md b/documentation/docs/steps/seleniumExecuteTests.md index 2f55c2423..51fc4a057 100644 --- a/documentation/docs/steps/seleniumExecuteTests.md +++ b/documentation/docs/steps/seleniumExecuteTests.md @@ -18,7 +18,7 @@ seleniumExecuteTests (script: this) { ### Example test using WebdriverIO -Example based on http://webdriver.io/guide/getstarted/modes.html and http://webdriver.io/guide.html +Example based on and #### Configuration for Local Docker Environment diff --git a/documentation/docs/steps/transportRequestRelease.md b/documentation/docs/steps/transportRequestRelease.md index 48cabfd7a..8902d603d 100644 --- a/documentation/docs/steps/transportRequestRelease.md +++ b/documentation/docs/steps/transportRequestRelease.md @@ -8,11 +8,8 @@ ## ${docGenParameters} - ## ${docGenConfiguration} - - The step is configured using a customer configuration file provided as resource in an custom shared library. diff --git a/pom.xml b/pom.xml index 016ea0313..228bbd356 100644 --- a/pom.xml +++ b/pom.xml @@ -17,10 +17,10 @@ https://sap.github.io/jenkins-library/ - - Apache License 2.0 - https://github.com/SAP/jenkins-library/blob/master/LICENSE - + + Apache License 2.0 + https://github.com/SAP/jenkins-library/blob/master/LICENSE + diff --git a/src/com/sap/piper/GitUtils.groovy b/src/com/sap/piper/GitUtils.groovy index b24a3ce84..ca1c12243 100644 --- a/src/com/sap/piper/GitUtils.groovy +++ b/src/com/sap/piper/GitUtils.groovy @@ -33,25 +33,27 @@ String getGitCommitId() { return sh(returnStdout: true, script: 'git rev-parse HEAD').trim() } -String[] extractLogLines(String filter = '', - String from = 'origin/master', - String to = 'HEAD', - String format = '%b') { +String[] extractLogLines( + String filter = '', + String from = 'origin/master', + String to = 'HEAD', + String format = '%b' +) { - // Checks below: there was an value provided from outside, but the value was null. - // Throwing an exception is more transparent than making a fallback to the defaults - // used in case the paramter is omitted in the signature. - if(filter == null) throw new IllegalArgumentException('Parameter \'filter\' not provided.') - if(! from?.trim()) throw new IllegalArgumentException('Parameter \'from\' not provided.') - if(! to?.trim()) throw new IllegalArgumentException('Parameter \'to\' not provided.') - if(! format?.trim()) throw new IllegalArgumentException('Parameter \'format\' not provided.') + // Checks below: there was an value provided from outside, but the value was null. + // Throwing an exception is more transparent than making a fallback to the defaults + // used in case the paramter is omitted in the signature. + if(filter == null) throw new IllegalArgumentException('Parameter \'filter\' not provided.') + if(! from?.trim()) throw new IllegalArgumentException('Parameter \'from\' not provided.') + if(! to?.trim()) throw new IllegalArgumentException('Parameter \'to\' not provided.') + if(! format?.trim()) throw new IllegalArgumentException('Parameter \'format\' not provided.') sh ( returnStdout: true, - script: """#!/bin/bash - git log --pretty=format:${format} ${from}..${to} - """ - )?.split('\n') - ?.findAll { line -> line ==~ /${filter}/ } + script: """#!/bin/bash + git log --pretty=format:${format} ${from}..${to} + """ + )?.split('\n') + ?.findAll { line -> line ==~ /${filter}/ } } diff --git a/src/com/sap/piper/cm/ChangeManagement.groovy b/src/com/sap/piper/cm/ChangeManagement.groovy index 99f196b08..de41edc70 100644 --- a/src/com/sap/piper/cm/ChangeManagement.groovy +++ b/src/com/sap/piper/cm/ChangeManagement.groovy @@ -17,32 +17,32 @@ public class ChangeManagement implements Serializable { } String getChangeDocumentId( - String from = 'origin/master', - String to = 'HEAD', - String label = 'ChangeDocument\\s?:', - String format = '%b' - ) { + String from = 'origin/master', + String to = 'HEAD', + String label = 'ChangeDocument\\s?:', + String format = '%b' + ) { return getLabeledItem('ChangeDocumentId', from, to, label, format) } String getTransportRequestId( - String from = 'origin/master', - String to = 'HEAD', - String label = 'TransportRequest\\s?:', - String format = '%b' - ) { + String from = 'origin/master', + String to = 'HEAD', + String label = 'TransportRequest\\s?:', + String format = '%b' + ) { return getLabeledItem('TransportRequestId', from, to, label, format) } private String getLabeledItem( - String name, - String from, - String to, - String label, - String format - ) { + String name, + String from, + String to, + String label, + String format + ) { if( ! gitUtils.insideWorkTree() ) { throw new ChangeManagementException("Cannot retrieve ${name}. Not in a git work tree. ${name} is extracted from git commit messages.") diff --git a/vars/durationMeasure.groovy b/vars/durationMeasure.groovy index aa86886f5..33f7a15a1 100644 --- a/vars/durationMeasure.groovy +++ b/vars/durationMeasure.groovy @@ -42,4 +42,3 @@ def call(Map parameters = [:], body) { return duration } - diff --git a/vars/pipelineExecute.groovy b/vars/pipelineExecute.groovy index f0ba5bf0c..a2dbdac5f 100644 --- a/vars/pipelineExecute.groovy +++ b/vars/pipelineExecute.groovy @@ -54,15 +54,19 @@ void call(Map parameters = [:]) { deleteDir() - checkout([$class: 'GitSCM', branches: [[name: config.branch]], - doGenerateSubmoduleConfigurations: false, - extensions: [[$class: 'SparseCheckoutPaths', - sparseCheckoutPaths: [[path: config.path]] - ]], - submoduleCfg: [], - userRemoteConfigs: [[credentialsId: config.credentialsId, - url: config.repoUrl - ]] + checkout([ + $class: 'GitSCM', + branches: [[name: config.branch]], + doGenerateSubmoduleConfigurations: false, + extensions: [[ + $class: 'SparseCheckoutPaths', + sparseCheckoutPaths: [[path: config.path]] + ]], + submoduleCfg: [], + userRemoteConfigs: [[ + credentialsId: config.credentialsId, + url: config.repoUrl + ]] ]) } diff --git a/vars/transportRequestCreate.groovy b/vars/transportRequestCreate.groovy index f4ff51da7..974d86911 100644 --- a/vars/transportRequestCreate.groovy +++ b/vars/transportRequestCreate.groovy @@ -184,30 +184,33 @@ void call(parameters = [:]) { try { if(backendType == BackendType.SOLMAN) { transportRequestId = cm.createTransportRequestSOLMAN( - configuration.changeManagement.solman.docker, - configuration.changeDocumentId, - configuration.developmentSystemId, - configuration.changeManagement.endpoint, - configuration.changeManagement.credentialsId, - configuration.changeManagement.clientOpts) + configuration.changeManagement.solman.docker, + configuration.changeDocumentId, + configuration.developmentSystemId, + configuration.changeManagement.endpoint, + configuration.changeManagement.credentialsId, + configuration.changeManagement.clientOpts + ) } else if(backendType == BackendType.CTS) { transportRequestId = cm.createTransportRequestCTS( - configuration.changeManagement.cts.docker, - configuration.transportType, - configuration.targetSystem, - configuration.description, - configuration.changeManagement.endpoint, - configuration.changeManagement.credentialsId, - configuration.changeManagement.clientOpts) + configuration.changeManagement.cts.docker, + configuration.transportType, + configuration.targetSystem, + configuration.description, + configuration.changeManagement.endpoint, + configuration.changeManagement.credentialsId, + configuration.changeManagement.clientOpts + ) } else if (backendType == BackendType.RFC) { transportRequestId = cm.createTransportRequestRFC( - configuration.changeManagement.rfc.docker, - configuration.changeManagement.endpoint, - configuration.changeManagement.rfc.developmentInstance, - configuration.changeManagement.rfc.developmentClient, - configuration.changeManagement.credentialsId, - configuration.description, - configuration.verbose) + configuration.changeManagement.rfc.docker, + configuration.changeManagement.endpoint, + configuration.changeManagement.rfc.developmentInstance, + configuration.changeManagement.rfc.developmentClient, + configuration.changeManagement.credentialsId, + configuration.description, + configuration.verbose + ) } else { throw new IllegalArgumentException("Invalid backend type: '${backendType}'.") } diff --git a/vars/transportRequestRelease.groovy b/vars/transportRequestRelease.groovy index 7df08c7ee..5bd950ff9 100644 --- a/vars/transportRequestRelease.groovy +++ b/vars/transportRequestRelease.groovy @@ -70,7 +70,7 @@ import static com.sap.piper.cm.StepHelpers.getBackendTypeAndLogInfoIfCMIntegrati 'transportRequestId', /** @see transportRequestCreate */ 'verbose', - ]) +]) /** Releases a Transport Request. */ @GenerateDocumentation From 8fcdd0e9b5f4b43a910d9244372359377f98517b Mon Sep 17 00:00:00 2001 From: Marcus Holl Date: Wed, 15 May 2019 16:12:02 +0200 Subject: [PATCH 16/21] Provide docu metadata in gh-pages branch --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 07419fba7..fe65a888b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -37,7 +37,10 @@ jobs: name: Create Documentation install: docker pull squidfunk/mkdocs-material:3.0.4 before_script: documentation/bin/createDocu.sh - script: docker run --rm -it -v ${TRAVIS_BUILD_DIR}/documentation:/docs squidfunk/mkdocs-material:3.0.4 build --clean --strict + script: + - docker run -u `id -u`:`id -g` --rm -it -v ${TRAVIS_BUILD_DIR}/documentation:/docs squidfunk/mkdocs-material:3.0.4 build --clean --strict + - mkdir -p documentation/docs-gen/misc + - cp target/docuMetaData.json documentation/docs-gen/misc deploy: on: branch: master From 9f7056b73f92d6a0eb409794f22d6f1e03bb5714 Mon Sep 17 00:00:00 2001 From: Marcus Holl Date: Fri, 17 May 2019 12:11:29 +0200 Subject: [PATCH 17/21] Reuse getSteps from StepHelper in common step tests --- test/groovy/CommonStepsTest.groovy | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/test/groovy/CommonStepsTest.groovy b/test/groovy/CommonStepsTest.groovy index 2b5f1e426..d684c7c19 100644 --- a/test/groovy/CommonStepsTest.groovy +++ b/test/groovy/CommonStepsTest.groovy @@ -4,6 +4,7 @@ import static org.hamcrest.Matchers.equalTo import static org.hamcrest.Matchers.is import static org.junit.Assert.assertThat import static org.junit.Assert.fail +import static util.StepHelper.getSteps import java.io.File; import java.util.stream.Collectors @@ -242,11 +243,4 @@ public class CommonStepsTest extends BasePiperTest{ assertThat("Steps with call methods with return types other than void: ${stepsWithCallMethodsOtherThanVoid}", stepsWithCallMethodsOtherThanVoid, is(empty())) } - - private static getSteps() { - List steps = [] - new File('vars').traverse(type: FileType.FILES, maxDepth: 0) - { if(it.getName().endsWith('.groovy')) steps << (it =~ /vars[\\\/](.*)\.groovy/)[0][1] } - return steps - } } From 77e344d181bbcb58b84f14d7977d692618d378a8 Mon Sep 17 00:00:00 2001 From: Christopher Fenner Date: Fri, 17 May 2019 13:20:13 +0200 Subject: [PATCH 18/21] fix code climate issues II (#715) * fix code climate issues * fix code climate issues * fix code climate issues --- README.md | 4 +-- documentation/docs/configuration.md | 8 ++--- documentation/docs/css/extra.css | 2 +- .../docs/scenarios/changeManagement.md | 8 ++--- .../docs/steps/checkChangeInDevelopment.md | 19 +++++----- documentation/docs/steps/influxWriteData.md | 2 +- documentation/docs/steps/kanikoExecute.md | 2 +- .../docs/steps/transportRequestCreate.md | 3 +- .../docs/steps/transportRequestUploadFile.md | 36 ++++++++++--------- pom.xml | 6 ++-- src/com/sap/piper/MtaUtils.groovy | 1 - src/com/sap/piper/cm/ChangeManagement.groovy | 16 ++++----- src/com/sap/piper/cm/StepHelpers.groovy | 14 ++++---- vars/artifactSetVersion.groovy | 6 ++-- vars/transportRequestUploadFile.groovy | 6 ++-- 15 files changed, 69 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index fc413e63d..cf4087feb 100644 --- a/README.md +++ b/README.md @@ -71,8 +71,7 @@ To setup the shared library, you need to perform the following steps: 1. Login to your Jenkins instance with administration privileges. 1. Open the system configuration page (*Manage Jenkins > Configure System*). -1. Scroll down to section *Global Pipeline Libraries* and add a new Library by - clicking the *Add* button. +1. Scroll down to section *Global Pipeline Libraries* and add a new Library by clicking the *Add* button. 1. set *Library Name* to `piper-lib-os` 1. set *Default Version* to the branch or tag you want to consume (e.g. `master` or `v0.1`) 1. set *Retrieval Method* to `Modern SCM` @@ -102,6 +101,7 @@ Feel free to open new issues for feature requests, bugs or general feedback on the [GitHub issues page of this project][piper-library-issues]. Register to our [google group][google-group] in order to get updates or for asking questions. + # Contributing Read and understand our [contribution guidelines][piper-library-contribution] diff --git a/documentation/docs/configuration.md b/documentation/docs/configuration.md index d61db5fb5..5be9daafc 100644 --- a/documentation/docs/configuration.md +++ b/documentation/docs/configuration.md @@ -11,10 +11,10 @@ Your configuration inherits from the default configuration located at [https://g Configuration of the Piper steps as well the Piper templates can be done in a hierarchical manner. 1. Directly passed step parameters will always take precedence over other configuration values and defaults -2. Stage configuration parameters define a Jenkins pipeline stage dependent set of parameters (e.g. deployment options for the `Acceptance` stage) -3. Step configuration defines how steps behave in general (e.g. step `cloudFoundryDeploy`) -4. General configuration parameters define parameters which are available across step boundaries -5. Default configuration comes with the Piper library and is always available +1. Stage configuration parameters define a Jenkins pipeline stage dependent set of parameters (e.g. deployment options for the `Acceptance` stage) +1. Step configuration defines how steps behave in general (e.g. step `cloudFoundryDeploy`) +1. General configuration parameters define parameters which are available across step boundaries +1. Default configuration comes with the Piper library and is always available ![Piper Configuration](images/piper_config.png) diff --git a/documentation/docs/css/extra.css b/documentation/docs/css/extra.css index 05406242d..a23270bb3 100644 --- a/documentation/docs/css/extra.css +++ b/documentation/docs/css/extra.css @@ -4,4 +4,4 @@ .md-typeset a:not(.headerlink):hover { text-decoration: underline; -} +} diff --git a/documentation/docs/scenarios/changeManagement.md b/documentation/docs/scenarios/changeManagement.md index 4786772d0..11500ae76 100644 --- a/documentation/docs/scenarios/changeManagement.md +++ b/documentation/docs/scenarios/changeManagement.md @@ -33,10 +33,10 @@ The basic workflow is as follows: **Note:** The blank line between message header and message description is mandatory. -2. To communicate with SAP Solution Manager, the pipeline uses credentials that must be stored on Jenkins using the credential ID `CM`. For more information, see [checkChangeInDevelopment](https://sap.github.io/jenkins-library/steps/checkChangeInDevelopment/). -3. The required transport request is created on the fly. **Note:** The change document can contain various components (for example, UI and backend components). -4. The changes of your development team trigger the Jenkins pipeline. It builds and validates the changes and attaches them to the respective transport request. -5. As soon as the development process is completed, the change document in SAP Solution Manager can be set to status `to be tested` and all components can be transported to the test system. +1. To communicate with SAP Solution Manager, the pipeline uses credentials that must be stored on Jenkins using the credential ID `CM`. For more information, see [checkChangeInDevelopment](https://sap.github.io/jenkins-library/steps/checkChangeInDevelopment/). +1. The required transport request is created on the fly. **Note:** The change document can contain various components (for example, UI and backend components). +1. The changes of your development team trigger the Jenkins pipeline. It builds and validates the changes and attaches them to the respective transport request. +1. As soon as the development process is completed, the change document in SAP Solution Manager can be set to status `to be tested` and all components can be transported to the test system. ![Hybrid Application Development Workflow](../images/Scenario_SolMan.png "Hybrid Application Development Workflow") ###### Hybrid Application Development Workflow diff --git a/documentation/docs/steps/checkChangeInDevelopment.md b/documentation/docs/steps/checkChangeInDevelopment.md index a3e30d438..e3396e75e 100644 --- a/documentation/docs/steps/checkChangeInDevelopment.md +++ b/documentation/docs/steps/checkChangeInDevelopment.md @@ -32,8 +32,7 @@ resource in an custom shared library. // inside the shared lib denoted by 'foo' the additional configuration file // needs to be located under 'resources' ('resoures/myConfig.yml') -prepareDefaultValues script: this, - customDefaults: 'myConfig.yml' +prepareDefaultValues script: this, customDefaults: 'myConfig.yml' ``` Example content of `'resources/myConfig.yml'` in branch `'master'` of the repository denoted by @@ -79,11 +78,13 @@ The parameters can also be provided when the step is invoked: // explict endpoint provided, we search for changeDocumentId // starting at the previous commit (HEAD~1) rather than on // 'origin/master' (the default). - checkChangeInDevelopment script:this - changeManagement: [ - endpoint: 'https:example.org/cm' - git: [ - from: 'HEAD~1' - ] - ] + checkChangeInDevelopment( + script: this + changeManagement: [ + endpoint: 'https:example.org/cm' + git: [ + from: 'HEAD~1' + ] + ] + ) ``` diff --git a/documentation/docs/steps/influxWriteData.md b/documentation/docs/steps/influxWriteData.md index 3842e4fca..8e288d224 100644 --- a/documentation/docs/steps/influxWriteData.md +++ b/documentation/docs/steps/influxWriteData.md @@ -43,7 +43,7 @@ Once you have started both docker containers and Influx and Grafana are running To setup your Jenkins you need to do two configuration steps: 1. Configure Jenkins (via Manage Jenkins) -2. Adapt pipeline configuration +1. Adapt pipeline configuration ### Configure Jenkins diff --git a/documentation/docs/steps/kanikoExecute.md b/documentation/docs/steps/kanikoExecute.md index 947d35cd4..4a369e55e 100644 --- a/documentation/docs/steps/kanikoExecute.md +++ b/documentation/docs/steps/kanikoExecute.md @@ -10,7 +10,7 @@ Kaniko expects a Docker `config.json` file containing the credential information You can create it like explained in the Docker Success Center in the articale about [How to generate a new auth in the config.json file](https://success.docker.com/article/generate-new-auth-in-config-json-file). Please copy this file and upload it to your Jenkins for example
-via _Jenkins_ -> _Credentials_ -> _System_ -> _Global credentials (unrestricted)_ -> _ Add Credentials_ -> +via _Jenkins_ -> _Credentials_ -> _System_ -> _Global credentials (unrestricted)_ -> _Add Credentials_ -> * Kind: _Secret file_ * File: upload your `config.json` file diff --git a/documentation/docs/steps/transportRequestCreate.md b/documentation/docs/steps/transportRequestCreate.md index 8d9a1bc81..0768d4d01 100644 --- a/documentation/docs/steps/transportRequestCreate.md +++ b/documentation/docs/steps/transportRequestCreate.md @@ -23,8 +23,7 @@ resource in an custom shared library. // inside the shared lib denoted by 'foo' the additional configuration file // needs to be located under 'resources' ('resoures/myConfig.yml') -prepareDefaultValues script: this, - customDefaults: 'myConfig.yml' +prepareDefaultValues script: this, customDefaults: 'myConfig.yml' ``` Example content of `'resources/myConfig.yml'` in branch `'master'` of the repository denoted by diff --git a/documentation/docs/steps/transportRequestUploadFile.md b/documentation/docs/steps/transportRequestUploadFile.md index 66facda78..da937eb3f 100644 --- a/documentation/docs/steps/transportRequestUploadFile.md +++ b/documentation/docs/steps/transportRequestUploadFile.md @@ -74,21 +74,25 @@ The parameters can also be provided when the step is invoked. For examples see b ```groovy // SOLMAN -transportRequestUploadFile script:this, - changeDocumentId: '001', // typically provided via git commit history - transportRequestId: '001', // typically provided via git commit history - applicationId: '001', - filePath: '/path', - changeManagement:[ - type: 'SOLMAN' - endpoint: 'https://example.org/cm' - ] +transportRequestUploadFile( + script: this, + changeDocumentId: '001', // typically provided via git commit history + transportRequestId: '001', // typically provided via git commit history + applicationId: '001', + filePath: '/path', + changeManagement: [ + type: 'SOLMAN' + endpoint: 'https://example.org/cm' + ] +) // CTS -transportRequestUploadFile script:this, - transportRequestId: '001', // typically provided via git commit history - filePath: '/path', - changeManagement:[ - type: 'CTS' - endpoint: 'https://example.org/cm' - ] +transportRequestUploadFile( + script: this, + transportRequestId: '001', // typically provided via git commit history + filePath: '/path', + changeManagement: [ + type: 'CTS' + endpoint: 'https://example.org/cm' + ] +) ``` diff --git a/pom.xml b/pom.xml index 228bbd356..295691205 100644 --- a/pom.xml +++ b/pom.xml @@ -1,7 +1,9 @@ - + org.jenkins-ci.plugins plugin diff --git a/src/com/sap/piper/MtaUtils.groovy b/src/com/sap/piper/MtaUtils.groovy index ea308be18..5c294a2de 100644 --- a/src/com/sap/piper/MtaUtils.groovy +++ b/src/com/sap/piper/MtaUtils.groovy @@ -37,4 +37,3 @@ class MtaUtils { if (!script.fileExists(targetMtaDescriptor)) throw new AbortException("'${targetMtaDescriptor}' has not been generated.") } } - diff --git a/src/com/sap/piper/cm/ChangeManagement.groovy b/src/com/sap/piper/cm/ChangeManagement.groovy index de41edc70..bcf9600b3 100644 --- a/src/com/sap/piper/cm/ChangeManagement.groovy +++ b/src/com/sap/piper/cm/ChangeManagement.groovy @@ -421,16 +421,16 @@ public class ChangeManagement implements Serializable { String clientOpts = '') { String cmCommandLine = '#!/bin/bash' if(clientOpts) { - cmCommandLine += """ - export CMCLIENT_OPTS="${clientOpts}" """ + cmCommandLine += """ + export CMCLIENT_OPTS="${clientOpts}" """ } cmCommandLine += """ - cmclient -e '$endpoint' \ - -u '$username' \ - -p '$password' \ - -t ${type} \ - ${command} ${(args as Iterable).join(' ')} - """ + cmclient -e '$endpoint' \ + -u '$username' \ + -p '$password' \ + -t ${type} \ + ${command} ${(args as Iterable).join(' ')} + """ return cmCommandLine } } diff --git a/src/com/sap/piper/cm/StepHelpers.groovy b/src/com/sap/piper/cm/StepHelpers.groovy index f48c29107..11542522b 100644 --- a/src/com/sap/piper/cm/StepHelpers.groovy +++ b/src/com/sap/piper/cm/StepHelpers.groovy @@ -23,7 +23,7 @@ public class StepHelpers { } script.echo "[INFO] Retrieving transport request id from commit history [from: ${configuration.changeManagement.git.from}, to: ${configuration.changeManagement.git.to}]." + - " Searching for pattern '${configuration.changeManagement.transportRequestLabel}'. Searching with format '${configuration.changeManagement.git.format}'." + " Searching for pattern '${configuration.changeManagement.transportRequestLabel}'. Searching with format '${configuration.changeManagement.git.format}'." try { transportRequestId = cm.getTransportRequestId( @@ -62,7 +62,7 @@ public class StepHelpers { } script.echo "[INFO] Retrieving ChangeDocumentId from commit history [from: ${configuration.changeManagement.git.from}, to: ${configuration.changeManagement.git.to}]." + - "Searching for pattern '${configuration.changeManagement.changeDocumentLabel}'. Searching with format '${configuration.changeManagement.git.format}'." + "Searching for pattern '${configuration.changeManagement.changeDocumentLabel}'. Searching with format '${configuration.changeManagement.git.format}'." try { changeDocumentId = cm.getChangeDocumentId( @@ -91,15 +91,15 @@ public class StepHelpers { backendType = configuration.changeManagement.type as BackendType } catch(IllegalArgumentException e) { script.error "Invalid backend type: '${configuration.changeManagement.type}'. " + - "Valid values: [${BackendType.values().join(', ')}]. " + - "Configuration: 'changeManagement/type'." + "Valid values: [${BackendType.values().join(', ')}]. " + + "Configuration: 'changeManagement/type'." } if (backendType == BackendType.NONE) { script.echo "[INFO] Change management integration intentionally switched off. " + - "In order to enable it provide 'changeManagement/type with one of " + - "[${BackendType.values().minus(BackendType.NONE).join(', ')}] and maintain " + - "other required properties like 'endpoint', 'credentialsId'." + "In order to enable it provide 'changeManagement/type with one of " + + "[${BackendType.values().minus(BackendType.NONE).join(', ')}] and maintain " + + "other required properties like 'endpoint', 'credentialsId'." } return backendType diff --git a/vars/artifactSetVersion.groovy b/vars/artifactSetVersion.groovy index 6bbc11a54..dd6139bec 100644 --- a/vars/artifactSetVersion.groovy +++ b/vars/artifactSetVersion.groovy @@ -169,9 +169,9 @@ void call(Map parameters = [:], Closure body = null) { try { sh """#!/bin/bash - git add . - git ${gitConfig} commit -m 'update version ${newVersion}' - git tag ${config.tagPrefix}${newVersion}""" + git add . + git ${gitConfig} commit -m 'update version ${newVersion}' + git tag ${config.tagPrefix}${newVersion}""" config.gitCommitId = gitUtils.getGitCommitIdOrNull() } catch (e) { error "[${STEP_NAME}]git commit and tag failed: ${e}" diff --git a/vars/transportRequestUploadFile.groovy b/vars/transportRequestUploadFile.groovy index 048ba93ab..657351177 100644 --- a/vars/transportRequestUploadFile.groovy +++ b/vars/transportRequestUploadFile.groovy @@ -177,9 +177,9 @@ void call(parameters = [:]) { "Change document id not provided (parameter: \'changeDocumentId\' or via commit history).") } configuration = configHelper - .withMandatoryProperty('transportRequestId', - "Transport request id not provided (parameter: \'transportRequestId\' or via commit history).") - .use() + .withMandatoryProperty('transportRequestId', + "Transport request id not provided (parameter: \'transportRequestId\' or via commit history).") + .use() def uploadingMessage = ['[INFO] Uploading file ' + "'${backendType == BackendType.RFC ? configuration.applicationUrl : configuration.filePath}' " + From e17753f3cd6798458aa3f6c7db2030dedfcc37ac Mon Sep 17 00:00:00 2001 From: Tom Kiemes Date: Wed, 22 May 2019 08:16:07 +0200 Subject: [PATCH 19/21] Fix formatting (#719) * Fix formatting Co-authored-by: Markus Winkler * Update createDocu.groovy --- documentation/bin/createDocu.groovy | 4 ++-- vars/pipelineRestartSteps.groovy | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/documentation/bin/createDocu.groovy b/documentation/bin/createDocu.groovy index 7dfe920aa..f18411dbc 100644 --- a/documentation/bin/createDocu.groovy +++ b/documentation/bin/createDocu.groovy @@ -621,9 +621,9 @@ def handleStep(stepName, prepareDefaultValuesStep, gse, customDefaults) { step.parameters['script'] = [ docu: 'The common script environment of the Jenkinsfile running. ' + 'Typically the reference to the script calling the pipeline ' + - 'step is provided with the this parameter, as in `script: this`. ' + + 'step is provided with the `this` parameter, as in `script: this`. ' + 'This allows the function to access the ' + - 'commonPipelineEnvironment for retrieving, for example, configuration parameters.', + '`commonPipelineEnvironment` for retrieving, e.g. configuration parameters.', required: true, GENERAL_CONFIG: false, diff --git a/vars/pipelineRestartSteps.groovy b/vars/pipelineRestartSteps.groovy index dcc4b3474..6380ed322 100644 --- a/vars/pipelineRestartSteps.groovy +++ b/vars/pipelineRestartSteps.groovy @@ -11,11 +11,11 @@ import groovy.transform.Field @Field Set STEP_CONFIG_KEYS = [ /** - * If it is set to true` the step `mailSendNotification` will be triggered in case of an error. + * If it is set to `true` the step `mailSendNotification` will be triggered in case of an error. */ 'sendMail', /** - * Defines the time period where the job waits for input. Default is 15 minutes. Once this time is passed the job enters state FAILED. + * Defines the time period where the job waits for input. Default is 15 minutes. Once this time is passed the job enters state `FAILED`. */ 'timeoutInSeconds' ] From dbf110504e580cfe02c32a5658b61fdd68f01938 Mon Sep 17 00:00:00 2001 From: Oliver Nocon <33484802+OliverNocon@users.noreply.github.com> Date: Wed, 22 May 2019 10:42:59 +0200 Subject: [PATCH 20/21] Templates - Add advanced confirm stage (#720) * Templates - Confirm Stage * include feedback: allow customizing of default message * adapt docs * add default message --- resources/default_pipeline_environment.yml | 2 + .../piper/GenerateStageDocumentation.groovy | 12 +++ .../PiperPipelineStageConfirmTest.groovy | 80 +++++++++++++++++++ .../groovy/templates/PiperPipelineTest.groovy | 51 +++++++++--- vars/piperPipeline.groovy | 4 +- vars/piperPipelineStageConfirm.groovy | 80 +++++++++++++++++++ 6 files changed, 217 insertions(+), 12 deletions(-) create mode 100644 src/com/sap/piper/GenerateStageDocumentation.groovy create mode 100644 test/groovy/templates/PiperPipelineStageConfirmTest.groovy create mode 100644 vars/piperPipelineStageConfirm.groovy diff --git a/resources/default_pipeline_environment.yml b/resources/default_pipeline_environment.yml index 71f11f6d1..9ffb0156a 100644 --- a/resources/default_pipeline_environment.yml +++ b/resources/default_pipeline_environment.yml @@ -44,6 +44,8 @@ general: # runAsUser: 1000 # fsGroup: 1000 manualConfirmation: true + manualConfirmationMessage: 'Shall we proceed to Promote & Release?' + manualConfirmationTimeout: 720 # 1 month productiveBranch: 'master' whitesource: serviceUrl: 'https://saas.whitesourcesoftware.com/api' diff --git a/src/com/sap/piper/GenerateStageDocumentation.groovy b/src/com/sap/piper/GenerateStageDocumentation.groovy new file mode 100644 index 000000000..1925655a1 --- /dev/null +++ b/src/com/sap/piper/GenerateStageDocumentation.groovy @@ -0,0 +1,12 @@ +package com.sap.piper + +import java.lang.annotation.ElementType +import java.lang.annotation.Retention +import java.lang.annotation.RetentionPolicy +import java.lang.annotation.Target + +@Retention(RetentionPolicy.RUNTIME) +@Target([ElementType.METHOD, ElementType.TYPE]) +public @interface GenerateStageDocumentation { + public String defaultStageName() +} diff --git a/test/groovy/templates/PiperPipelineStageConfirmTest.groovy b/test/groovy/templates/PiperPipelineStageConfirmTest.groovy new file mode 100644 index 000000000..696f6e741 --- /dev/null +++ b/test/groovy/templates/PiperPipelineStageConfirmTest.groovy @@ -0,0 +1,80 @@ +#!groovy +package templates + +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.RuleChain +import util.* + +import static org.hamcrest.Matchers.containsString +import static org.hamcrest.Matchers.is +import static org.junit.Assert.assertThat + +class PiperPipelineStageConfirmTest extends BasePiperTest { + private JenkinsStepRule jsr = new JenkinsStepRule(this) + private JenkinsLoggingRule jlr = new JenkinsLoggingRule(this) + + private Map timeoutSettings + private Map inputSettings + + @Rule + public RuleChain rules = Rules + .getCommonRules(this) + .around(new JenkinsReadYamlRule(this)) + .around(jlr) + .around(jsr) + + @Before + void init() { + binding.variables.env.STAGE_NAME = 'Confirm' + + helper.registerAllowedMethod('timeout', [Map.class, Closure.class], {m, body -> + timeoutSettings = m + return body() + }) + + helper.registerAllowedMethod('input', [Map.class], {m -> + inputSettings = m + return [reason: 'this is my test reason for failing step 1 and step 3', acknowledgement: true] + }) + } + + @Test + void testStageDefault() { + + jsr.step.piperPipelineStageConfirm( + script: nullScript + ) + assertThat(timeoutSettings.unit, is('HOURS')) + assertThat(timeoutSettings.time, is(720)) + assertThat(inputSettings.message, is('Shall we proceed to Promote & Release?')) + } + + @Test + void testStageBuildUnstable() { + + binding.setVariable('currentBuild', [result: 'UNSTABLE']) + nullScript.commonPipelineEnvironment.setValue('unstableSteps', ['step1', 'step3']) + + helper.registerAllowedMethod('text', [Map.class], {m -> + assertThat(m.defaultValue, containsString('step1:')) + assertThat(m.defaultValue, containsString('step3:')) + assertThat(m.description, is('Please provide a reason for overruling following failed steps:')) + assertThat(m.name, is('reason')) + }) + + helper.registerAllowedMethod('booleanParam', [Map.class], {m -> + assertThat(m.description, is('I acknowledge that for traceability purposes the approval reason is stored together with my user name / user id:')) + assertThat(m.name, is('acknowledgement')) + }) + + jsr.step.piperPipelineStageConfirm( + script: nullScript + ) + assertThat(inputSettings.message, is('Approve continuation of pipeline, although some steps failed.')) + + assertThat(jlr.log, containsString('this is my test reason')) + assertThat(jlr.log, containsString('Acknowledged:\n-------------\ntrue')) + } +} diff --git a/test/groovy/templates/PiperPipelineTest.groovy b/test/groovy/templates/PiperPipelineTest.groovy index f7439fa47..1d9295875 100644 --- a/test/groovy/templates/PiperPipelineTest.groovy +++ b/test/groovy/templates/PiperPipelineTest.groovy @@ -54,17 +54,41 @@ class PiperPipelineTest extends BasePiperTest { helper.registerAllowedMethod('when', [Closure.class], {cWhen -> - helper.registerAllowedMethod('allOf', [Closure.class], null) + helper.registerAllowedMethod('allOf', [Closure.class], {cAllOf -> + def branchResult = false + helper.registerAllowedMethod('branch', [String.class], {branchName -> + if (!branchResult) + branchResult = (branchName == env.BRANCH_NAME) + if( !branchResult) { + throw new PipelineWhenException("Stage '${stageName}' skipped - expression: '${branchResult}'") + } + }) + helper.registerAllowedMethod('expression', [Closure.class], { Closure cExp -> + def result = cExp() + if(!result) { + throw new PipelineWhenException("Stage '${stageName}' skipped - expression: '${result}'") + } + return result + }) + return cAllOf() + }) helper.registerAllowedMethod('anyOf', [Closure.class], {cAnyOf -> def result = false helper.registerAllowedMethod('branch', [String.class], {branchName -> if (!result) result = (branchName == env.BRANCH_NAME) - if( !result) { - throw new PipelineWhenException("Stage '${stageName}' skipped - expression: '${result}'") - } + return result }) + helper.registerAllowedMethod('expression', [Closure.class], { Closure cExp -> + if (!result) + result = cExp() + return result + }) + cAnyOf() + if(!result) { + throw new PipelineWhenException("Stage '${stageName}' skipped - anyOf: '${result}'") + } return cAnyOf() }) @@ -151,8 +175,8 @@ class PiperPipelineTest extends BasePiperTest { helper.registerAllowedMethod('piperPipelineStageCompliance', [Map.class], {m -> stepsCalled.add('piperPipelineStageCompliance') }) - helper.registerAllowedMethod('input', [Map.class], {m -> - stepsCalled.add('input') + helper.registerAllowedMethod('piperPipelineStageConfirm', [Map.class], {m -> + stepsCalled.add('piperPipelineStageConfirm') }) helper.registerAllowedMethod('piperPipelineStagePromote', [Map.class], {m -> stepsCalled.add('piperPipelineStagePromote') @@ -188,10 +212,17 @@ class PiperPipelineTest extends BasePiperTest { } @Test - void testConfirm() { + void testConfirmUnstable() { + nullScript.commonPipelineEnvironment.configuration = [ + general: [ + manualConfirmation: false + ] + ] + binding.setVariable('currentBuild', [result: 'UNSTABLE']) + jsr.step.piperPipeline(script: nullScript) - assertThat(stepsCalled, hasItem('input')) + assertThat(stepsCalled, hasItem('piperPipelineStageConfirm')) } @@ -204,7 +235,7 @@ class PiperPipelineTest extends BasePiperTest { ] jsr.step.piperPipeline(script: nullScript) - assertThat(stepsCalled, not(hasItem('input'))) + assertThat(stepsCalled, not(hasItem('piperPipelineStageConfirm'))) } @Test @@ -232,7 +263,7 @@ class PiperPipelineTest extends BasePiperTest { 'piperPipelineStageSecurity', 'piperPipelineStagePerformance', 'piperPipelineStageCompliance', - 'input', + 'piperPipelineStageConfirm', 'piperPipelineStagePromote', 'piperPipelineStageRelease', 'piperPipelineStagePost' diff --git a/vars/piperPipeline.groovy b/vars/piperPipeline.groovy index 4bad91770..5abdbb024 100644 --- a/vars/piperPipeline.groovy +++ b/vars/piperPipeline.groovy @@ -62,9 +62,9 @@ void call(parameters) { } stage('Confirm') { agent none - when {allOf {branch parameters.script.commonPipelineEnvironment.getStepConfiguration('', '').productiveBranch; expression {return parameters.script.commonPipelineEnvironment.getStepConfiguration('piperInitRunStageConfiguration', env.STAGE_NAME).manualConfirmation}}} + when {allOf {expression { env.BRANCH_NAME ==~ parameters.script.commonPipelineEnvironment.getStepConfiguration('', '').productiveBranch }; anyOf {expression {return (currentBuild.result == 'UNSTABLE')}; expression {return parameters.script.commonPipelineEnvironment.getStepConfiguration('piperInitRunStageConfiguration', env.STAGE_NAME).manualConfirmation}}}} steps { - input message: 'Shall we proceed to promotion & release?' + piperPipelineStageConfirm script: parameters.script } } stage('Promote') { diff --git a/vars/piperPipelineStageConfirm.groovy b/vars/piperPipelineStageConfirm.groovy new file mode 100644 index 000000000..94a464778 --- /dev/null +++ b/vars/piperPipelineStageConfirm.groovy @@ -0,0 +1,80 @@ +import com.sap.piper.ConfigurationHelper +import com.sap.piper.GenerateStageDocumentation +import groovy.transform.Field + +import static com.sap.piper.Prerequisites.checkScript + +@Field String STEP_NAME = getClass().getName() + +@Field Set GENERAL_CONFIG_KEYS = [ + /** + * Specifies if a manual confirmation is active before running the __Promote__ and __Release__ stages of the pipeline. + * @possibleValues `true`, `false` + */ + 'manualConfirmation', + /** Defines message displayed as default manual confirmation. Please note: only used in case pipeline is in state __SUCCESSFUL__ */ + 'manualConfirmationMessage', + /** Defines how many hours a manual confirmation is possible for a dedicated pipeline. */ + 'manualConfirmationTimeout' + +] +@Field Set STEP_CONFIG_KEYS = GENERAL_CONFIG_KEYS +@Field Set PARAMETER_KEYS = STEP_CONFIG_KEYS + +/** + * In this stage a manual confirmation is requested before processing subsequent stages like __Promote__ and __Release__. + * + * This stage will be active in two scenarios: + * - manual activation of this stage + * - in case of an 'UNSTABLE' build (even when manual confirmation is inactive) + */ +@GenerateStageDocumentation(defaultStageName = 'Confirm') +void call(Map parameters = [:]) { + def script = checkScript(this, parameters) ?: this + def stageName = parameters.stageName?:env.STAGE_NAME + + Map config = ConfigurationHelper.newInstance(this) + .loadStepDefaults() + .mixinGeneralConfig(script.commonPipelineEnvironment, GENERAL_CONFIG_KEYS) + .mixinStageConfig(script.commonPipelineEnvironment, stageName, STEP_CONFIG_KEYS) + .mixin(parameters, PARAMETER_KEYS) + .use() + + String unstableStepNames = script.commonPipelineEnvironment.getValue('unstableSteps') ? "${script.commonPipelineEnvironment.getValue('unstableSteps').join(':\n------\n')}:" : '' + + boolean approval = false + def userInput + + timeout( + unit: 'HOURS', + time: config.manualConfirmationTimeout + ){ + if (currentBuild.result == 'UNSTABLE') { + while(!approval) { + userInput = input( + message: 'Approve continuation of pipeline, although some steps failed.', + ok: 'Approve', + parameters: [ + text( + defaultValue: unstableStepNames, + description: 'Please provide a reason for overruling following failed steps:', + name: 'reason' + ), + booleanParam( + defaultValue: false, + description: 'I acknowledge that for traceability purposes the approval reason is stored together with my user name / user id:', + name: 'acknowledgement' + ) + ] + ) + approval = userInput.acknowledgement && userInput.reason?.length() > (unstableStepNames.length() + 10) + } + echo "Reason:\n-------------\n${userInput.reason}" + echo "Acknowledged:\n-------------\n${userInput.acknowledgement}" + } else { + input message: config.manualConfirmationMessage + } + + } + +} From 5fcefb55b856f6f86372b444c29dafb8288bfe33 Mon Sep 17 00:00:00 2001 From: Oliver Feldmann Date: Thu, 23 May 2019 08:13:23 +0200 Subject: [PATCH 21/21] Fix typo in yaml (#722) --- documentation/docs/scenarios/CAP_Scenario.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/scenarios/CAP_Scenario.md b/documentation/docs/scenarios/CAP_Scenario.md index 32a9d8164..2975ee2f5 100644 --- a/documentation/docs/scenarios/CAP_Scenario.md +++ b/documentation/docs/scenarios/CAP_Scenario.md @@ -47,7 +47,7 @@ node(){ ```yaml steps: - mtaBuild + mtaBuild: buildTarget: 'CF' cloudFoundryDeploy: cloudFoundry: