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

Merge pull request #166 from marcusholl/cm

isChangeInDevelopment
This commit is contained in:
Marcus Holl 2018-06-29 08:25:15 +02:00 committed by GitHub
commit 3766bf4794
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 602 additions and 25 deletions

View File

@ -0,0 +1,51 @@
# checkChangeInDevelopment
## Description
Checks if a Change Document is in status 'in development'. The change document id is retrieved from the git commit history. The change document id
can also be provided via parameter `changeDocumentId`. Any value provided as parameter has a higher precedence than a value from the commit history.
By default the git commit messages between `origin/master` and `HEAD` are scanned for a line like `ChangeDocument : <changeDocumentId>`. The commit
range and the pattern can be configured. For details see 'parameters' table.
## Prerequisites
* **[Change Management Client 2.0.0 or compatible version](http://central.maven.org/maven2/com/sap/devops/cmclient/dist.cli/)** - available for download on Maven Central.
## Parameters
| parameter | mandatory | default | possible values |
| -------------------|-----------|--------------------------------------------------------|--------------------|
| `script` | yes | | |
| `changeDocumentId` | yes | | |
| `credentialsId` | yes | | |
| `endpoint` | yes | | |
| `git_from` | no | `origin/master` | |
| `git_to` | no | `HEAD` | |
| `git_label` | no | `ChangeDocument\s?:` | regex pattern |
* `script` - 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`. This allows the function to access the [`commonPipelineEnvironment`](commonPipelineEnvironment.md) for retrieving, for example, configuration parameters.
* `changeDocumentId` - The id of the change document to transport. If not provided, it is retrieved from the git commit history.
* `credentialsId` - The credentials to connect to the Solution Manager.
* `endpoint` - The address of the Solution Manager.
* `git_from` - The starting point for retrieving the change document id
* `git_to` - The end point for retrieving the change document id
* `git_label` - A pattern used for identifying lines holding the change document id.
## Step configuration
The following parameters can also be specified as step parameters using the global configuration file:
* `credentialsId`
* `endpoint`
## Return value
`true` in case the change document is in status 'in development'. Otherwise an hudson.AbortException is thrown. In case `failIfStatusIsNotInDevelopment`
is set to `false`, `false` is returned in case the change document is not in status 'in development'
## Exceptions
* `AbortException`:
* If the change id is not provided via parameter and if the change document id cannot be retrieved from the commit history.
* If the change is not in status `in development`. In this case no exception will be thrown when `failIfStatusIsNotInDevelopment` is set to `false`.
## Example
```groovy
checkChangeInDevelopment script:this
```

View File

@ -3,6 +3,7 @@ pages:
- Home: index.md
- 'Library steps':
- artifactSetVersion: steps/artifactSetVersion.md
- checkChangeInDevelopment: steps/checkChangeInDevelopment.md
- commonPipelineEnvironment: steps/commonPipelineEnvironment.md
- dockerExecute: steps/dockerExecute.md
- durationMeasure: steps/durationMeasure.md

View File

@ -163,3 +163,10 @@ steps:
allowEmptyResults: true
archive: false
active: false
checkChangeInDevelopment:
credentialsId: 'CM'
failIfStatusIsNotInDevelopment: true
git_from: 'origin/master'
git_to: 'HEAD'
git_label: 'ChangeDocument\s?:'
git_format: '%b'

View File

@ -1,28 +1,89 @@
package com.sap.piper.cm
import java.util.Map
import com.sap.piper.GitUtils
import hudson.AbortException
public class ChangeManagement implements Serializable {
private script
private GitUtils gitUtils
public ChangeManagement(def script) {
public ChangeManagement(def script, GitUtils gitUtils = null) {
this.script = script
this.gitUtils = gitUtils ?: new GitUtils()
}
String getChangeDocumentId(Map config) {
if(config.changeDocumentId) {
script.echo "[INFO] Use changeDocumentId '${config.changeDocumentId}' from configuration."
return config.changeDocumentId
}
script.echo "[INFO] Retrieving changeDocumentId from git commit(s) [FROM: ${config.git_from}, TO: ${config.git_to}]"
def changeDocumentId = getChangeDocumentId(
config.git_from,
config.git_to,
config.git_label,
config.git_format
)
script.echo "[INFO] ChangeDocumentId '${changeDocumentId}' retrieved from git commit(s)."
return changeDocumentId
}
String getChangeDocumentId(
String from = 'origin/master',
String to = 'HEAD',
String label = 'ChangeDocument\\s?:',
String format = '%b'
) {
if( ! gitUtils.insideWorkTree() ) {
throw new ChangeManagementException('Cannot retrieve change document id. Not in a git work tree. Change document id is extracted from git commit messages.')
}
def changeIds = gitUtils.extractLogLines(".*${label}.*", from, to, format)
.collect { line -> line?.replaceAll(label,'')?.trim() }
.unique()
changeIds.retainAll { line -> line != null && ! line.isEmpty() }
if( changeIds.size() == 0 ) {
throw new ChangeManagementException("Cannot retrieve changeId from git commits. Change id retrieved from git commit messages via pattern '${label}'.")
} else if (changeIds.size() > 1) {
throw new ChangeManagementException("Multiple ChangeIds found: ${changeIds}. Change id retrieved from git commit messages via pattern '${label}'.")
}
return changeIds.get(0)
}
boolean isChangeInDevelopment(String changeId, String endpoint, String username, String password, String cmclientOpts = '') {
int rc = script.sh(returnStatus: true,
script: getCMCommandLine(endpoint, username, password,
'is-change-in-development', ['-cID', "'${changeId}'",
'--return-code'],
cmclientOpts))
if(rc == 0) {
return true
} else if(rc == 3) {
return false
} else {
throw new ChangeManagementException("Cannot retrieve status for change document '${changeId}'. Does this change exist? Return code from cmclient: ${rc}.")
}
}
String createTransportRequest(String changeId, String developmentSystemId, String endpoint, String username, String password) {
try {
String transportRequest = script.sh(returnStdout: true,
script:
"""#!/bin/bash
cmclient -e '$endpoint' \
-u '$username' \
-p '$password' \
-t SOLMAN \
create-transport -cID '$changeId' -dID '$developmentSystemId'
""")
script: getCMCommandLine(endpoint, username, password, 'create-transport', ['-cID', changeId,
'-dID', developmentSystemId]))
return transportRequest.trim()
} catch(AbortException e) {
throw new ChangeManagementException("Cannot create a transport request for change id '$changeId'. $e.message.")
@ -32,14 +93,10 @@ public class ChangeManagement implements Serializable {
void uploadFileToTransportRequest(String changeId, String transportRequestId, String applicationId, String filePath, String endpoint, String username, String password) {
int rc = script.sh(returnStatus: true,
script:
"""#!/bin/bash
cmclient -e '$endpoint' \
-u '$username' \
-p '$password' \
-t SOLMAN \
upload-file-to-transport -cID '$changeId' -tID '$transportRequestId' '$applicationId' '$filePath'
""")
script: getCMCommandLine(endpoint, username, password,
'upload-file-to-transport', ['-cID', changeId,
'-tID', transportRequestId,
applicationId, filePath]))
if(rc == 0) {
return
@ -51,14 +108,9 @@ public class ChangeManagement implements Serializable {
void releaseTransportRequest(String changeId, String transportRequestId, String endpoint, String username, String password) {
int rc = script.sh(returnStatus: true,
script:
"""#!/bin/bash
cmclient -e '$endpoint' \
-u '$username' \
-p '$password' \
-t SOLMAN \
release-transport -cID '$changeId' -tID '$transportRequestId'
""")
script: getCMCommandLine(endpoint, username, password,
'release-transport', ['-cID', changeId,
'-tID', transportRequestId]))
if(rc == 0) {
return
@ -66,4 +118,25 @@ public class ChangeManagement implements Serializable {
throw new ChangeManagementException("Cannot release Transport Request '$transportRequestId'. Return code from cmclient: $rc.")
}
}
String getCMCommandLine(String endpoint,
String username,
String password,
String command,
List<String> args,
String cmclientOpts = '') {
String cmCommandLine = '#!/bin/bash'
if(cmclientOpts) {
cmCommandLine += """
export CMCLIENT_OPTS="${cmclientOpts}" """
}
cmCommandLine += """
cmclient -e '$endpoint' \
-u '$username' \
-p '$password' \
-t SOLMAN \
${command} ${(args as Iterable).join(' ')}
"""
return cmCommandLine
}
}

View File

@ -0,0 +1,158 @@
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.ExpectedException
import org.junit.rules.RuleChain
import com.sap.piper.GitUtils
import com.sap.piper.cm.ChangeManagement
import com.sap.piper.cm.ChangeManagementException
import hudson.AbortException
import util.BasePiperTest
import util.JenkinsStepRule
import util.Rules
class CheckChangeInDevelopmentTest extends BasePiperTest {
private ExpectedException thrown = ExpectedException.none()
private JenkinsStepRule jsr = new JenkinsStepRule(this)
@Rule
public RuleChain ruleChain = Rules
.getCommonRules(this)
.around(thrown)
.around(jsr)
@Before
public void setup() {
helper.registerAllowedMethod('usernamePassword', [Map], { Map m ->
binding.setProperty('username', 'defaultUser')
binding.setProperty('password', '********')
})
helper.registerAllowedMethod('withCredentials', [List, Closure], { List l, Closure c ->
c()
})
}
@After
public void tearDown() {
cmUtilReceivedParams.clear()
}
private Map cmUtilReceivedParams = [:]
@Test
public void changeIsInStatusDevelopmentTest() {
ChangeManagement cm = getChangeManagementUtils(true)
boolean inDevelopment = jsr.step.checkChangeInDevelopment(
cmUtils: cm,
endpoint: 'https://example.org/cm')
assert inDevelopment
assert cmUtilReceivedParams == [
changeId: '001',
endpoint: 'https://example.org/cm',
userName: 'defaultUser',
password: '********',
cmclientOpts: null
]
}
@Test
public void changeIsNotInStatusDevelopmentTest() {
thrown.expect(AbortException)
thrown.expectMessage("Change '001' is not in status 'in development'")
ChangeManagement cm = getChangeManagementUtils(false)
jsr.step.checkChangeInDevelopment(
cmUtils: cm,
endpoint: 'https://example.org/cm')
}
@Test
public void changeIsNotInStatusDevelopmentButWeWouldLikeToSkipFailureTest() {
ChangeManagement cm = getChangeManagementUtils(false)
boolean inDevelopment = jsr.step.checkChangeInDevelopment(
cmUtils: cm,
endpoint: 'https://example.org/cm',
failIfStatusIsNotInDevelopment: false)
assert !inDevelopment
}
@Test
public void changeDocumentIdRetrievalFailsTest() {
thrown.expect(AbortException)
thrown.expectMessage('Something went wrong')
ChangeManagement cm = new ChangeManagement(nullScript, null) {
String getChangeDocumentId(
String filter,
String from,
String to,
String format) {
throw new ChangeManagementException('Something went wrong')
}
}
jsr.step.checkChangeInDevelopment(
cmUtils: cm,
endpoint: 'https://example.org/cm')
}
@Test
public void nullChangeDocumentIdTest() {
thrown.expect(AbortException)
thrown.expectMessage("ChangeId is null or empty.")
ChangeManagement cm = getChangeManagementUtils(false, null)
jsr.step.checkChangeInDevelopment(
cmUtils: cm,
endpoint: 'https://example.org/cm')
}
@Test
public void emptyChangeDocumentIdTest() {
thrown.expect(AbortException)
thrown.expectMessage("ChangeId is null or empty.")
ChangeManagement cm = getChangeManagementUtils(false, '')
jsr.step.checkChangeInDevelopment(
cmUtils: cm,
endpoint: 'https://example.org/cm')
}
private ChangeManagement getChangeManagementUtils(boolean inDevelopment, String changeDocumentId = '001') {
return new ChangeManagement(nullScript, null) {
String getChangeDocumentId(
String filter,
String from,
String to,
String format) {
return changeDocumentId
}
boolean isChangeInDevelopment(String changeId, String endpoint, String userName, String password, String cmclientOpts) {
cmUtilReceivedParams.changeId = changeId
cmUtilReceivedParams.endpoint = endpoint
cmUtilReceivedParams.userName = userName
cmUtilReceivedParams.password = password
cmUtilReceivedParams.cmclientOpts = cmclientOpts
return inDevelopment
}
}
}
}

View File

@ -0,0 +1,195 @@
package com.sap.piper.cm
import static org.hamcrest.Matchers.allOf
import static org.hamcrest.Matchers.containsString
import static org.hamcrest.Matchers.equalTo
import static org.hamcrest.Matchers.hasItem
import static org.hamcrest.Matchers.is
import static org.hamcrest.Matchers.not
import static org.junit.Assert.assertThat
import org.hamcrest.Matchers
import org.junit.Assert
import org.junit.Rule
import org.junit.Test
import org.junit.rules.ExpectedException
import org.junit.rules.RuleChain
import com.sap.piper.GitUtils
import util.BasePiperTest
import util.JenkinsLoggingRule
import util.JenkinsShellCallRule
import util.Rules
public class ChangeManagementTest extends BasePiperTest {
private ExpectedException thrown = ExpectedException.none()
private JenkinsShellCallRule script = new JenkinsShellCallRule(this)
private JenkinsLoggingRule logging = new JenkinsLoggingRule(this)
@Rule
public RuleChain rules = Rules.getCommonRules(this)
.around(thrown)
.around(script)
.around(logging)
@Test
public void testGetChangeIdFromConfigWhenProvidedInsideConfig() {
String[] viaGitUtils = ['0815']
def changeDocumentId = new ChangeManagement(nullScript, gitUtilsMock(false, viaGitUtils))
.getChangeDocumentId([changeDocumentId: '0042'])
assertThat(logging.log, containsString('[INFO] Use changeDocumentId \'0042\' from configuration.'))
assertThat(changeDocumentId, is(equalTo('0042')))
}
@Test
public void testRetrieveChangeDocumentIdOutsideGitWorkTreeTest() {
thrown.expect(ChangeManagementException)
thrown.expectMessage('Cannot retrieve change document id. ' +
'Not in a git work tree. ' +
'Change document id is extracted from git commit messages.')
new ChangeManagement(nullScript, gitUtilsMock(false, new String[0])).getChangeDocumentId()
}
@Test
public void testRetrieveChangeDocumentIdNothingFound() {
thrown.expect(ChangeManagementException)
thrown.expectMessage('Cannot retrieve changeId from git commits.')
new ChangeManagement(nullScript, gitUtilsMock(true, new String[0])).getChangeDocumentId()
}
@Test
public void testRetrieveChangeDocumentIdReturnsArrayWithNullValue() {
thrown.expect(ChangeManagementException)
thrown.expectMessage('Cannot retrieve changeId from git commits.')
new ChangeManagement(nullScript, gitUtilsMock(true, (String[])[ null ])).getChangeDocumentId()
}
@Test
public void testRetrieveChangeDocumentNotUnique() {
thrown.expect(ChangeManagementException)
thrown.expectMessage('Multiple ChangeIds found')
String[] changeIds = [ 'a', 'b' ]
new ChangeManagement(nullScript, gitUtilsMock(true, changeIds)).getChangeDocumentId()
}
@Test
public void testRetrieveChangeDocumentSameChangeIdFoundTwice() {
String[] changeIds = [ 'a', 'a' ]
def changeID = new ChangeManagement(nullScript, gitUtilsMock(true, changeIds)).getChangeDocumentId()
assert changeID == 'a'
}
@Test
public void testRetrieveChangeDocumentWithUniqueResult() {
String[] changeIds = [ 'a' ];
def params = [ git_from: 'origin/master',
git_to: 'HEAD',
git_label: 'ChangeDocument\\s?:',
git_format: '%b']
def changeID = new ChangeManagement(nullScript, gitUtilsMock(true, changeIds)).getChangeDocumentId(params)
assertThat(logging.log, containsString('[INFO] ChangeDocumentId \'a\' retrieved from git commit(s). '))
assert changeID == 'a'
}
@Test
public void testIsChangeInDevelopmentReturnsTrueWhenChangeIsInDevelopent() {
script.setReturnValue(JenkinsShellCallRule.Type.REGEX, "cmclient.*is-change-in-development -cID '001'", 0)
boolean inDevelopment = new ChangeManagement(nullScript, null).isChangeInDevelopment('001', 'endpoint', 'user', 'password')
assertThat(inDevelopment, is(equalTo(true)))
assertThat(script.shell[0], allOf(containsString("cmclient"),
containsString("-u 'user'"),
containsString("-p 'password'"),
containsString("-e 'endpoint'"),
containsString('is-change-in-development'),
containsString("-cID '001'"),
containsString("-t SOLMAN")))
}
@Test
public void testIsChangeInDevelopmentReturnsFalseWhenChangeIsNotInDevelopent() {
script.setReturnValue(JenkinsShellCallRule.Type.REGEX, "cmclient.*is-change-in-development -cID '001'", 3)
boolean inDevelopment = new ChangeManagement(nullScript, null)
.isChangeInDevelopment('001',
'endpoint',
'user',
'password')
assertThat(inDevelopment, is(equalTo(false)))
}
@Test
public void testIsChangeInDevelopmentThrowsExceptionWhenCMClientReturnsUnexpectedExitCode() {
thrown.expect(ChangeManagementException)
thrown.expectMessage('Cannot retrieve status for change document \'001\'. Does this change exist? Return code from cmclient: 1.')
script.setReturnValue(JenkinsShellCallRule.Type.REGEX, "cmclient.*is-change-in-development -cID '001'", 1)
new ChangeManagement(nullScript, null).isChangeInDevelopment('001', 'endpoint', 'user', 'password')
}
@Test
public void testGetCommandLineWithoutCMClientOpts() {
String commandLine = new ChangeManagement(nullScript, null)
.getCMCommandLine('https://example.org/cm',
"me",
"topSecret",
"the-command",
["-key1", "val1", "-key2", "val2"])
commandLine = commandLine.replaceAll(' +', " ")
assertThat(commandLine, not(containsString("CMCLIENT_OPTS")))
assertThat(commandLine, containsString("cmclient -e 'https://example.org/cm' -u 'me' -p 'topSecret' -t SOLMAN the-command -key1 val1 -key2 val2"))
}
@Test
public void testGetCommandLineWithCMClientOpts() {
String commandLine = new ChangeManagement(nullScript, null)
.getCMCommandLine('https://example.org/cm',
"me",
"topSecret",
"the-command",
["-key1", "val1", "-key2", "val2"],
'-Djavax.net.debug=all')
commandLine = commandLine.replaceAll(' +', " ")
assertThat(commandLine, containsString('export CMCLIENT_OPTS="-Djavax.net.debug=all"'))
}
private GitUtils gitUtilsMock(boolean insideWorkTree, String[] changeIds) {
return new GitUtils() {
public boolean insideWorkTree() {
return insideWorkTree
}
public String[] extractLogLines(
String filter = '',
String from = 'origin/master',
String to = 'HEAD',
String format = '%b') {
return changeIds
}
}
}
}

View File

@ -0,0 +1,92 @@
import com.sap.piper.GitUtils
import groovy.transform.Field
import hudson.AbortException
import com.sap.piper.ConfigurationMerger
import com.sap.piper.cm.ChangeManagement
import com.sap.piper.cm.ChangeManagementException
@Field def STEP_NAME = 'checkChangeInDevelopment'
@Field Set parameterKeys = [
'changeDocumentId',
'cmClientOpts',
'credentialsId',
'endpoint',
'failIfStatusIsNotInDevelopment',
'git_from',
'git_to',
'git_label',
'git_format'
]
@Field Set stepConfigurationKeys = [
'changeDocumentId',
'cmClientOpts',
'credentialsId',
'endpoint',
'failIfStatusIsNotInDevelopment',
'git_from',
'git_to',
'git_label',
'git_format'
]
def call(parameters = [:]) {
handlePipelineStepErrors (stepName: STEP_NAME, stepParameters: parameters) {
prepareDefaultValues script: this
GitUtils gitUtils = parameters?.gitUtils ?: new GitUtils()
ChangeManagement cm = parameters?.cmUtils ?: new ChangeManagement(parameters.script, gitUtils)
Map configuration = ConfigurationMerger.merge(parameters.script, STEP_NAME,
parameters, parameterKeys,
stepConfigurationKeys)
def changeId
try {
changeId = cm.getChangeDocumentId(configuration)
if(! changeId?.trim()) {
throw new ChangeManagementException("ChangeId is null or empty.")
}
} catch(ChangeManagementException ex) {
throw new AbortException(ex.getMessage())
}
boolean isInDevelopment
echo "[INFO] Checking if change document '$changeId' is in development."
withCredentials([usernamePassword(
credentialsId: configuration.credentialsId,
passwordVariable: 'password',
usernameVariable: 'username')]) {
try {
isInDevelopment = cm.isChangeInDevelopment(changeId, configuration.endpoint, username, password, configuration.cmClientOpts)
} catch(ChangeManagementException ex) {
throw new AbortException(ex.getMessage())
}
}
if(isInDevelopment) {
echo "[INFO] Change '${changeId}' is in status 'in development'."
return true
} else {
if(configuration.failIfStatusIsNotInDevelopment.toBoolean()) {
throw new AbortException("Change '${changeId}' is not in status 'in development'.")
} else {
echo "[WARNING] Change '${changeId}' is not in status 'in development'. Failing the pipeline has been explicitly disabled."
return false
}
}
}
}