diff --git a/.ci/pipeline-library/build.gradle b/.ci/pipeline-library/build.gradle index 2753750871ad5..ac5e7a4ed034a 100644 --- a/.ci/pipeline-library/build.gradle +++ b/.ci/pipeline-library/build.gradle @@ -20,6 +20,7 @@ dependencies { implementation 'org.jenkins-ci.plugins.workflow:workflow-step-api:2.19@jar' testImplementation 'com.lesfurets:jenkins-pipeline-unit:1.4' testImplementation 'junit:junit:4.12' + testImplementation 'org.mockito:mockito-core:2.+' testImplementation 'org.assertj:assertj-core:3.15+' // Temporary https://github.com/jenkinsci/JenkinsPipelineUnit/issues/209 } diff --git a/.ci/pipeline-library/src/test/KibanaBasePipelineTest.groovy b/.ci/pipeline-library/src/test/KibanaBasePipelineTest.groovy index 23282089ab76c..086484f2385b0 100644 --- a/.ci/pipeline-library/src/test/KibanaBasePipelineTest.groovy +++ b/.ci/pipeline-library/src/test/KibanaBasePipelineTest.groovy @@ -19,9 +19,9 @@ class KibanaBasePipelineTest extends BasePipelineTest { env.BUILD_DISPLAY_NAME = "#${env.BUILD_ID}" env.JENKINS_URL = 'http://jenkins.localhost:8080' - env.BUILD_URL = "${env.JENKINS_URL}/job/elastic+kibana+${env.BRANCH_NAME}/${env.BUILD_ID}/" + env.BUILD_URL = "${env.JENKINS_URL}/job/elastic+kibana+${env.BRANCH_NAME}/${env.BUILD_ID}/".toString() - env.JOB_BASE_NAME = "elastic / kibana # ${env.BRANCH_NAME}" + env.JOB_BASE_NAME = "elastic / kibana # ${env.BRANCH_NAME}".toString() env.JOB_NAME = env.JOB_BASE_NAME env.WORKSPACE = 'WS' @@ -31,6 +31,9 @@ class KibanaBasePipelineTest extends BasePipelineTest { getBuildStatus: { 'SUCCESS' }, printStacktrace: { ex -> print ex }, ], + githubPr: [ + isPr: { false }, + ], jenkinsApi: [ getFailedSteps: { [] } ], testUtils: [ getFailures: { [] } ], ]) diff --git a/.ci/pipeline-library/src/test/buildState.groovy b/.ci/pipeline-library/src/test/buildState.groovy new file mode 100644 index 0000000000000..b748cce29e7f4 --- /dev/null +++ b/.ci/pipeline-library/src/test/buildState.groovy @@ -0,0 +1,48 @@ +import org.junit.* +import static groovy.test.GroovyAssert.* + +class BuildStateTest extends KibanaBasePipelineTest { + def buildState + + @Before + void setUp() { + super.setUp() + + buildState = loadScript("vars/buildState.groovy") + } + + @Test + void 'get() returns existing data'() { + buildState.add('test', 1) + def actual = buildState.get('test') + assertEquals(1, actual) + } + + @Test + void 'get() returns null for missing data'() { + def actual = buildState.get('missing_key') + assertEquals(null, actual) + } + + @Test + void 'add() does not overwrite existing keys'() { + assertTrue(buildState.add('test', 1)) + assertFalse(buildState.add('test', 2)) + + def actual = buildState.get('test') + + assertEquals(1, actual) + } + + @Test + void 'set() overwrites existing keys'() { + assertFalse(buildState.has('test')) + buildState.set('test', 1) + assertTrue(buildState.has('test')) + buildState.set('test', 2) + + def actual = buildState.get('test') + + assertEquals(2, actual) + } +} diff --git a/.ci/pipeline-library/src/test/githubCommitStatus.groovy b/.ci/pipeline-library/src/test/githubCommitStatus.groovy new file mode 100644 index 0000000000000..17878624b73cf --- /dev/null +++ b/.ci/pipeline-library/src/test/githubCommitStatus.groovy @@ -0,0 +1,85 @@ +import org.junit.* +import static org.mockito.Mockito.*; + +class GithubCommitStatusTest extends KibanaBasePipelineTest { + def githubCommitStatus + def githubApiMock + def buildStateMock + + def EXPECTED_STATUS_URL = 'repos/elastic/kibana/statuses/COMMIT_HASH' + def EXPECTED_CONTEXT = 'kibana-ci' + def EXPECTED_BUILD_URL = 'http://jenkins.localhost:8080/job/elastic+kibana+master/1/' + + interface BuildState { + Object get(String key) + } + + interface GithubApi { + Object post(String url, Map data) + } + + @Before + void setUp() { + super.setUp() + + buildStateMock = mock(BuildState) + githubApiMock = mock(GithubApi) + + when(buildStateMock.get('checkoutInfo')).thenReturn([ commit: 'COMMIT_HASH', ]) + when(githubApiMock.post(any(), any())).thenReturn(null) + + props([ + buildState: buildStateMock, + githubApi: githubApiMock, + ]) + + githubCommitStatus = loadScript("vars/githubCommitStatus.groovy") + } + + void verifyStatusCreate(String state, String description) { + verify(githubApiMock).post( + EXPECTED_STATUS_URL, + [ + 'state': state, + 'description': description, + 'context': EXPECTED_CONTEXT, + 'target_url': EXPECTED_BUILD_URL, + ] + ) + } + + @Test + void 'onStart() should create a pending status'() { + githubCommitStatus.onStart() + verifyStatusCreate('pending', 'Build started.') + } + + @Test + void 'onFinish() should create a success status'() { + githubCommitStatus.onFinish() + verifyStatusCreate('success', 'Build completed successfully.') + } + + @Test + void 'onFinish() should create an error status for failed builds'() { + mockFailureBuild() + githubCommitStatus.onFinish() + verifyStatusCreate('error', 'Build failed.') + } + + @Test + void 'onStart() should exit early for PRs'() { + prop('githubPr', [ isPr: { true } ]) + + githubCommitStatus.onStart() + verifyZeroInteractions(githubApiMock) + } + + @Test + void 'onFinish() should exit early for PRs'() { + prop('githubPr', [ isPr: { true } ]) + + githubCommitStatus.onFinish() + verifyZeroInteractions(githubApiMock) + } +} diff --git a/Jenkinsfile b/Jenkinsfile index 4180e6989e1d3..9e0cc8d7ccddf 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -3,7 +3,7 @@ library 'kibana-pipeline-library' kibanaLibrary.load() -kibanaPipeline(timeoutMinutes: 135, checkPrChanges: true) { +kibanaPipeline(timeoutMinutes: 155, checkPrChanges: true, setCommitStatus: true) { githubPr.withDefaultPrComments { ciStats.trackBuild { catchError { diff --git a/vars/buildState.groovy b/vars/buildState.groovy new file mode 100644 index 0000000000000..365705661350c --- /dev/null +++ b/vars/buildState.groovy @@ -0,0 +1,30 @@ +import groovy.transform.Field + +public static @Field JENKINS_BUILD_STATE = [:] + +def add(key, value) { + if (!buildState.JENKINS_BUILD_STATE.containsKey(key)) { + buildState.JENKINS_BUILD_STATE[key] = value + return true + } + + return false +} + +def set(key, value) { + buildState.JENKINS_BUILD_STATE[key] = value +} + +def get(key) { + return buildState.JENKINS_BUILD_STATE[key] +} + +def has(key) { + return buildState.JENKINS_BUILD_STATE.containsKey(key) +} + +def get() { + return buildState.JENKINS_BUILD_STATE +} + +return this diff --git a/vars/githubCommitStatus.groovy b/vars/githubCommitStatus.groovy new file mode 100644 index 0000000000000..4cd4228d55f03 --- /dev/null +++ b/vars/githubCommitStatus.groovy @@ -0,0 +1,42 @@ +def shouldCreateStatuses() { + return !githubPr.isPr() && buildState.get('checkoutInfo') +} + +def onStart() { + catchError { + if (!shouldCreateStatuses()) { + return + } + + def checkoutInfo = buildState.get('checkoutInfo') + create(checkoutInfo.commit, 'pending', 'Build started.') + } +} + +def onFinish() { + catchError { + if (!shouldCreateStatuses()) { + return + } + + def checkoutInfo = buildState.get('checkoutInfo') + def status = buildUtils.getBuildStatus() + + if (status == 'SUCCESS' || status == 'UNSTABLE') { + create(checkoutInfo.commit, 'success', 'Build completed successfully.') + } else if(status == 'ABORTED') { + create(checkoutInfo.commit, 'error', 'Build aborted or timed out.') + } else { + create(checkoutInfo.commit, 'error', 'Build failed.') + } + } +} + +// state: error|failure|pending|success +def create(sha, state, description, context = 'kibana-ci') { + withGithubCredentials { + return githubApi.post("repos/elastic/kibana/statuses/${sha}", [ state: state, description: description, context: context, target_url: env.BUILD_URL ]) + } +} + +return this diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index 76da8ec6dd9ab..dfc21ae3da0f7 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -197,12 +197,15 @@ def runErrorReporter() { } def call(Map params = [:], Closure closure) { - def config = [timeoutMinutes: 135, checkPrChanges: false] + params + def config = [timeoutMinutes: 135, checkPrChanges: false, setCommitStatus: false] + params stage("Kibana Pipeline") { timeout(time: config.timeoutMinutes, unit: 'MINUTES') { timestamps { ansiColor('xterm') { + if (config.setCommitStatus) { + buildState.set('shouldSetCommitStatus', true) + } if (config.checkPrChanges && githubPr.isPr()) { pipelineLibraryTests() @@ -213,7 +216,13 @@ def call(Map params = [:], Closure closure) { return } } - closure() + try { + closure() + } finally { + if (config.setCommitStatus) { + githubCommitStatus.onFinish() + } + } } } } diff --git a/vars/workers.groovy b/vars/workers.groovy index e44df9ef75ad0..8b7e8525a7ce3 100644 --- a/vars/workers.groovy +++ b/vars/workers.groovy @@ -65,6 +65,12 @@ def base(Map params, Closure closure) { dir("kibana") { checkoutInfo = getCheckoutInfo() + + // use `checkoutInfo` as a flag to indicate that we've already reported the pending commit status + if (buildState.get('shouldSetCommitStatus') && !buildState.has('checkoutInfo')) { + buildState.set('checkoutInfo', checkoutInfo) + githubCommitStatus.onStart() + } } ciStats.reportGitInfo(