diff --git a/.gitignore b/.gitignore index 8e150e0a0..f26049779 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ hs_err_pid* target/ targets/ documentation/docs-gen + +consumer-test/**/workspace diff --git a/.travis.yml b/.travis.yml index e0ab22dfd..50b9ca4c4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,7 +35,11 @@ jobs: cp -r documentation/docs documentation/docs-tmp documentation/bin/createDocu.sh vars documentation/docs-tmp/steps docker run --rm -it -v ${TRAVIS_BUILD_DIR}:/docs -w /docs/documentation squidfunk/mkdocs-material:3.0.4 build --clean --verbose --strict - + - name: Consumer Tests for s4sdk pipeline (CloudFoundry) + script: cd consumer-test/s4sdk/CloudFoundry && chmod +x runTests.sh && ./runTests.sh + - name: Consumer Tests for s4sdk pipeline (Neo Environment) + script: cd consumer-test/s4sdk/NeoEnvironment && chmod +x runTests.sh && ./runTests.sh + - stage: Docs name: Deploy if: repo = "SAP/jenkins-library" AND branch = master AND NOT type = pull_request diff --git a/Jenkinsfile b/Jenkinsfile deleted file mode 100644 index 713d24ebf..000000000 --- a/Jenkinsfile +++ /dev/null @@ -1,19 +0,0 @@ -node { - try { - lock(resource: "sap-jenkins-library/10", inversePrecedence: true) { - milestone 10 - deleteDir() - stage ('Checkout'){ - checkout scm - } - stage ('Test') { - sh "mvn clean test --batch-mode" - } - } - } catch (Throwable err) { - echo "Error occured: ${err}" - currentBuild.result = 'FAILURE' - mail subject: '[Build failed] SAP/jenkins-library', body: 'Fix the build.', to: 'marcus.holl@sap.com,oliver.nocon@sap.com' - throw err - } -} diff --git a/README.md b/README.md index 3167d9055..edcecd3df 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ Register to our [google group][google-group] in order to get updates or for aski Read and understand our [contribution guidelines][piper-library-contribution] before opening a pull request. -# [License][piper-library-license] +# License Copyright (c) 2017 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted diff --git a/consumer-test/s4sdk/CloudFoundry/runTests.sh b/consumer-test/s4sdk/CloudFoundry/runTests.sh new file mode 100755 index 000000000..74130533b --- /dev/null +++ b/consumer-test/s4sdk/CloudFoundry/runTests.sh @@ -0,0 +1,6 @@ +#!/bin/bash -e + +source ../prepareTests.sh consumer-test + +docker run -v /var/run/docker.sock:/var/run/docker.sock -v "${PWD}":/workspace -v /tmp -e CASC_JENKINS_CONFIG=/workspace/jenkins.yml \ + -e CX_INFRA_IT_CF_USERNAME -e CX_INFRA_IT_CF_PASSWORD -e BRANCH_NAME=consumer-test ppiper/jenkinsfile-runner diff --git a/consumer-test/s4sdk/NeoEnvironment/runTests.sh b/consumer-test/s4sdk/NeoEnvironment/runTests.sh new file mode 100755 index 000000000..5fc36bab1 --- /dev/null +++ b/consumer-test/s4sdk/NeoEnvironment/runTests.sh @@ -0,0 +1,6 @@ +#!/bin/bash -e + +source ../prepareTests.sh consumer-test-neo + +docker run -v /var/run/docker.sock:/var/run/docker.sock -v "${PWD}":/workspace -v /tmp -e CASC_JENKINS_CONFIG=/workspace/jenkins.yml \ + -e CX_INFRA_IT_CF_USERNAME -e CX_INFRA_IT_CF_PASSWORD -e BRANCH_NAME=consumer-test-neo ppiper/jenkinsfile-runner diff --git a/consumer-test/s4sdk/jenkins.yml b/consumer-test/s4sdk/jenkins.yml new file mode 100644 index 000000000..84eff57ee --- /dev/null +++ b/consumer-test/s4sdk/jenkins.yml @@ -0,0 +1,29 @@ +jenkins: + numExecutors: 10 +unclassified: + globallibraries: + libraries: + - defaultVersion: "master" + name: "s4sdk-pipeline-library" + retriever: + modernSCM: + scm: + git: + remote: "https://github.com/SAP/cloud-s4-sdk-pipeline-lib.git" + - defaultVersion: "master" + name: "piper-library-os" + retriever: + modernSCM: + scm: + git: + remote: "https://github.com/__REPO_SLUG__.git" +credentials: + system: + domainCredentials: + - credentials: + - usernamePassword: + scope: GLOBAL + id: "devops-docker-images-IT-cf" + username: ${CX_INFRA_IT_CF_USERNAME} + password: ${CX_INFRA_IT_CF_PASSWORD} + description: "SAP CP Trail account for test deployment" diff --git a/consumer-test/s4sdk/prepareTests.sh b/consumer-test/s4sdk/prepareTests.sh new file mode 100755 index 000000000..2663a1c9b --- /dev/null +++ b/consumer-test/s4sdk/prepareTests.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +EXAMPLE_PROJECT_BRANCH=$1 + +LIBRARY_VERSION_UNDER_TEST=$(git log --format="%H" -n 1) +REPOSITORY_UNDER_TEST=${TRAVIS_REPO_SLUG:-SAP/jenkins-library} + +rm -rf workspace +git clone -b "${EXAMPLE_PROJECT_BRANCH}" https://github.com/sap/cloud-s4-sdk-book workspace +cp -f ../jenkins.yml workspace +cd workspace || exit 1 + +# Configure path to library-repository under test in Jenkins config +sed -i -e "s:__REPO_SLUG__:${REPOSITORY_UNDER_TEST}:g" jenkins.yml + +# Force usage of library version under test by setting it in the Jenkinsfile which is then the first definition and thus has the highest precedence +echo "@Library(\"piper-library-os@$LIBRARY_VERSION_UNDER_TEST\") _" | cat - Jenkinsfile > temp && mv temp Jenkinsfile + +# Commit the changed version because artifactSetVersion expects the git repo not to be dirty +git commit --all --author="piper-testing-bot " --message="Set piper lib version for test" diff --git a/documentation/docs/steps/checkChangeInDevelopment.md b/documentation/docs/steps/checkChangeInDevelopment.md index ecf035b39..b164dce27 100644 --- a/documentation/docs/steps/checkChangeInDevelopment.md +++ b/documentation/docs/steps/checkChangeInDevelopment.md @@ -2,7 +2,7 @@ ## Description -Content here is generated from corresponnding step, see `vars`. +Content here is generated from corresponding step, see `vars`. ## Prerequisites @@ -10,11 +10,11 @@ Content here is generated from corresponnding step, see `vars`. ## Parameters -Content here is generated from corresponnding step, see `vars`. +Content here is generated from corresponding step, see `vars`. ## Step configuration -Content here is generated from corresponnding step, see `vars`. +Content here is generated from corresponding step, see `vars`. ## Exceptions diff --git a/documentation/docs/steps/dockerExecute.md b/documentation/docs/steps/dockerExecute.md index 5b4eaa1e9..bb327eb1e 100644 --- a/documentation/docs/steps/dockerExecute.md +++ b/documentation/docs/steps/dockerExecute.md @@ -2,11 +2,11 @@ ## Description -Content here is generated from corresponnding step, see `vars`. +Content here is generated from corresponding step, see `vars`. ## Parameters -Content here is generated from corresponnding step, see `vars`. +Content here is generated from corresponding step, see `vars`. ## Kubernetes support @@ -14,7 +14,7 @@ If the Jenkins is setup on a Kubernetes cluster, then you can execute the closur ## Step configuration -Content here is generated from corresponnding step, see `vars`. +Content here is generated from corresponding step, see `vars`. ## Side effects diff --git a/documentation/docs/steps/dockerExecuteOnKubernetes.md b/documentation/docs/steps/dockerExecuteOnKubernetes.md index 22ddbbbfb..013ce042a 100644 --- a/documentation/docs/steps/dockerExecuteOnKubernetes.md +++ b/documentation/docs/steps/dockerExecuteOnKubernetes.md @@ -2,7 +2,7 @@ ## Description -Content here is generated from corresponnding step, see `vars`. +Content here is generated from corresponding step, see `vars`. ## Prerequisites @@ -13,11 +13,11 @@ Content here is generated from corresponnding step, see `vars`. ## Parameters -Content here is generated from corresponnding step, see `vars`. +Content here is generated from corresponding step, see `vars`. ## Step configuration -Content here is generated from corresponnding step, see `vars`. +Content here is generated from corresponding step, see `vars`. ## Side effects diff --git a/documentation/docs/steps/handlePipelineStepErrors.md b/documentation/docs/steps/handlePipelineStepErrors.md index df2b7fd95..67f61fd40 100644 --- a/documentation/docs/steps/handlePipelineStepErrors.md +++ b/documentation/docs/steps/handlePipelineStepErrors.md @@ -19,23 +19,23 @@ none * `stepParameters` - The parameters from the step to be executed. The list of parameters is then shown in the console output. * `stepName` - The name of the step executed to be shown in the console output. * `echoDetails` - If set to true the following will be output to the console: - 1. Step beginning: `--- BEGIN LIBRARY STEP: ${stepName}.groovy ---` - 2. Step end: `--- END LIBRARY STEP: ${stepName}.groovy ---` + 1. Step beginning: `--- Begin library step: ${stepName}.groovy ---` + 2. Step end: `--- End library step: ${stepName}.groovy ---` 3. Step errors: ```log ---------------------------------------------------------- - --- ERROR OCCURED IN LIBRARY STEP: ${stepName} + --- An error occurred in the library step: ${stepName} ---------------------------------------------------------- - FOLLOWING PARAMETERS WERE AVAILABLE TO THIS STEP: + The following parameters were available to the step: *** ${stepParameters} *** - ERROR WAS: + The error was: *** ${err} *** - FURTHER INFORMATION: + Further information: * Documentation of step ${stepName}: .../${stepName}/ * Pipeline documentation: https://... * GitHub repository for pipeline steps: https://... diff --git a/documentation/docs/steps/mtaBuild.md b/documentation/docs/steps/mtaBuild.md index 393d702dd..f2c21843f 100644 --- a/documentation/docs/steps/mtaBuild.md +++ b/documentation/docs/steps/mtaBuild.md @@ -8,29 +8,21 @@ Before doing this, validates that SAP Multitarget Application Archive Builder ex Note that a version is formed by `major.minor.patch`, and a version is compatible to another version if the minor and patch versions are higher, but the major version is not, e.g. if 3.39.10 is the expected version, 3.39.11 and 3.40.1 would be compatible versions, but 4.0.1 would not be a compatible version. -## Prerequisites - -* A docker image meeting the following requirements - * **SAP MTA Archive Builder 1.0.6 or compatible version** - can be downloaded from [SAP Development Tools](https://tools.hana.ondemand.com/#cloud). - * **Java 8 or compatible version** - necessary to run the `mta.jar` file. - * **NodeJS installed** - the MTA Builder uses `npm` to download node module dependencies such as `grunt`. - ## Parameters | parameter | mandatory | default | possible values | | -----------------|-----------|--------------------------------------------------------|--------------------| | `script` | yes | | | -| `dockerImage` | yes | | | +| `dockerImage` | no | `ppiper/mta-archive-builder` | | | `dockerOptions` | no | '' | | | `buildTarget` | yes | `'NEO'` | 'CF', 'NEO', 'XSA' | -| `extension` | no | | | -| `mtaJarLocation` | no | `'mta.jar'` | | +| `extension` | no | | | +| `mtaJarLocation` | no | `'/opt/sap/mta/lib/mta.jar'` | | | `applicationName`| no | | | * `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. * `dockerImage` - The Docker image to execute the MTA build. - A custom built image needs to include Multi-target Application Archive Builder. - Refer to [SAP Help Portal](https://help.sap.com/viewer/58746c584026430a890170ac4d87d03b/Cloud/en-US/ba7dd5a47b7a4858a652d15f9673c28d.html) for information on how to set it up. + Note that you can provide your own image if required, but for most cases, the default should be fine. * `dockerOptions` Docker options to be set when starting the container. It can be a list or a string. * `buildTarget` - The target platform to which the mtar can be deployed. * `extension` - The path to the extension descriptor file. diff --git a/documentation/docs/steps/npmExecute.md b/documentation/docs/steps/npmExecute.md new file mode 100644 index 000000000..619cb9cb1 --- /dev/null +++ b/documentation/docs/steps/npmExecute.md @@ -0,0 +1,23 @@ +# npmExecute + +## Description + +Content here is generated from corresponding step, see `vars`. + +## Parameters + +Content here is generated from corresponding step, see `vars`. + +## Step configuration + +Content here is generated from corresponding step, see `vars`. + +## Exceptions + +None + +## Examples + +```groovy +npmExecute script: this, dockerImage: 'node:8-stretch', npmCommand: 'run build' +``` diff --git a/documentation/docs/steps/slackSendNotification.md b/documentation/docs/steps/slackSendNotification.md new file mode 100644 index 000000000..4357d0978 --- /dev/null +++ b/documentation/docs/steps/slackSendNotification.md @@ -0,0 +1,73 @@ +# slackSendNotification + +## Description + +Sends notifications to the Slack channel about the build status. + +Notification contains: + +* Build status; +* Repo Owner; +* Repo Name; +* Branch Name; +* Jenkins Build Number; +* Jenkins Build URL. + +## Prerequisites + +Installed and configured [Jenkins Slack plugin](https://github.com/jenkinsci/slack-plugin). + +## Example + +Usage of pipeline step: + +```groovy +try { + stage('..') {..} + stage('..') {..} + stage('..') {..} + currentBuild.result = 'SUCCESS' +} catch (Throwable err) { + currentBuild.result = 'FAILURE' + throw err +} finally { + stage('report') { + slackSendNotification script: this + } +} +``` + +## Parameters + +| parameter | mandatory | default | possible values | +| ----------|-----------|---------|-----------------| +|script|yes||| +|baseUrl|no||| +|channel|no||| +|color|no|`${buildStatus == 'SUCCESS'?'#008000':'#E60000'}`|| +|credentialsId|no||| +|message|no||| + +### Details + +* `script` defines the global script environment of the Jenkinsfile run. Typically `this` is passed to this parameter. This allows the function to access the [`commonPipelineEnvironment`](commonPipelineEnvironment.md) for storing the measured duration. +* `baseUrl` allows overriding the Slack Plugin Integration Base Url specified in the global configuration. +* `color` defines the message color. +* If `channel` is defined another than the default channel will be used. +* `credentialsId` defines the Jenkins credentialId which holds the Slack token +* With parameter `message` a custom message can be defined which is sent into the Slack channel. + +## Step configuration + +We recommend to define values of step parameters via [config.yml file](../configuration.md). + +In following sections the configuration is possible: + +| parameter | general | step | stage | +| ----------|-----------|---------|-----------------| +|script|||| +|baseUrl||X|X| +|channel||X|X| +|color||X|X| +|credentialsId||X|X| +|message||X|X| diff --git a/documentation/docs/steps/uiVeri5ExecuteTests.md b/documentation/docs/steps/uiVeri5ExecuteTests.md index 09816bffa..25e4d9e39 100644 --- a/documentation/docs/steps/uiVeri5ExecuteTests.md +++ b/documentation/docs/steps/uiVeri5ExecuteTests.md @@ -2,17 +2,17 @@ ## Description -Content here is generated from corresponnding step, see `vars`. +Content here is generated from corresponding step, see `vars`. ## Prerequisites ## Parameters -Content here is generated from corresponnding step, see `vars`. +Content here is generated from corresponding step, see `vars`. ## Step configuration -Content here is generated from corresponnding step, see `vars`. +Content here is generated from corresponding step, see `vars`. ## Exceptions diff --git a/documentation/mkdocs.yml b/documentation/mkdocs.yml index 4ad088c00..84af48355 100644 --- a/documentation/mkdocs.yml +++ b/documentation/mkdocs.yml @@ -24,12 +24,14 @@ nav: - mtaBuild: steps/mtaBuild.md - neoDeploy: steps/neoDeploy.md - newmanExecute: steps/newmanExecute.md + - npmExecute: steps/npmExecute.md - pipelineExecute: steps/pipelineExecute.md - pipelineRestartSteps: steps/pipelineRestartSteps.md - pipelineStashFiles: steps/pipelineStashFiles.md - prepareDefaultValues: steps/prepareDefaultValues.md - seleniumExecuteTests: steps/seleniumExecuteTests.md - setupCommonPipelineEnvironment: steps/setupCommonPipelineEnvironment.md + - slackSendNotification: steps/slackSendNotification.md - testsPublishResults: steps/testsPublishResults.md - toolValidate: steps/toolValidate.md - transportRequestCreate: steps/transportRequestCreate.md diff --git a/resources/com.sap.piper/templates/error.log b/resources/com.sap.piper/templates/error.log index 3058ed649..6cfcd9f3b 100644 --- a/resources/com.sap.piper/templates/error.log +++ b/resources/com.sap.piper/templates/error.log @@ -1,18 +1,18 @@ ---------------------------------------------------------- ---- ERROR OCCURRED IN LIBRARY STEP: ${stepName} +--- An error occurred in the library step: ${stepName} ---------------------------------------------------------- -FOLLOWING PARAMETERS WERE AVAILABLE TO THIS STEP: +The following parameters were available to the step: *** ${stepParameters} *** -ERROR WAS: +The error was: *** ${error} *** -FURTHER INFORMATION: +Further information: * Documentation of library step ${stepName}: https://sap.github.io/jenkins-library/steps/${stepName}/ * Source code of library step ${stepName}: https://github.com/SAP/jenkins-library/blob/master/vars/${stepName}.groovy * Library documentation: https://sap.github.io/jenkins-library/ diff --git a/resources/default_pipeline_environment.yml b/resources/default_pipeline_environment.yml index 9c781f17b..1a7b95c89 100644 --- a/resources/default_pipeline_environment.yml +++ b/resources/default_pipeline_environment.yml @@ -232,7 +232,8 @@ steps: logSuccessfulMavenTransfers: false mtaBuild: buildTarget: 'NEO' - mtaJarLocation: 'mta.jar' + mtaJarLocation: '/opt/sap/mta/lib/mta.jar' + dockerImage: 'ppiper/mta-archive-builder' neoDeploy: dockerImage: 's4sdk/docker-neo-cli' deployMode: 'mta' @@ -251,6 +252,8 @@ steps: newmanRunCommand: "run '${config.newmanCollection}' --environment '${config.newmanEnvironment}' --globals '${config.newmanGlobals}' --reporters junit,html --reporter-junit-export 'target/newman/TEST-${collectionDisplayName}.xml' --reporter-html-export 'target/newman/TEST-${collectionDisplayName}.html'" stashContent: - 'tests' + npmExecute: + dockerImage: 'node:8-stretch' pipelineRestartSteps: sendMail: true timeoutInSeconds: 900 @@ -305,6 +308,9 @@ steps: dockerImage: 'node:8-stretch' dockerName: 'npm' dockerWorkspace: '/home/node' + slackSendNotification: + color: "${buildStatus == 'SUCCESS'?'#008000':'#E60000'}" + defaultMessage: "${buildStatus}: Job ${env.JOB_NAME} <${env.BUILD_URL}|#${env.BUILD_NUMBER}>" snykExecute: buildDescriptorFile: './package.json' dockerImage: 'node:8-stretch' diff --git a/src/com/sap/piper/Utils.groovy b/src/com/sap/piper/Utils.groovy index 47f1e8f49..c8587bb7f 100644 --- a/src/com/sap/piper/Utils.groovy +++ b/src/com/sap/piper/Utils.groovy @@ -2,6 +2,7 @@ package com.sap.piper import com.cloudbees.groovy.cps.NonCPS import com.sap.piper.analytics.Telemetry +import groovy.text.SimpleTemplateEngine import java.nio.charset.StandardCharsets import java.security.MessageDigest @@ -95,3 +96,10 @@ void pushToSWA(Map parameters, Map config) { // some error occured in telemetry reporting. This should not break anything though. } } + +@NonCPS +static String fillTemplate(String templateText, Map binding){ + def engine = new SimpleTemplateEngine() + String result = engine.createTemplate(templateText).make(binding) + return result +} diff --git a/src/com/sap/piper/analytics/Telemetry.groovy b/src/com/sap/piper/analytics/Telemetry.groovy index 1a386d6b9..d721afb86 100644 --- a/src/com/sap/piper/analytics/Telemetry.groovy +++ b/src/com/sap/piper/analytics/Telemetry.groovy @@ -30,7 +30,7 @@ class Telemetry implements Serializable{ static notify(Script steps, Map config, Map payload){ //allow opt-out via configuration if (!config?.collectTelemetryData) { - steps.echo "[${payload.step}] Telemetry reporting disabled!" + steps.echo "[${payload.step}] Sending telemetry data is disabled." return } diff --git a/src/com/sap/piper/testutils/JenkinsController.groovy b/src/com/sap/piper/testutils/JenkinsController.groovy new file mode 100644 index 000000000..1e4d0476d --- /dev/null +++ b/src/com/sap/piper/testutils/JenkinsController.groovy @@ -0,0 +1,122 @@ +package com.sap.piper.jenkins + +import com.cloudbees.groovy.cps.NonCPS + +class JenkinsController implements Serializable { + def script + String jenkinsUrl + def timeout + + JenkinsController(script, String jenkinsUrl = "http://localhost:8080", timeout = 3600) { + this.script = script + this.jenkinsUrl = jenkinsUrl + this.timeout = timeout + } + + def waitForJenkinsStarted() { + def timeout = 120 + def timePerLoop = 5 + + for (int i = 0; i < timeout; i += timePerLoop) { + script.sleep timePerLoop + try { + if (retrieveJenkinsStatus() == 'NORMAL') { + return true + } + } catch (Exception e) { + script.echo "Could not retrieve status for Jenkins at ${jenkinsUrl}/api/json. Message: ${e.getMessage()}. Retrying..." + e.printStackTrace() + continue + } + return false + } + script.error("Timeout: Jenkins did not start within the expected time frame.") + } + + private retrieveJenkinsStatus() { + def apiUrl = "${jenkinsUrl}/api/json" + script.echo "Checking Jenkins Status" + def response = getTextFromUrl(apiUrl) + def result = script.readJSON text: response + return result.mode + } + + //Trigger scanning of the multi branch builds + def buildJob(String jobName) { + script.sh "curl -s -X POST ${jenkinsUrl}/job/${URLEncoder.encode(jobName, 'UTF-8')}/build" + } + + def waitForSuccess(String jobName, String branch) { + if (this.waitForJobStatus(jobName, branch, 'SUCCESS')) { + this.printConsoleText(jobName, branch) + script.echo "Build was successful" + } else { + this.printConsoleText(jobName, branch) + script.error("Build of ${jobName} ${branch} was not successfull") + } + } + + def getBuildUrl(String jobName, String branch) { + return "${jenkinsUrl}/job/${URLEncoder.encode(jobName, 'UTF-8')}/job/${URLEncoder.encode(branch, 'UTF-8')}/lastBuild/" + } + + def waitForJobStatus(String jobName, String branch, String status) { + def buildUrl = getBuildUrl(jobName, branch) + def timePerLoop = 10 + + for (int i = 0; i < timeout; i += timePerLoop) { + script.sleep timePerLoop + try { + script.echo "Checking Build Status of ${jobName} ${branch}" + def buildInformation = retrieveBuildInformation(jobName, branch) + + if (buildInformation.building) { + script.echo "Build is still in progress" + continue + } + if (buildInformation.result == status) { + return true + } + } catch (Exception e) { + script.echo "Could not retrieve status for ${buildUrl}. Message: ${e.getMessage()}. Retrying..." + continue + } + return false + } + script.error("Timeout: Build of job ${jobName}, branch ${branch} did not finish in the expected time frame.") + } + + def getConsoleText(String jobName, String branch) { + def consoleUrl = this.getBuildUrl(jobName, branch) + "/consoleText" + return getTextFromUrl(consoleUrl) + } + + def printConsoleText(String jobName, String branch) { + String consoleOutput = getConsoleText(jobName, branch) + + script.echo '***********************************************' + script.echo '** Begin Output of Example Application Build **' + script.echo '***********************************************' + + script.echo consoleOutput + + script.echo '*********************************************' + script.echo '** End Output of Example Application Build **' + script.echo '*********************************************' + } + + def retrieveBuildInformation(String jobName, String branch) { + def buildUrl = getBuildUrl(jobName, branch) + def url = "${buildUrl}/api/json" + script.echo "Checking Build Status of ${jobName} ${branch}" + script.echo "${jenkinsUrl}/job/${URLEncoder.encode(jobName, 'UTF-8')}/job/${URLEncoder.encode(branch, 'UTF-8')}/" + def response = getTextFromUrl(url) + def result = script.readJSON text: response + return result + } + + @NonCPS + private static String getTextFromUrl(url) { + return new URL(url).getText() + } +} diff --git a/test/groovy/HandlePipelineStepErrorTest.groovy b/test/groovy/HandlePipelineStepErrorTest.groovy index 1ccf80c84..394101466 100644 --- a/test/groovy/HandlePipelineStepErrorTest.groovy +++ b/test/groovy/HandlePipelineStepErrorTest.groovy @@ -37,8 +37,8 @@ class HandlePipelineStepErrorsTest extends BasePiperTest { } // asserts assertThat(isExecuted, is(true)) - assertThat(loggingRule.log, containsString('--- BEGIN LIBRARY STEP: testStep')) - assertThat(loggingRule.log, containsString('--- END LIBRARY STEP: testStep')) + assertThat(loggingRule.log, containsString('--- Begin library step of: testStep')) + assertThat(loggingRule.log, containsString('--- End library step of: testStep')) } @Test @@ -54,9 +54,9 @@ class HandlePipelineStepErrorsTest extends BasePiperTest { } catch (ignore) { } finally { // asserts - assertThat(loggingRule.log, not(containsString('--- BEGIN LIBRARY STEP: testStep'))) - assertThat(loggingRule.log, not(containsString('--- END LIBRARY STEP: testStep'))) - assertThat(loggingRule.log, not(containsString('--- ERROR OCCURRED IN LIBRARY STEP: testStep'))) + assertThat(loggingRule.log, not(containsString('--- Begin library step of: testStep'))) + assertThat(loggingRule.log, not(containsString('--- End library step: testStep'))) + assertThat(loggingRule.log, not(containsString('--- An error occurred in the library step: testStep'))) } } @@ -75,7 +75,7 @@ class HandlePipelineStepErrorsTest extends BasePiperTest { } finally { // asserts assertThat(isReported, is(true)) - assertThat(loggingRule.log, containsString('--- ERROR OCCURRED IN LIBRARY STEP: testStep')) + assertThat(loggingRule.log, containsString('--- An error occurred in the library step: testStep')) assertThat(loggingRule.log, containsString('[something:anything]')) } } diff --git a/test/groovy/MtaBuildTest.groovy b/test/groovy/MtaBuildTest.groovy index 7f0ca874c..d5de756cc 100644 --- a/test/groovy/MtaBuildTest.groovy +++ b/test/groovy/MtaBuildTest.groovy @@ -73,7 +73,7 @@ public class MtaBuildTest extends BasePiperTest { @Test - void mtarFilePathFromCommonPipelineEnviromentTest() { + void mtarFilePathFromCommonPipelineEnvironmentTest() { stepRule.step.mtaBuild(script: nullScript, buildTarget: 'NEO') @@ -163,9 +163,9 @@ public class MtaBuildTest extends BasePiperTest { stepRule.step.mtaBuild(script: nullScript, buildTarget: 'NEO') - assert shellRule.shell.find(){ c -> c.contains("-jar mta.jar --mtar")} - assert loggingRule.log.contains("SAP Multitarget Application Archive Builder file 'mta.jar' retrieved from configuration.") - assert loggingRule.log.contains("Using SAP Multitarget Application Archive Builder 'mta.jar'.") + assert shellRule.shell.find(){ c -> c.contains("-jar /opt/sap/mta/lib/mta.jar --mtar")} + assert loggingRule.log.contains("SAP Multitarget Application Archive Builder file '/opt/sap/mta/lib/mta.jar' retrieved from configuration.") + assert loggingRule.log.contains("Using SAP Multitarget Application Archive Builder '/opt/sap/mta/lib/mta.jar'.") } @@ -174,7 +174,7 @@ public class MtaBuildTest extends BasePiperTest { stepRule.step.mtaBuild(script: nullScript, buildTarget: 'NEO') - assert shellRule.shell.find { c -> c.contains('java -jar mta.jar --mtar com.mycompany.northwind.mtar --build-target=NEO build')} + assert shellRule.shell.find { c -> c.contains('java -jar /opt/sap/mta/lib/mta.jar --mtar com.mycompany.northwind.mtar --build-target=NEO build')} } @@ -185,7 +185,7 @@ public class MtaBuildTest extends BasePiperTest { stepRule.step.mtaBuild(script: nullScript) - assert shellRule.shell.find(){ c -> c.contains('java -jar mta.jar --mtar com.mycompany.northwind.mtar --build-target=NEO build')} + assert shellRule.shell.find(){ c -> c.contains('java -jar /opt/sap/mta/lib/mta.jar --mtar com.mycompany.northwind.mtar --build-target=NEO build')} } @Test @@ -211,7 +211,7 @@ public class MtaBuildTest extends BasePiperTest { stepRule.step.mtaBuild(script: nullScript) - assert shellRule.shell.find { c -> c.contains('java -jar mta.jar --mtar com.mycompany.northwind.mtar --build-target=NEO build')} + assert shellRule.shell.find { c -> c.contains('java -jar /opt/sap/mta/lib/mta.jar --mtar com.mycompany.northwind.mtar --build-target=NEO build')} } @@ -220,7 +220,7 @@ public class MtaBuildTest extends BasePiperTest { stepRule.step.mtaBuild(script: nullScript, buildTarget: 'NEO', extension: 'param_extension') - assert shellRule.shell.find { c -> c.contains('java -jar mta.jar --mtar com.mycompany.northwind.mtar --build-target=NEO --extension=param_extension build')} + assert shellRule.shell.find { c -> c.contains('java -jar /opt/sap/mta/lib/mta.jar --mtar com.mycompany.northwind.mtar --build-target=NEO --extension=param_extension build')} } @@ -231,7 +231,7 @@ public class MtaBuildTest extends BasePiperTest { stepRule.step.mtaBuild(script: nullScript) - assert shellRule.shell.find(){ c -> c.contains('java -jar mta.jar --mtar com.mycompany.northwind.mtar --build-target=NEO --extension=config_extension build')} + assert shellRule.shell.find(){ c -> c.contains('java -jar /opt/sap/mta/lib/mta.jar --mtar com.mycompany.northwind.mtar --build-target=NEO --extension=config_extension build')} } diff --git a/test/groovy/NpmExecuteTest.groovy b/test/groovy/NpmExecuteTest.groovy new file mode 100644 index 000000000..610f23ad3 --- /dev/null +++ b/test/groovy/NpmExecuteTest.groovy @@ -0,0 +1,56 @@ +import static org.junit.Assert.assertEquals +import hudson.AbortException +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.ExpectedException +import org.junit.rules.RuleChain +import util.BasePiperTest +import util.JenkinsDockerExecuteRule +import util.JenkinsReadYamlRule +import util.JenkinsShellCallRule +import util.JenkinsStepRule +import util.Rules + +class NpmExecuteTest extends BasePiperTest { + + private ExpectedException thrown = new ExpectedException().none() + private JenkinsShellCallRule shellRule = new JenkinsShellCallRule(this) + private JenkinsDockerExecuteRule dockerExecuteRule = new JenkinsDockerExecuteRule(this) + private JenkinsStepRule stepRule = new JenkinsStepRule(this) + private JenkinsReadYamlRule yamlRule = new JenkinsReadYamlRule(this) + + @Rule + public RuleChain ruleChain = Rules + .getCommonRules(this) + .around(thrown) + .around(yamlRule) + .around(dockerExecuteRule) + .around(shellRule) + .around(stepRule) + + @Before + void init() { + helper.registerAllowedMethod 'fileExists', [String], { s -> s == 'package.json' } + } + + @Test + void testNpmExecute() { + stepRule.step.npmExecute(script: nullScript, dockerImage: 'node:8-stretch') + assertEquals 'node:8-stretch', dockerExecuteRule.dockerParams.dockerImage + } + + @Test + void testNpmExecuteWithClosure() { + stepRule.step.npmExecute(script: nullScript, dockerImage: 'node:8-stretch', npmCommand: 'run build') { } + assert shellRule.shell.find { c -> c.contains('npm run build') } + } + + @Test + void testNoPackageJson() { + helper.registerAllowedMethod 'fileExists', [String], { false } + thrown.expect AbortException + thrown.expectMessage '[npmExecute] package.json is not found.' + stepRule.step.npmExecute(script: nullScript, dockerImage: 'node:8-stretch', npmCommand: 'run build') + } +} diff --git a/test/groovy/SlackSendNotificationTest.groovy b/test/groovy/SlackSendNotificationTest.groovy new file mode 100644 index 000000000..c59f82b41 --- /dev/null +++ b/test/groovy/SlackSendNotificationTest.groovy @@ -0,0 +1,88 @@ +#!groovy +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.RuleChain +import util.BasePiperTest +import util.JenkinsLoggingRule +import util.JenkinsReadYamlRule +import util.JenkinsStepRule +import util.Rules + +import static org.junit.Assert.* + +class SlackSendNotificationTest extends BasePiperTest { + def slackCallMap = [:] + + private JenkinsLoggingRule loggingRule = new JenkinsLoggingRule(this) + private JenkinsStepRule stepRule = new JenkinsStepRule(this) + + @Rule + public RuleChain ruleChain = Rules + .getCommonRules(this) + .around(new JenkinsReadYamlRule(this)) + .around(loggingRule) + .around(stepRule) + + @Before + void init() throws Exception { + helper.registerAllowedMethod("slackSend", [Map.class], {m -> slackCallMap = m}) + } + + @Test + void testNotificationBuildSuccessDefaultChannel() throws Exception { + stepRule.step.slackSendNotification(script: [currentBuild: [result: 'SUCCESS']]) + // asserts + assertEquals('Message not set correctly', 'SUCCESS: Job p ', slackCallMap.message.toString()) + assertNull('Channel not set correctly', slackCallMap.channel) + assertEquals('Color not set correctly', '#008000', slackCallMap.color) + assertJobStatusSuccess() + } + + @Test + void testNotificationBuildSuccessCustomChannel() throws Exception { + stepRule.step.slackSendNotification(script: [currentBuild: [result: 'SUCCCESS']], channel: 'Test') + // asserts + assertEquals('Channel not set correctly', 'Test', slackCallMap.channel) + assertJobStatusSuccess() + } + + @Test + void testNotificationBuildFailed() throws Exception { + stepRule.step.slackSendNotification(script: [currentBuild: [result: 'FAILURE']]) + // asserts + assertEquals('Message not set correctly', 'FAILURE: Job p ', slackCallMap.message.toString()) + assertEquals('Color not set correctly', '#E60000', slackCallMap.color) + } + + @Test + void testNotificationBuildStatusNull() throws Exception { + stepRule.step.slackSendNotification(script: [currentBuild: [:]]) + // asserts + assertTrue('Missing build status not detected', loggingRule.log.contains('currentBuild.result is not set. Skipping Slack notification')) + assertJobStatusSuccess() + } + + @Test + void testNotificationCustomMessageAndColor() throws Exception { + stepRule.step.slackSendNotification(script: [currentBuild: [:]], message: 'Custom Message', color: '#AAAAAA') + // asserts + assertEquals('Custom message not set correctly', 'Custom Message', slackCallMap.message.toString()) + assertEquals('Custom color not set correctly', '#AAAAAA', slackCallMap.color) + assertJobStatusSuccess() + } + + @Test + void testNotificationWithCustomCredentials() throws Exception { + stepRule.step.slackSendNotification( + script: [currentBuild: [:]], + message: 'I am no Message', + baseUrl: 'https://my.base.url', + credentialsId: 'MY_TOKEN_ID' + ) + // asserts + assertEquals('Custom base url not set correctly', 'https://my.base.url', slackCallMap.baseUrl) + assertEquals('Custom token id not set correctly', 'MY_TOKEN_ID', slackCallMap.tokenCredentialId) + assertJobStatusSuccess() + } +} diff --git a/test/groovy/com/sap/piper/analytics/TelemetryTest.groovy b/test/groovy/com/sap/piper/analytics/TelemetryTest.groovy index 2d930930f..dbb0c0c21 100644 --- a/test/groovy/com/sap/piper/analytics/TelemetryTest.groovy +++ b/test/groovy/com/sap/piper/analytics/TelemetryTest.groovy @@ -82,7 +82,7 @@ class TelemetryTest extends BasePiperTest { Telemetry.notify(nullScript, [collectTelemetryData: false], [step: 'anyStep', anything: 'something']) // asserts assertThat(Telemetry.getInstance().listenerList, is(not(empty()))) - assertThat(jlr.log, containsString("[anyStep] Telemetry reporting disabled!")) + assertThat(jlr.log, containsString("[anyStep] Sending telemetry data is disabled.")) assertThat(notificationPayload.keySet(), is(empty())) } @@ -99,7 +99,7 @@ class TelemetryTest extends BasePiperTest { Telemetry.notify(nullScript, [:], [step: 'anyStep', anything: 'something']) // asserts assertThat(Telemetry.getInstance().listenerList, is(not(empty()))) - assertThat(jlr.log, containsString("[anyStep] Telemetry reporting disabled!")) + assertThat(jlr.log, containsString("[anyStep] Sending telemetry data is disabled.")) assertThat(notificationPayload.keySet(), is(empty())) } @@ -116,7 +116,7 @@ class TelemetryTest extends BasePiperTest { Telemetry.notify(nullScript, null, [step: 'anyStep', anything: 'something']) // asserts assertThat(Telemetry.getInstance().listenerList, is(not(empty()))) - assertThat(jlr.log, containsString("[anyStep] Telemetry reporting disabled!")) + assertThat(jlr.log, containsString("[anyStep] Sending telemetry data is disabled.")) assertThat(notificationPayload.keySet(), is(empty())) } diff --git a/vars/checkChangeInDevelopment.groovy b/vars/checkChangeInDevelopment.groovy index fdd8540a6..46a9ae4c0 100644 --- a/vars/checkChangeInDevelopment.groovy +++ b/vars/checkChangeInDevelopment.groovy @@ -16,17 +16,13 @@ import static com.sap.piper.cm.StepHelpers.getBackendTypeAndLogInfoIfCMIntegrati @Field def STEP_NAME = getClass().getName() -@Field Set GENERAL_CONFIG_KEYS = STEP_CONFIG_KEYS - -@Field Set STEP_CONFIG_KEYS = [ - 'changeManagement', +@Field Set GENERAL_CONFIG_KEYS = ['changeManagement'] +@Field Set STEP_CONFIG_KEYS = GENERAL_CONFIG_KEYS.plus( /** * When set to `false` the step will not fail in case the step is not in status 'in development'. * @possibleValues `true`, `false` */ - 'failIfStatusIsNotInDevelopment' - ] - + 'failIfStatusIsNotInDevelopment') @Field Set PARAMETER_KEYS = STEP_CONFIG_KEYS.plus('changeDocumentId') /** diff --git a/vars/handlePipelineStepErrors.groovy b/vars/handlePipelineStepErrors.groovy index c304ac7f0..bf05f027b 100644 --- a/vars/handlePipelineStepErrors.groovy +++ b/vars/handlePipelineStepErrors.groovy @@ -10,10 +10,10 @@ void call(Map parameters = [:], body) { def message = '' try { if (stepParameters == null && stepName == null) - error "step handlePipelineStepErrors requires following mandatory parameters: stepParameters, stepName" + error "The step handlePipelineStepErrors requires following mandatory parameters: stepParameters, stepName" if (verbose) - echo "--- BEGIN LIBRARY STEP: ${stepName} ---" + echo "--- Begin library step of: ${stepName} ---" body() } catch (Throwable err) { @@ -28,7 +28,7 @@ void call(Map parameters = [:], body) { throw err } finally { if (verbose) - message += "--- END LIBRARY STEP: ${stepName} ---" + message += "--- End library step of: ${stepName} ---" echo message } } diff --git a/vars/npmExecute.groovy b/vars/npmExecute.groovy new file mode 100644 index 000000000..06e980737 --- /dev/null +++ b/vars/npmExecute.groovy @@ -0,0 +1,73 @@ +import static com.sap.piper.Prerequisites.checkScript +import com.sap.piper.GenerateDocumentation +import com.sap.piper.ConfigurationHelper +import com.sap.piper.Utils +import groovy.transform.Field + +@Field def STEP_NAME = getClass().getName() +@Field Set GENERAL_CONFIG_KEYS = [] +@Field Set STEP_CONFIG_KEYS = [ + /** + * Name of the docker image that should be used, in which node should be installed and configured. Default value is 'node:8-stretch'. + */ + 'dockerImage', + /** + * URL of default NPM registry + */ + 'defaultNpmRegistry', + /** + * Which NPM command should be executed. + */ + 'npmCommand'] +@Field Set PARAMETER_KEYS = STEP_CONFIG_KEYS + [ + /** + * Docker options to be set when starting the container. + */ + 'dockerOptions'] +/** + * Executes NPM commands inside a docker container. + * Docker image, docker options and npm commands can be specified or configured. + */ +@GenerateDocumentation +void call(Map parameters = [:], body = null) { + handlePipelineStepErrors(stepName: STEP_NAME, stepParameters: parameters) { + + final script = checkScript(this, parameters) ?: this + + // load default & individual configuration + Map configuration = ConfigurationHelper.newInstance(this) + .loadStepDefaults() + .mixinGeneralConfig(script.commonPipelineEnvironment, GENERAL_CONFIG_KEYS) + .mixinStepConfig(script.commonPipelineEnvironment, STEP_CONFIG_KEYS) + .mixinStageConfig(script.commonPipelineEnvironment, parameters.stageName?:env.STAGE_NAME, STEP_CONFIG_KEYS) + .mixin(parameters, PARAMETER_KEYS) + .use() + + new Utils().pushToSWA([ + step: STEP_NAME, + stepParamKey1: 'scriptMissing', + stepParam1: parameters?.script == null + ], configuration) + + try { + if (!fileExists('package.json')) { + error "[${STEP_NAME}] package.json is not found." + } + dockerExecute(script: script, dockerImage: configuration.dockerImage, dockerOptions: configuration.dockerOptions) { + if (configuration.defaultNpmRegistry) { + sh "npm config set registry ${configuration.defaultNpmRegistry}" + } + if (configuration.npmCommand) { + sh "npm ${configuration.npmCommand}" + } + if (body) { + body() + } + } + } catch (Exception e) { + println "Error while executing npm. Here are the logs:" + sh "cat ~/.npm/_logs/*" + throw e + } + } +} diff --git a/vars/slackSendNotification.groovy b/vars/slackSendNotification.groovy new file mode 100644 index 000000000..33c1a0cee --- /dev/null +++ b/vars/slackSendNotification.groovy @@ -0,0 +1,53 @@ +import static com.sap.piper.Prerequisites.checkScript + +import com.sap.piper.ConfigurationHelper +import com.sap.piper.Utils +import groovy.transform.Field +import groovy.text.SimpleTemplateEngine + +@Field String STEP_NAME = getClass().getName() + +@Field Set GENERAL_CONFIG_KEYS = [] +@Field Set STEP_CONFIG_KEYS = GENERAL_CONFIG_KEYS.plus([ + 'baseUrl', + 'channel', + 'color', + 'credentialsId', + 'message' +]) +@Field Set PARAMETER_KEYS = STEP_CONFIG_KEYS + +void call(Map parameters = [:]) { + handlePipelineStepErrors (stepName: STEP_NAME, stepParameters: parameters) { + def utils = parameters.juStabUtils ?: new Utils() + def script = checkScript(this, parameters) ?: this + // load default & individual configuration + Map config = ConfigurationHelper.newInstance(this) + .loadStepDefaults() + .mixinGeneralConfig(script.commonPipelineEnvironment, GENERAL_CONFIG_KEYS) + .mixinStepConfig(script.commonPipelineEnvironment, STEP_CONFIG_KEYS) + .mixinStageConfig(script.commonPipelineEnvironment, parameters.stageName?:env.STAGE_NAME, STEP_CONFIG_KEYS) + .mixin(parameters, PARAMETER_KEYS) + .use() + + new Utils().pushToSWA([step: STEP_NAME], config) + + def buildStatus = script.currentBuild.result + // resolve templates + config.color = SimpleTemplateEngine.newInstance().createTemplate(config.color).make([buildStatus: buildStatus]).toString() + if (!config?.message){ + if (!buildStatus) { + echo "[${STEP_NAME}] currentBuild.result is not set. Skipping Slack notification" + return + } + config.message = SimpleTemplateEngine.newInstance().createTemplate(config.defaultMessage).make([buildStatus: buildStatus, env: env]).toString() + } + Map options = [:] + if(config.credentialsId) + options.put('tokenCredentialId', config.credentialsId) + for(String entry : STEP_CONFIG_KEYS.minus('credentialsId')) + if(config.get(entry)) + options.put(entry, config.get(entry)) + slackSend(options) + } +}