1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2024-12-12 10:55:20 +02:00

Merge remote-tracking branch 'github/master' into HEAD

This commit is contained in:
Marcus Holl 2019-05-24 14:26:07 +02:00
commit 56b651dbe9
93 changed files with 770 additions and 259 deletions

View File

@ -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

View File

@ -71,11 +71,9 @@ 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 *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`
@ -103,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]

View File

@ -3,6 +3,7 @@ import groovy.json.JsonOutput
import org.yaml.snakeyaml.Yaml
import org.codehaus.groovy.control.CompilerConfiguration
import com.sap.piper.GenerateDocumentation
import com.sap.piper.GenerateStageDocumentation
import java.util.regex.Matcher
import groovy.text.StreamingTemplateEngine
@ -23,7 +24,7 @@ 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) : renderSimpleDefaultValue(props.defaultValue)
t += "| `${it}` | ${props.mandatory ?: props.required ? 'yes' : 'no'} | ${defaultValue} | ${props.value ?: ''} |\n"
}
@ -48,6 +49,11 @@ class TemplateHelper {
.join('<br />')
}
private static renderSimpleDefaultValue(def _default) {
if (_default == null) return ''
return "`${_default}`"
}
static createParameterDescriptionSection(Map parameters) {
def t = ''
parameters.keySet().toSorted().each {
@ -78,6 +84,98 @@ class TemplateHelper {
t.trim()
}
static createStageContentSection(Map stageDescriptions) {
def t = 'This stage comprises following steps which are activated depending on your use-case/configuration:\n\n'
t += '| step | step description |\n'
t += '| ---- | ---------------- |\n'
stageDescriptions.each {step, description ->
t += "| [${step}](../steps/${step}.md) | ${description.trim()} |\n"
}
return t
}
static createStageActivationSection() {
def t = '''This stage will be active if any one of the following conditions is met:
* Stage configuration in [config.yml file](../configuration.md) contains entries for this stage.
* Any of the conditions are met which are explained in the section [Step Activation](#step-activation).
'''
return t.trim()
}
static createStepActivationSection(Map configConditions) {
if (!configConditions) return 'For this stage no conditions are assigned to steps.'
def t = 'Certain steps will be activated automatically depending on following conditions:\n\n'
t += '| step | config key | config value | file pattern |\n'
t += '| ---- | ---------- | ------------ | ------------ |\n'
configConditions?.each {stepName, conditions ->
t += "| ${stepName} "
t += "| ${renderValueList(conditions?.configKeys)} "
t += "| ${renderValueList(mapToValueList(conditions?.config))} "
List filePatterns = []
if (conditions?.filePattern) filePatterns.add(conditions?.filePattern)
if (conditions?.filePatternFromConfig) filePatterns.add(conditions?.filePatternFromConfig)
t += "| ${renderValueList(filePatterns)} |\n"
}
t += '''
!!! info "Step condition details"
There are currently several conditions which can be checked.<br /> This is done in the [Init stage](init.md) of the pipeline shortly after checkout of the source code repository.<br/ >
**Important: It will be sufficient that any one condition per step is met.**
* `config key`: Checks if a defined configuration parameter is set.
* `config value`: Checks if a configuration parameter has a defined value.
* `file pattern`: Checks if files according a defined pattern exist in the project. Either the pattern is speficified direcly or it is retrieved from a configuration parameter.
!!! note "Overruling step activation conditions"
It is possible to overrule the automatically detected step activation status.<br />
Just add to your stage configuration `<stepName>: false`, for example `deployToKubernetes: false`.
For details about the configuration options, please see [Configuration of Piper](../configuration.md).
'''
return t
}
private static renderValueList(List valueList) {
if (!valueList) return ''
if (valueList.size() > 1) {
List quotedList = []
valueList.each {listItem ->
quotedList.add("-`${listItem}`")
}
return quotedList.join('<br />')
} else {
return "`${valueList[0]}`"
}
}
private static mapToValueList(Map map) {
List valueList = []
map?.each {key, value ->
if (value instanceof List) {
value.each {listItem ->
valueList.add("${key}: ${listItem}")
}
} else {
valueList.add("${key}: ${value}")
}
}
return valueList
}
static createStageConfigurationSection() {
return 'The stage parameters need to be defined in the section `stages` of [config.yml file](../configuration.md).'
}
}
//
@ -118,6 +216,11 @@ class Helper {
prepareDefaultValuesStep
}
static Map getYamlResource(String resource) {
def ymlContent = new File(projectRoot,"resources/${resource}").text
return new Yaml().load(ymlContent)
}
static getDummyScript(def prepareDefaultValuesStep, def stepName, Map prepareDefaultValuesStepParams) {
def _prepareDefaultValuesStep = prepareDefaultValuesStep
@ -346,6 +449,15 @@ class Helper {
return params
}
static getStageStepKeys(def script) {
try {
return script.STAGE_STEP_KEYS ?: []
} catch (groovy.lang.MissingPropertyException ex) {
System.err << "[INFO] STAGE_STEP_KEYS not set for: ${script.STEP_NAME}.\n"
return []
}
}
static getRequiredParameters(File f) {
def params = [] as Set
f.eachLine {
@ -386,7 +498,7 @@ class Helper {
def scriptName = (it =~ /vars\${File.separator}(.*)\.groovy/)[0][1]
def stepScript = gse.createScript("${scriptName}.groovy", new Binding())
for (def method in stepScript.getClass().getMethods()) {
if(method.getName() == 'call' && method.getAnnotation(GenerateDocumentation) != null) {
if(method.getName() == 'call' && (method.getAnnotation(GenerateDocumentation) != null || method.getAnnotation(GenerateStageDocumentation) != null)) {
docuRelevantSteps << scriptName
break
}
@ -395,6 +507,26 @@ class Helper {
}
docuRelevantSteps
}
static resolveDocuRelevantStages(GroovyScriptEngine gse, File stepsDir) {
def docuRelevantStages = [:]
stepsDir.traverse(type: FileType.FILES, maxDepth: 0) {
if(it.getName().endsWith('.groovy')) {
def scriptName = (it =~ /vars\${File.separator}(.*)\.groovy/)[0][1]
def stepScript = gse.createScript("${scriptName}.groovy", new Binding())
for (def method in stepScript.getClass().getMethods()) {
GenerateStageDocumentation stageDocsAnnotation = method.getAnnotation(GenerateStageDocumentation)
if(method.getName() == 'call' && stageDocsAnnotation != null) {
docuRelevantStages[scriptName] = stageDocsAnnotation.defaultStageName()
break
}
}
}
}
docuRelevantStages
}
}
roots = [
@ -404,33 +536,65 @@ roots = [
stepsDir = null
stepsDocuDir = null
String customDefaults = null
stagesDocuDir = null
customDefaults = null
steps = []
//
// assign parameters
if(args.length >= 1)
stepsDir = new File(args[0])
def cli = new CliBuilder(
usage: 'groovy createDocu [<options>]',
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\'.'
p longOpt: 'docuDirStages', args: 1, argName: 'dir', 'The directory containing the docu stubs for pipeline stages. Defaults to \'documentation/docs/stages\'.'
c longOpt: 'customDefaults', args: 1, argName: 'file', 'Additional custom default configuration'
i longOpt: 'stageInitFile', args: 1, argName: 'file', 'The file containing initialization data for step piperInitRunStageConfiguration'
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){
System.err << "[INFO] Using custom step root: ${options.s}.\n"
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) {
System.err << "[INFO] Using custom doc dir for steps: ${options.d}.\n"
stepsDocuDir = new File(Helper.projectRoot, options.d)
}
stepsDocuDir = stepsDocuDir ?: new File(Helper.projectRoot, "documentation/docs/steps")
def argsDrop = 2
if(args.length >= 3 && args[2].contains('.yml')) {
customDefaults = args[2]
argsDrop ++
if(options.p) {
System.err << "[INFO] Using custom doc dir for stages: ${options.p}.\n"
stagesDocuDir = new File(Helper.projectRoot, options.p)
}
if(args.length >= 3)
steps = (args as List).drop(argsDrop) // the first two entries are stepsDir and docuDir
// the other parts are considered as step names
stagesDocuDir = stagesDocuDir ?: new File(Helper.projectRoot, "documentation/docs/stages")
if(options.c) {
System.err << "[INFO] Using custom defaults: ${options.c}.\n"
customDefaults = options.c
}
steps.addAll(options.arguments())
// assign parameters
//
@ -461,6 +625,16 @@ if( ! steps) {
System.err << "[INFO] Generating docu only for step ${steps.size > 1 ? 's' : ''} ${steps}.\n"
}
// find all the stages that we have to document
Map stages = Helper.resolveDocuRelevantStages(gse, stepsDir)
// retrieve default conditions for steps
//ToDo: allow passing config file name via parameter
Map stageConfig
if (options.s) {
stageConfig = Helper.getYamlResource(options.s)
}
def prepareDefaultValuesStep = Helper.getPrepareDefaultValuesStep(gse)
boolean exceptionCaught = false
@ -490,6 +664,39 @@ for(step in stepDescriptors) {
}
}
//update stepDescriptors: remove stages and put into separate stageDescriptors map
def stageDescriptors = [:]
stages.each {key, value ->
System.err << "[INFO] Processing stage '${key}' ...\n"
stageDescriptors."${key}" = [:] << stepDescriptors."${key}"
stepDescriptors.remove(key)
//add stage name to stageDescriptors
stageDescriptors."${key}".name = value
//add stepCondition informmation to stageDescriptors
stageDescriptors."${key}".configConditions = stageConfig?.stages?.get(value)?.stepConditions
//identify step keys in stages
def stageStepKeys = Helper.getStageStepKeys(gse.createScript( "${key}.groovy", new Binding() ))
// prepare step descriptions
stageDescriptors."${key}".stepDescriptions = [:]
stageDescriptors."${key}".parameters.each {paramKey, paramValue ->
if (paramKey in stageStepKeys) {
stageDescriptors."${key}".stepDescriptions."${paramKey}" = "${paramValue.docu ?: ''}\n"
}
}
//remove details from parameter map
stageStepKeys.each {stepKey ->
stageDescriptors."${key}".parameters.remove(stepKey)
}
}
for(step in stepDescriptors) {
try {
renderStep(step.key, step.value)
@ -500,6 +707,16 @@ for(step in stepDescriptors) {
}
}
for (stage in stageDescriptors) {
try {
renderStage(stage.key, stage.value)
System.err << "[INFO] Stage '${stage.key}' has been rendered.\n"
} catch(Exception e) {
exceptionCaught = true
System.err << "${e.getClass().getName()} caught while rendering stage '${stage}': ${e.getMessage()}.\n"
}
}
if(exceptionCaught) {
System.err << "[ERROR] Exception caught during generating documentation. Check earlier log for details.\n"
System.exit(1)
@ -532,6 +749,31 @@ void renderStep(stepName, stepProperties) {
theStepDocu.withWriter { w -> w.write text }
}
void renderStage(stageName, stageProperties) {
def stageFileName = stageName.indexOf('Stage') != -1 ? stageName.split('Stage')[1].toLowerCase() : stageFileName
File theStageDocu = new File(stagesDocuDir, "${stageFileName}.md")
if(!theStageDocu.exists()) {
System.err << "[WARNING] stage docu input file for stage '${stageName}' is missing.\n"
return
}
def binding = [
docGenStageName : stageProperties.name,
docGenDescription : stageProperties.description,
docGenStageContent : 'Stage Content\n\n' + TemplateHelper.createStageContentSection(stageProperties.stepDescriptions),
docGenStageActivation: 'Stage Activation\n\n' + TemplateHelper.createStageActivationSection(),
docGenStepActivation: 'Step Activation\n\n' + TemplateHelper.createStepActivationSection(stageProperties.configConditions),
docGenStageParameters : 'Additional Stage Parameters\n\n' + TemplateHelper.createParametersSection(stageProperties.parameters),
docGenStageConfiguration : 'Configuration of Additional Stage Parameters\n\n' + TemplateHelper.createStageConfigurationSection()
]
def template = new StreamingTemplateEngine().createTemplate(theStageDocu.text)
String text = template.make(binding)
theStageDocu.withWriter { w -> w.write text }
}
def fetchTextFrom(def step, def parameterName, def steps) {
try {
def docuFromOtherStep = steps[step]?.parameters[parameterName]?.docu
@ -561,6 +803,12 @@ def handleStep(stepName, prepareDefaultValuesStep, gse, customDefaults) {
File theStep = new File(stepsDir, "${stepName}.groovy")
File theStepDocu = new File(stepsDocuDir, "${stepName}.md")
if (!theStepDocu.exists() && stepName.indexOf('Stage') != -1) {
//try to get a corresponding stage documentation
def stageName = stepName.split('Stage')[1].toLowerCase()
theStepDocu = new File(stagesDocuDir,"${stageName}.md" )
}
if(!theStepDocu.exists()) {
System.err << "[WARNING] step docu input file for step '${stepName}' is missing.\n"
return
@ -620,9 +868,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,

View File

@ -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)

View File

@ -4,4 +4,4 @@
.md-typeset a:not(.headerlink):hover {
text-decoration: underline;
}
}

View File

@ -47,7 +47,7 @@ node(){
```yaml
steps:
mtaBuild
mtaBuild:
buildTarget: 'CF'
cloudFoundryDeploy:
cloudFoundry:

View File

@ -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

View File

@ -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:

View File

@ -0,0 +1,13 @@
# ${docGenStageName}
${docGenDescription}
## ${docGenStageContent}
## ${docGenStageActivation}
## ${docGenStepActivation}
## ${docGenStageParameters}
## ${docGenStageConfiguration}

View File

@ -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'
]
]
)
```

View File

@ -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:
@ -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

View File

@ -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<br />
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

View File

@ -1,7 +1,6 @@
# ${docGenStepName}
## ${docGenDescription}
## ${docGenParameters}

View File

@ -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 <http://webdriver.io/guide/getstarted/modes.html> and <http://webdriver.io/guide.html>
#### Configuration for Local Docker Environment

View File

@ -6,7 +6,7 @@
* Installed and configured [Slack JenkinsCI integration](https://my.slack.com/services/new/jenkins-ci)
* *secret text* Jenkins credentials with the Slack token
* Installed and configured [Jenkins Slack plugin](https://github.com/jenkinsci/slack-plugin#install-instructions-for-slack).
* Installed and configured [Jenkins Slack plugin](https://github.com/jenkinsci/slack-plugin#install-instructions-for-slack)
## ${docGenParameters}

View File

@ -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

View File

@ -8,11 +8,8 @@
## ${docGenParameters}
## ${docGenConfiguration}
The step is configured using a customer configuration file provided as
resource in an custom shared library.

View File

@ -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'
]
)
```

14
pom.xml
View File

@ -1,7 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<project
xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>plugin</artifactId>
@ -17,10 +19,10 @@
<url>https://sap.github.io/jenkins-library/</url>
<licenses>
<license>
<name>Apache License 2.0</name>
<comments>https://github.com/SAP/jenkins-library/blob/master/LICENSE</comments>
</license>
<license>
<name>Apache License 2.0</name>
<comments>https://github.com/SAP/jenkins-library/blob/master/LICENSE</comments>
</license>
</licenses>
<repositories>

View File

@ -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'

View File

@ -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()
}

View File

@ -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}/ }
}

View File

@ -37,4 +37,3 @@ class MtaUtils {
if (!script.fileExists(targetMtaDescriptor)) throw new AbortException("'${targetMtaDescriptor}' has not been generated.")
}
}

View File

@ -1,4 +1,4 @@
package com.sap.piper.cm;
package com.sap.piper.cm
public enum BackendType {
SOLMAN, CTS, RFC, NONE

View File

@ -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.")
@ -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
}
}

View File

@ -1,4 +1,4 @@
package com.sap.piper.cm;
package com.sap.piper.cm
public class ChangeManagementException extends RuntimeException {

View File

@ -1,4 +1,4 @@
package com.sap.piper.cm;
package com.sap.piper.cm
import com.cloudbees.groovy.cps.NonCPS
@ -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

View File

@ -24,6 +24,6 @@ enum DeployMode {
throw new IllegalArgumentException("${value} is not in the list of possible values ${stringValues()}")
}
return enumValue;
return enumValue
}
}

View File

@ -131,7 +131,7 @@ class NeoCommandHelper {
def environment = deploymentConfiguration.environment
if (!(environment in Map)) {
step.error("The environment variables for the deployment to Neo have to be defined as a map.");
step.error("The environment variables for the deployment to Neo have to be defined as a map.")
}
def keys = environment.keySet()

View File

@ -21,6 +21,6 @@ enum WarAction {
throw new IllegalArgumentException("${value} is not in the list of possible values ${stringValues()}")
}
return enumValue;
return enumValue
}
}

View File

@ -1,4 +1,3 @@
#!groovy
import com.lesfurets.jenkins.unit.BasePipelineTest
import static org.junit.Assert.assertEquals

View File

@ -1,5 +1,3 @@
#!groovy
import org.junit.Before
import org.junit.Rule
import org.junit.Test

View File

@ -1,4 +1,3 @@
#!groovy
import com.sap.piper.JenkinsUtils
import org.junit.Before
import org.junit.Rule

View File

@ -4,8 +4,9 @@ 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.io.File
import java.util.stream.Collectors
import java.lang.reflect.Field
@ -192,7 +193,7 @@ public class CommonStepsTest extends BasePiperTest{
continue
}
boolean notAccessible = false;
boolean notAccessible = false
def fieldName
if(!stepNameField.isAccessible()) {
@ -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
}
}

View File

@ -1,4 +1,3 @@
#!groovy
import org.junit.Before
import org.junit.Rule
import org.junit.Test

View File

@ -207,7 +207,7 @@ class DockerExecuteOnKubernetesTest extends BasePiperTest {
@Test
void testDockerExecuteOnKubernetesEmptyContainerMapNoDockerImage() throws Exception {
exception.expect(IllegalArgumentException.class);
exception.expect(IllegalArgumentException.class)
stepRule.step.dockerExecuteOnKubernetes(
script: nullScript,
juStabUtils: utils,

View File

@ -1,5 +1,3 @@
#!groovy
import com.sap.piper.analytics.InfluxData
import org.junit.Rule

View File

@ -1,4 +1,3 @@
#!groovy
import org.junit.Before
import org.junit.Rule
import org.junit.Test

View File

@ -1,4 +1,3 @@
#!groovy
import groovy.json.JsonSlurperClassic
import org.junit.Before
import org.junit.Rule

View File

@ -1,4 +1,3 @@
#!groovy
import hudson.AbortException
import static org.hamcrest.Matchers.is

View File

@ -1,4 +1,3 @@
#!groovy
import org.junit.Before
import org.junit.Rule
import org.junit.Test

View File

@ -1,4 +1,3 @@
#!groovy
import com.sap.piper.DefaultValueCache
import com.sap.piper.analytics.InfluxData

View File

@ -1,4 +1,3 @@
#!groovy
import org.junit.Before
import org.junit.Rule
import org.junit.Test

View File

@ -1,4 +1,3 @@
#!groovy
import org.junit.Before
import org.junit.Rule
import org.junit.Test

View File

@ -1,4 +1,3 @@
#!groovy
import org.junit.Before
import org.junit.Rule
import org.junit.Test

View File

@ -40,9 +40,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
@ -56,9 +53,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'

View File

@ -1,4 +1,3 @@
#!groovy
import org.jenkinsci.plugins.workflow.steps.FlowInterruptedException
import org.junit.Rule
import org.junit.Test

View File

@ -1,4 +1,3 @@
#!groovy
package stages
import org.junit.Before

View File

@ -1,4 +1,3 @@
#!groovy
package stages
import org.junit.Before

View File

@ -1,4 +1,3 @@
#!groovy
import org.junit.Before
import org.junit.Rule
import org.junit.Test

View File

@ -1,15 +1,15 @@
import org.junit.Before
import org.junit.Rule;
import org.junit.Rule
import org.junit.Test
import org.junit.rules.ExpectedException
import org.junit.rules.RuleChain;
import org.junit.rules.RuleChain
import com.sap.piper.DefaultValueCache
import util.BasePiperTest
import util.JenkinsLoggingRule
import util.JenkinsReadYamlRule
import util.JenkinsShellCallRule
import util.JenkinsStepRule;
import util.JenkinsStepRule
import util.Rules

View File

@ -1,4 +1,3 @@
#!groovy
import org.junit.Before
import org.junit.Rule
import org.junit.Test

View File

@ -1,5 +1,3 @@
#!groovy
import static org.hamcrest.Matchers.*
import org.junit.Before

View File

@ -1,4 +1,3 @@
#!groovy
import com.sap.piper.DescriptorUtils
import com.sap.piper.JsonUtils
import com.sap.piper.integration.WhitesourceOrgAdminRepository

View File

@ -22,7 +22,7 @@ class MapUtilsTest {
c: [d: '1',
e: '2']],
b = [b: '2',
c: [d: 'x']];
c: [d: 'x']]
Map merged = MapUtils.merge(a, b)

View File

@ -25,7 +25,7 @@ class MtaUtilsTest extends BasePiperTest {
private File badJson
private mtaUtils
private ExpectedException thrown= ExpectedException.none();
private ExpectedException thrown= ExpectedException.none()
@ClassRule
public static TemporaryFolder tmp = new TemporaryFolder()

View File

@ -8,7 +8,7 @@ import static org.junit.Assert.assertEquals
import static org.junit.Assert.assertNotNull
class SystemEnvTest {
SystemEnv env = null;
SystemEnv env = null
Map systemEnvironmentMock = [:]
@Before
void setUp() {

View File

@ -1,4 +1,3 @@
#!groovy
package templates
import org.junit.Before

View File

@ -1,4 +1,3 @@
#!groovy
package templates
import org.junit.Before

View File

@ -1,4 +1,3 @@
#!groovy
package templates
import org.junit.Before

View File

@ -1,4 +1,3 @@
#!groovy
package templates
import org.junit.Before

View File

@ -1,4 +1,3 @@
#!groovy
package templates
import org.junit.Before

View File

@ -0,0 +1,79 @@
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'))
}
}

View File

@ -1,4 +1,3 @@
#!groovy
package templates
import org.junit.Before

View File

@ -1,4 +1,3 @@
#!groovy
package templates
import org.junit.Before

View File

@ -1,4 +1,3 @@
#!groovy
package templates
import org.junit.Before

View File

@ -1,4 +1,3 @@
#!groovy
package templates
import org.junit.Before

View File

@ -1,4 +1,3 @@
#!groovy
package templates
import org.junit.Before

View File

@ -1,4 +1,3 @@
#!groovy
package templates
import org.junit.Before

View File

@ -1,4 +1,3 @@
#!groovy
package templates
import org.junit.Before

View File

@ -1,4 +1,3 @@
#!groovy
package templates
import org.junit.Before
@ -54,17 +53,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 +174,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 +211,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 +234,7 @@ class PiperPipelineTest extends BasePiperTest {
]
jsr.step.piperPipeline(script: nullScript)
assertThat(stepsCalled, not(hasItem('input')))
assertThat(stepsCalled, not(hasItem('piperPipelineStageConfirm')))
}
@Test
@ -232,7 +262,7 @@ class PiperPipelineTest extends BasePiperTest {
'piperPipelineStageSecurity',
'piperPipelineStagePerformance',
'piperPipelineStageCompliance',
'input',
'piperPipelineStageConfirm',
'piperPipelineStagePromote',
'piperPipelineStageRelease',
'piperPipelineStagePost'

View File

@ -1,5 +1,3 @@
#!groovy
package util
import com.lesfurets.jenkins.unit.BasePipelineTest

View File

@ -1,5 +1,3 @@
#!groovy
package util
import com.sap.piper.DescriptorUtils

View File

@ -8,7 +8,7 @@ import org.junit.runner.Description
import org.junit.runners.model.Statement
import static org.hamcrest.Matchers.containsString
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertThat
import org.hamcrest.Matchers

View File

@ -31,8 +31,8 @@ class JenkinsShellCallRule implements TestRule {
@Override
public boolean equals(Object obj) {
if (obj == null || !obj instanceof Command) return false;
Command other = (Command) obj;
if (obj == null || !obj instanceof Command) return false
Command other = (Command) obj
return type == other.type && script == other.script
}
}

View File

@ -79,6 +79,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
@ -87,6 +88,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()
@ -112,6 +114,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
@ -121,6 +124,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()
@ -181,6 +185,7 @@ class LibraryLoadingTestExecutionListener extends AbstractTestExecutionListener
static class PipelineTestHelperHook {
def helper = new PipelineTestHelper() {
def clearAllowedMethodCallbacks(Collection c = []) {
List itemsToRemove = []
c.each {

View File

@ -5,6 +5,6 @@ import hudson.AbortException
class PipelineWhenException extends AbortException{
public PipelineWhenException(String message)
{
super(message);
super(message)
}
}

View File

@ -1,4 +1,3 @@
#!groovy
package util
import com.lesfurets.jenkins.unit.global.lib.SourceRetriever

View File

@ -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
}
}

View File

@ -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())
}
}

View File

@ -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}"

View File

@ -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

View File

@ -42,4 +42,3 @@ def call(Map parameters = [:], body) {
return duration
}

View File

@ -197,7 +197,7 @@ def getCulprits(config, branch, numberOfCommits) {
ignoreMissing: true
) {
def pullRequestID = branch.replaceAll('PR-', '')
def localBranchName = "pr" + pullRequestID;
def localBranchName = "pr" + pullRequestID
sh """git init
git fetch ${config.gitUrl} pull/${pullRequestID}/head:${localBranchName} > /dev/null 2>&1
git checkout -f ${localBranchName} > /dev/null 2>&1

View File

@ -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
]]
])
}

View File

@ -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'
]

View File

@ -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') {

View File

@ -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
}
}
}

View File

@ -40,12 +40,12 @@ import groovy.text.SimpleTemplateEngine
*
* Notification contains:
*
* * Build status;
* * Repo Owner;
* * Repo Name;
* * Branch Name;
* * Jenkins Build Number;
* * Jenkins Build URL.
* * Build status
* * Repo Owner
* * Repo Name
* * Branch Name
* * Jenkins Build Number
* * Jenkins Build URL
*/
@GenerateDocumentation
void call(Map parameters = [:]) {

View File

@ -160,7 +160,7 @@ void call(Map parameters = [:]) {
switch(config.pullRequestProvider){
case 'github':
config.options.add("sonar.pullrequest.github.repository=${config.githubOrg}/${config.githubRepo}")
break;
break
default: error "Pull-Request provider '${config.pullRequestProvider}' is not supported!"
}
workerForGithubAuth(config)

View File

@ -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}'.")
}

View File

@ -70,7 +70,7 @@ import static com.sap.piper.cm.StepHelpers.getBackendTypeAndLogInfoIfCMIntegrati
'transportRequestId',
/** @see transportRequestCreate */
'verbose',
])
])
/** Releases a Transport Request. */
@GenerateDocumentation

View File

@ -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}' " +