Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
0d1b3f8
[maven-release-plugin] prepare release bstack-test
shandli123 Mar 6, 2025
9cad71d
[maven-release-plugin] prepare for next development iteration
shandli123 Mar 6, 2025
4afaf2f
[maven-release-plugin] prepare release browserstack-integration-1.2.18
shandli123 Mar 8, 2025
1f5e919
[maven-release-plugin] prepare for next development iteration
shandli123 Mar 8, 2025
74f637f
[maven-release-plugin] prepare release browserstack-integration-1.2.19
shandli123 Mar 8, 2025
835e379
[maven-release-plugin] prepare for next development iteration
shandli123 Mar 8, 2025
98383b4
[maven-release-plugin] prepare release browserstack-integration-1.2.20
shandli123 Mar 8, 2025
f0f049d
[maven-release-plugin] prepare for next development iteration
shandli123 Mar 8, 2025
4b2f799
intg 1575 adding support for showing bstack report in jenkins
shandli123 Mar 16, 2025
43b637b
intg 1575 rename classes for report
shandli123 Mar 16, 2025
739aa74
intg 1575 edge cases handling
shandli123 Mar 16, 2025
7844fe8
intg 1575 refactoring building query params
shandli123 Mar 19, 2025
6b0360c
updating hosts intg 1575
shandli123 Mar 19, 2025
943c3ab
intg 1575 refactoring under integration service package
shandli123 Mar 19, 2025
598f380
intg 1575 refactoring
shandli123 Mar 19, 2025
ca8bde4
intg 1575 operation name update
shandli123 Mar 20, 2025
035d1ec
intg 1575 adding symbol to invoke action
shandli123 Mar 20, 2025
d097b0b
intg 1575 symbol name change
shandli123 Mar 20, 2025
3f95ab7
intg 1575 demo enhancements
shandli123 Mar 21, 2025
8820fe6
intg 1575 formatting constants
shandli123 Mar 21, 2025
3b71266
intg 1575 format actions
shandli123 Mar 21, 2025
d46db06
intg 1575 demo updates
shandli123 Mar 23, 2025
259e14f
intg 1575 adding report summary on status page
shandli123 Mar 23, 2025
f234211
intg 1575 review comments resolution
shandli123 Mar 24, 2025
19f19ee
intg 1575 updating retry time
shandli123 Mar 24, 2025
ad22c2f
intg 1575 url endpoints constants
shandli123 Mar 24, 2025
5aec015
intg 1575 freestyle job refactor
shandli123 Mar 24, 2025
aa4225f
intg 1575 version update
shandli123 Mar 24, 2025
ee5802c
revamp based on new architecture
shandli123 May 16, 2025
f62cea1
response key change
shandli123 May 16, 2025
273ef30
dev test fixes
shandli123 May 16, 2025
77c599a
url changes
shandli123 May 16, 2025
940c3be
refactoring the code
shandli123 May 16, 2025
9ef21c5
adding requestType param
shandli123 May 19, 2025
0e535c3
contract changes
shandli123 May 21, 2025
1e17a08
dev testing enhancements
shandli123 May 23, 2025
19d18bb
handling report available in case of retry failure
shandli123 May 24, 2025
5091706
adding test available handling if next call fails
shandli123 May 27, 2025
76981eb
replacing old automate report with new Cad reports
shandli123 Jun 24, 2025
162fd1e
updating version
shandli123 Jun 24, 2025
7ec7273
Merge branch 'master' into INTG_1575_JENKINS_REPORT
shandli123 Jun 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
</parent>

<artifactId>browserstack-integration</artifactId>
<version>1.2.17-SNAPSHOT</version>
<version>1.2.18-SNAPSHOT</version>
<packaging>hpi</packaging>

<name>BrowserStack</name>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.browserstack.automate.ci.jenkins.integrationService;

public enum BrowserStackReportStatus {
IN_PROGRESS,
COMPLETED,
TEST_AVAILABLE,
NOT_AVAILABLE
}
Original file line number Diff line number Diff line change
@@ -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<String, Object> 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<String, Object> createReportParams() {
String RquestTypeForJenkins = "POLL";
Map<String, Object> 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<String, Object> 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 = "<h1>No Report Found</h1>";
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 = "<!DOCTYPE html> <html><head> <head>" + basicHtml + "</html>";

// 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;
}
}
Original file line number Diff line number Diff line change
@@ -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<String, String> 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<String, String> getCustomEnvVars() {
return customEnvVars;
}

@Override
public BuildStepMonitor getRequiredMonitorService() {
return BuildStepMonitor.NONE;
}

@Symbol(Constants.BROWSERSTACK_REPORT_PIPELINE_FUNCTION)
@Extension
public static final class DescriptorImpl extends BuildStepDescriptor<Publisher> {

@Override
@SuppressWarnings("rawtypes")
public boolean isApplicable(Class<? extends AbstractProject> 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;
}

}
}
Loading