From 76c9f608ae3194389c85e296826a59ac71a45f74 Mon Sep 17 00:00:00 2001 From: Yuxiang Zhu Date: Sep 19 2018 05:46:22 +0000 Subject: CI/CD - Updating Pagure PR status and sending an email after build completes --- diff --git a/openshift/README.md b/openshift/README.md index e61e439..7ad96f6 100644 --- a/openshift/README.md +++ b/openshift/README.md @@ -15,7 +15,10 @@ Before using the Pipeline, please ensure your Jenkins master has the following p - [Openshift Sync plugin][] [1] - [Openshift Client plugin][OpenShift Jenkins Pipeline (DSL) Plugin] [1] - [Kubernetes plugin][] [1] -- [SSH Agent plugin][]: +- [Timestamper plugin][] +- [SSH Agent plugin][] +- [Email Extension plugin][] +- [Ownership plugin][] Notes: [1]: Those plugins are preinstalled if you are using the Jenkins master shipped with OpenShift. @@ -116,6 +119,17 @@ cat "$HOME/.ssh/id_rsa_pagure.pub" oc label secret pagure-doc-secret credential.sync.jenkins.openshift.io=true ``` +#### Configure a Pagure API Key for Updating Pull-Request Status +This section is not required if updating Pagure PR status is not needed. + +- Go to your Pagure repository settings, and locate to the 'API Keys' section. +- Click on the `Create new key` button to add new API key with the `Flag a pull-request` permission. +- Add your newly-created API key to OpenShift: +```bash + oc create secret generic pagure-api-key --from-literal=secrettext= + oc label secret pagure-api-key credential.sync.jenkins.openshift.io=true +``` + #### Build Jenkins slave container image Before running the pipeline, you need to build a container image for Jenkins slave pods. This step should be repeated every time you change @@ -269,6 +283,9 @@ You can go to the OpenShift Web console for more details of the pipeline build. [OpenShift Jenkins Pipeline (DSL) Plugin]: https://github.com/openshift/jenkins-client-plugin [Openshift Sync plugin]: https://github.com/openshift/jenkins-sync-plugin [Kubernetes plugin]: https://github.com/jenkinsci/kubernetes-plugin +[Timestamper plugin]: https://github.com/jenkinsci/timestamper-plugin [SSH Agent plugin]: https://github.com/jenkinsci/ssh-agent-plugin +[Email Extension plugin]: https://github.com/jenkinsci/email-ext-plugin +[Ownership plugin]: https://github.com/jenkinsci/ownership-plugin [OpenShift secret for registries]:https://docs.openshift.com/container-platform/3.9/dev_guide/builds/build_inputs.html#using-docker-credentials-for-private-registries [OpenShift secret for SSH key authentication]: https://docs.openshift.com/container-platform/3.9/dev_guide/builds/build_inputs.html#source-secrets-ssh-key-authentication diff --git a/openshift/pipelines/templates/waiverdb-dev-template.yaml b/openshift/pipelines/templates/waiverdb-dev-template.yaml index d6e2d4d..7fc9e52 100644 --- a/openshift/pipelines/templates/waiverdb-dev-template.yaml +++ b/openshift/pipelines/templates/waiverdb-dev-template.yaml @@ -86,7 +86,7 @@ parameters: - name: WAIVERDB_INTEGRATION_TEST_BUILD_CONFIG_NAME displayName: Name of BuildConfig for running integration tests required: true - value: waiverdb-integration-test + value: waiverdb-dev-integration-test - name: WAIVERDB_INTEGRATION_TEST_BUILD_CONFIG_NAMESPACE displayName: Namespace of BuildConfig for running integration tests required: false @@ -162,7 +162,7 @@ objects: labels: app: "${NAME}" spec: - runPolicy: "Serial" # FIXME: Parallel is supported, but we have limited quota in UpShift. + runPolicy: "Serial" completionDeadlineSeconds: 1800 source: git: diff --git a/openshift/pipelines/templates/waiverdb-polling-pagure.yaml b/openshift/pipelines/templates/waiverdb-polling-pagure.yaml index bca30a1..64d5ba5 100644 --- a/openshift/pipelines/templates/waiverdb-polling-pagure.yaml +++ b/openshift/pipelines/templates/waiverdb-polling-pagure.yaml @@ -42,6 +42,14 @@ parameters: - name: DEV_PIPELINE_BC_NAMESPACE displayName: Namespace of BuildConfig for starting dev pipeline builds required: false +- name: PAGURE_API_KEY_SECRET_NAME + displayName: Name of Secret for updating Pagure pull-requests status + value: 'pagure-api-key' + required: false +- name: MAIL_ENABLED + displayName: Whether to send an email + value: 'true' + required: true - name: JENKINS_AGENT_IMAGE displayName: Container image for Jenkins slave pods required: true @@ -76,7 +84,6 @@ objects: app: "${NAME}" spec: runPolicy: "Serial" - completionDeadlineSeconds: 1800 strategy: type: JenkinsPipeline jenkinsPipelineStrategy: @@ -105,35 +112,36 @@ objects: tty: true resources: requests: - memory: 256Mi + memory: 378Mi cpu: 200m limits: - memory: 384Mi - cpu: 300m + memory: 768Mi + cpu: 500m """ } } options { timestamps() } + environment { + PIPELINE_NAMESPACE = readFile('/run/secrets/kubernetes.io/serviceaccount/namespace').trim() + PAGURE_URL = 'https://pagure.io' + PAGURE_API = "${env.PAGURE_URL}/api/0" + PAGURE_REPO_IS_FORK = "${PAGURE_REPO_IS_FORK}" + PAGURE_POLLING_FOR_PR = "${PAGURE_POLLING_FOR_PR}" + PAGURE_REPO_HOME = "${env.PAGURE_URL}${env.PAGURE_REPO_IS_FORK == 'true' ? '/fork' : ''}/${PAGURE_REPO_NAME}" + GIT_URL = "${env.PAGURE_URL}/${env.PAGURE_REPO_IS_FORK == 'true' ? 'forks/' : ''}${PAGURE_REPO_NAME}.git" + } triggers { pollSCM("${PAGURE_POLLING_SCHEDULE}") } stages { stage('Prepare') { agent { label 'master' } steps { script { - if (env.PAGURE_REPO_IS_FORK == 'true') { - env.PAGURE_HOME = "https://pagure.io/fork/${PAGURE_REPO_NAME}" - env.GIT_URL = "https://pagure.io/forks/${PAGURE_REPO_NAME}.git" - env.API_PREFIX = "https://pagure.io/api/0/fork/${PAGURE_REPO_NAME}" - } else { - env.PAGURE_HOME = "https://pagure.io/${PAGURE_REPO_NAME}" - env.GIT_URL = "https://pagure.io/${PAGURE_REPO_NAME}.git" - env.API_PREFIX = "https://pagure.io/api/0/${PAGURE_REPO_NAME}" - } - def pollingBranch = env.PAGURE_POLLING_FOR_PR == 'true' ? 'origin/pr/*' : "origin/${PAGURE_POLLED_BRANCH}" + // checking out the polled branch + def polledBranch = env.PAGURE_POLLING_FOR_PR == 'true' ? 'origin/pr/*' : "origin/${PAGURE_POLLED_BRANCH}" def scmVars = checkout([$class: 'GitSCM', - branches: [[name: pollingBranch]], + branches: [[name: polledBranch]], userRemoteConfigs: [ [ name: 'origin', @@ -145,6 +153,7 @@ objects: ]) echo "Build on branch=${scmVars.GIT_BRANCH}, commit=${scmVars.GIT_COMMIT}" env.GIT_COMMIT = scmVars.GIT_COMMIT + env.GIT_AUTHOR_EMAIL = scmVars.GIT_AUTHOR_EMAIL // setting build display name def prefix = 'origin/' def branch = scmVars.GIT_BRANCH.startsWith(prefix) ? scmVars.GIT_BRANCH.substring(prefix.size()) @@ -154,22 +163,22 @@ objects: currentBuild.displayName = "${PAGURE_POLLED_BRANCH}" } else if (env.PAGURE_POLLING_FOR_PR == 'true' && branch ==~ /^pr\/[0-9]+$/) { - prNo = branch.split('/')[-1] - def pagureUrl = "${env.PAGURE_HOME}/pull-request/${prNo}" - def pagureLink = """PR-${prNo}""" + env.PR_NO = branch.split('/')[-1] + env.PR_URL = "${env.PAGURE_REPO_HOME}/pull-request/${env.PR_NO}" + // To HTML syntax in build description, go to `Jenkins/Global Security/Markup Formatter` and select 'Safe HTML'. + def pagureLink = """PR-${env.PR_NO}""" try { - def response = httpRequest "${env.API_PREFIX}/pull-request/${prNo}" - def content = readJSON text: response.content - pagureLink = """${content.title}""" + def prInfo = getPagurePRInfo() + pagureLink = """${prInfo.title}""" } catch (Exception e) { - echo 'Error using pagure API:' - echo e.message - // ignoring this... + echo "Error using pagure API: ${e}" + // ignoring this... } - echo "Building PR #${prNo}: ${pagureUrl}" + echo "Building PR #${env.PR_NO}: ${env.PR_URL}" // FIXME: We are going to pass the display name to the triggered dev pipeline build, // however OpenShift Pipeline DSL is buggy to handle arguments with special bash characters (like whitespaces, #, etc). - currentBuild.displayName = "PR-${prNo}" + // https://bugzilla.redhat.com/show_bug.cgi?id=1625518 + currentBuild.displayName = "PR-${env.PR_NO}" currentBuild.description = pagureLink } else { // This shouldn't happen. error("Build is aborted due to unexpected polling trigger actions.") @@ -177,7 +186,7 @@ objects: } } } - stage('Trigger Dev Build') { + stage('Run Dev Build') { steps { script { openshift.withCluster() { @@ -185,7 +194,7 @@ objects: def bcSelector = openshift.selector('bc', "${DEV_PIPELINE_BC_NAME}") echo 'Starting a dev pipeline build...' def isMaster = env.PAGURE_POLLING_FOR_PR != 'true' - def buildSelector = bcSelector.startBuild( + def devBuild = bcSelector.startBuild( '-e', "WAIVERDB_GIT_REPO=${env.GIT_URL}", '-e', "WAIVERDB_GIT_REF=${env.GIT_COMMIT}", '-e', "FORCE_PUBLISH_IMAGE=${isMaster}", @@ -193,12 +202,136 @@ objects: '-e', "WAIVERDB_MAIN_BRANCH=${PAGURE_POLLED_BRANCH}", '-e', "BUILD_DISPLAY_RENAME_TO=${currentBuild.displayName}", ) - def buildName = buildSelector.object().metadata.name - echo "Pipeline job build ${buildName} triggered." + devBuild.watch { + return !(it.object().status.phase in ["New", "Pending"]) + } + def devBuildInfo = devBuild.object() + env.DEV_BUILD_URL = devBuildInfo.metadata.annotations['openshift.io/jenkins-build-uri'] ?: env.BUILD_URL + echo "Waiting for dev build ${devBuildInfo.metadata.name}(${env.DEV_BUILD_URL}) to complete..." + devBuild.watch { + return it.object().status.phase != "Running" + } + devBuildInfo = devBuild.object() + echo "Dev build ${devBuildInfo.metadata.name}(${env.DEV_BUILD_URL}) finished with status ${devBuildInfo.status.phase}." + if (devBuildInfo.status.phase != "Complete") { + error("Dev build ${devBuildInfo.metadata.name}(${env.DEV_BUILD_URL}) failed.") + } } } } } } } + post { + success { + script { + // updating Pagure PR flag + if (env.PAGURE_POLLING_FOR_PR == 'true' && "${PAGURE_API_KEY_SECRET_NAME}") { + try { + setBuildStatusOnPagurePR(100, 'Build passed.') + echo "Updated PR #${env.PR_NO} status to PASS." + } catch (e) { + echo "Error updating PR #${env.PR_NO} status to PASS: ${e}" + } + } + // sending email + if ("${MAIL_ENABLED}" == 'true' && env.PAGURE_POLLING_FOR_PR == 'true'){ + try { + sendBuildStatusEmail(true) + } catch (e) { + echo "Error sending email: ${e}" + } + } + } + } + failure { + script { + // updating Pagure PR flag + if (env.PAGURE_POLLING_FOR_PR == 'true' && "${PAGURE_API_KEY_SECRET_NAME}") { + try { + setBuildStatusOnPagurePR(0, 'Build failed.') + echo "Updated PR #${env.PR_NO} status to FAILURE." + } catch (e) { + echo "Error updating PR #${env.PR_NO} status to FAILURE: ${e}" + } + } + // sending email + if ("${MAIL_ENABLED}" == 'true'){ + try { + sendBuildStatusEmail(false) + } catch (e) { + echo "Error sending email: ${e}" + } + } + } + } + } + } + import java.net.URLEncoder + class PagureClient { + String pagureApiUrl + String token + def steps + def callApi(String httpMode, String apiPath, Map payload = null) { + def headers = [] + if (token) { + headers << [name: 'Authorization', value: "token ${token}", maskValue: true] + } + def payloadItems = [] + if (payload) { + payloadItems = payload.collect { + URLEncoder.encode(it.key.toString(), 'utf-8') + + '=' + URLEncoder.encode(it.value.toString(), 'utf-8') + } + } + return steps.httpRequest( + httpMode: httpMode, + url: "${pagureApiUrl}/${apiPath}", + acceptType: 'APPLICATION_JSON', + contentType: 'APPLICATION_FORM', + requestBody: payloadItems.join('&'), + customHeaders: headers, + ) + } + def getPR(Map args) { + def apiPath = "${args.fork?'fork/':''}${args.repo}/pull-request/${args.pr}" + def response = callApi('GET', apiPath) + return steps.readJSON(text: response.content) + } + def updatePRStatus(Map args) { + def apiPath = "${args.fork?'fork/':''}${args.repo}/pull-request/${args.pr}/flag" + def response = callApi('POST', apiPath, [ + 'username': args.username, + 'uid': args.uid, + 'percent': args.percent, + 'comment': args.comment, + 'url': args.url, + ]) + return steps.readJSON(text: response.content) + } + } + def getPagurePRInfo() { + def pagureClient = new PagureClient (pagureApiUrl: env.PAGURE_API, steps: steps) + return pagureClient.getPR(fork: env.PAGURE_REPO_IS_FORK == 'true', repo: "${PAGURE_REPO_NAME}", pr: env.PR_NO) + } + def setBuildStatusOnPagurePR(int percent, String comment) { + withCredentials([string(credentialsId: "${env.PIPELINE_NAMESPACE}-${PAGURE_API_KEY_SECRET_NAME}", variable: 'TOKEN')]) { + def pagureClient = new PagureClient (pagureApiUrl: env.PAGURE_API, token: env.TOKEN, steps: steps) + pagureClient.updatePRStatus( + username: 'c3i-jenkins', uid: 'ci-pre-merge', url: env.DEV_BUILD_URL, + percent: percent, comment: comment, pr: env.PR_NO, + repo: "${PAGURE_REPO_NAME}", fork: env.PAGURE_REPO_IS_FORK == 'true') + } + } + def sendBuildStatusEmail(boolean success) { + def status = success ? 'passed' : 'failed' + def reciepent = env.PAGURE_POLLING_FOR_PR != 'true' && ownership.job.ownershipEnabled && ownership.job.primaryOwnerEmail ? + ownership.job.primaryOwnerEmail : env.GIT_AUTHOR_EMAIL + def subject = "Jenkins job ${env.JOB_NAME} #${env.BUILD_NUMBER} ${status}." + def body = "Build URL: ${env.DEV_BUILD_URL}" + if (env.PAGURE_POLLING_FOR_PR == 'true') { + subject = "Jenkins job ${env.JOB_NAME}, PR #${env.PR_NO} ${status}." + body += "\nPull Request: ${env.PR_URL}" + } + emailext to: reciepent, subject: subject, body: body }