Meridio: Fix unstable job due to github commit status
[infra/cicd.git] / jjb / nsm / Jenkinsfile
1 /*
2 Copyright (c) 2022 Nordix Foundation
3
4 Licensed under the Apache License, Version 2.0 (the "License");
5 you may not use this file except in compliance with the License.
6 You may obtain a copy of the License at
7
8     http://www.apache.org/licenses/LICENSE-2.0
9
10 Unless required by applicable law or agreed to in writing, software
11 distributed under the License is distributed on an "AS IS" BASIS,
12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 See the License for the specific language governing permissions and
14 limitations under the License.
15 */
16
17 import org.jenkinsci.plugins.pipeline.modeldefinition.Utils
18
19 pending = 'PENDING'
20 success = 'SUCCESS'
21 failure = 'FAILURE'
22 base_image = 'base-image'
23 in_progress = 'In Progress.'
24 completed = 'Completed.'
25 failed = 'Failed'
26
27 node('nordix-nsm-build-ubuntu1804') {
28     build_number = env.BUILD_NUMBER
29     workspace = env.WORKSPACE
30     ws("${workspace}/${build_number}") {
31         def image_names = params.IMAGE_NAMES.split(' ')
32         def version = params.IMAGE_VERSION
33         def e2e_enabled = params.E2E_ENABLED
34         def git_project = params.GIT_PROJECT
35         def current_branch = params.CURRENT_BRANCH
36         def default_branch = params.DEFAULT_BRANCH
37         def build_steps = params.BUILD_STEPS
38         def image_registry = params.IMAGE_REGISTRY
39         def local_version =  "${env.JOB_NAME}-${build_number}"
40
41         timeout(30) {
42             stage('Clone/Checkout') {
43                 git branch: default_branch, url: git_project
44                 checkout([
45                     $class: 'GitSCM',
46                     branches: [[name: current_branch]],
47                     extensions: [],
48                     userRemoteConfigs: [[
49                         refspec: '+refs/pull/*/head:refs/remotes/origin/pr/*',
50                         url: git_project
51                     ]]
52                 ])
53                 sh 'git show'
54             }
55             stage('Verify') {
56                 Verify().call()
57             }
58             stage('Docker login') {
59                 withCredentials([usernamePassword(credentialsId: 'nordix-cicd-harbor-credentials', passwordVariable: 'HARBOR_PASSWORD', usernameVariable: 'HARBOR_USERNAME')]) {
60                     sh '''#!/bin/bash -eu
61                     echo $HARBOR_PASSWORD | docker login --username $HARBOR_USERNAME --password-stdin $IMAGE_REGISTRY
62                     '''
63                 }
64             }
65             stage('Base Image') {
66                 BaseImage(version, build_steps, image_registry, local_version).call()
67             }
68             stage('Images') {
69                 Images(image_names, version, build_steps, image_registry, local_version).call()
70             }
71             stage('E2E') {
72                 E2e(e2e_enabled).call()
73             }
74         }
75         stage('Cleanup') {
76             Cleanup()
77         }
78     }
79 }
80
81 // Static analysis: Runs the GeneratedCode function and then UnitTests and Linter in parallel
82 def Verify() {
83     return {
84         GeneratedCode().call() // cannot generate code and run the linter and tests at the same time
85         // Linter().call()
86         // UnitTests().call()
87         def stages = [:]
88         stages.put('Unit Tests', UnitTests())
89         stages.put('Linter', Linter())
90         // stages.put('Generated code verification', GeneratedCode())
91         parallel(stages)
92     }
93 }
94
95 // Runs the unit tests and set the github commit status
96 def UnitTests() {
97     return {
98         def context = 'Unit Tests'
99         stage('Unit Tests') {
100             try {
101                 SetBuildStatus(in_progress, context, pending)
102                 sh '''
103                     . \${HOME}/.profile
104                     make test
105                 '''
106                 SetBuildStatus(completed, context, success)
107             } catch (Exception e) {
108                 SetBuildStatus(failed, context, failure)
109                 Error(e).call()
110             }
111         }
112     }
113 }
114
115 // Runs the linter and set the github commit status
116 def Linter() {
117     return {
118         def context = 'Linter'
119         stage('Linter') {
120             try {
121                 SetBuildStatus(in_progress, context, pending)
122                 sh '''
123                     . \${HOME}/.profile
124                    make lint
125                 '''
126                 SetBuildStatus(completed, context, success)
127             } catch (Exception e) {
128                 SetBuildStatus(failed, context, failure)
129                 Error(e).call()
130             }
131         }
132     }
133 }
134
135 // Check if code has been generated correctly and set the github commit status:
136 // go.mod: runs "go mod tidy"
137 // go generate ./...: Code should be generated using "make genrate" command
138 // proto: skipped due to version of protoc
139 // If files are generated correctly then GetModifiedFiles function should return an empty string
140 def GeneratedCode() {
141     return {
142         def context = 'Generated code verification'
143         def exception_message = 'Generated code verification failed'
144         SetBuildStatus(in_progress, context, pending)
145         stage('go mod tidy') {
146             try {
147                 sh '''
148                     . \${HOME}/.profile
149                     go mod tidy
150                 '''
151                 if (GetModifiedFiles() != '') {
152                     throw new Exception(exception_message)
153                 }
154             } catch (Exception e) {
155                 SetBuildStatus(failed, context, failure)
156                 sh 'git diff'
157                 sh 'git status -s'
158                 Error(e).call()
159             }
160         }
161         stage('go generate ./...') {
162             try {
163                 sh '''
164                     . \${HOME}/.profile
165                     make generate
166                 '''
167                 if (GetModifiedFiles() != '') {
168                     throw new Exception(exception_message)
169                 }
170             } catch (Exception e) {
171                 SetBuildStatus(failed, context, failure)
172                 sh 'git diff'
173                 sh 'git status -s'
174                 Error(e).call()
175             }
176         }
177         stage('Proto') {
178             // TODO: protoc version could be different
179             Utils.markStageSkippedForConditional('Proto')
180         // try {
181         //     sh 'make proto'
182         //     if (GetModifiedFiles() != '') {
183         //         throw new Exception(exception_message)
184         //     }
185         // } catch (Exception e) {
186         //     SetBuildStatus(failed, context, failure)
187         //     sh 'git diff'
188         //     sh 'git status -s'
189         //     Error(e).call()
190         // }
191         }
192         SetBuildStatus(completed, context, success)
193     }
194 }
195
196 def BaseImage(version, build_steps, registry, local_version) {
197     return {
198         Build(base_image, version, build_steps, registry, local_version).call()
199     }
200 }
201
202 // Call Build function for every images in parallel
203 def Images(images, version, build_steps, registry, local_version) {
204     return {
205         def stages = [:]
206         for (i in images) {
207             stages.put(i, Build(i, version, build_steps, registry, local_version))
208         }
209         parallel(stages)
210     }
211 }
212
213 // Build set the github commit status
214 def Build(image, version, build_steps, registry, local_version) {
215     return {
216         stage("${image} (${version}): ${build_steps}") {
217             def context = "Image: ${image}"
218             def in_progress_message = "${in_progress} (${build_steps})"
219             def completed_message = "${completed} (${build_steps})"
220             def failed_message = "${failed} (${build_steps})"
221             try {
222                 SetBuildStatus(in_progress_message, context, pending)
223                 sh "make ${image} VERSION=${version} BUILD_STEPS='${build_steps}' REGISTRY=${registry} LOCAL_VERSION=${local_version} BASE_IMAGE=${base_image}:${local_version}"
224                 SetBuildStatus(completed_message, context, success)
225             } catch (Exception e) {
226                 SetBuildStatus(failed_message, context, failure)
227                 Error(e).call()
228             }
229         }
230     }
231 }
232
233 // Run the E2e Tests
234 // Currently skipped
235 def E2e(e2e_enabled) {
236     if (e2e_enabled == 'true') {
237         return {
238             echo 'make e2e' // todo
239         }
240     } else {
241         return {
242             Utils.markStageSkippedForConditional('E2E')
243         }
244     }
245 }
246
247 // Raise error in Jenkins job
248 def Error(e) {
249     return {
250         Cleanup()
251         error e
252     }
253 }
254
255 // Cleanup directory
256 def Cleanup() {
257     cleanWs()
258 }
259
260 // Set the commit status on Github
261 // https://plugins.jenkins.io/github/#plugin-content-pipeline-examples
262 def SetBuildStatus(String message, String context, String state) {
263     step([
264         $class: 'GitHubCommitStatusSetter',
265         reposSource: [$class: 'ManuallyEnteredRepositorySource', url: 'https://github.com/Nordix/Meridio'],
266         commitShaSource: [$class: 'ManuallyEnteredShaSource', sha: GetCommitSha()],
267         contextSource: [$class: 'ManuallyEnteredCommitContextSource', context: context],
268         errorHandlers: [[$class: 'ShallowAnyErrorHandler']], // Prevent GitHubCommitStatusSetter to set the job status to unstable
269         statusResultSource: [ $class: 'ConditionalStatusResultSource', results: [[$class: 'AnyBuildResult', message: message, state: state]] ]
270   ])
271 }
272
273 // Return the current commit sha
274 def GetCommitSha() {
275     return sh(script: 'git rev-parse HEAD', returnStdout: true).trim()
276 }
277
278 // Returns if any files has been modified/added/removed
279 def GetModifiedFiles() {
280     return sh(script: 'git status -s', returnStdout: true).trim()
281 }