diff --git a/README.md b/README.md index a95f838..bf54ded 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,12 @@ An example on how to do this: `((RemoteWebDriver) driver).getSessionId().toStrin A full example that you can use is available on our GitHub [Jenkins-Demo](https://github.com/testingbot/Jenkins-Demo) page. +## Embedded TestingBot Build report + +Whenever the plugin injects credentials — through the freestyle **Build Environment** option or the `testingbot { }` / `testingbotTunnel { }` pipeline steps — it exposes a `TESTINGBOT_BUILD` environment variable and adds a **TestingBot Build** page to the build. + +Set your test's `build` desired capability to `$TESTINGBOT_BUILD` so all sessions from a single Jenkins build are grouped under one TestingBot build. The **TestingBot Build** link on the build page then embeds that build's TestingBot report (every session, with status and video) directly inside Jenkins, without leaving the CI UI. + ## Pipeline The plugin offers pipeline support, which can be used with a Jenkinsfile. @@ -75,6 +81,7 @@ Inside **both** `testingbot { }` and `testingbotTunnel { }` blocks: * `TESTINGBOT_KEY` / `TB_KEY` – your TestingBot API key * `TESTINGBOT_SECRET` / `TB_SECRET` – your TestingBot API secret (masked in the build log) +* `TESTINGBOT_BUILD` – a per-build identifier. Pass its value as your test's `build` desired capability so all sessions from this Jenkins build are grouped together and shown on the build's embedded **TestingBot Build** page (see below). Inside a `testingbotTunnel { }` block only (these describe the tunnel started for that block): diff --git a/src/main/java/testingbot/TestingBotBuildReportAction.java b/src/main/java/testingbot/TestingBotBuildReportAction.java new file mode 100644 index 0000000..43ee486 --- /dev/null +++ b/src/main/java/testingbot/TestingBotBuildReportAction.java @@ -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. + * + *
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/
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.
+ */ + 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); + } + } +} diff --git a/src/main/java/testingbot/TestingBotBuildWrapper.java b/src/main/java/testingbot/TestingBotBuildWrapper.java index c419c82..e23a3fb 100644 --- a/src/main/java/testingbot/TestingBotBuildWrapper.java +++ b/src/main/java/testingbot/TestingBotBuildWrapper.java @@ -85,10 +85,14 @@ public Environment setUp(AbstractBuild build, Launcher launcher, BuildListener l build.addAction(action); } + // Expose the build id and add the embeddable build-report page so all sessions using + // build=$TESTINGBOT_BUILD are grouped and viewable inside Jenkins. + final String buildId = credentials != null ? TestingBotBuildReportAction.attach(build, credentials) : null; + if (this.useTunnel) { if (credentials == null) { listener.getLogger().println("No TestingBot key/secret found while trying to start a TestingBot Tunnel"); - return new TestingBotBuildEnvironment(null, null); + return new TestingBotBuildEnvironment(null, null, null); } final App app = new App(); @@ -102,10 +106,10 @@ public Environment setUp(AbstractBuild build, Launcher launcher, BuildListener l TunnelManager.stop(app, listener); throw new IOException("Failed to start TestingBot tunnel", ex); } - return new TestingBotBuildEnvironment(credentials, app); + return new TestingBotBuildEnvironment(credentials, app, buildId); } - return new TestingBotBuildEnvironment(credentials, null); + return new TestingBotBuildEnvironment(credentials, null, buildId); } /** @@ -149,10 +153,12 @@ private class TestingBotBuildEnvironment extends BuildWrapper.Environment { private final App app; private final TestingBotCredentials credentials; + private final String buildId; - public TestingBotBuildEnvironment(TestingBotCredentials credentials, @Nullable App app) { + public TestingBotBuildEnvironment(TestingBotCredentials credentials, @Nullable App app, @Nullable String buildId) { this.credentials = credentials; this.app = app; + this.buildId = buildId; } @Override @@ -161,6 +167,10 @@ public void buildEnvVars(Map
+ All TestingBot sessions run under this build (tests using
+ build=${it.buildId}, exposed as $TESTINGBOT_BUILD).
+