getQualityGates() {
+ return qualityGates;
+ }
+
+ /**
+ * Defines the ID of the results. The ID is used as URL of the results and as name in UI elements. If no ID is
+ * given, then the ID of the associated result object is used.
+ *
+ * Note: this property is not used if {@link #isAggregatingResults} is {@code false}. It is also not visible in the
+ * UI in order to simplify the user interface.
+ *
+ *
+ * @param id
+ * the ID of the results
+ */
+ @DataBoundSetter
+ public void setId(final String id) {
+ this.id = id;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ /**
+ * Defines the name of the results. The name is used for all labels in the UI. If no name is given, then the name of
+ * the associated {@link StaticAnalysisLabelProvider} is used.
+ *
+ * Note: this property is not used if {@link #isAggregatingResults} is {@code false}. It is also not visible in the
+ * UI in order to simplify the user interface.
+ *
+ *
+ * @param name
+ * the name of the results
+ */
+ @DataBoundSetter
+ public void setName(final String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Gets the static analysis tools that will scan files and create issues.
+ *
+ * @return the static analysis tools (wrapped as {@link ToolProxy})
+ * @see #getTools
+ * @deprecated this method is only intended to be called by the UI
+ */
+ @Nullable
+ @Deprecated
+ public List getToolProxies() {
+ return analysisTools.stream().map(ToolProxy::new).collect(Collectors.toList());
+ }
+
+ /**
+ * Sets the static analysis tools that will scan files and create issues.
+ *
+ * @param toolProxies
+ * the static analysis tools (wrapped as {@link ToolProxy})
+ *
+ * @see #setTools(List)
+ * @see #setTool(Tool)
+ * @deprecated this method is only intended to be called by the UI
+ */
+ @DataBoundSetter
+ @Deprecated
+ public void setToolProxies(final List toolProxies) {
+ analysisTools = toolProxies.stream().map(ToolProxy::getTool).collect(Collectors.toList());
+ }
+
+ /**
+ * Sets the static analysis tools that will scan files and create issues.
+ *
+ * @param tools
+ * the static analysis tools
+ *
+ * @see #setTool(Tool)
+ */
+ @DataBoundSetter
+ public void setTools(final List tools) {
+ analysisTools = new ArrayList<>(tools);
+ }
+
+ /**
+ * Sets the static analysis tools that will scan files and create issues.
+ *
+ * @param tool
+ * the static analysis tool
+ * @param additionalTools
+ * additional static analysis tools (might be empty)
+ *
+ * @see #setTool(Tool)
+ * @see #setTools(List)
+ */
+ public void setTools(final Tool tool, final Tool... additionalTools) {
+ IssuesRecorder.ensureThatToolIsValid(tool);
+ for (Tool additionalTool : additionalTools) {
+ IssuesRecorder.ensureThatToolIsValid(additionalTool);
+ }
+ analysisTools = new ArrayList<>();
+ analysisTools.add(tool);
+ Collections.addAll(analysisTools, additionalTools);
+ }
+
+ /**
+ * Returns the static analysis tools that will scan files and create issues.
+ *
+ * @return the static analysis tools
+ */
+ public List getTools() {
+ return new ArrayList<>(analysisTools);
+ }
+
+ /**
+ * Sets the static analysis tool that will scan files and create issues.
+ *
+ * @param tool
+ * the static analysis tool
+ */
+ @DataBoundSetter
+ public void setTool(final Tool tool) {
+ IssuesRecorder.ensureThatToolIsValid(tool);
+
+ analysisTools = Collections.singletonList(tool);
+ }
+
+ /**
+ * Always returns {@code null}. Note: this method is required for Jenkins data binding.
+ *
+ * @return {@code null}
+ */
+ @Nullable
+ public Tool getTool() {
+ return null;
+ }
+
+ @Nullable
+ public String getSourceCodeEncoding() {
+ return sourceCodeEncoding;
+ }
+
+ /**
+ * Sets the encoding to use to read source files.
+ *
+ * @param sourceCodeEncoding
+ * the encoding, e.g. "ISO-8859-1"
+ */
+ @DataBoundSetter
+ public void setSourceCodeEncoding(final String sourceCodeEncoding) {
+ this.sourceCodeEncoding = sourceCodeEncoding;
+ }
+
+ /**
+ * Returns whether the results for each configured static analysis result should be aggregated into a single result
+ * or if every tool should get an individual result.
+ *
+ * @return {@code true} if the results of each static analysis tool should be aggregated into a single result,
+ * {@code false} if every tool should get an individual result.
+ */
+ @SuppressWarnings("PMD.BooleanGetMethodName")
+ public boolean getAggregatingResults() {
+ return isAggregatingResults;
+ }
+
+ @DataBoundSetter
+ public void setAggregatingResults(final boolean aggregatingResults) {
+ isAggregatingResults = aggregatingResults;
+ }
+
+ /**
+ * Returns whether SCM blaming should be disabled.
+ *
+ * @return {@code true} if SCM blaming should be disabled
+ */
+ @SuppressWarnings("PMD.BooleanGetMethodName")
+ public boolean getBlameDisabled() {
+ return isBlameDisabled;
+ }
+
+ @DataBoundSetter
+ public void setBlameDisabled(final boolean blameDisabled) {
+ isBlameDisabled = blameDisabled;
+ }
+
+ /**
+ * Returns whether recording should be enabled for failed builds as well.
+ *
+ * @return {@code true} if recording should be enabled for failed builds as well, {@code false} if recording is
+ * enabled for successful or unstable builds only
+ */
+ @SuppressWarnings("PMD.BooleanGetMethodName")
+ public boolean getEnabledForFailure() {
+ return isEnabledForFailure;
+ }
+
+ @DataBoundSetter
+ public void setEnabledForFailure(final boolean enabledForFailure) {
+ isEnabledForFailure = enabledForFailure;
+ }
+
+ /**
+ * If {@code true}, then the result of the quality gate is ignored when selecting a reference build. This option is
+ * disabled by default so a failing quality gate will be passed from build to build until the original reason for
+ * the failure has been resolved.
+ *
+ * @param ignoreQualityGate
+ * if {@code true} then the result of the quality gate is ignored, otherwise only build with a successful
+ * quality gate are selected
+ */
+ @DataBoundSetter
+ public void setIgnoreQualityGate(final boolean ignoreQualityGate) {
+ this.ignoreQualityGate = ignoreQualityGate;
+ }
+
+ @SuppressWarnings("PMD.BooleanGetMethodName")
+ public boolean getIgnoreQualityGate() {
+ return ignoreQualityGate;
+ }
+
+ /**
+ * If {@code true}, then only successful or unstable reference builds will be considered. This option is enabled by
+ * default, since analysis results might be inaccurate if the build failed. If {@code false}, every build that
+ * contains a static analysis result is considered, even if the build failed.
+ *
+ * @param ignoreFailedBuilds
+ * if {@code true} then a stable build is used as reference
+ */
+ @DataBoundSetter
+ public void setIgnoreFailedBuilds(final boolean ignoreFailedBuilds) {
+ this.ignoreFailedBuilds = ignoreFailedBuilds;
+ }
+
+ @SuppressWarnings("PMD.BooleanGetMethodName")
+ public boolean getIgnoreFailedBuilds() {
+ return ignoreFailedBuilds;
+ }
+
+ /**
+ * Sets the reference job to get the results for the issue difference computation.
+ *
+ * @param referenceJobName
+ * the name of reference job
+ */
+ @DataBoundSetter
+ public void setReferenceJobName(final String referenceJobName) {
+ if (IssuesRecorder.NO_REFERENCE_JOB.equals(referenceJobName)) {
+ this.referenceJobName = StringUtils.EMPTY;
+ }
+ this.referenceJobName = referenceJobName;
+ }
+
+ /**
+ * Returns the reference job to get the results for the issue difference computation. If the job is not defined,
+ * then {@link IssuesRecorder#NO_REFERENCE_JOB} is returned.
+ *
+ * @return the name of reference job, or {@link IssuesRecorder#NO_REFERENCE_JOB} if undefined
+ */
+ public String getReferenceJobName() {
+ if (StringUtils.isBlank(referenceJobName)) {
+ return IssuesRecorder.NO_REFERENCE_JOB;
+ }
+ return referenceJobName;
+ }
+
+ public int getHealthy() {
+ return healthy;
+ }
+
+ /**
+ * Sets the healthy threshold, i.e. the number of issues when health is reported as 100%.
+ *
+ * @param healthy
+ * the number of issues when health is reported as 100%
+ */
+ @DataBoundSetter
+ public void setHealthy(final int healthy) {
+ this.healthy = healthy;
+ }
+
+ public int getUnhealthy() {
+ return unhealthy;
+ }
+
+ /**
+ * Sets the healthy threshold, i.e. the number of issues when health is reported as 0%.
+ *
+ * @param unhealthy
+ * the number of issues when health is reported as 0%
+ */
+ @DataBoundSetter
+ public void setUnhealthy(final int unhealthy) {
+ this.unhealthy = unhealthy;
+ }
+
+ @Nullable
+ public String getMinimumSeverity() {
+ return minimumSeverity.getName();
+ }
+
+ /**
+ * Sets the minimum severity to consider when computing the health report. Issues with a severity less than this
+ * value will be ignored.
+ *
+ * @param minimumSeverity
+ * the severity to consider
+ */
+ @DataBoundSetter
+ public void setMinimumSeverity(final String minimumSeverity) {
+ this.minimumSeverity = Severity.valueOf(minimumSeverity, Severity.WARNING_LOW);
+ }
+
+ public List getFilters() {
+ return new ArrayList<>(filters);
+ }
+
+ @DataBoundSetter
+ public void setFilters(final List filters) {
+ this.filters = new ArrayList<>(filters);
+ }
+
+ @Override
+ public StepExecution start(StepContext context) throws Exception {
+ return new Execution(context, this);
+ }
+
+ /**
+ * Actually performs the execution of the associated step.
+ */
+ static class Execution extends AnalysisExecution {
+ private static final long serialVersionUID = 1L;
+ private final RecordIssuesStep step;
+
+ Execution(@NonNull final StepContext context, final RecordIssuesStep step) {
+ super(context);
+ this.step = step;
+ }
+
+ @Override
+ protected Void run() throws IOException, InterruptedException {
+ IssuesRecorder recorder = new IssuesRecorder();
+ recorder.setTools(step.getTools());
+ recorder.setSourceCodeEncoding(step.getSourceCodeEncoding());
+ recorder.setIgnoreQualityGate(step.getIgnoreQualityGate());
+ recorder.setIgnoreFailedBuilds(step.getIgnoreFailedBuilds());
+ recorder.setReferenceJobName(step.getReferenceJobName());
+ recorder.setHealthy(step.getHealthy());
+ recorder.setUnhealthy(step.getUnhealthy());
+ recorder.setMinimumSeverity(step.getMinimumSeverity());
+ recorder.setFilters(step.getFilters());
+ recorder.setEnabledForFailure(step.getEnabledForFailure());
+ recorder.setAggregatingResults(step.getAggregatingResults());
+ recorder.setBlameDisabled(step.getBlameDisabled());
+ recorder.setId(step.getId());
+ recorder.setName(step.getName());
+ recorder.setQualityGates(step.getQualityGates());
+
+ QualityGateStatusHandler statusHandler = new QualityGateStatusHandler.PipelineStatusHandler(getRun(),
+ getContext().get(FlowNode.class));
+
+ FilePath workspace = getWorkspace();
+ workspace.mkdirs();
+ recorder.perform(getRun(), workspace, getTaskListener(), statusHandler);
+ return null;
+ }
+
+ }
+
+ /**
+ * Descriptor for this step: defines the context and the UI labels.
+ */
+ @Extension
+ @SuppressWarnings("unused") // most methods are used by the corresponding jelly view
+ public static class Descriptor extends StepDescriptor {
+ private final ModelValidation model = new ModelValidation();
+
+ @Override
+ public String getFunctionName() {
+ return "recordIssues";
+ }
+
+ @NonNull
+ @Override
+ public String getDisplayName() {
+ return Messages.ScanAndPublishIssues_DisplayName();
+ }
+
+ @Override
+ public Set extends Class>> getRequiredContext() {
+ return Sets.immutable.of(FilePath.class, FlowNode.class, Run.class, TaskListener.class).castToSet();
+ }
+
+ /**
+ * Returns a model with all available charsets.
+ *
+ * @return a model with all available charsets
+ */
+ public ComboBoxModel doFillSourceCodeEncodingItems() {
+ return model.getAllCharsets();
+ }
+
+ /**
+ * Returns a model with all available severity filters.
+ *
+ * @return a model with all available severity filters
+ */
+ public ListBoxModel doFillMinimumSeverityItems() {
+ return model.getAllSeverityFilters();
+ }
+
+ /**
+ * Returns the model with the possible reference jobs.
+ *
+ * @return the model with the possible reference jobs
+ */
+ public ComboBoxModel doFillReferenceJobNameItems() {
+ return model.getAllJobs();
+ }
+
+ /**
+ * Performs on-the-fly validation of the reference job.
+ *
+ * @param referenceJobName
+ * the reference job
+ *
+ * @return the validation result
+ */
+ public FormValidation doCheckReferenceJobName(@QueryParameter final String referenceJobName) {
+ return model.validateJob(referenceJobName);
+ }
+
+ /**
+ * Performs on-the-fly validation of the character encoding.
+ *
+ * @param reportEncoding
+ * the character encoding
+ *
+ * @return the validation result
+ */
+ public FormValidation doCheckReportEncoding(@QueryParameter final String reportEncoding) {
+ return model.validateCharset(reportEncoding);
+ }
+
+ /**
+ * Performs on-the-fly validation on the character encoding.
+ *
+ * @param sourceCodeEncoding
+ * the character encoding
+ *
+ * @return the validation result
+ */
+ public FormValidation doCheckSourceCodeEncoding(@QueryParameter final String sourceCodeEncoding) {
+ return model.validateCharset(sourceCodeEncoding);
+ }
+
+ /**
+ * Performs on-the-fly validation of the health report thresholds.
+ *
+ * @param healthy
+ * the healthy threshold
+ * @param unhealthy
+ * the unhealthy threshold
+ *
+ * @return the validation result
+ */
+ public FormValidation doCheckHealthy(@QueryParameter final int healthy, @QueryParameter final int unhealthy) {
+ return model.validateHealthy(healthy, unhealthy);
+ }
+
+ /**
+ * Performs on-the-fly validation of the health report thresholds.
+ *
+ * @param healthy
+ * the healthy threshold
+ * @param unhealthy
+ * the unhealthy threshold
+ *
+ * @return the validation result
+ */
+ public FormValidation doCheckUnhealthy(@QueryParameter final int healthy, @QueryParameter final int unhealthy) {
+ return model.validateUnhealthy(healthy, unhealthy);
+ }
+ }
+}
diff --git a/src/main/java/io/jenkins/plugins/analysis/core/util/QualityGateStatus.java b/src/main/java/io/jenkins/plugins/analysis/core/util/QualityGateStatus.java
index 554ba40ad0..90bb2cfc00 100644
--- a/src/main/java/io/jenkins/plugins/analysis/core/util/QualityGateStatus.java
+++ b/src/main/java/io/jenkins/plugins/analysis/core/util/QualityGateStatus.java
@@ -48,6 +48,15 @@ public boolean isSuccessful() {
return this == PASSED || this == INACTIVE;
}
+ /**
+ * Returns the associated {@link Result}.
+ *
+ * @return the associated {@link Result}
+ */
+ public Result getResult() {
+ return result;
+ }
+
/**
* Sets the result of the specified run to the associated value of this quality gate status.
*
@@ -55,9 +64,7 @@ public boolean isSuccessful() {
* the run to set the result for
*/
public void setResult(final Run, ?> run) {
- if (!isSuccessful()) {
- run.setResult(result);
- }
+ new QualityGateStatusHandler.SetBuildResultStatusHandler(run).handleStatus(this);
}
/**
diff --git a/src/main/java/io/jenkins/plugins/analysis/core/util/QualityGateStatusHandler.java b/src/main/java/io/jenkins/plugins/analysis/core/util/QualityGateStatusHandler.java
new file mode 100644
index 0000000000..8217963557
--- /dev/null
+++ b/src/main/java/io/jenkins/plugins/analysis/core/util/QualityGateStatusHandler.java
@@ -0,0 +1,80 @@
+package io.jenkins.plugins.analysis.core.util;
+
+import hudson.model.Result;
+import hudson.model.Run;
+import org.jenkinsci.plugins.workflow.actions.WarningAction;
+import org.jenkinsci.plugins.workflow.graph.FlowNode;
+
+/**
+ * Interface used to handle {@link QualityGateStatus}. Default implementation is {@link SetBuildResultStatusHandler}
+ */
+public interface QualityGateStatusHandler {
+
+ /**
+ * Called to handle the {@link QualityGateStatus} created by a
+ * {@link QualityGateEvaluator#evaluate(IssuesStatistics, FormattedLogger)} call.
+ *
+ * @param status
+ * the status to handle
+ */
+ void handleStatus(QualityGateStatus status);
+
+ /**
+ * {@link QualityGateStatusHandler} that sets the overall build result if the status is unsuccessful.
+ */
+ class SetBuildResultStatusHandler implements QualityGateStatusHandler {
+ private final Run, ?> run;
+
+ /**
+ * Creates a new instance of {@link SetBuildResultStatusHandler}.
+ *
+ * @param run
+ * the run to set the result for
+ */
+ public SetBuildResultStatusHandler(final Run, ?> run) {
+ this.run = run;
+ }
+
+ @Override
+ public void handleStatus(final QualityGateStatus status) {
+ if (!status.isSuccessful()) {
+ run.setResult(status.getResult());
+ }
+ }
+ }
+
+ /**
+ * {@link QualityGateStatusHandler} that sets the overall build result and annotates the given
+ * Pipeline step with a {@link WarningAction} if the status is unsuccessful.
+ */
+ class PipelineStatusHandler implements QualityGateStatusHandler {
+ private final Run, ?> run;
+ private final FlowNode flowNode;
+
+ /**
+ * Creates a new instance of {@link PipelineStatusHandler}.
+ *
+ * @param run
+ * the run to set the result for
+ * @param flowNode
+ * the flow node to add a warning to
+ */
+ public PipelineStatusHandler(final Run, ?> run, final FlowNode flowNode) {
+ this.run = run;
+ this.flowNode = flowNode;
+ }
+
+ @Override
+ public void handleStatus(final QualityGateStatus status) {
+ if (!status.isSuccessful()) {
+ Result result = status.getResult();
+ run.setResult(result);
+ WarningAction existing = flowNode.getPersistentAction(WarningAction.class);
+ if (existing == null || existing.getResult().isBetterThan(result)) {
+ flowNode.addOrReplaceAction(new WarningAction(result)
+ .withMessage("Some quality gates have been missed: overall result is " + status));
+ }
+ }
+ }
+ }
+}
diff --git a/src/test/java/io/jenkins/plugins/analysis/warnings/StepsITest.java b/src/test/java/io/jenkins/plugins/analysis/warnings/StepsITest.java
index f5675aba15..a8950c4fb8 100644
--- a/src/test/java/io/jenkins/plugins/analysis/warnings/StepsITest.java
+++ b/src/test/java/io/jenkins/plugins/analysis/warnings/StepsITest.java
@@ -19,8 +19,12 @@
import org.kohsuke.stapler.HttpResponse;
import org.jenkinsci.Symbol;
+import org.jenkinsci.plugins.workflow.actions.WarningAction;
import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
+import org.jenkinsci.plugins.workflow.graph.FlowNode;
+import org.jenkinsci.plugins.workflow.graphanalysis.DepthFirstScanner;
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
+import org.jenkinsci.plugins.workflow.job.WorkflowRun;
import hudson.model.Result;
import hudson.model.Run;
import hudson.model.UnprotectedRootAction;
@@ -562,6 +566,48 @@ public void shouldUseOtherJobAsReference() {
// TODO: add verification for io.jenkins.plugins.analysis.core.model.IssueDifference
}
+ /**
+ * Verifies that when publishIssues marks the build as unstable it also marks the step with
+ * WarningAction so that visualizations can display the step as unstable rather than just
+ * the whole build.
+ *
+ * @see Issue 39203
+ */
+ @Test
+ public void publishIssuesShouldMarkStepWithWarningAction() {
+ WorkflowJob job = createPipelineWithWorkspaceFiles("javac.txt");
+ job.setDefinition(asStage(createScanForIssuesStep(new Java(), "java"),
+ "publishIssues(issues:[java], qualityGates: [[threshold: 1, type: 'TOTAL', unstable: true]])"));
+ WorkflowRun run = (WorkflowRun)buildWithResult(job, Result.UNSTABLE);
+ FlowNode publishIssuesNode = new DepthFirstScanner().findFirstMatch(run.getExecution(),
+ node -> node.getDisplayFunctionName().equals("publishIssues"));
+ assertThat(publishIssuesNode).isNotNull();
+ WarningAction warningAction = publishIssuesNode.getPersistentAction(WarningAction.class);
+ assertThat(warningAction).isNotNull();
+ assertThat(warningAction.getMessage()).isEqualTo("Some quality gates have been missed: overall result is WARNING");
+ }
+
+ /**
+ * Verifies that when recordIssues marks the build as unstable it also marks the step with
+ * WarningAction so that visualizations can display the step as unstable rather than just
+ * the whole build.
+ *
+ * @see Issue 39203
+ */
+ @Test
+ public void recordIssuesShouldMarkStepWithWarningAction() {
+ WorkflowJob job = createPipelineWithWorkspaceFiles("javac.txt");
+ job.setDefinition(asStage("recordIssues(tool: java(pattern:'**/*issues.txt', reportEncoding:'UTF-8'),"
+ + "qualityGates: [[threshold: 1, type: 'TOTAL', unstable: true]])"));
+ WorkflowRun run = (WorkflowRun)buildWithResult(job, Result.UNSTABLE);
+ FlowNode publishIssuesNode = new DepthFirstScanner().findFirstMatch(run.getExecution(),
+ node -> node.getDisplayFunctionName().equals("recordIssues"));
+ assertThat(publishIssuesNode).isNotNull();
+ WarningAction warningAction = publishIssuesNode.getPersistentAction(WarningAction.class);
+ assertThat(warningAction).isNotNull();
+ assertThat(warningAction.getMessage()).isEqualTo("Some quality gates have been missed: overall result is WARNING");
+ }
+
/**
* Verifies that parsers based on Digester are not vulnerable to an XXE attack. Previous versions allowed any user
* with an ability to configure a job to read any file from the Jenkins Master (even on hardened systems where