-
Notifications
You must be signed in to change notification settings - Fork 8
Restore node-agent support (#29) and GitHub-checks publisher (#30) onto master #32
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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") + " |"); | ||
|
Comment on lines
+98
to
+110
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Aggregate duplicate session outcomes before choosing the conclusion. Lines 101-104 intentionally allow one session to occur in several cases, but the first case wins. If that occurrence passed and a later case for the same session failed, the failure is discarded and this check can incorrectly conclude 🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| 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)) | ||
|
Comment on lines
+132
to
+147
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win Do not publish the TestingBot report auth token to GitHub.
🤖 Prompt for AI Agents |
||
| .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; | ||
| } | ||
| } | ||
| } | ||
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.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Show
testingbotChecksafterjunit.The publisher only reads sessions already attached through
AbstractTestResultAction. Calling this example beforejunitfinds no sessions and can publish a success fallback. Document it afterjunit—typically in analwayspost block—so failed test runs still publish the correct check.🤖 Prompt for AI Agents