From ac719367f66122dfa24f03e4200d2a2c7e00ba5d Mon Sep 17 00:00:00 2001 From: Rob Zienert Date: Mon, 30 Nov 2020 12:28:26 -0800 Subject: [PATCH] chore(jenkins): Include breadcrumb error message when jenkins job fails (#4011) Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- .../pipeline/model/StageExecutionImpl.java | 1 + .../igor/tasks/MonitorJenkinsJobTask.groovy | 55 ++++++++++++++----- .../tasks/MonitorJenkinsJobTaskSpec.groovy | 29 ++++++++++ 3 files changed, 71 insertions(+), 14 deletions(-) diff --git a/orca-core/src/main/java/com/netflix/spinnaker/orca/pipeline/model/StageExecutionImpl.java b/orca-core/src/main/java/com/netflix/spinnaker/orca/pipeline/model/StageExecutionImpl.java index 08a8dd01dc..7a1c9d377f 100644 --- a/orca-core/src/main/java/com/netflix/spinnaker/orca/pipeline/model/StageExecutionImpl.java +++ b/orca-core/src/main/java/com/netflix/spinnaker/orca/pipeline/model/StageExecutionImpl.java @@ -634,6 +634,7 @@ public void appendErrorMessage(String errorMessage) { (List) exceptionDetails.getOrDefault("errors", new ArrayList()); exceptionDetails.putIfAbsent("errors", errors); + // Path: exception.details.errors errors.add(errorMessage); // This might be a no-op, but if there wasn't an exception object there, we should add it to the diff --git a/orca-igor/src/main/groovy/com/netflix/spinnaker/orca/igor/tasks/MonitorJenkinsJobTask.groovy b/orca-igor/src/main/groovy/com/netflix/spinnaker/orca/igor/tasks/MonitorJenkinsJobTask.groovy index 300018b473..6aecaaecfa 100644 --- a/orca-igor/src/main/groovy/com/netflix/spinnaker/orca/igor/tasks/MonitorJenkinsJobTask.groovy +++ b/orca-igor/src/main/groovy/com/netflix/spinnaker/orca/igor/tasks/MonitorJenkinsJobTask.groovy @@ -16,6 +16,7 @@ package com.netflix.spinnaker.orca.igor.tasks +import com.google.common.base.Enums import com.netflix.spinnaker.kork.core.RetrySupport import com.netflix.spinnaker.orca.api.pipeline.models.ExecutionStatus import com.netflix.spinnaker.orca.api.pipeline.OverridableTimeoutRetryableTask @@ -42,13 +43,6 @@ class MonitorJenkinsJobTask implements OverridableTimeoutRetryableTask { @Autowired RetrySupport retrySupport - private static Map statusMap = [ - 'ABORTED' : ExecutionStatus.CANCELED, - 'FAILURE' : ExecutionStatus.TERMINAL, - 'SUCCESS' : ExecutionStatus.SUCCEEDED, - 'UNSTABLE': ExecutionStatus.TERMINAL - ] - @Override TaskResult execute(StageExecution stage) { String master = stage.context.master @@ -70,15 +64,23 @@ class MonitorJenkinsJobTask implements OverridableTimeoutRetryableTask { outputs.buildInfo = build - if (statusMap.containsKey(result)) { - ExecutionStatus status = statusMap[result] - if (result == 'UNSTABLE' && stage.context.markUnstableAsSuccessful) { - status = ExecutionStatus.SUCCEEDED - } - return TaskResult.builder(status).context(outputs).outputs(outputs).build() - } else { + def jobStatus = Optional.ofNullable(result) + .map { Enums.getIfPresent(JenkinsJobStatus.class, it).orNull() } + .orElse(null) + if (jobStatus == null) { return TaskResult.builder(ExecutionStatus.RUNNING).context([buildInfo: build]).build() } + + ExecutionStatus taskStatus = jobStatus.executionStatusEquivalent + if (jobStatus == JenkinsJobStatus.UNSTABLE && stage.context.markUnstableAsSuccessful) { + taskStatus = JenkinsJobStatus.SUCCESS.executionStatusEquivalent + } + + if (jobStatus.errorBreadcrumb != null) { + stage.appendErrorMessage(jobStatus.errorBreadcrumb) + } + + return TaskResult.builder(taskStatus).context(outputs).outputs(outputs).build() } catch (RetrofitError e) { if ([503, 500, 404].contains(e.response?.status)) { log.warn("Http ${e.response.status} received from `igor`, retrying...") @@ -88,4 +90,29 @@ class MonitorJenkinsJobTask implements OverridableTimeoutRetryableTask { throw e } } + + /** + * Maps Jenkins job statuses to {@link ExecutionStatus} + */ + static enum JenkinsJobStatus { + ABORTED(ExecutionStatus.CANCELED, "Job was aborted (see Jenkins)"), + FAILURE(ExecutionStatus.TERMINAL, "Job failed (see Jenkins)"), + SUCCESS(ExecutionStatus.SUCCEEDED, null), + UNSTABLE(ExecutionStatus.TERMINAL, "Job is unstable") + + /** + * Orca's equivalent execution status value. + */ + ExecutionStatus executionStatusEquivalent + + /** + * An optional error message breadcrumb that is surfaced to users when the jenkins job fails. + */ + String errorBreadcrumb + + JenkinsJobStatus(ExecutionStatus executionStatusEquivalent, String errorBreadcrumb) { + this.executionStatusEquivalent = executionStatusEquivalent + this.errorBreadcrumb = errorBreadcrumb + } + } } diff --git a/orca-igor/src/test/groovy/com/netflix/spinnaker/orca/igor/tasks/MonitorJenkinsJobTaskSpec.groovy b/orca-igor/src/test/groovy/com/netflix/spinnaker/orca/igor/tasks/MonitorJenkinsJobTaskSpec.groovy index 8909b10eca..3929879b3c 100644 --- a/orca-igor/src/test/groovy/com/netflix/spinnaker/orca/igor/tasks/MonitorJenkinsJobTaskSpec.groovy +++ b/orca-igor/src/test/groovy/com/netflix/spinnaker/orca/igor/tasks/MonitorJenkinsJobTaskSpec.groovy @@ -17,6 +17,7 @@ package com.netflix.spinnaker.orca.igor.tasks import com.netflix.spinnaker.orca.api.pipeline.models.ExecutionStatus +import com.netflix.spinnaker.orca.clouddriver.pipeline.job.model.JobStatus import com.netflix.spinnaker.orca.igor.BuildService import com.netflix.spinnaker.orca.pipeline.model.PipelineExecutionImpl import com.netflix.spinnaker.orca.pipeline.model.StageExecutionImpl @@ -154,4 +155,32 @@ class MonitorJenkinsJobTaskSpec extends Specification { false | ExecutionStatus.TERMINAL null | ExecutionStatus.TERMINAL } + + @Unroll + def 'provides breadcrumb stage error message in failure states'() { + given: + def stage = new StageExecutionImpl(pipeline, "jenkins", [master: "builds", job: "orca", buildNumber: 4]) + + and: + task.buildService = Stub(BuildService) { + getBuild(stage.context.buildNumber, stage.context.master, stage.context.job) >> [result: jobState] + } + + when: + task.execute(stage) + + then: + if (expectedErrorMessage) { + stage.context.exception.details.errors.contains(expectedErrorMessage) + } else { + stage.context.exception == null + } + + where: + jobState || expectedErrorMessage + MonitorJenkinsJobTask.JenkinsJobStatus.ABORTED || "Job was aborted (see Jenkins)" + MonitorJenkinsJobTask.JenkinsJobStatus.FAILURE || "Job failed (see Jenkins)" + MonitorJenkinsJobTask.JenkinsJobStatus.SUCCESS || false + MonitorJenkinsJobTask.JenkinsJobStatus.UNSTABLE || "Job is unstable" + } }