diff --git a/pom.xml b/pom.xml index b3315612..7bea2dbe 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ browserstack-integration - 1.2.17-SNAPSHOT + 1.2.18-SNAPSHOT hpi BrowserStack diff --git a/src/main/java/com/browserstack/automate/ci/common/BrowserStackEnvVars.java b/src/main/java/com/browserstack/automate/ci/common/BrowserStackEnvVars.java index 9a7b8c4d..ed496640 100644 --- a/src/main/java/com/browserstack/automate/ci/common/BrowserStackEnvVars.java +++ b/src/main/java/com/browserstack/automate/ci/common/BrowserStackEnvVars.java @@ -9,6 +9,8 @@ public interface BrowserStackEnvVars { String BROWSERSTACK_LOCAL_IDENTIFIER = "BROWSERSTACK_LOCAL_IDENTIFIER"; String BROWSERSTACK_BUILD = "BROWSERSTACK_BUILD"; String BROWSERSTACK_BUILD_NAME = "BROWSERSTACK_BUILD_NAME"; + + String BROWSERSTACK_PROJECT_NAME = "BROWSERSTACK_PROJECT_NAME"; String BROWSERSTACK_APP_ID = "BROWSERSTACK_APP_ID"; String BROWSERSTACK_RERUN = "BROWSERSTACK_RERUN"; String BROWSERSTACK_RERUN_TESTS = "BROWSERSTACK_RERUN_TESTS"; diff --git a/src/main/java/com/browserstack/automate/ci/common/constants/Constants.java b/src/main/java/com/browserstack/automate/ci/common/constants/Constants.java index a9f6d90e..6d957d45 100644 --- a/src/main/java/com/browserstack/automate/ci/common/constants/Constants.java +++ b/src/main/java/com/browserstack/automate/ci/common/constants/Constants.java @@ -15,6 +15,18 @@ public class Constants { public static final String BROWSERSTACK_REPORT_PATH_PATTERN = "**/browserstack-artifacts/*"; public static final String JENKINS_CI_PLUGIN = "JenkinsCiPlugin"; + public static final String CAD_BASE_URL = "https://api-observability.browserstack.com/ext"; + public static final String BROWSERSTACK_CONFIG_DETAILS_ENDPOINT = "/v1/builds/buildReport"; + + public static final String INTEGRATIONS_TOOL_KEY = "jenkins"; + + public static final String BROWSERSTACK_TEST_REPORT_URL = "testReportBrowserStack"; + public static final String BROWSERSTACK_CAD_REPORT_DISPLAY_NAME = "BrowserStack Test Report and Insights"; + public static final String BROWSERSTACK_REPORT_FILENAME = "browserstack-report"; + public static final String BROWSERSTACK_REPORT_FOLDER = "browserstack-artifacts"; + + public static final String BROWSERSTACK_REPORT_AUT_PIPELINE_FUNCTION = "browserStackReportAut"; + // Product public static final String AUTOMATE = "automate"; public static final String APP_AUTOMATE = "app-automate"; diff --git a/src/main/java/com/browserstack/automate/ci/jenkins/integrationService/BrowserStackReportStatus.java b/src/main/java/com/browserstack/automate/ci/jenkins/integrationService/BrowserStackReportStatus.java new file mode 100644 index 00000000..ae7d5e20 --- /dev/null +++ b/src/main/java/com/browserstack/automate/ci/jenkins/integrationService/BrowserStackReportStatus.java @@ -0,0 +1,8 @@ +package com.browserstack.automate.ci.jenkins.integrationService; + +public enum BrowserStackReportStatus { + IN_PROGRESS, + COMPLETED, + TEST_AVAILABLE, + NOT_AVAILABLE +} diff --git a/src/main/java/com/browserstack/automate/ci/jenkins/integrationService/BrowserStackTestReportAction.java b/src/main/java/com/browserstack/automate/ci/jenkins/integrationService/BrowserStackTestReportAction.java new file mode 100644 index 00000000..6328e1fa --- /dev/null +++ b/src/main/java/com/browserstack/automate/ci/jenkins/integrationService/BrowserStackTestReportAction.java @@ -0,0 +1,244 @@ +package com.browserstack.automate.ci.jenkins.integrationService; + +import com.browserstack.automate.ci.common.constants.Constants; +import com.browserstack.automate.ci.jenkins.BrowserStackCredentials; +import com.google.gson.Gson; +import hudson.EnvVars; +import hudson.FilePath; +import hudson.model.Action; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.tasks.ArtifactArchiver; +import org.json.JSONObject; +import okhttp3.*; + +import java.io.IOException; +import java.io.PrintStream; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import static com.browserstack.automate.ci.common.logger.PluginLogger.log; +import static com.browserstack.automate.ci.common.logger.PluginLogger.logError; + +public class BrowserStackTestReportAction implements Action { + + private static final String DEFAULT_REPORT_TIMEOUT = "120"; + private static final String SUCCESS_REPORT = "SUCCESS_REPORT"; + private static final String REPORT_IN_PROGRESS = "REPORT_IN_PROGRESS"; + private static final String REPORT_FAILED = "REPORT_FAILED"; + private static final String RETRY_REPORT = "RETRY_REPORT"; + private static final String RATE_LIMIT = "RATE_LIMIT"; + private static final String TEST_AVAILABLE = "TEST_AVAILABLE"; + private static final int MAX_ATTEMPTS = 3; + + private final transient PrintStream logger; + private final RequestsUtil requestsUtil; + private final BrowserStackCredentials credentials; + private final String buildName; + private final String buildCreatedAt; + + private Run run; + private String reportHtml; + private String reportStyle; + private String reportStatus; + private int maxRetryReportAttempt; + + public BrowserStackTestReportAction(Run run, BrowserStackCredentials credentials, String buildName, String buildCreatedAt, final PrintStream logger) { + this.run = run; + this.credentials = credentials; + this.buildName = buildName; + this.buildCreatedAt = buildCreatedAt; + this.logger = logger; + this.requestsUtil = new RequestsUtil(); + this.reportHtml = null; + this.reportStyle = ""; + this.reportStatus = ""; + this.maxRetryReportAttempt = MAX_ATTEMPTS; + } + + public String getReportHtml() { + ensureReportFetched(); + return reportHtml; + } + + public String getReportStyle() { + ensureReportFetched(); + return reportStyle; + } + + private void ensureReportFetched() { + if (!isReportCompletedOrFailed()) { + fetchReport(); + } + } + + private boolean isReportCompletedOrFailed() { + return reportStatus.equals(SUCCESS_REPORT) || reportStatus.equals(REPORT_FAILED); + } + + private void fetchReport() { + Map params = createReportParams(); + String reportUrl = Constants.CAD_BASE_URL + Constants.BROWSERSTACK_CONFIG_DETAILS_ENDPOINT; + + try { + log(logger, "Fetching BrowserStack report..."); + Response response = requestsUtil.makeRequest(reportUrl, credentials, createRequestBody(params)); + handleResponse(response); + } catch (Exception e) { + if(!isReportTestAvailable()) { + handleFetchException(e); + } + } + } + + private Map createReportParams() { + String RquestTypeForJenkins = "POLL"; + Map params = new HashMap<>(); + params.put("buildStartedAt", buildCreatedAt); + params.put("originalBuildName", buildName); + params.put("requestingCi", Constants.INTEGRATIONS_TOOL_KEY); + params.put("reportFormat", Arrays.asList("richHtml", "basicHtml")); + params.put("requestType", RquestTypeForJenkins); + params.put("userTimeout", DEFAULT_REPORT_TIMEOUT); + return params; + } + + private RequestBody createRequestBody(Map params) { + Gson gson = new Gson(); + String json = gson.toJson(params); + return RequestBody.create(MediaType.parse("application/json"), json); + } + + private void handleResponse(Response response) throws Exception { + if (response.isSuccessful()) { + processSuccessfulResponse(response); + } else { + if(!isReportTestAvailable()) { + if (response.code() == 429) { + reportStatus = RATE_LIMIT; + } else { + reportStatus = REPORT_FAILED; + logError(logger, "Non-success response while fetching report: " + response.code()); + } + } + } + } + + private void processSuccessfulResponse(Response response) throws Exception { + assert response.body() != null; + JSONObject reportResponse = new JSONObject(response.body().string()); + String responseReportStatus = reportResponse.optString("reportStatus"); + JSONObject report = reportResponse.optJSONObject("report"); + + switch (responseReportStatus.toUpperCase()) { + case "COMPLETED": + case "NOT_AVAILABLE": + setReportSuccess(report); + break; + case "IN_PROGRESS": + reportStatus = REPORT_IN_PROGRESS; + break; + case "TEST_AVAILABLE": + setReportSuccess(report); + reportStatus = TEST_AVAILABLE; + break; + default: + reportStatus = REPORT_FAILED; + } + } + + private void setReportSuccess(JSONObject report) { + String defaultHTML = "

No Report Found

"; + reportStatus = SUCCESS_REPORT; + reportHtml = report != null ? report.optString("richHtml", defaultHTML) : defaultHTML; + reportStyle = report != null ? report.optString("richCss", "") : ""; + + try { + String basicHtml = report != null ? report.optString("basicHtml", defaultHTML) : defaultHTML; + String fullHtml = " " + basicHtml + ""; + + // Save the HTML content to a file in the workspace + FilePath workspace = new FilePath(run.getRootDir()).getParent(); + ArtifactArchiver artifactArchiver = getArtifactArchiver(workspace, fullHtml); + artifactArchiver.perform(run, workspace, new EnvVars(), null, TaskListener.NULL); + } catch (Exception e) { + logError(logger, "Failed to save or archive report artifact: " + e.getMessage()); + } + } + + private static ArtifactArchiver getArtifactArchiver(FilePath workspace, String fullHtml) throws IOException, InterruptedException { + FilePath artifactsDir = new FilePath(workspace, Constants.BROWSERSTACK_REPORT_FOLDER); + artifactsDir.mkdirs(); + String htmlFileName = Constants.BROWSERSTACK_REPORT_FILENAME + ".html"; + + FilePath htmlFile = new FilePath(artifactsDir, htmlFileName); + + htmlFile.write(fullHtml, "UTF-8"); + String artifactFilePath = Constants.BROWSERSTACK_REPORT_FOLDER + "/" + htmlFileName; + // Archive the file as an artifact + ArtifactArchiver artifactArchiver = new ArtifactArchiver(artifactFilePath); + artifactArchiver.setAllowEmptyArchive(false); + return artifactArchiver; + } + + private void handleFetchException(Exception e) { + reportStatus = RETRY_REPORT; + maxRetryReportAttempt--; + if (maxRetryReportAttempt < 0) { + reportStatus = REPORT_FAILED; + } + logError(logger, "Exception while fetching the report: " + Arrays.toString(e.getStackTrace())); + } + + public boolean isReportInProgress() { + return reportStatus.equals(REPORT_IN_PROGRESS); + } + + public boolean isReportFailed() { + return reportStatus.equals(REPORT_FAILED); + } + + public boolean reportRetryRequired() { + return reportStatus.equals(RETRY_REPORT); + } + + public boolean isUserRateLimited() { + return reportStatus.equals(RATE_LIMIT); + } + + public boolean isReportAvailable() { + return reportStatus.equals(SUCCESS_REPORT) || reportStatus.equals(TEST_AVAILABLE); + } + + public boolean isReportTestAvailable() { + return reportStatus.equals(TEST_AVAILABLE); + } + + public boolean reportHasStatus() { + return !reportStatus.isEmpty() && (reportStatus.equals(REPORT_IN_PROGRESS) || reportStatus.equals(REPORT_FAILED)); + } + + public Run getBuild() { + return run; + } + + public void setBuild(Run build) { + this.run = build; + } + + @Override + public String getIconFileName() { + return Constants.BROWSERSTACK_LOGO; + } + + @Override + public String getDisplayName() { + return Constants.BROWSERSTACK_REPORT_DISPLAY_NAME; + } + + @Override + public String getUrlName() { + return Constants.BROWSERSTACK_TEST_REPORT_URL; + } +} \ No newline at end of file diff --git a/src/main/java/com/browserstack/automate/ci/jenkins/integrationService/BrowserStackTestReportPublisher.java b/src/main/java/com/browserstack/automate/ci/jenkins/integrationService/BrowserStackTestReportPublisher.java new file mode 100644 index 00000000..9fa3ae9a --- /dev/null +++ b/src/main/java/com/browserstack/automate/ci/jenkins/integrationService/BrowserStackTestReportPublisher.java @@ -0,0 +1,101 @@ +package com.browserstack.automate.ci.jenkins.integrationService; + +import com.browserstack.automate.ci.common.BrowserStackEnvVars; +import com.browserstack.automate.ci.common.constants.Constants; +import com.browserstack.automate.ci.jenkins.BrowserStackBuildAction; +import com.browserstack.automate.ci.jenkins.BrowserStackCredentials; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.EnvVars; +import hudson.Extension; +import hudson.model.AbstractProject; +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.FilePath; +import hudson.Launcher; +import jenkins.tasks.SimpleBuildStep; +import org.jenkinsci.Symbol; +import org.kohsuke.stapler.DataBoundConstructor; + +import javax.annotation.CheckForNull; +import java.io.IOException; +import java.io.PrintStream; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Logger; + +import static com.browserstack.automate.ci.common.logger.PluginLogger.log; +import static com.browserstack.automate.ci.common.logger.PluginLogger.logError; + +public class BrowserStackTestReportPublisher extends Recorder implements SimpleBuildStep { + private static final Logger LOGGER = Logger.getLogger(BrowserStackTestReportPublisher.class.getName()); + private Map customEnvVars; + + @DataBoundConstructor + public BrowserStackTestReportPublisher(@CheckForNull String product) { + this.customEnvVars = new ConcurrentHashMap<>(); + } + + @Override + public void perform(Run build, @NonNull FilePath workspace, @NonNull Launcher launcher, TaskListener listener) throws IOException, InterruptedException { + final PrintStream logger = listener.getLogger(); + log(logger, "Adding BrowserStack Report"); + + EnvVars parentEnvs = build.getEnvironment(listener); + parentEnvs.putAll(getCustomEnvVars()); + + String browserStackBuildName = Optional.ofNullable(parentEnvs.get(BrowserStackEnvVars.BROWSERSTACK_BUILD_NAME)) + .orElse(parentEnvs.get(Constants.JENKINS_BUILD_TAG)); + + BrowserStackBuildAction buildAction = build.getAction(BrowserStackBuildAction.class); + if (buildAction == null || buildAction.getBrowserStackCredentials() == null) { + logError(logger, "No BrowserStackBuildAction or credentials found"); + return; + } + + BrowserStackCredentials credentials = buildAction.getBrowserStackCredentials(); + + LOGGER.info("Adding BrowserStack Report Action"); + + + Date buildTimestamp = new Date(build.getStartTimeInMillis()); + + // Format the timestamp (e.g., YYYY-MM-DD HH:MM:SS) + long unixTimestamp = buildTimestamp.getTime() / 1000; + + String buildCreatedAt = String.valueOf(unixTimestamp); + + build.addAction(new BrowserStackTestReportAction(build, credentials, browserStackBuildName,buildCreatedAt, logger)); + + } + + + public Map getCustomEnvVars() { + return customEnvVars; + } + + @Override + public BuildStepMonitor getRequiredMonitorService() { + return BuildStepMonitor.NONE; + } + + @Symbol(Constants.BROWSERSTACK_REPORT_PIPELINE_FUNCTION) + @Extension + public static final class DescriptorImpl extends BuildStepDescriptor { + + @Override + @SuppressWarnings("rawtypes") + public boolean isApplicable(Class aClass) { + // indicates that this builder can be used with all kinds of project types + return true; + } + @Override + public String getDisplayName() { + return Constants.BROWSERSTACK_CAD_REPORT_DISPLAY_NAME; + } + + } +} diff --git a/src/main/java/com/browserstack/automate/ci/jenkins/integrationService/RequestsUtil.java b/src/main/java/com/browserstack/automate/ci/jenkins/integrationService/RequestsUtil.java new file mode 100644 index 00000000..15affc05 --- /dev/null +++ b/src/main/java/com/browserstack/automate/ci/jenkins/integrationService/RequestsUtil.java @@ -0,0 +1,50 @@ +package com.browserstack.automate.ci.jenkins.integrationService; + +import com.browserstack.automate.ci.jenkins.BrowserStackCredentials; +import okhttp3.*; +import org.apache.http.client.utils.URIBuilder; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.Map; + +public class RequestsUtil { + private transient OkHttpClient client; + + + public Response makeRequest(String url, BrowserStackCredentials browserStackCredentials, RequestBody body) throws Exception { + try { + Request request = new Request.Builder() + .url(url) + .header("Authorization", Credentials.basic(browserStackCredentials.getUsername(), browserStackCredentials.getDecryptedAccesskey())) + .post(body) + .build(); + return getClient().newCall(request).execute(); + } catch (IOException e) { + e.printStackTrace(); + throw e; + } + } + + public String buildQueryParams(String url, Map params) throws URISyntaxException { + try { + URIBuilder builder = new URIBuilder(url); + for (String key : params.keySet()) { + builder.addParameter(key, params.get(key)); + } + String fullUrl = builder.build().toString(); + return fullUrl; + } catch (URISyntaxException uriSyntaxException) { + uriSyntaxException.printStackTrace(); + throw uriSyntaxException; + } + } + + private OkHttpClient getClient() { + if (client == null) { + client = new OkHttpClient(); + } + return client; + } +} + diff --git a/src/main/java/com/browserstack/automate/ci/jenkins/pipeline/BrowserStackReportStep.java b/src/main/java/com/browserstack/automate/ci/jenkins/pipeline/BrowserStackReportStep.java index 670e688a..5c88cb6c 100644 --- a/src/main/java/com/browserstack/automate/ci/jenkins/pipeline/BrowserStackReportStep.java +++ b/src/main/java/com/browserstack/automate/ci/jenkins/pipeline/BrowserStackReportStep.java @@ -47,7 +47,7 @@ public Set> getRequiredContext() { @Override public String getFunctionName() { - return Constants.BROWSERSTACK_REPORT_PIPELINE_FUNCTION; + return Constants.BROWSERSTACK_REPORT_AUT_PIPELINE_FUNCTION ; // deprecated Constants.BROWSERSTACK_REPORT_PIPELINE_FUNCTION; } @Override diff --git a/src/main/resources/com/browserstack/automate/ci/jenkins/BrowserStackCypressReportPublisher/config.jelly b/src/main/resources/com/browserstack/automate/ci/jenkins/BrowserStackCypressReportPublisher/config.jelly index ab7b8940..b85648ad 100644 --- a/src/main/resources/com/browserstack/automate/ci/jenkins/BrowserStackCypressReportPublisher/config.jelly +++ b/src/main/resources/com/browserstack/automate/ci/jenkins/BrowserStackCypressReportPublisher/config.jelly @@ -1,4 +1,6 @@ - This step will generate BrowserStack Cypress Test Report by utilising the build name from the environment variable BROWSERSTACK_BUILD_NAME set by the BrowserStack plugin + This step will generate BrowserStack Cypress Test Report by utilising the build name from the environment variable BROWSERSTACK_BUILD_NAME set by the BrowserStack plugin. +
+ ⚠️ Note: This post-build action is deprecated and will no longer be supported in future releases. To continue generating test reports, please use the new BrowserStack Test Report and Insights post-build action.
diff --git a/src/main/resources/com/browserstack/automate/ci/jenkins/BrowserStackReportPublisher/config.jelly b/src/main/resources/com/browserstack/automate/ci/jenkins/BrowserStackReportPublisher/config.jelly index a2a83252..6bd3e9e7 100644 --- a/src/main/resources/com/browserstack/automate/ci/jenkins/BrowserStackReportPublisher/config.jelly +++ b/src/main/resources/com/browserstack/automate/ci/jenkins/BrowserStackReportPublisher/config.jelly @@ -1,4 +1,6 @@ - This step will generate BrowserStack Test Report by utilising the build name from the environment variable BROWSERSTACK_BUILD_NAME set by the BrowserStack plugin + This step will generate BrowserStack Test Report by utilising the build name from the environment variable BROWSERSTACK_BUILD_NAME set by the BrowserStack plugin. +
+ ⚠️ Note: This post-build action is deprecated and will no longer be supported in future releases. To continue generating test reports, please use the new BrowserStack Test Report and Insights post-build action.
diff --git a/src/main/resources/com/browserstack/automate/ci/jenkins/integrationService/BrowserStackTestReportAction/index.jelly b/src/main/resources/com/browserstack/automate/ci/jenkins/integrationService/BrowserStackTestReportAction/index.jelly new file mode 100644 index 00000000..0ea323f4 --- /dev/null +++ b/src/main/resources/com/browserstack/automate/ci/jenkins/integrationService/BrowserStackTestReportAction/index.jelly @@ -0,0 +1,107 @@ + + + + + + + +
+
+ +

BrowserStack Build Test Report generation In Progress, please refresh after sometime

+
+ +

BrowserStack Build Test Report Could Not Be Fetched

+
+

BrowserStack build test report could not be fetched for this build since something went wrong. Please ensure that:

+
+
    +
  • You have set valid BrowserStack credentials via BrowserStack Plugin.
  • +
  • You have used BROWSERSTACK_BUILD_NAME as your build name
  • +
+
+ +

Unable to Fetch Report something went wrong try refreshing...

+
+ +

You have been rate limited, please retry after sometime

+
+
+
+
+ + +
+ +
+
+
+
+
diff --git a/src/main/resources/com/browserstack/automate/ci/jenkins/integrationService/BrowserStackTestReportAction/summary.jelly b/src/main/resources/com/browserstack/automate/ci/jenkins/integrationService/BrowserStackTestReportAction/summary.jelly new file mode 100644 index 00000000..2ff0fbc7 --- /dev/null +++ b/src/main/resources/com/browserstack/automate/ci/jenkins/integrationService/BrowserStackTestReportAction/summary.jelly @@ -0,0 +1,121 @@ + + + + + +
+
+ +
+ BUILD +

${it.build}

+
+

BrowserStack Build Test Report In Progress...

+ +
+ +

No BrowserStack Test Report Available

+
+

BrowserStack test report could not be generated for this build. Please ensure that:

+
+
    +
  • You have set valid BrowserStack credentials via BrowserStack Plugin.
  • +
  • You have used BROWSERSTACK_BUILD_NAME as your build name
  • +
  • try passing BROWSERSTACK_PROJECT_NAME environment variable
  • + +
+
+
+
+
+ +
+
+ BUILD +

${it.build}

+
+ +
+
+
+
diff --git a/src/main/resources/com/browserstack/automate/ci/jenkins/integrationService/BrowserStackTestReportPublisher/config.jelly b/src/main/resources/com/browserstack/automate/ci/jenkins/integrationService/BrowserStackTestReportPublisher/config.jelly new file mode 100644 index 00000000..ffd00905 --- /dev/null +++ b/src/main/resources/com/browserstack/automate/ci/jenkins/integrationService/BrowserStackTestReportPublisher/config.jelly @@ -0,0 +1,4 @@ + + + This step will generate BrowserStack Test Report and Insights by using the build name from the environment variable BROWSERSTACK_BUILD_NAME set by the BrowserStack plugin +