diff --git a/action.yml b/action.yml index 9afdbfa..ab1a8c2 100644 --- a/action.yml +++ b/action.yml @@ -20,7 +20,7 @@ inputs: runs: using: 'docker' - image: 'docker://uhafner/quality-monitor:1.2.0-SNAPSHOT' + image: 'docker://uhafner/quality-monitor:1.2.0' env: CONFIG: ${{ inputs.config }} CHECKS_NAME: ${{ inputs.checks-name }} diff --git a/doc/dependency-graph.puml b/doc/dependency-graph.puml index 62e5683..6fb841b 100644 --- a/doc/dependency-graph.puml +++ b/doc/dependency-graph.puml @@ -49,7 +49,7 @@ rectangle "spotbugs-annotations\n\n4.8.2" as com_github_spotbugs_spotbugs_annota rectangle "error_prone_annotations\n\n2.23.0" as com_google_errorprone_error_prone_annotations_jar rectangle "streamex\n\n0.8.2" as one_util_streamex_jar rectangle "codingstyle\n\n3.30.0" as edu_hm_hafner_codingstyle_jar -rectangle "quality-monitor\n\n1.2.0-SNAPSHOT" as edu_hm_hafner_quality_monitor_jar +rectangle "quality-monitor\n\n1.2.0" as edu_hm_hafner_quality_monitor_jar edu_hm_hafner_analysis_model_jar -[#000000]-> org_jsoup_jsoup_jar org_apache_commons_commons_digester3_jar -[#000000]-> cglib_cglib_jar org_apache_commons_commons_digester3_jar -[#000000]-> commons_logging_commons_logging_jar diff --git a/pom.xml b/pom.xml index 3e32e0b..d2fcede 100644 --- a/pom.xml +++ b/pom.xml @@ -13,7 +13,7 @@ edu.hm.hafner quality-monitor - 1.2.0-SNAPSHOT + 1.2.0 jar @@ -29,16 +29,65 @@ 17 - 3.4.1 + 3.22.0 + 1.319 1.19.6 + + 3.4.1 edu.hm.hafner - autograding-github-action - 3.14.0 + autograding-model + ${autograding-model.version} + + + com.google.errorprone + error_prone_annotations + + + com.github.spotbugs + spotbugs-annotations + + + org.apache.commons + commons-lang3 + + + commons-io + commons-io + + + codingstyle + edu.hm.hafner + + + streamex + one.util + + + + + + org.kohsuke + github-api + ${github-api.version} + + + org.apache.commons + commons-lang3 + + + commons-io + commons-io + + + com.fasterxml.jackson.core + jackson-databind + + diff --git a/src/main/java/edu/hm/hafner/grading/github/GitHubAnnotationsBuilder.java b/src/main/java/edu/hm/hafner/grading/github/GitHubAnnotationsBuilder.java new file mode 100644 index 0000000..ef4c197 --- /dev/null +++ b/src/main/java/edu/hm/hafner/grading/github/GitHubAnnotationsBuilder.java @@ -0,0 +1,48 @@ +package edu.hm.hafner.grading.github; + +import org.apache.commons.lang3.StringUtils; + +import edu.hm.hafner.grading.CommentBuilder; + +import org.kohsuke.github.GHCheckRun.AnnotationLevel; +import org.kohsuke.github.GHCheckRunBuilder.Annotation; +import org.kohsuke.github.GHCheckRunBuilder.Output; + +/** + * Creates GitHub annotations for static analysis warnings, for lines with missing coverage, and for lines with + * survived mutations. + * + * @author Ullrich Hafner + */ +class GitHubAnnotationsBuilder extends CommentBuilder { + private static final String GITHUB_WORKSPACE_REL = "/github/workspace/./"; + private static final String GITHUB_WORKSPACE_ABS = "/github/workspace/"; + + private final Output output; + + GitHubAnnotationsBuilder(final Output output, final String prefix) { + super(prefix, GITHUB_WORKSPACE_REL, GITHUB_WORKSPACE_ABS); + + this.output = output; + } + + @Override + @SuppressWarnings("checkstyle:ParameterNumber") + protected void createComment(final CommentType commentType, final String relativePath, + final int lineStart, final int lineEnd, + final String message, final String title, + final int columnStart, final int columnEnd, + final String details) { + Annotation annotation = new Annotation(relativePath, + lineStart, lineEnd, AnnotationLevel.WARNING, message).withTitle(title); + + if (lineStart == lineEnd) { + annotation.withStartColumn(columnStart).withEndColumn(columnEnd); + } + if (StringUtils.isNotBlank(details)) { + annotation.withRawDetails(details); + } + + output.add(annotation); + } +} diff --git a/src/main/java/edu/hm/hafner/grading/github/QualityMonitor.java b/src/main/java/edu/hm/hafner/grading/github/QualityMonitor.java index 3826e6d..3a1d03a 100644 --- a/src/main/java/edu/hm/hafner/grading/github/QualityMonitor.java +++ b/src/main/java/edu/hm/hafner/grading/github/QualityMonitor.java @@ -1,23 +1,42 @@ package edu.hm.hafner.grading.github; +import java.io.IOException; import java.io.PrintStream; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.time.Instant; +import java.util.Date; +import org.apache.commons.lang3.StringUtils; + +import edu.hm.hafner.grading.AggregatedScore; +import edu.hm.hafner.grading.AutoGradingRunner; +import edu.hm.hafner.grading.GradingReport; +import edu.hm.hafner.util.FilteredLog; import edu.hm.hafner.util.VisibleForTesting; +import org.kohsuke.github.GHCheckRun; +import org.kohsuke.github.GHCheckRun.Conclusion; +import org.kohsuke.github.GHCheckRun.Status; +import org.kohsuke.github.GHCheckRunBuilder; +import org.kohsuke.github.GHCheckRunBuilder.Output; +import org.kohsuke.github.GitHub; +import org.kohsuke.github.GitHubBuilder; + /** * GitHub action entrypoint for the quality monitor action. * * @author Ullrich Hafner */ -public class QualityMonitor extends GitHubAutoGradingRunner { +public class QualityMonitor extends AutoGradingRunner { static final String QUALITY_MONITOR = "Quality Monitor"; /** - * Public entry point for the GitHub action in the docker container, simply calls the action. - * - * @param unused - * not used - */ + * Public entry point for the GitHub action in the docker container, simply calls the action. + * + * @param unused + * not used + */ public static void main(final String... unused) { new QualityMonitor().run(); } @@ -43,4 +62,117 @@ protected String getDisplayName() { protected String getDefaultConfigurationPath() { return "/default-no-score-config.json"; } + + @Override + protected void publishGradingResult(final AggregatedScore score, final FilteredLog log) { + var errors = createErrorMessageMarkdown(log); + + var results = new GradingReport(); + addComment(score, + results.getTextSummary(score, getChecksName()), + results.getMarkdownDetails(score, getChecksName()) + errors, + results.getSubScoreDetails(score) + errors, + results.getMarkdownSummary(score, getChecksName()) + errors, + errors.isBlank() ? Conclusion.SUCCESS : Conclusion.FAILURE, log); + + try { + var environmentVariables = createEnvironmentVariables(score, log); + Files.writeString(Paths.get("metrics.env"), environmentVariables); + } + catch (IOException exception) { + log.logException(exception, "Can't write environment variables to 'metrics.env'"); + } + + log.logInfo("GitHub Action has finished"); + } + + @Override + protected void publishError(final AggregatedScore score, final FilteredLog log, final Throwable exception) { + var results = new GradingReport(); + + var markdownErrors = results.getMarkdownErrors(score, exception); + addComment(score, results.getTextSummary(score, getChecksName()), + markdownErrors, markdownErrors, markdownErrors, Conclusion.FAILURE, log); + } + + private void addComment(final AggregatedScore score, final String textSummary, + final String markdownDetails, final String markdownSummary, final String prSummary, + final Conclusion conclusion, final FilteredLog log) { + try { + var repository = getEnv("GITHUB_REPOSITORY", log); + if (repository.isBlank()) { + log.logError("No GITHUB_REPOSITORY defined - skipping"); + + return; + } + String oAuthToken = getEnv("GITHUB_TOKEN", log); + if (oAuthToken.isBlank()) { + log.logError("No valid GITHUB_TOKEN found - skipping"); + + return; + } + + String sha = getEnv("GITHUB_SHA", log); + + GitHub github = new GitHubBuilder().withAppInstallationToken(oAuthToken).build(); + GHCheckRunBuilder check = github.getRepository(repository) + .createCheckRun(getChecksName(), sha) + .withStatus(Status.COMPLETED) + .withStartedAt(Date.from(Instant.now())) + .withConclusion(conclusion); + + Output output = new Output(textSummary, markdownSummary).withText(markdownDetails); + + if (getEnv("SKIP_ANNOTATIONS", log).isEmpty()) { + var annotationBuilder = new GitHubAnnotationsBuilder(output, computeAbsolutePathPrefixToRemove(log)); + annotationBuilder.createAnnotations(score); + } + + check.add(output); + + GHCheckRun run = check.create(); + log.logInfo("Successfully created check " + run); + + var prNumber = getEnv("PR_NUMBER", log); + if (!prNumber.isBlank()) { // optional PR comment + github.getRepository(repository) + .getPullRequest(Integer.parseInt(prNumber)) + .comment(prSummary + addCheckLink(run)); + log.logInfo("Successfully commented PR#" + prNumber); + } + } + catch (IOException exception) { + log.logException(exception, "Could not create check"); + } + } + + String createEnvironmentVariables(final AggregatedScore score, final FilteredLog log) { + var metrics = new StringBuilder(); + score.getMetrics().forEach((metric, value) -> metrics.append(String.format("%s=%d%n", metric, value))); + log.logInfo("---------------"); + log.logInfo("Metrics Summary"); + log.logInfo("---------------"); + log.logInfo(metrics.toString()); + return metrics.toString(); + } + + private String getChecksName() { + return StringUtils.defaultIfBlank(System.getenv("CHECKS_NAME"), getDisplayName()); + } + + private String computeAbsolutePathPrefixToRemove(final FilteredLog log) { + return String.format("%s/%s/", getEnv("RUNNER_WORKSPACE", log), + StringUtils.substringAfter(getEnv("GITHUB_REPOSITORY", log), "/")); + } + + private String addCheckLink(final GHCheckRun run) { + return String.format("%n%n More details are available in the [GitHub Checks Result](%s).%n", + run.getDetailsUrl().toString()); + } + + private String getEnv(final String key, final FilteredLog log) { + String value = StringUtils.defaultString(System.getenv(key)); + log.logInfo(">>>> " + key + ": " + value); + return value; + } } diff --git a/src/test/java/edu/hm/hafner/grading/github/QualityMonitorDockerITest.java b/src/test/java/edu/hm/hafner/grading/github/QualityMonitorDockerITest.java index 8a8c2f1..94120ad 100644 --- a/src/test/java/edu/hm/hafner/grading/github/QualityMonitorDockerITest.java +++ b/src/test/java/edu/hm/hafner/grading/github/QualityMonitorDockerITest.java @@ -195,7 +195,7 @@ void shouldShowErrors() throws TimeoutException { } private GenericContainer createContainer() { - return new GenericContainer<>(DockerImageName.parse("uhafner/quality-monitor:1.2.0-SNAPSHOT")); + return new GenericContainer<>(DockerImageName.parse("uhafner/quality-monitor:1.2.0")); } private String readStandardOut(final GenericContainer> container) throws TimeoutException {