-
Notifications
You must be signed in to change notification settings - Fork 8
Publish TestingBot results as a GitHub check (testingbotChecks) #30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+244
−0
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
197 changes: 197 additions & 0 deletions
197
src/main/java/testingbot/TestingBotChecksPublisher.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,197 @@ | ||
| package testingbot; | ||
|
|
||
| import edu.umd.cs.findbugs.annotations.NonNull; | ||
| import hudson.EnvVars; | ||
| import hudson.Extension; | ||
| import hudson.FilePath; | ||
| import hudson.Launcher; | ||
| import hudson.Util; | ||
| import hudson.model.AbstractProject; | ||
| import hudson.model.Result; | ||
| import hudson.model.Run; | ||
| import hudson.model.TaskListener; | ||
| import hudson.tasks.BuildStepDescriptor; | ||
| import hudson.tasks.BuildStepMonitor; | ||
| import hudson.tasks.Publisher; | ||
| import hudson.tasks.Recorder; | ||
| import hudson.tasks.junit.CaseResult; | ||
| import hudson.tasks.junit.SuiteResult; | ||
| import hudson.tasks.junit.TestResult; | ||
| import hudson.tasks.test.AbstractTestResultAction; | ||
| import io.jenkins.plugins.checks.api.ChecksConclusion; | ||
| import io.jenkins.plugins.checks.api.ChecksDetails; | ||
| import io.jenkins.plugins.checks.api.ChecksOutput; | ||
| import io.jenkins.plugins.checks.api.ChecksPublisherFactory; | ||
| import io.jenkins.plugins.checks.api.ChecksStatus; | ||
| import java.time.Instant; | ||
| import java.time.LocalDateTime; | ||
| import java.time.ZoneOffset; | ||
| import java.util.ArrayList; | ||
| import java.util.LinkedHashSet; | ||
| import java.util.List; | ||
| import java.util.Set; | ||
| import jenkins.tasks.SimpleBuildStep; | ||
| import org.jenkinsci.Symbol; | ||
| import org.jenkinsci.plugins.displayurlapi.DisplayURLProvider; | ||
| import org.kohsuke.stapler.DataBoundConstructor; | ||
| import org.kohsuke.stapler.DataBoundSetter; | ||
|
|
||
| /** | ||
| * Publishes the TestingBot outcome of a build as a GitHub check (✓/✗ on the commit & PR), via the | ||
| * {@code checks-api} plugin. The check <em>name</em> (its context) and <em>summary</em> (message) are | ||
| * configurable — the modern equivalent of the "custom context + message" that Sauce Labs exposes for | ||
| * upstream GitHub Pull Request Builder jobs. | ||
| * | ||
| * <p>Works in freestyle (post-build action) and Pipeline (the {@code testingbotChecks} step). The | ||
| * actual GitHub delivery is done by the {@code github-checks} plugin; when it is not installed the | ||
| * publish is a safe no-op (a {@code NullChecksPublisher}).</p> | ||
| */ | ||
| public class TestingBotChecksPublisher extends Recorder implements SimpleBuildStep { | ||
|
|
||
| private static final String DEFAULT_NAME = "TestingBot"; | ||
|
|
||
| private String name; | ||
| private String message; | ||
|
|
||
| @DataBoundConstructor | ||
| public TestingBotChecksPublisher() { | ||
| } | ||
|
|
||
| /** The check name — the "context" shown on the commit/PR. Defaults to {@code TestingBot}. */ | ||
| public String getName() { | ||
| return Util.fixEmptyAndTrim(name) == null ? DEFAULT_NAME : name; | ||
| } | ||
|
|
||
| @DataBoundSetter | ||
| public void setName(String name) { | ||
| this.name = Util.fixEmptyAndTrim(name); | ||
| } | ||
|
|
||
| /** Optional custom summary/message for the check; a session summary is generated when empty. */ | ||
| public String getMessage() { | ||
| return message; | ||
| } | ||
|
|
||
| @DataBoundSetter | ||
| public void setMessage(String message) { | ||
| this.message = Util.fixEmptyAndTrim(message); | ||
| } | ||
|
|
||
| @Override | ||
| public BuildStepMonitor getRequiredMonitorService() { | ||
| return BuildStepMonitor.NONE; | ||
| } | ||
|
|
||
| @Override | ||
| public void perform(@NonNull Run<?, ?> run, @NonNull FilePath workspace, @NonNull EnvVars env, | ||
| @NonNull Launcher launcher, @NonNull TaskListener listener) { | ||
| int total = 0; | ||
| int passed = 0; | ||
| List<String> rows = new ArrayList<>(); | ||
| Set<String> seenSessions = new LinkedHashSet<>(); | ||
|
|
||
| AbstractTestResultAction<?> testResultAction = run.getAction(AbstractTestResultAction.class); | ||
| if (testResultAction != null && testResultAction.getResult() instanceof TestResult) { | ||
| TestResult testResult = (TestResult) testResultAction.getResult(); | ||
| for (SuiteResult sr : testResult.getSuites()) { | ||
| for (CaseResult cr : sr.getCases()) { | ||
| boolean casePassed = cr.isPassed(); | ||
| for (String sessionId : TestingBotReportFactory.findSessionIDs(cr)) { | ||
| String id = sessionId == null ? "" : sessionId.trim(); | ||
| // Count each distinct TestingBot session once, even if it appears in several | ||
| // cases or a case logs several sessions; cases with no sessions add nothing. | ||
| if (id.isEmpty() || !seenSessions.add(id)) { | ||
| continue; | ||
| } | ||
| total++; | ||
| if (casePassed) { | ||
| passed++; | ||
| } | ||
| rows.add("| " + cr.getFullName() + " | " + (casePassed ? "✅ passed" : "❌ failed") + " |"); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| ChecksConclusion conclusion; | ||
| if (total > 0) { | ||
| conclusion = passed == total ? ChecksConclusion.SUCCESS : ChecksConclusion.FAILURE; | ||
| } else { | ||
| conclusion = fromBuildResult(run.getResult()); | ||
| } | ||
|
|
||
| String summary = getMessage() != null ? getMessage() : defaultSummary(total, passed); | ||
|
|
||
| StringBuilder text = new StringBuilder(); | ||
| if (total > 0) { | ||
| text.append("| Test | Result |\n| --- | --- |\n"); | ||
| for (String row : rows) { | ||
| text.append(row).append('\n'); | ||
| } | ||
| } | ||
| TestingBotBuildReportAction reportAction = run.getAction(TestingBotBuildReportAction.class); | ||
| if (reportAction != null) { | ||
| text.append("\n[View the full TestingBot build report](").append(reportAction.getReportUrl()).append(")\n"); | ||
| } | ||
|
|
||
| ChecksOutput output = new ChecksOutput.ChecksOutputBuilder() | ||
| .withTitle(getName()) | ||
| .withSummary(summary) | ||
| .withText(text.toString()) | ||
| .build(); | ||
|
|
||
| ChecksDetails details = new ChecksDetails.ChecksDetailsBuilder() | ||
| .withName(getName()) | ||
| .withStatus(ChecksStatus.COMPLETED) | ||
| .withConclusion(conclusion) | ||
| .withDetailsURL(detailsUrl(run, reportAction)) | ||
| .withStartedAt(LocalDateTime.ofInstant(Instant.ofEpochMilli(run.getStartTimeInMillis()), ZoneOffset.UTC)) | ||
| .withCompletedAt(LocalDateTime.now(ZoneOffset.UTC)) | ||
| .withOutput(output) | ||
| .build(); | ||
|
|
||
| // A NullChecksPublisher is returned (and this is a no-op) when no GitHub checks implementation | ||
| // is installed, so this is always safe to call. | ||
| ChecksPublisherFactory.fromRun(run, listener).publish(details); | ||
| } | ||
|
|
||
| private static String detailsUrl(Run<?, ?> run, TestingBotBuildReportAction reportAction) { | ||
| if (reportAction != null) { | ||
| return reportAction.getReportUrl(); | ||
| } | ||
| return DisplayURLProvider.get().getRunURL(run); | ||
| } | ||
|
|
||
| private static String defaultSummary(int total, int passed) { | ||
| if (total == 0) { | ||
| return "No TestingBot sessions were found in this build."; | ||
| } | ||
| return passed + " of " + total + " TestingBot session(s) passed."; | ||
| } | ||
|
|
||
| private static ChecksConclusion fromBuildResult(Result result) { | ||
| if (result == null || result == Result.SUCCESS) { | ||
| return ChecksConclusion.SUCCESS; | ||
| } | ||
| if (result == Result.FAILURE || result == Result.UNSTABLE) { | ||
| return ChecksConclusion.FAILURE; | ||
| } | ||
| return ChecksConclusion.NEUTRAL; | ||
| } | ||
|
|
||
| @Extension | ||
| @Symbol("testingbotChecks") | ||
| public static final class DescriptorImpl extends BuildStepDescriptor<Publisher> { | ||
|
|
||
| @NonNull | ||
| @Override | ||
| public String getDisplayName() { | ||
| return "Publish TestingBot results as a GitHub check"; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean isApplicable(Class<? extends AbstractProject> jobType) { | ||
| return true; | ||
| } | ||
| } | ||
| } |
9 changes: 9 additions & 0 deletions
9
src/main/resources/testingbot/TestingBotChecksPublisher/config.jelly
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| <?jelly escape-by-default='true'?> | ||
| <j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form"> | ||
| <f:entry title="Check name (context)" field="name"> | ||
| <f:textbox default="TestingBot"/> | ||
| </f:entry> | ||
| <f:entry title="Summary message" field="message"> | ||
| <f:textbox/> | ||
| </f:entry> | ||
| </j:jelly> |
6 changes: 6 additions & 0 deletions
6
src/main/resources/testingbot/TestingBotChecksPublisher/help-message.html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| <div> | ||
| Optional summary shown on the check. When left empty, a summary is generated from the TestingBot | ||
| sessions in the build (for example, <code>3 of 4 TestingBot session(s) passed.</code>). | ||
| The check links to the embedded TestingBot build report when one is available, otherwise to the | ||
| Jenkins run. | ||
| </div> |
5 changes: 5 additions & 0 deletions
5
src/main/resources/testingbot/TestingBotChecksPublisher/help-name.html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| <div> | ||
| The name of the GitHub check (its <em>context</em>) that appears on the commit and pull request, | ||
| e.g. <code>TestingBot</code>. Use a distinct name if you publish more than one check from a job. | ||
| Defaults to <code>TestingBot</code>. | ||
| </div> |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Use the repository’s required heading style.
markdownlint-cli2reports MD003 here because this section uses an ATX heading while the repository expects setext headings. Convert it to:Proposed fix
📝 Committable suggestion
🧰 Tools
🪛 markdownlint-cli2 (0.23.0)
[warning] 151-151: Heading style
Expected: setext; Actual: atx
(MD003, heading-style)
🤖 Prompt for AI Agents
Source: Linters/SAST tools