/* Copyright (c) 2022 Nordix Foundation Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ import org.jenkinsci.plugins.pipeline.modeldefinition.Utils pending = 'PENDING' success = 'SUCCESS' failure = 'FAILURE' base_image = 'base-image' in_progress = 'In Progress.' completed = 'Completed.' failed = 'Failed' exception_message_exec = 'failed to execute the following command: ' exception_message_code_generation = 'Generated code verification failed' node('nordix-nsm-build-ubuntu2204') { build_number = env.BUILD_NUMBER workspace = env.WORKSPACE ws("${workspace}/${build_number}") { def image_names = params.IMAGE_NAMES.split(' ') def version = params.IMAGE_VERSION def e2e_enabled = params.E2E_ENABLED def git_project = params.GIT_PROJECT def current_branch = params.CURRENT_BRANCH def default_branch = params.DEFAULT_BRANCH def build_steps = params.BUILD_STEPS def image_registry = params.IMAGE_REGISTRY def local_version = "${env.JOB_NAME}-${build_number}" timeout(30) { stage('Clone/Checkout') { git branch: default_branch, url: git_project checkout([ $class: 'GitSCM', branches: [[name: current_branch]], extensions: [], userRemoteConfigs: [[ refspec: '+refs/pull/*/head:refs/remotes/origin/pr/*', url: git_project ]] ]) sh 'git show' } stage('Verify') { Verify().call() if (currentBuild.result == 'FAILURE') { Error('Failed at verification stage').call() } } stage('Docker login') { if (env.DRY_RUN != 'true') { withCredentials([usernamePassword(credentialsId: 'nordix-cicd-harbor-credentials', passwordVariable: 'HARBOR_PASSWORD', usernameVariable: 'HARBOR_USERNAME')]) { sh '''#!/bin/bash -eu echo $HARBOR_PASSWORD | docker login --username $HARBOR_USERNAME --password-stdin $IMAGE_REGISTRY ''' } } else { Utils.markStageSkippedForConditional('Docker login') } } stage('Base Image') { BaseImage(version, build_steps, image_registry, local_version).call() } stage('Images') { Images(image_names, version, build_steps, image_registry, local_version).call() if (currentBuild.result == 'FAILURE') { Error('Failed to build image(s)').call() } } stage('E2E') { if (e2e_enabled == 'true' && env.DRY_RUN != 'true') { E2e(e2e_enabled).call() } else { Utils.markStageSkippedForConditional('E2E') } } } stage('Cleanup') { Cleanup() } } } // Static analysis: Runs the GeneratedCode function and then UnitTests and Linter in parallel def Verify() { return { GeneratedCode().call() // cannot generate code and run the linter and tests at the same time // Linter().call() // UnitTests().call() def stages = [:] stages.put('Unit Tests', UnitTests()) stages.put('Linter', Linter()) // stages.put('Generated code verification', GeneratedCode()) parallel(stages) } } // Runs the unit tests and set the github commit status def UnitTests() { return { def context = 'Unit Tests' stage('Unit Tests') { def command = 'make test' try { SetBuildStatus(in_progress, context, pending) ExecSh(command).call() SetBuildStatus(completed, context, success) } catch (Exception e) { SetBuildStatus(failed, context, failure) unstable "${exception_message_exec} ${command}" currentBuild.result = 'FAILURE' } } } } // Runs the linter and set the github commit status def Linter() { return { def context = 'Linter' stage('Linter') { def command = 'make lint' try { SetBuildStatus(in_progress, context, pending) ExecSh(command).call() SetBuildStatus(completed, context, success) } catch (Exception e) { SetBuildStatus(failed, context, failure) unstable "${exception_message_exec} ${command}" currentBuild.result = 'FAILURE' } } } } // Check if code has been generated correctly and set the github commit status: // go.mod: runs "go mod tidy" // go generate ./...: Code should be generated using "make genrate" command // proto: skipped due to version of protoc // If files are generated correctly then GetModifiedFiles function should return an empty string def GeneratedCode() { return { def context = 'Generated code verification' SetBuildStatus(in_progress, context, pending) stage('go mod tidy') { def command = 'go mod tidy' try { ExecSh(command).call() if (GetModifiedFiles() != '') { throw new Exception(exception_message_code_generation) } } catch (Exception e) { SetBuildStatus(failed, context, failure) Error(exception_message_exec + command).call() } } stage('go generate ./...') { def command = 'make generate' try { ExecSh(command).call() if (GetModifiedFiles() != '') { throw new Exception(exception_message_code_generation) } } catch (Exception e) { SetBuildStatus(failed, context, failure) Error(exception_message_exec + command).call() } } stage('Proto') { // TODO: protoc version could be different Utils.markStageSkippedForConditional('Proto') } SetBuildStatus(completed, context, success) } } def BaseImage(version, build_steps, registry, local_version) { return { Build(base_image, version, build_steps, registry, local_version).call() } } // Call Build function for every images in parallel def Images(images, version, build_steps, registry, local_version) { return { def stages = [:] for (i in images) { stages.put(i, Build(i, version, build_steps, registry, local_version)) } parallel(stages) } } // Build set the github commit status def Build(image, version, build_steps, registry, local_version) { return { stage("${image} (${version}): ${build_steps}") { def context = "Image: ${image}" def in_progress_message = "${in_progress} (${build_steps})" def completed_message = "${completed} (${build_steps})" def failed_message = "${failed} (${build_steps})" def command = "make ${image} VERSION=${version} BUILD_STEPS='${build_steps}' REGISTRY=${registry} LOCAL_VERSION=${local_version} BASE_IMAGE=${base_image}:${local_version}" try { SetBuildStatus(in_progress_message, context, pending) ExecSh(command).call() SetBuildStatus(completed_message, context, success) } catch (Exception e) { SetBuildStatus(failed_message, context, failure) unstable "${exception_message_exec} ${command}" currentBuild.result = 'FAILURE' } } } } // Run the E2e Tests // Currently skipped def E2e(e2e_enabled) { return { echo 'make e2e' // todo } } // Raise error in Jenkins job def Error(e) { return { sh 'git diff' sh 'git status -s' Cleanup() error e } } // Cleanup directory def Cleanup() { cleanWs() } // Execute command def ExecSh(command) { return { if (env.DRY_RUN != 'true') { sh """ . \${HOME}/.profile ${command} """ } else { echo "${command}" } } } // Set the commit status on Github // https://plugins.jenkins.io/github/#plugin-content-pipeline-examples def SetBuildStatus(String message, String context, String state) { if (env.DRY_RUN != 'true') { step([ $class: 'GitHubCommitStatusSetter', reposSource: [$class: 'ManuallyEnteredRepositorySource', url: 'https://github.com/Nordix/Meridio'], commitShaSource: [$class: 'ManuallyEnteredShaSource', sha: GetCommitSha()], contextSource: [$class: 'ManuallyEnteredCommitContextSource', context: context], errorHandlers: [[$class: 'ShallowAnyErrorHandler']], // Prevent GitHubCommitStatusSetter to set the job status to unstable statusResultSource: [ $class: 'ConditionalStatusResultSource', results: [[$class: 'AnyBuildResult', message: message, state: state]] ] ]) } } // Return the current commit sha def GetCommitSha() { return sh(script: 'git rev-parse HEAD', returnStdout: true).trim() } // Returns if any files has been modified/added/removed def GetModifiedFiles() { return sh(script: 'git status -s', returnStdout: true).trim() }