-
Notifications
You must be signed in to change notification settings - Fork 8
Add TESTINGBOT_BUILD env var and embedded build-report page #28
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,139 @@ | ||
| package testingbot; | ||
|
|
||
| import hudson.model.Run; | ||
| import java.nio.charset.StandardCharsets; | ||
| import java.security.MessageDigest; | ||
| import java.security.NoSuchAlgorithmException; | ||
| import jenkins.model.RunAction2; | ||
|
|
||
| /** | ||
| * Adds a "TestingBot Build" page to a build that embeds the TestingBot build report (all TestingBot | ||
| * sessions grouped under this build) in an iframe. | ||
| * | ||
| * <p>Tests associate themselves with the build by using the {@code TESTINGBOT_BUILD} environment | ||
| * variable (exposed by the {@code testingbot}/{@code testingbotTunnel} steps and the freestyle build | ||
| * wrapper) as their {@code build} desired capability. The embedded report lives at | ||
| * {@code /mini/builds/<clientKey>/<buildId>} and is authenticated with a plain | ||
| * {@code MD5(clientKey:clientSecret:buildId)} token — computed once here so the raw secret is never | ||
| * stored on the build.</p> | ||
| */ | ||
| public class TestingBotBuildReportAction implements RunAction2 { | ||
|
|
||
| /** | ||
| * Env var holding the build identifier. Tests should pass its value as their {@code build} | ||
| * desired capability so their sessions are grouped under this Jenkins build's report — the | ||
| * TestingBot analog of Sauce Labs' {@code SAUCE_BUILD_NAME}. | ||
| */ | ||
| public static final String TESTINGBOT_BUILD = "TESTINGBOT_BUILD"; | ||
|
|
||
| private transient Run<?, ?> run; | ||
| private final String clientKey; | ||
| private final String buildId; | ||
| private final String authHash; | ||
|
|
||
| private TestingBotBuildReportAction(String clientKey, String buildId, String authHash) { | ||
| this.clientKey = clientKey; | ||
| this.buildId = buildId; | ||
| this.authHash = authHash; | ||
| } | ||
|
|
||
| /** | ||
| * Builds the action for a run, computing the embed auth token from the credentials. | ||
| */ | ||
| public static TestingBotBuildReportAction create(Run<?, ?> run, TestingBotCredentials credentials) { | ||
| String buildId = buildIdFor(run); | ||
| String auth = md5(credentials.getKey() + ":" + credentials.getDecryptedSecret() + ":" + buildId); | ||
| return new TestingBotBuildReportAction(credentials.getKey(), buildId, auth); | ||
| } | ||
|
|
||
| /** | ||
| * The value tests should use as their {@code build} capability to appear in this build's report. | ||
| * Safe both as a capability value and as a URL path segment, and injective: distinct job | ||
| * names never collapse to the same identifier. | ||
| */ | ||
| public static String buildIdFor(Run<?, ?> run) { | ||
| String raw = run.getParent().getFullName() + "_" + run.getNumber(); | ||
| String sanitized = raw.replaceAll("[^A-Za-z0-9._-]", "-"); | ||
| // Sanitizing can map distinct names onto the same string (e.g. "qa smoke" and "qa-smoke"). | ||
| // When characters were actually replaced, append a short deterministic hash of the raw value | ||
| // so the identifier stays unique; already-safe names are returned unchanged for readability. | ||
| if (sanitized.equals(raw)) { | ||
| return sanitized; | ||
| } | ||
| return sanitized + "-" + md5(raw).substring(0, 8); | ||
| } | ||
|
Comment on lines
+54
to
+64
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 Make the build identifier collision-resistant. Line 54 maps distinct job names such as Proposed fix+import java.util.Base64;
...
public static String buildIdFor(Run<?, ?> run) {
- return (run.getParent().getFullName() + "_" + run.getNumber()).replaceAll("[^A-Za-z0-9._-]", "-");
+ String rawId = run.getParent().getFullName() + "_" + run.getNumber();
+ return Base64.getUrlEncoder()
+ .withoutPadding()
+ .encodeToString(rawId.getBytes(StandardCharsets.UTF_8));
}🤖 Prompt for AI Agents |
||
|
|
||
| /** | ||
| * Adds the report action to the run (once) and returns the build identifier to expose as | ||
| * {@link #TESTINGBOT_BUILD}. Callers that only need the identifier — e.g. a run without | ||
| * resolvable credentials — can use {@link #buildIdFor(Run)} directly. | ||
| * | ||
| * <p>Synchronized on the run so that concurrent steps in one build cannot attach duplicate | ||
| * actions. If an action already exists for a different TestingBot account (different client key), | ||
| * it is replaced so the embedded report and its auth token match the credentials now in effect.</p> | ||
| */ | ||
| public static String attach(Run<?, ?> run, TestingBotCredentials credentials) { | ||
| synchronized (run) { | ||
| TestingBotBuildReportAction existing = run.getAction(TestingBotBuildReportAction.class); | ||
| if (existing != null && existing.clientKey.equals(credentials.getKey())) { | ||
| return existing.getBuildId(); | ||
| } | ||
| if (existing != null) { | ||
| run.removeAction(existing); | ||
| } | ||
| TestingBotBuildReportAction created = create(run, credentials); | ||
| run.addAction(created); | ||
| return created.getBuildId(); | ||
| } | ||
| } | ||
|
|
||
| public String getReportUrl() { | ||
| return "https://testingbot.com/mini/builds/" + clientKey + "/" + buildId + "?auth=" + authHash; | ||
| } | ||
|
|
||
| public String getBuildId() { | ||
| return buildId; | ||
| } | ||
|
|
||
| public Run<?, ?> getRun() { | ||
| return run; | ||
| } | ||
|
|
||
| @Override | ||
| public String getIconFileName() { | ||
| return "/plugin/testingbot/images/logo.svg"; | ||
| } | ||
|
|
||
| @Override | ||
| public String getDisplayName() { | ||
| return "TestingBot Build"; | ||
| } | ||
|
|
||
| @Override | ||
| public String getUrlName() { | ||
| return "testingbot-build"; | ||
| } | ||
|
|
||
| @Override | ||
| public void onAttached(Run<?, ?> run) { | ||
| this.run = run; | ||
| } | ||
|
|
||
| @Override | ||
| public void onLoad(Run<?, ?> run) { | ||
| this.run = run; | ||
| } | ||
|
|
||
| private static String md5(String value) { | ||
| try { | ||
| byte[] digest = MessageDigest.getInstance("MD5").digest(value.getBytes(StandardCharsets.UTF_8)); | ||
| StringBuilder sb = new StringBuilder(digest.length * 2); | ||
| for (byte b : digest) { | ||
| sb.append(Character.forDigit((b >> 4) & 0xF, 16)).append(Character.forDigit(b & 0xF, 16)); | ||
| } | ||
| return sb.toString(); | ||
| } catch (NoSuchAlgorithmException e) { | ||
| throw new IllegalStateException("MD5 not available", e); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -37,6 +37,7 @@ | |
| import org.kohsuke.stapler.DataBoundSetter; | ||
| import org.kohsuke.stapler.verb.POST; | ||
| import testingbot.TestingBotBuildAction; | ||
| import testingbot.TestingBotBuildReportAction; | ||
| import testingbot.TestingBotCredentials; | ||
| import testingbot.TunnelManager; | ||
|
|
||
|
|
@@ -235,6 +236,9 @@ public boolean start() throws Exception { | |
|
|
||
| HashMap<String, String> env = new HashMap<>(); | ||
| TunnelManager.populateCredentialEnv(env, tbCredentials); | ||
| // Expose the build id and add the embeddable build-report page so all sessions using | ||
| // build=$TESTINGBOT_BUILD are grouped and viewable inside Jenkins. | ||
| env.put(TestingBotBuildReportAction.TESTINGBOT_BUILD, TestingBotBuildReportAction.attach(run, tbCredentials)); | ||
|
Comment on lines
+239
to
+241
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. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Map the relevant files first.
ast-grep outline src/main/java/testingbot/pipeline/TestingBotStep.java --view expanded || true
ast-grep outline src/main/java/testingbot/pipeline/TestingBotTunnelStep.java --view expanded || true
ast-grep outline src/main/java/testingbot/TestingBotBuildReportAction.java --view expanded || true
# Show the relevant line ranges with line numbers.
printf '\n--- TestingBotStep.java ---\n'
sed -n '1,180p' src/main/java/testingbot/pipeline/TestingBotStep.java | cat -n
printf '\n--- TestingBotTunnelStep.java ---\n'
sed -n '200,280p' src/main/java/testingbot/pipeline/TestingBotTunnelStep.java | cat -n
printf '\n--- TestingBotBuildReportAction.java ---\n'
sed -n '1,260p' src/main/java/testingbot/TestingBotBuildReportAction.java | cat -nRepository: testingbot/TestingBot-Jenkins-Plugin Length of output: 19661 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '\n--- usages of TestingBotBuildReportAction.attach ---\n'
rg -n "TestingBotBuildReportAction\.attach\(" src/main/java || true
printf '\n--- TestingBotBuildAction.java ---\n'
sed -n '1,260p' src/main/java/testingbot/TestingBotBuildAction.java | cat -n
printf '\n--- testingbot credential usages ---\n'
rg -n "getCredentialsId\\(|TestingBotCredentials\\.getCredentials|run\\.addAction\\(new TestingBotBuildAction|TestingBotBuildReportAction" src/main/java/testingbot -g '!**/target/**' || trueRepository: testingbot/TestingBot-Jenkins-Plugin Length of output: 4928 Reject mixed TestingBot credentials on the same run.
📍 Affects 2 files
🤖 Prompt for AI Agents |
||
| env.put(TESTINGBOT_TUNNEL_IDENTIFIER, tunnelIdentifier); | ||
| String hubHost = "localhost"; | ||
| String hubPort = Integer.toString(defaultSeleniumPort()); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| <?jelly escape-by-default='true'?> | ||
| <j:jelly xmlns:j="jelly:core" xmlns:l="/lib/layout" xmlns:st="jelly:stapler"> | ||
| <l:layout title="TestingBot Build"> | ||
| <l:side-panel> | ||
| <st:include page="sidepanel.jelly" it="${it.run}" optional="true" /> | ||
| </l:side-panel> | ||
| <l:main-panel> | ||
| <h1>TestingBot Build</h1> | ||
| <p> | ||
| All TestingBot sessions run under this build (tests using | ||
| <code>build=${it.buildId}</code>, exposed as <code>$TESTINGBOT_BUILD</code>). | ||
| </p> | ||
| <a href="${it.reportUrl}&ref=jenkins" target="_blank">View on TestingBot</a> | ||
| <l:ajax> | ||
| <div> | ||
| <iframe src="${it.reportUrl}&ref=jenkins" title="Embedded TestingBot build report" style="width: 100%; min-height: 100vh; border: none"></iframe> | ||
| </div> | ||
| </l:ajax> | ||
| </l:main-panel> | ||
| </l:layout> | ||
| </j:jelly> |
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 configured setext heading style.
Markdownlint expects a setext heading here.
Proposed fix
📝 Committable suggestion
🧰 Tools
🪛 markdownlint-cli2 (0.23.0)
[warning] 57-57: Heading style
Expected: setext; Actual: atx
(MD003, heading-style)
🤖 Prompt for AI Agents
Source: Linters/SAST tools