diff --git a/pom.xml b/pom.xml index d5864edad6..e1c5dc3f06 100644 --- a/pom.xml +++ b/pom.xml @@ -4,8 +4,8 @@ org.jenkins-ci.plugins plugin - 3.42 - + 3.43 + io.jenkins.plugins @@ -25,7 +25,7 @@ UTF-8 - 2.121.1 + 2.138.4 8 2.49 @@ -61,6 +61,7 @@ 1.13 1.24 3.9.1 + 2.34-rc901.34c79c66fab6 2.19 2.32 1.17 @@ -248,6 +249,11 @@ script-security ${script-security.version} + + org.jenkins-ci.plugins.workflow + workflow-api + ${workflow-api.version} + org.jenkins-ci.plugins.workflow workflow-step-api @@ -440,12 +446,6 @@ - - org.jenkins-ci.plugins.workflow - workflow-api - 2.33 - test - org.jenkins-ci.plugins.workflow workflow-support diff --git a/src/main/java/io/jenkins/plugins/analysis/core/steps/IssuesAggregator.java b/src/main/java/io/jenkins/plugins/analysis/core/steps/IssuesAggregator.java index 663658c5fe..5d72c29f0c 100644 --- a/src/main/java/io/jenkins/plugins/analysis/core/steps/IssuesAggregator.java +++ b/src/main/java/io/jenkins/plugins/analysis/core/steps/IssuesAggregator.java @@ -17,6 +17,7 @@ import io.jenkins.plugins.analysis.core.model.AnalysisResult; import io.jenkins.plugins.analysis.core.model.ResultAction; +import io.jenkins.plugins.analysis.core.util.QualityGateStatusHandler; /** * Aggregates the {@link AnalysisResult}s of all {@link ResultAction}s of several {@link MatrixRun}s into {@link @@ -91,7 +92,8 @@ private void updateMap(final List actions) { public boolean endBuild() { for (Entry> reportsPerId : results.entrySet()) { AnnotatedReport aggregatedReport = new AnnotatedReport(reportsPerId.getKey(), reportsPerId.getValue()); - recorder.publishResult(build, listener, Messages.Tool_Default_Name(), aggregatedReport, StringUtils.EMPTY); + recorder.publishResult(build, listener, Messages.Tool_Default_Name(), aggregatedReport, StringUtils.EMPTY, + new QualityGateStatusHandler.SetBuildResultStatusHandler(build)); } return true; } diff --git a/src/main/java/io/jenkins/plugins/analysis/core/steps/IssuesPublisher.java b/src/main/java/io/jenkins/plugins/analysis/core/steps/IssuesPublisher.java index 31c8741073..82edfcb38e 100644 --- a/src/main/java/io/jenkins/plugins/analysis/core/steps/IssuesPublisher.java +++ b/src/main/java/io/jenkins/plugins/analysis/core/steps/IssuesPublisher.java @@ -27,6 +27,9 @@ import io.jenkins.plugins.analysis.core.util.LogHandler; import io.jenkins.plugins.analysis.core.util.QualityGateEvaluator; import io.jenkins.plugins.analysis.core.util.QualityGateStatus; +import io.jenkins.plugins.analysis.core.util.QualityGateStatusHandler; + +import org.jenkinsci.plugins.workflow.graph.FlowNode; import static io.jenkins.plugins.analysis.core.model.AnalysisHistory.JobResultEvaluationMode.*; import static io.jenkins.plugins.analysis.core.model.AnalysisHistory.QualityGateEvaluationMode.*; @@ -48,12 +51,14 @@ class IssuesPublisher { private final QualityGateEvaluationMode qualityGateEvaluationMode; private final JobResultEvaluationMode jobResultEvaluationMode; private final LogHandler logger; + private final QualityGateStatusHandler qualityGateStatusHandler; @SuppressWarnings("ParameterNumber") IssuesPublisher(final Run run, final AnnotatedReport report, final HealthDescriptor healthDescriptor, final QualityGateEvaluator qualityGate, final String name, final String referenceJobName, final boolean ignoreQualityGate, - final boolean ignoreFailedBuilds, final Charset sourceCodeEncoding, final LogHandler logger) { + final boolean ignoreFailedBuilds, final Charset sourceCodeEncoding, final LogHandler logger, + final QualityGateStatusHandler qualityGateStatusHandler) { this.report = report; this.run = run; this.healthDescriptor = healthDescriptor; @@ -64,6 +69,7 @@ class IssuesPublisher { qualityGateEvaluationMode = ignoreQualityGate ? IGNORE_QUALITY_GATE : SUCCESSFUL_QUALITY_GATE; jobResultEvaluationMode = ignoreFailedBuilds ? NO_JOB_FAILURE : IGNORE_JOB_RESULT; this.logger = logger; + this.qualityGateStatusHandler = qualityGateStatusHandler; } private String getId() { @@ -142,7 +148,7 @@ private QualityGateStatus evaluateQualityGate(final Report filtered, final Delta else { filtered.logInfo("-> Some quality gates have been missed: overall result is %s", qualityGateStatus); } - qualityGateStatus.setResult(run); + qualityGateStatusHandler.handleStatus(qualityGateStatus); } else { filtered.logInfo("No quality gates have been set - skipping"); diff --git a/src/main/java/io/jenkins/plugins/analysis/core/steps/IssuesRecorder.java b/src/main/java/io/jenkins/plugins/analysis/core/steps/IssuesRecorder.java index cac059899e..b3fdfe298d 100644 --- a/src/main/java/io/jenkins/plugins/analysis/core/steps/IssuesRecorder.java +++ b/src/main/java/io/jenkins/plugins/analysis/core/steps/IssuesRecorder.java @@ -16,7 +16,7 @@ import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.QueryParameter; -import org.jenkinsci.Symbol; +import org.jenkinsci.plugins.workflow.graph.FlowNode; import hudson.Extension; import hudson.FilePath; import hudson.Launcher; @@ -52,6 +52,7 @@ import io.jenkins.plugins.analysis.core.util.QualityGate.QualityGateResult; import io.jenkins.plugins.analysis.core.util.QualityGate.QualityGateType; import io.jenkins.plugins.analysis.core.util.QualityGateEvaluator; +import io.jenkins.plugins.analysis.core.util.QualityGateStatusHandler; /** * Freestyle or Maven job {@link Recorder} that scans report files or the console log for issues. Stores the created @@ -73,7 +74,7 @@ */ @SuppressWarnings({"PMD.ExcessivePublicCount", "PMD.ExcessiveClassLength", "PMD.ExcessiveImports", "PMD.TooManyFields", "PMD.DataClass", "ClassDataAbstractionCoupling", "ClassFanOutComplexity"}) public class IssuesRecorder extends Recorder implements SimpleBuildStep { - private static final String NO_REFERENCE_JOB = "-"; + static final String NO_REFERENCE_JOB = "-"; private List analysisTools = new ArrayList<>(); @@ -284,7 +285,7 @@ public void setTool(final Tool tool) { analysisTools = Collections.singletonList(tool); } - private void ensureThatToolIsValid(final Tool tool) { + static void ensureThatToolIsValid(final Tool tool) { if (tool == null) { throw new IllegalArgumentException("No valid tool defined! You probably used a symbol in the tools " + "definition that is also a symbol in another plugin. " @@ -504,9 +505,19 @@ public Descriptor getDescriptor() { public void perform(@NonNull final Run run, @NonNull final FilePath workspace, @NonNull final Launcher launcher, @NonNull final TaskListener listener) throws InterruptedException, IOException { + perform(run, workspace, listener, new QualityGateStatusHandler.SetBuildResultStatusHandler(run)); + } + + /** + * Executes the build step. Used from {@link RecordIssuesStep} to provide a {@link QualityGateStatusHandler} + * that has Pipeline-specific behavior. + */ + void perform(@NonNull final Run run, @NonNull final FilePath workspace, + @NonNull final TaskListener listener, @NonNull final QualityGateStatusHandler statusHandler) + throws InterruptedException, IOException { Result overallResult = run.getResult(); if (isEnabledForFailure || overallResult == null || overallResult.isBetterOrEqualTo(Result.UNSTABLE)) { - record(run, workspace, listener); + record(run, workspace, listener, statusHandler); } else { LogHandler logHandler = new LogHandler(listener, createLoggerPrefix()); @@ -518,7 +529,8 @@ private String createLoggerPrefix() { return analysisTools.stream().map(Tool::getActualName).collect(Collectors.joining()); } - private void record(final Run run, final FilePath workspace, final TaskListener listener) + private void record(final Run run, final FilePath workspace, final TaskListener listener, + final QualityGateStatusHandler statusHandler) throws IOException, InterruptedException { for (Tool tool : getTools()) { ensureThatToolIsValid(tool); @@ -529,7 +541,7 @@ private void record(final Run run, final FilePath workspace, final TaskLis totalIssues.add(scanWithTool(run, workspace, listener, tool), tool.getActualId()); } String toolName = StringUtils.defaultIfEmpty(getName(), Messages.Tool_Default_Name()); - publishResult(run, listener, toolName, totalIssues, toolName); + publishResult(run, listener, toolName, totalIssues, toolName, statusHandler); } else { for (Tool tool : analysisTools) { @@ -543,7 +555,7 @@ private void record(final Run run, final FilePath workspace, final TaskLis report.logInfo("Ignoring name='%s' and id='%s' when publishing non-aggregating reports", name, id); } - publishResult(run, listener, tool.getActualName(), report, getReportName(tool)); + publishResult(run, listener, tool.getActualName(), report, getReportName(tool), statusHandler); } } } @@ -605,7 +617,7 @@ private Charset getCharset(final String encoding) { */ @SuppressWarnings("deprecation") void publishResult(final Run run, final TaskListener listener, final String loggerName, - final AnnotatedReport report, final String reportName) { + final AnnotatedReport report, final String reportName, QualityGateStatusHandler statusHandler) { QualityGateEvaluator qualityGate = new QualityGateEvaluator(); if (qualityGates.isEmpty()) { qualityGates.addAll(QualityGate.map(thresholds)); @@ -614,7 +626,7 @@ void publishResult(final Run run, final TaskListener listener, final Strin IssuesPublisher publisher = new IssuesPublisher(run, report, new HealthDescriptor(healthy, unhealthy, minimumSeverity), qualityGate, reportName, referenceJobName, ignoreQualityGate, ignoreFailedBuilds, getSourceCodeCharset(), - new LogHandler(listener, loggerName, report.getReport())); + new LogHandler(listener, loggerName, report.getReport()), statusHandler); publisher.attachAction(); } @@ -1030,7 +1042,6 @@ public int getFailedNewLow() { * Descriptor for this step: defines the context and the UI elements. */ @Extension - @Symbol("recordIssues") @SuppressWarnings("unused") // most methods are used by the corresponding jelly view public static class Descriptor extends BuildStepDescriptor { /** Retain backward compatibility. */ diff --git a/src/main/java/io/jenkins/plugins/analysis/core/steps/PublishIssuesStep.java b/src/main/java/io/jenkins/plugins/analysis/core/steps/PublishIssuesStep.java index fc57605971..82ae884714 100644 --- a/src/main/java/io/jenkins/plugins/analysis/core/steps/PublishIssuesStep.java +++ b/src/main/java/io/jenkins/plugins/analysis/core/steps/PublishIssuesStep.java @@ -15,6 +15,8 @@ import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.QueryParameter; +import org.jenkinsci.plugins.workflow.actions.WarningAction; +import org.jenkinsci.plugins.workflow.graph.FlowNode; import org.jenkinsci.plugins.workflow.steps.Step; import org.jenkinsci.plugins.workflow.steps.StepContext; import org.jenkinsci.plugins.workflow.steps.StepDescriptor; @@ -22,6 +24,7 @@ import hudson.Extension; import hudson.model.Action; import hudson.model.Job; +import hudson.model.Result; import hudson.model.Run; import hudson.model.TaskListener; import hudson.util.ComboBoxModel; @@ -38,6 +41,8 @@ import io.jenkins.plugins.analysis.core.util.QualityGate.QualityGateResult; import io.jenkins.plugins.analysis.core.util.QualityGate.QualityGateType; import io.jenkins.plugins.analysis.core.util.QualityGateEvaluator; +import io.jenkins.plugins.analysis.core.util.QualityGateStatus; +import io.jenkins.plugins.analysis.core.util.QualityGateStatusHandler; /** * Publish issues created by a static analysis build. The recorded issues are stored as a {@link ResultAction} in the @@ -569,10 +574,13 @@ protected ResultAction run() throws IOException, InterruptedException, IllegalSt } report.addAll(reports); + QualityGateStatusHandler statusHandler = new QualityGateStatusHandler.PipelineStatusHandler(getRun(), + getContext().get(FlowNode.class)); IssuesPublisher publisher = new IssuesPublisher(getRun(), report, healthDescriptor, qualityGate, name, referenceJobName, ignoreQualityGate, ignoreFailedBuilds, - getCharset(sourceCodeEncoding), getLogger(report)); - return publisher.attachAction(); + getCharset(sourceCodeEncoding), getLogger(report), statusHandler); + ResultAction resultAction = publisher.attachAction(); + return resultAction; } private LogHandler getLogger(final AnnotatedReport report) throws InterruptedException { @@ -590,7 +598,7 @@ public static class Descriptor extends StepDescriptor { @Override public Set> getRequiredContext() { - return Sets.immutable.of(Run.class, TaskListener.class).castToSet(); + return Sets.immutable.of(FlowNode.class, Run.class, TaskListener.class).castToSet(); } @Override diff --git a/src/main/java/io/jenkins/plugins/analysis/core/steps/RecordIssuesStep.java b/src/main/java/io/jenkins/plugins/analysis/core/steps/RecordIssuesStep.java new file mode 100644 index 0000000000..d653274db8 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/analysis/core/steps/RecordIssuesStep.java @@ -0,0 +1,599 @@ +package io.jenkins.plugins.analysis.core.steps; + +import edu.hm.hafner.analysis.Severity; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; + +import hudson.Extension; +import hudson.FilePath; +import hudson.model.Result; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.util.ComboBoxModel; +import hudson.util.FormValidation; +import hudson.util.ListBoxModel; + +import io.jenkins.plugins.analysis.core.filter.RegexpFilter; +import io.jenkins.plugins.analysis.core.model.AnalysisResult; +import io.jenkins.plugins.analysis.core.model.HealthReportBuilder; +import io.jenkins.plugins.analysis.core.model.ResultAction; +import io.jenkins.plugins.analysis.core.model.StaticAnalysisLabelProvider; +import io.jenkins.plugins.analysis.core.model.Tool; +import io.jenkins.plugins.analysis.core.util.ModelValidation; +import io.jenkins.plugins.analysis.core.util.QualityGate; +import io.jenkins.plugins.analysis.core.util.QualityGateEvaluator; +import io.jenkins.plugins.analysis.core.util.QualityGateStatusHandler; + +import java.io.IOException; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.StringUtils; +import org.eclipse.collections.impl.factory.Sets; +import org.jenkinsci.plugins.workflow.graph.FlowNode; +import org.jenkinsci.plugins.workflow.steps.Step; +import org.jenkinsci.plugins.workflow.steps.StepContext; +import org.jenkinsci.plugins.workflow.steps.StepDescriptor; +import org.jenkinsci.plugins.workflow.steps.StepExecution; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; +import org.kohsuke.stapler.QueryParameter; + +/** + * Pipeline step that scans report files or the console log for issues. Stores the created + * issues in an {@link AnalysisResult}. The result is attached to the {@link Run} by registering a {@link + * ResultAction}. + *

+ * Additional features: + *

    + *
  • It provides a {@link QualityGateEvaluator} that is checked after each run. If the quality gate is not passed, + * then the + * build will be set to {@link Result#UNSTABLE} or {@link Result#FAILURE}, depending on the configuration + * properties.
  • + *
  • It provides thresholds for the build health that could be adjusted in the configuration screen. + * These values are used by the {@link HealthReportBuilder} to compute the health and the health trend graph. + *
  • + *
+ */ +public class RecordIssuesStep extends Step implements Serializable { + private static final long serialVersionUID = 1L; + + private List analysisTools = new ArrayList<>(); + private String sourceCodeEncoding = StringUtils.EMPTY; + private boolean ignoreQualityGate = false; // by default, a successful quality gate is mandatory; + private boolean ignoreFailedBuilds = true; // by default, failed builds are ignored; + private String referenceJobName; + private int healthy; + private int unhealthy; + private Severity minimumSeverity = Severity.WARNING_LOW; + private List filters = new ArrayList<>(); + private boolean isEnabledForFailure; + private boolean isAggregatingResults; + private boolean isBlameDisabled; + private String id; + private String name; + private List qualityGates = new ArrayList<>(); + + /** + * Creates a new instance of {@link RecordIssuesStep}. + */ + @DataBoundConstructor + public RecordIssuesStep() { } + + /** + * Defines the optional list of quality gates. + * + * @param qualityGates + * the quality gates + */ + @SuppressWarnings("unused") // used by Stapler view data binding + @DataBoundSetter + public void setQualityGates(final List qualityGates) { + this.qualityGates = qualityGates; + } + + /** + * Appends the specified quality gates to the end of the list of quality gates. + * + * @param size + * the minimum number of issues that fails the quality gate + * @param type + * the type of the quality gate + * @param result + * determines whether the quality gate is a warning or failure + */ + public void addQualityGate(final int size, final QualityGate.QualityGateType type, final QualityGate.QualityGateResult result) { + qualityGates.add(new QualityGate(size, type, result)); + } + + @SuppressWarnings({"unused", "WeakerAccess"}) // used by Stapler view data binding + public List 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> 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