diff --git a/pom.xml b/pom.xml
index 2754938..719d9b2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -37,8 +37,8 @@
HEAD
-
@@ -53,7 +53,7 @@
-
2.334
8
@@ -62,8 +62,8 @@
0.8
UTF-8
-
UA-79358556-2
@@ -177,6 +177,13 @@
1.24
+
+ org.zeroturnaround
+ zt-zip
+ 1.16
+ jar
+
+
com.google.code.gson
@@ -286,6 +293,7 @@
spring-beans
5.3.18
+
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 92623a8..97240c0 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
@@ -61,4 +61,28 @@ public static final class SessionStatus {
public static final String UNMARKED = "unmarked";
public static final String PASSED = "passed";
}
+
+ public static final class QualityDashboardAPI {
+ public static final String URL_BASE = "https://quality-dashboard.browserstack.com/api/v1/jenkins";
+
+ public static final String LOG_MESSAGE = URL_BASE + "/log-message";
+ public static final String IS_INIT_SETUP_REQUIRED = URL_BASE + "/init-setup-required";
+
+ public static final String HISTORY_FOR_DAYS = URL_BASE + "/history-for-days";
+
+ public static final String SAVE_PIPELINES = URL_BASE + "/save-pipelines";
+
+ public static final String SAVE_PIPELINE_RESULTS = URL_BASE + "/save-pipeline-results";
+
+ public static final String ITEM_CRUD = URL_BASE + "/item";
+ public static final String IS_QD_ENABLED = URL_BASE + "/qd-enabled";
+ public static final String IS_PIPELINE_ENABLED = URL_BASE + "/pipeline-enabled";
+ public static final String GET_RESULT_DIRECTORY = URL_BASE + "/get-result-directory";
+
+ public static final String UPLOAD_RESULT_ZIP = URL_BASE + "/upload-result";
+ public static final String STORE_PIPELINE_RESULTS = URL_BASE + "/save-results";
+
+ public static final String PROJECTS_PAGE_SIZE = URL_BASE + "/projects-page-size";
+ public static final String RESULTS_PAGE_SIZE = URL_BASE + "/results-page-size";
+ }
}
diff --git a/src/main/java/com/browserstack/automate/ci/jenkins/BrowserStackBuildWrapperDescriptor.java b/src/main/java/com/browserstack/automate/ci/jenkins/BrowserStackBuildWrapperDescriptor.java
index aae934f..ad96d32 100644
--- a/src/main/java/com/browserstack/automate/ci/jenkins/BrowserStackBuildWrapperDescriptor.java
+++ b/src/main/java/com/browserstack/automate/ci/jenkins/BrowserStackBuildWrapperDescriptor.java
@@ -3,6 +3,7 @@
import com.browserstack.automate.ci.common.BrowserStackBuildWrapperOperations;
import com.browserstack.automate.ci.common.analytics.Analytics;
import com.browserstack.automate.ci.jenkins.local.LocalConfig;
+import com.browserstack.automate.ci.jenkins.qualityDashboard.QualityDashboardInit;
import hudson.Extension;
import hudson.model.AbstractProject;
import hudson.model.Item;
@@ -51,8 +52,11 @@ public boolean configure(StaplerRequest req, JSONObject formData) throws FormExc
if (config.has("usageStatsEnabled")) {
setEnableUsageStats(config.getBoolean("usageStatsEnabled"));
}
+ if (config.has("credentialsId")){
+ QualityDashboardInit qdInit = new QualityDashboardInit();
+ qdInit.pluginConfiguredNotif();
+ }
}
-
return true;
}
diff --git a/src/main/java/com/browserstack/automate/ci/jenkins/qualityDashboard/QualityDashboardAPIUtil.java b/src/main/java/com/browserstack/automate/ci/jenkins/qualityDashboard/QualityDashboardAPIUtil.java
new file mode 100644
index 0000000..963416e
--- /dev/null
+++ b/src/main/java/com/browserstack/automate/ci/jenkins/qualityDashboard/QualityDashboardAPIUtil.java
@@ -0,0 +1,92 @@
+package com.browserstack.automate.ci.jenkins.qualityDashboard;
+
+import com.browserstack.automate.ci.common.constants.Constants;
+import com.browserstack.automate.ci.jenkins.BrowserStackCredentials;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import okhttp3.*;
+import java.io.IOException;
+import java.io.Serializable;
+
+public class QualityDashboardAPIUtil {
+
+ OkHttpClient client = new OkHttpClient();
+
+ public Response makeGetRequestToQd(String getUrl, BrowserStackCredentials browserStackCredentials) {
+ try {
+ Request request = new Request.Builder()
+ .url(getUrl)
+ .header("Authorization", Credentials.basic(browserStackCredentials.getUsername(), browserStackCredentials.getDecryptedAccesskey()))
+ .build();
+ Response response = client.newCall(request).execute();
+ return response;
+ } catch(IOException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ public Response makePostRequestToQd(String postUrl, BrowserStackCredentials browserStackCredentials, RequestBody requestBody) {
+ try {
+ Request request = new Request.Builder()
+ .url(postUrl)
+ .header("Authorization", Credentials.basic(browserStackCredentials.getUsername(), browserStackCredentials.getDecryptedAccesskey()))
+ .post(requestBody)
+ .build();
+ Response response = client.newCall(request).execute();
+ return response;
+ } catch(IOException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ public Response makePutRequestToQd(String postUrl, BrowserStackCredentials browserStackCredentials, RequestBody requestBody) {
+ try {
+ Request request = new Request.Builder()
+ .url(postUrl)
+ .header("Authorization", Credentials.basic(browserStackCredentials.getUsername(), browserStackCredentials.getDecryptedAccesskey()))
+ .put(requestBody)
+ .build();
+ Response response = client.newCall(request).execute();
+ return response;
+ } catch(IOException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ public Response makeDeleteRequestToQd(String postUrl, BrowserStackCredentials browserStackCredentials, RequestBody requestBody) {
+ try {
+ Request request = new Request.Builder()
+ .url(postUrl)
+ .header("Authorization", Credentials.basic(browserStackCredentials.getUsername(), browserStackCredentials.getDecryptedAccesskey()))
+ .delete(requestBody)
+ .build();
+ Response response = client.newCall(request).execute();
+ return response;
+ } catch(IOException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ public void logToQD(BrowserStackCredentials browserStackCredentials, String logMessage) throws JsonProcessingException {
+ LogMessage logMessageObj = new LogMessage(logMessage);
+ ObjectMapper objectMapper = new ObjectMapper();
+ String jsonBody = objectMapper.writeValueAsString(logMessageObj);
+ RequestBody requestBody = RequestBody.create(MediaType.parse("application/json"), jsonBody);
+ makePostRequestToQd(Constants.QualityDashboardAPI.LOG_MESSAGE, browserStackCredentials, requestBody);
+ }
+}
+
+class LogMessage implements Serializable {
+
+ @JsonProperty("message")
+ private String message;
+
+ public LogMessage(String message) {
+ this.message = message;
+ }
+}
diff --git a/src/main/java/com/browserstack/automate/ci/jenkins/qualityDashboard/QualityDashboardInit.java b/src/main/java/com/browserstack/automate/ci/jenkins/qualityDashboard/QualityDashboardInit.java
new file mode 100644
index 0000000..52da1fe
--- /dev/null
+++ b/src/main/java/com/browserstack/automate/ci/jenkins/qualityDashboard/QualityDashboardInit.java
@@ -0,0 +1,316 @@
+package com.browserstack.automate.ci.jenkins.qualityDashboard;
+
+import com.browserstack.automate.ci.common.constants.Constants;
+import com.browserstack.automate.ci.jenkins.BrowserStackCredentials;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.Lists;
+import hudson.Extension;
+import hudson.init.InitMilestone;
+import hudson.init.Initializer;
+import hudson.model.Result;
+import java.sql.Timestamp;
+import java.time.Instant;
+import jenkins.model.Jenkins;
+import okhttp3.*;
+import org.jenkinsci.plugins.workflow.job.WorkflowJob;
+import org.jenkinsci.plugins.workflow.job.WorkflowRun;
+import java.time.temporal.ChronoUnit;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.util.ArrayList;
+import java.util.List;
+
+@Extension
+public class QualityDashboardInit {
+
+ static QualityDashboardAPIUtil apiUtil = new QualityDashboardAPIUtil();
+
+ @Initializer(after = InitMilestone.PLUGINS_PREPARED)
+ public static void postInstall() {
+ try {
+ initQDSetupIfRequired();
+ } catch(Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void pluginConfiguredNotif() {
+ try {
+ initQDSetupIfRequired();
+ } catch(Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ private static String exceptionToString(Throwable throwable) {
+ return throwable.toString();
+ }
+
+ private static void initQDSetupIfRequired() throws JsonProcessingException {
+ BrowserStackCredentials browserStackCredentials = QualityDashboardUtil.getBrowserStackCreds();
+ try {
+ if(browserStackCredentials!=null) {
+ apiUtil.logToQD(browserStackCredentials,"Starting plugin data export to QD");
+ checkQDIntegrationAndDumpMetaData(browserStackCredentials);
+ }
+ } catch (Exception e) {
+ try {
+ apiUtil.logToQD(browserStackCredentials, "Global exception in data export is:");
+ } catch (Exception ex) {
+ String exceptionString = exceptionToString(ex);
+ apiUtil.logToQD(browserStackCredentials, "Global exception in exception data export is:" + exceptionString);
+ }
+
+ }
+
+ }
+
+ private static void checkQDIntegrationAndDumpMetaData(BrowserStackCredentials browserStackCredentials) throws JsonProcessingException {
+ if(initialQDSetupRequired(browserStackCredentials)) {
+ List allPipelines = getAllPipelines(browserStackCredentials);
+ if(!allPipelines.isEmpty()){
+ boolean projectsSavedSuccessfully = sendPipelinesPaginated(browserStackCredentials, allPipelines);
+ if(projectsSavedSuccessfully) {
+ List allBuilds = getAllBuilds(browserStackCredentials);
+ if(!allBuilds.isEmpty()){
+ sendBuildsPaginated(browserStackCredentials, allBuilds);
+ } else {
+ apiUtil.logToQD(browserStackCredentials,"No Build Results data found");
+ }
+ } else {
+ apiUtil.logToQD(browserStackCredentials,"Projects import failed, so not importing build results");
+ }
+ } else {
+ apiUtil.logToQD(browserStackCredentials,"No pipelines detected");
+ }
+ }
+ }
+
+ private static boolean initialQDSetupRequired(BrowserStackCredentials browserStackCredentials) throws JsonProcessingException {
+ try {
+ Response response = apiUtil.makeGetRequestToQd(Constants.QualityDashboardAPI.IS_INIT_SETUP_REQUIRED, browserStackCredentials);
+ if (response != null && response.code() == HttpURLConnection.HTTP_OK) {
+ ResponseBody responseBody = response.body();
+ if(responseBody != null && responseBody.string().equals("REQUIRED")) {
+ apiUtil.logToQD(browserStackCredentials,"Initial QD setup is required");
+ return true;
+ }
+ }
+ } catch(IOException e) {
+ e.printStackTrace();
+ }
+ apiUtil.logToQD(browserStackCredentials,"Initial QD setup is not required");
+ return false;
+ }
+
+ private static List getAllPipelines(BrowserStackCredentials browserStackCredentials) throws JsonProcessingException {
+ List allPipelines = new ArrayList<>();
+ Jenkins jenkins = Jenkins.getInstanceOrNull();
+ if (jenkins != null) {
+ jenkins.getAllItems().forEach(job -> {
+ if(job instanceof WorkflowJob) {
+ String pipelineName = job.getFullName();
+ allPipelines.add(pipelineName);
+ }
+ });
+ } else {
+ apiUtil.logToQD(browserStackCredentials,"Issue getting Jenkins Instance");
+ }
+ return allPipelines;
+ }
+
+ private static boolean sendPipelinesPaginated(BrowserStackCredentials browserStackCredentials, List allPipelines) {
+ boolean isSuccess = true;
+ int pageSize = getProjectPageSize(browserStackCredentials);
+ List> pipelinesInSmallerBatches = Lists.partition(allPipelines, pageSize);
+ int totalPages = !pipelinesInSmallerBatches.isEmpty() ? pipelinesInSmallerBatches.size() : 0;
+ int page = 0;
+ for(List singlePagePipelineList : pipelinesInSmallerBatches) {
+ try {
+ page++;
+ ObjectMapper objectMapper = new ObjectMapper();
+ PipelinesPaginated pipelinesPaginated = new PipelinesPaginated(page, totalPages, singlePagePipelineList);
+ String jsonBody = objectMapper.writeValueAsString(pipelinesPaginated);
+ RequestBody requestBody = RequestBody.create(MediaType.parse("application/json"), jsonBody);
+ Response response = apiUtil.makePostRequestToQd(Constants.QualityDashboardAPI.SAVE_PIPELINES, browserStackCredentials, requestBody);
+ if (response == null || response.code() != HttpURLConnection.HTTP_OK) {
+ apiUtil.logToQD(browserStackCredentials,"Got Non 200 response while saving projects");
+ isSuccess = false;
+ break;
+ }
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return isSuccess;
+ }
+
+ private static List getAllBuilds(BrowserStackCredentials browserStackCredentials) {
+ List allBuildResults = new ArrayList<>();
+ Jenkins jenkins = Jenkins.getInstanceOrNull();
+ Instant thresholdInstant = Instant.now().minus(getHistoryForDays(browserStackCredentials), ChronoUnit.DAYS);
+ if (jenkins != null) {
+ jenkins.getAllItems().forEach(job -> {
+ if (job instanceof WorkflowJob) {
+ String pipelineName = job.getFullName();
+ List allBuilds = ((WorkflowJob) job).getBuilds();
+ if(!allBuilds.isEmpty()) {
+ allBuilds.stream().filter(build -> Instant.ofEpochMilli(build.getTimeInMillis()).isAfter(thresholdInstant)).forEach(
+ build -> {
+ int buildNumber = build.getNumber();
+ long duration = build.getDuration();
+ Result overallResult = build.getResult();
+ long endTimeInMillis = build.getTimeInMillis();
+ Timestamp endTime = new Timestamp(endTimeInMillis);
+ String result = overallResult != null ? overallResult.toString() : null;
+ PipelineDetails pipelineDetail = new PipelineDetails(pipelineName, buildNumber, duration, result, endTime);
+ allBuildResults.add(pipelineDetail);
+ }
+ );
+ }
+ }
+ });
+ }
+ return allBuildResults;
+ }
+
+ private static void sendBuildsPaginated(BrowserStackCredentials browserStackCredentials, List allBuilds) {
+ int pageSize = getResultPageSize(browserStackCredentials);
+ List> buildResultsInSmallerBatches = Lists.partition(allBuilds, pageSize);
+ int totalPages = !buildResultsInSmallerBatches.isEmpty() ? buildResultsInSmallerBatches.size() : 0;
+ int page = 0;
+ for(List buildResultList : buildResultsInSmallerBatches) {
+ try {
+ page++;
+ ObjectMapper objectMapper = new ObjectMapper();
+ BuildResultsPaginated buildResultsPaginated = new BuildResultsPaginated(page, totalPages, buildResultList);
+ String jsonBody = objectMapper.writeValueAsString(buildResultsPaginated);
+ RequestBody requestBody = RequestBody.create(MediaType.parse("application/json"), jsonBody);
+ Response response = apiUtil.makePostRequestToQd(Constants.QualityDashboardAPI.SAVE_PIPELINE_RESULTS, browserStackCredentials, requestBody);
+ if (response == null || response.code() != HttpURLConnection.HTTP_OK) {
+ apiUtil.logToQD(browserStackCredentials,"Got Non 200 response while saving projects");
+ break;
+ }
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ private static int getHistoryForDays(BrowserStackCredentials browserStackCredentials) {
+ int no_of_days = 90;
+ try {
+ Response response = apiUtil.makeGetRequestToQd(Constants.QualityDashboardAPI.HISTORY_FOR_DAYS, browserStackCredentials);
+ if (response != null && response.code() == HttpURLConnection.HTTP_OK) {
+ ResponseBody responseBody = response.body();
+ if(responseBody != null) {
+ String responseBodyStr = responseBody.string();
+ if(responseBodyStr!=null)
+ no_of_days = Integer.parseInt(responseBodyStr);
+ }
+ }
+ } catch(IOException e) {
+ e.printStackTrace();
+ } finally {
+ return no_of_days;
+ }
+ }
+
+ private static int getProjectPageSize(BrowserStackCredentials browserStackCredentials) {
+ int projectPageSize = 2000;
+ try {
+ Response response = apiUtil.makeGetRequestToQd(Constants.QualityDashboardAPI.PROJECTS_PAGE_SIZE, browserStackCredentials);
+ if (response != null && response.code() == HttpURLConnection.HTTP_OK) {
+ ResponseBody responseBody = response.body();
+ if(responseBody != null) {
+ String responseBodyStr = responseBody.string();
+ if(responseBodyStr!=null)
+ projectPageSize = Integer.parseInt(responseBodyStr);
+ }
+ }
+ } catch(IOException e) {
+ e.printStackTrace();
+ } finally {
+ return projectPageSize;
+ }
+ }
+
+ private static int getResultPageSize(BrowserStackCredentials browserStackCredentials) {
+ int resultPageSize = 1000;
+ try {
+ Response response = apiUtil.makeGetRequestToQd(Constants.QualityDashboardAPI.RESULTS_PAGE_SIZE, browserStackCredentials);
+ if (response != null && response.code() == HttpURLConnection.HTTP_OK) {
+ ResponseBody responseBody = response.body();
+ if(responseBody != null) {
+ String responseBodyStr = responseBody.string();
+ if(responseBodyStr!=null)
+ resultPageSize = Integer.parseInt(responseBodyStr);
+ }
+ }
+ } catch(IOException e) {
+ e.printStackTrace();
+ } finally {
+ return resultPageSize;
+ }
+ }
+}
+
+class PipelineDetails {
+
+ @JsonProperty("pipelineName")
+ private String pipelineName;
+
+ @JsonProperty("buildNumber")
+ private Integer buildNumber;
+ @JsonProperty("buildDuration")
+ private Long buildDuration;
+ @JsonProperty("buildStatus")
+ private String buildStatus;
+
+ @JsonProperty("endTime")
+ private Timestamp endTime;
+
+ public PipelineDetails(String pipelineName, Integer buildNumber, Long buildDuration, String buildStatus, Timestamp endTime) {
+ this.pipelineName = pipelineName;
+ this.buildNumber = buildNumber;
+ this.buildDuration = buildDuration;
+ this.buildStatus = buildStatus;
+ this.endTime = endTime;
+ }
+}
+
+class PipelinesPaginated {
+ @JsonProperty("page")
+ private int page;
+
+ @JsonProperty("totalPages")
+ private int totalPages;
+
+ @JsonProperty("pipelines")
+ private List pipelines;
+
+ public PipelinesPaginated(int page, int totalPages, List pipelines) {
+ this.page = page;
+ this.totalPages = totalPages;
+ this.pipelines = pipelines;
+ }
+}
+
+class BuildResultsPaginated {
+ @JsonProperty("page")
+ private int page;
+
+ @JsonProperty("totalPages")
+ private int totalPages;
+
+ @JsonProperty("builds")
+ private List builds;
+
+ public BuildResultsPaginated(int page, int totalPages, List builds) {
+ this.page = page;
+ this.totalPages = totalPages;
+ this.builds = builds;
+ }
+}
diff --git a/src/main/java/com/browserstack/automate/ci/jenkins/qualityDashboard/QualityDashboardInitItemListener.java b/src/main/java/com/browserstack/automate/ci/jenkins/qualityDashboard/QualityDashboardInitItemListener.java
new file mode 100644
index 0000000..86bec77
--- /dev/null
+++ b/src/main/java/com/browserstack/automate/ci/jenkins/qualityDashboard/QualityDashboardInitItemListener.java
@@ -0,0 +1,125 @@
+package com.browserstack.automate.ci.jenkins.qualityDashboard;
+
+import com.browserstack.automate.ci.common.constants.Constants;
+import com.browserstack.automate.ci.jenkins.BrowserStackCredentials;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import hudson.Extension;
+import hudson.model.Item;
+import hudson.model.listeners.ItemListener;
+import okhttp3.MediaType;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import org.jenkinsci.plugins.workflow.job.WorkflowJob;
+import java.io.IOException;
+import java.io.Serializable;
+
+@Extension
+public class QualityDashboardInitItemListener extends ItemListener {
+
+ @Override
+ public void onCreated(Item job) {
+ String itemName = job.getFullName();
+ String itemType = getItemTypeModified(job);
+ if(itemType != null && itemType.equals("PIPELINE")) {
+ try {
+ String jsonBody = getJsonReqBody(new ItemUpdate(itemName, itemType));
+ syncItemListToQD(jsonBody, Constants.QualityDashboardAPI.ITEM_CRUD, "POST");
+ } catch(IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ @Override
+ public void onDeleted(Item job) {
+ String itemName = job.getFullName();
+ String itemType = getItemTypeModified(job);
+ if(itemType != null) {
+ try {
+ String jsonBody = getJsonReqBody(new ItemUpdate(itemName, itemType));
+ syncItemListToQD(jsonBody, Constants.QualityDashboardAPI.ITEM_CRUD, "DELETE");
+ } catch(IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ @Override
+ public void onRenamed(Item job, String oldName, String newName) {
+ String itemType = getItemTypeModified(job);
+ if(itemType != null) {
+ try {
+ oldName = job.getParent().getFullName() + "/" + oldName;
+ newName = job.getParent().getFullName() + "/" + newName;
+ String jsonBody = getJsonReqBody(new ItemRename(oldName, newName, itemType));
+ syncItemListToQD(jsonBody, Constants.QualityDashboardAPI.ITEM_CRUD, "PUT");
+ } catch(IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ private String getItemTypeModified(Item job) {
+ String itemType = null;
+ boolean isFolderRenamed = job.getClass().getName().contains("Folder");
+ boolean isPipelineRenamed = job instanceof WorkflowJob;
+ if(isFolderRenamed || isPipelineRenamed) {
+ itemType = isPipelineRenamed ? "PIPELINE" : "FOLDER";
+ }
+ return itemType;
+ }
+
+ private String getJsonReqBody( T item) throws JsonProcessingException {
+ ObjectMapper objectMapper = new ObjectMapper();
+ String jsonBody = objectMapper.writeValueAsString(item);
+ return jsonBody;
+ }
+
+ private Response syncItemListToQD(String jsonBody, String url, String typeOfRequest) throws JsonProcessingException {
+ RequestBody requestBody = RequestBody.create(MediaType.parse("application/json"), jsonBody);
+ QualityDashboardAPIUtil apiUtil = new QualityDashboardAPIUtil();
+ BrowserStackCredentials browserStackCredentials = QualityDashboardUtil.getBrowserStackCreds();
+ if(typeOfRequest.equals("PUT")) {
+ apiUtil.logToQD(browserStackCredentials, "Syncing Item Update - PUT");
+ return apiUtil.makePutRequestToQd(url, browserStackCredentials, requestBody);
+ } else if(typeOfRequest.equals("DELETE")) {
+ apiUtil.logToQD(browserStackCredentials, "Syncing Item Deleted - DELETE");
+ return apiUtil.makeDeleteRequestToQd(url, browserStackCredentials, requestBody);
+ } else {
+ apiUtil.logToQD(browserStackCredentials, "Syncing Item Added - POST");
+ return apiUtil.makePostRequestToQd(url, browserStackCredentials, requestBody);
+ }
+ }
+}
+
+class ItemUpdate implements Serializable {
+ @JsonProperty("item")
+ private String itemName;
+
+ @JsonProperty("itemType")
+ private String itemType;
+
+ public ItemUpdate(String itemName, String itemType) {
+ this.itemName = itemName;
+ this.itemType = itemType;
+ }
+}
+
+class ItemRename implements Serializable {
+ @JsonProperty("fromName")
+ private String fromItemName;
+
+ @JsonProperty("toName")
+ private String toItemName;
+
+ @JsonProperty("itemType")
+ private String itemType;
+
+ public ItemRename(String fromItemName, String toItemName, String itemType) {
+ this.fromItemName = fromItemName;
+ this.toItemName = toItemName;
+ this.itemType = itemType;
+ }
+}
diff --git a/src/main/java/com/browserstack/automate/ci/jenkins/qualityDashboard/QualityDashboardPipelineTracker.java b/src/main/java/com/browserstack/automate/ci/jenkins/qualityDashboard/QualityDashboardPipelineTracker.java
new file mode 100644
index 0000000..fb7d80e
--- /dev/null
+++ b/src/main/java/com/browserstack/automate/ci/jenkins/qualityDashboard/QualityDashboardPipelineTracker.java
@@ -0,0 +1,311 @@
+package com.browserstack.automate.ci.jenkins.qualityDashboard;
+
+import com.browserstack.automate.ci.common.constants.Constants;
+import com.browserstack.automate.ci.jenkins.BrowserStackCredentials;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import hudson.Extension;
+import hudson.model.*;
+import hudson.model.listeners.RunListener;
+import io.jenkins.cli.shaded.org.apache.commons.lang.StringUtils;
+import jenkins.model.Jenkins;
+import okhttp3.*;
+import org.apache.commons.io.FileUtils;
+import org.jenkinsci.plugins.workflow.job.WorkflowJob;
+import org.jenkinsci.plugins.workflow.job.WorkflowRun;
+import org.zeroturnaround.zip.ZipUtil;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.Serializable;
+import java.net.HttpURLConnection;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.sql.Timestamp;
+import java.util.List;
+
+@Extension
+public class QualityDashboardPipelineTracker extends RunListener {
+
+ QualityDashboardAPIUtil apiUtil = new QualityDashboardAPIUtil();
+
+ @Override
+ public void onCompleted(Run run, TaskListener listener) {
+ super.onCompleted(run, listener);
+ BrowserStackCredentials browserStackCredentials = QualityDashboardUtil.getBrowserStackCreds();
+ if(browserStackCredentials!=null) {
+ WorkflowRun workflowRun = (WorkflowRun) run;
+ WorkflowJob workflowJob = workflowRun.getParent();
+ String jobName = workflowJob.getFullName();
+ int buildNumber = run.getNumber();
+ try {
+ if(isQDEnabled(browserStackCredentials) && isPipelineEnabledForQD(browserStackCredentials, jobName)) {
+ Result overallResult = run.getResult();
+ if(overallResult != null) {
+ String qdS3Url = null;
+ String finalPathToZip = getFinalZipPath(run, browserStackCredentials);
+ apiUtil.logToQD(browserStackCredentials, "Final Computed Zip Path for jobName: " + jobName + " and buildNumber: " + buildNumber + " is: " + finalPathToZip);
+ if(StringUtils.isNotEmpty(finalPathToZip)) {
+ apiUtil.logToQD(browserStackCredentials, "Found artifacts in configured path for jobName: " + jobName + " and buildNumber: " + buildNumber);
+ copyDirectoryToParentIfRequired(run, finalPathToZip, browserStackCredentials);
+ qdS3Url = zipArtifactsAndUploadToQD(finalPathToZip, browserStackCredentials, jobName, buildNumber);
+ } else if(run.getHasArtifacts()) {
+ apiUtil.logToQD(browserStackCredentials, "No artifacts in configured path but found archive artifacts for jobName: " + jobName + " and buildNumber: " + buildNumber);
+ finalPathToZip = run.getArtifactsDir().getAbsolutePath();
+ apiUtil.logToQD(browserStackCredentials, "Got artifact path for jobName: " + jobName + " and buildNumber: " + buildNumber + " as: " + finalPathToZip);
+ qdS3Url = zipArtifactsAndUploadToQD(finalPathToZip, browserStackCredentials, jobName, buildNumber);
+ } else {
+ apiUtil.logToQD(browserStackCredentials, "Finally no artifacts found for jobName: " + jobName + " and buildNumber: " + buildNumber);
+ }
+ sendBuildDataToQD(run, overallResult, qdS3Url, browserStackCredentials);
+ } else {
+ apiUtil.logToQD(browserStackCredentials, "Null Result Captured for jobName: " + jobName + " and buildNumber: " + buildNumber);
+ }
+ }
+ } catch (IOException e) {
+ try {
+ apiUtil.logToQD(browserStackCredentials, "Global Exception for jobName: " + jobName + " and buildNumber: " + buildNumber + " is: " + e.toString());
+ } catch (JsonProcessingException ex) {
+ throw new RuntimeException(ex);
+ }
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ private String zipArtifactsAndUploadToQD (String finalPathToZip, BrowserStackCredentials browserStackCredentials, String jobName, int buildNumber) throws IOException {
+ String finalZipFilePath = packZip(finalPathToZip, jobName, browserStackCredentials);
+ apiUtil.logToQD(browserStackCredentials, "Final zip file's path for jobName: " + jobName + " and buildNumber: " + buildNumber + " is:" + finalZipFilePath);
+ String qdS3Url = uploadZipToQd(finalZipFilePath, browserStackCredentials, jobName, buildNumber);
+ if(StringUtils.isNotEmpty(finalZipFilePath)) {
+ Files.deleteIfExists(Paths.get(finalZipFilePath));
+ apiUtil.logToQD(browserStackCredentials, "Deleted file from server after upload for jobName: " + jobName + " and buildNumber: " + buildNumber);
+ } else {
+ apiUtil.logToQD(browserStackCredentials, "No zip file to delete for jobName: " + jobName + " and buildNumber: " + buildNumber);
+ }
+ return qdS3Url;
+ }
+
+ private void sendBuildDataToQD(Run run, Result overallResult, String finalZipPath, BrowserStackCredentials browserStackCredentials) {
+ Long pipelineDuration = getPipelineDuration(run);
+ try {
+ String jobName = run.getParent().getFullName();
+ int buildNumber = run.getNumber();
+ long endTimeInMillis = run.getTimeInMillis();
+ Timestamp endTime = new Timestamp(endTimeInMillis);
+ PipelineResults pipelineResultsReqObj = new PipelineResults(buildNumber, pipelineDuration, overallResult.toString(), finalZipPath, jobName, endTime);
+ ObjectMapper objectMapper = new ObjectMapper();
+ String jsonBody = objectMapper.writeValueAsString(pipelineResultsReqObj);
+
+ RequestBody requestBody = RequestBody.create(MediaType.parse("application/json"), jsonBody);
+ apiUtil.logToQD(browserStackCredentials, "Sending Final Results for jobName: " + jobName + " and buildNumber: " + buildNumber);
+ apiUtil.makePostRequestToQd(Constants.QualityDashboardAPI.STORE_PIPELINE_RESULTS, browserStackCredentials, requestBody);
+ } catch(IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private Long getPipelineDuration(Run build) {
+ long startTime = build.getStartTimeInMillis();
+ long endTime = System.currentTimeMillis();
+ long duration = (endTime - startTime) / 1000;
+ return duration;
+ }
+
+ private boolean checkIfPathIsFound(String filePath) {
+ Path path = Paths.get(filePath);
+ return Files.exists(path) ? true : false;
+ }
+ private String getFinalZipPath(Run run, BrowserStackCredentials browserStackCredentials) throws JsonProcessingException {
+ String finalZipPath = null;
+ String currentResultDir = getResultDirForPipeline(getUrlForPipeline(run), browserStackCredentials, run.getNumber());
+ if(StringUtils.isNotEmpty(currentResultDir) && checkIfPathIsFound(currentResultDir)) {
+ finalZipPath = currentResultDir;
+ } else {
+ String defaultWorkspaceDir = getDefaultWorkspaceDirectory(run);
+ if(StringUtils.isNotEmpty(defaultWorkspaceDir)) {
+ String jobName = run.getParent().getName();
+ defaultWorkspaceDir = defaultWorkspaceDir + "/workspace/" + jobName + "/browserstack-artifacts";
+ finalZipPath = checkIfPathIsFound(defaultWorkspaceDir) ? defaultWorkspaceDir : null;
+ }
+ }
+ return finalZipPath;
+ }
+
+ private String getDefaultWorkspaceDirectory(Run run) {
+ Jenkins jenkins = Jenkins.getInstanceOrNull();
+ String workspacePath = jenkins != null && jenkins.getRootDir() != null ? jenkins.getRootDir().getAbsolutePath() : null;
+ return StringUtils.isNotEmpty(workspacePath) ? workspacePath : null;
+ }
+
+ private String getUrlForPipeline(Run, ?> build) {
+ return build.getParent().getFullName();
+ }
+
+ private boolean isQDEnabled(BrowserStackCredentials browserStackCredentials) throws IOException {
+ Response response = apiUtil.makeGetRequestToQd(Constants.QualityDashboardAPI.IS_QD_ENABLED, browserStackCredentials);
+ if (response != null && response.code() == HttpURLConnection.HTTP_OK) {
+ ResponseBody responseBody = response.body();
+ if(responseBody != null && Boolean.parseBoolean(response.body().string())) {
+ apiUtil.logToQD(browserStackCredentials, "QD enabled check passed");
+ return true;
+ }
+ }
+ apiUtil.logToQD(browserStackCredentials, "QD enabled check failed");
+ return false;
+ }
+
+ private boolean isPipelineEnabledForQD(BrowserStackCredentials browserStackCredentials, String pipelineName) throws IOException {
+ QualityDashboardGetDetailsForPipeline getPipelineEnabledObj = new QualityDashboardGetDetailsForPipeline(pipelineName);
+ ObjectMapper objectMapper = new ObjectMapper();
+ String jsonBody = objectMapper.writeValueAsString(getPipelineEnabledObj);
+ RequestBody requestBody = RequestBody.create(MediaType.parse("application/json"), jsonBody);
+ Response response = apiUtil.makePostRequestToQd(Constants.QualityDashboardAPI.IS_PIPELINE_ENABLED, browserStackCredentials, requestBody);
+ if (response != null && response.code() == HttpURLConnection.HTTP_OK) {
+ ResponseBody responseBody = response.body();
+ if(responseBody != null && Boolean.parseBoolean(response.body().string())) {
+ apiUtil.logToQD(browserStackCredentials, "Pipeline enabled - pipelineName: " + pipelineName);
+ return true;
+ }
+ }
+ apiUtil.logToQD(browserStackCredentials, "Pipeline disabled - pipelineName: " + pipelineName);
+ return false;
+ }
+
+ private String getResultDirForPipeline(String pipelineUrl, BrowserStackCredentials browserStackCredentials, int buildNumber) throws JsonProcessingException {
+ String resultDir = null;
+ try {
+ QualityDashboardGetDetailsForPipeline getResultDirReqObj = new QualityDashboardGetDetailsForPipeline(pipelineUrl);
+ ObjectMapper objectMapper = new ObjectMapper();
+ String jsonBody = objectMapper.writeValueAsString(getResultDirReqObj);
+
+ RequestBody requestBody = RequestBody.create(MediaType.parse("application/json"), jsonBody);
+ Response response = apiUtil.makePostRequestToQd(Constants.QualityDashboardAPI.GET_RESULT_DIRECTORY, browserStackCredentials, requestBody);
+ if (response != null && response.code() == HttpURLConnection.HTTP_OK) {
+ String responseBody = response.body() !=null ? response.body().string() : null;
+ resultDir = responseBody;
+ }
+ } catch(IOException e) {
+ e.printStackTrace();
+ }
+ resultDir = resultDir !=null && resultDir.contains("%build_number%") ? resultDir.replace("%build_number%", String.valueOf(buildNumber)) : resultDir;
+ apiUtil.logToQD(browserStackCredentials, "Result Directory for jobName: " + pipelineUrl + " and buildNumber: " + buildNumber + " is resultDir: " + resultDir);
+ return resultDir;
+ }
+
+ private String packZip(String sourceDirPath, String jobName, BrowserStackCredentials browserStackCredentials) throws JsonProcessingException {
+ Path zipPath = Paths.get(sourceDirPath).getParent();
+ String zipFile = zipPath.toString() + "/browserstack-artifacts.zip";
+ Path zipFilePath = Paths.get(zipFile);
+ apiUtil.logToQD(browserStackCredentials, "zipFilePath for jobName: " + jobName + " is:" + zipFilePath);
+ try {
+ Files.deleteIfExists(zipFilePath);
+ ZipUtil.pack(new File(sourceDirPath), new File(zipFile));
+ apiUtil.logToQD(browserStackCredentials, "zipFile size for jobName: " + jobName + " is:" + Files.size(zipFilePath));
+ } catch (IOException e) {
+ String exceptionString = exceptionToString(e);
+ apiUtil.logToQD(browserStackCredentials, "Error creating zip for jobName: " + jobName + " is:" + exceptionString);
+ }
+ return zipFile;
+ }
+
+ private String exceptionToString(Throwable throwable) {
+ return throwable.toString();
+ }
+
+ private String uploadZipToQd(String pathToZip, BrowserStackCredentials browserStackCredentials, String jobName, int buildNumber) throws IOException {
+ String qdS3Url = null;
+ File fileToUpload = new File(pathToZip);
+ RequestBody requestBody = new MultipartBody.Builder()
+ .setType(MultipartBody.FORM)
+ .addFormDataPart("file", fileToUpload.getName(),
+ RequestBody.create(MediaType.parse("application/octet-stream"), fileToUpload))
+ .addFormDataPart("jobName", jobName)
+ .addFormDataPart("buildNumber", String.valueOf(buildNumber))
+ .build();
+
+ Response response = apiUtil.makePostRequestToQd(Constants.QualityDashboardAPI.UPLOAD_RESULT_ZIP, browserStackCredentials, requestBody);
+ if (response != null && response.code() == HttpURLConnection.HTTP_OK) {
+ qdS3Url = response.body() !=null ? response.body().string() : null;
+ }
+ return qdS3Url;
+ }
+
+ private void copyDirectoryToParentIfRequired(Run run, String finalParentPathFrom, BrowserStackCredentials browserStackCredentials) throws IOException {
+ String finalParentPathTo = null;
+ String upStreamProj = upStreamPipelineUrl(run);
+ if(StringUtils.isNotEmpty(upStreamProj)) {
+ String parentResultDir = getResultDirForPipeline(upStreamProj, browserStackCredentials, run.getNumber());
+ if(StringUtils.isNotEmpty(parentResultDir) && checkIfPathIsFound(parentResultDir)) {
+ finalParentPathTo = parentResultDir;
+ } else {
+ String defaultWorkspaceDir = getDefaultWorkspaceDirectory(run);
+ if(StringUtils.isNotEmpty(defaultWorkspaceDir) && checkIfPathIsFound(defaultWorkspaceDir)) {
+ defaultWorkspaceDir = defaultWorkspaceDir + "/workspace/" + upStreamProj + "/browserstack-artifacts";
+ boolean pathAlreadyExists = checkIfPathIsFound(defaultWorkspaceDir);
+ if(!pathAlreadyExists) {
+ Files.createDirectory(Paths.get(defaultWorkspaceDir));
+ }
+ finalParentPathTo = defaultWorkspaceDir;
+ }
+ }
+ if(StringUtils.isNotEmpty(finalParentPathTo)) {
+ FileUtils.copyDirectoryToDirectory(new File(finalParentPathFrom), new File(finalParentPathTo));
+ int buildNum = run.getNumber();
+ File finalParentFromFile = new File(finalParentPathFrom);
+ File newZipDir = new File(finalParentPathTo + "/" + finalParentFromFile.getName() + "_" + buildNum);
+ FileUtils.moveDirectory(new File(finalParentPathTo + "/" + finalParentFromFile.getName()), newZipDir);
+ }
+ }
+ }
+
+ private String upStreamPipelineUrl(Run run) {
+ String upstreamProjectName = null;
+ List causes = run.getCauses();
+ for (Cause cause : causes) {
+ if (cause instanceof Cause.UpstreamCause) {
+ Cause.UpstreamCause upstreamCause = (Cause.UpstreamCause) cause;
+ upstreamProjectName = upstreamCause.getUpstreamProject();
+ }
+ }
+ return upstreamProjectName;
+ }
+}
+
+class QualityDashboardGetDetailsForPipeline implements Serializable {
+ @JsonProperty("url")
+ private String pipeline;
+ public QualityDashboardGetDetailsForPipeline(String pipeline) {
+ this.pipeline = pipeline;
+ }
+}
+
+class PipelineResults implements Serializable {
+
+ @JsonProperty("buildNumber")
+ private Integer buildNumber;
+
+ @JsonProperty("pipelineName")
+ private String pipelineName;
+ @JsonProperty("buildDuration")
+ private Long buildDuration;
+
+ @JsonProperty("endTime")
+ private Timestamp endTime;
+ @JsonProperty("buildStatus")
+ private String buildStatus;
+
+ @JsonProperty("zipFile")
+ private String zipFile;
+
+ public PipelineResults(Integer buildNumber, Long buildDuration, String buildStatus, String zipFile, String pipelineName, Timestamp endTime) {
+ this.buildNumber = buildNumber;
+ this.buildDuration = buildDuration;
+ this.buildStatus = buildStatus;
+ this.zipFile = zipFile;
+ this.pipelineName = pipelineName;
+ this.endTime = endTime;
+ }
+}
diff --git a/src/main/java/com/browserstack/automate/ci/jenkins/qualityDashboard/QualityDashboardUtil.java b/src/main/java/com/browserstack/automate/ci/jenkins/qualityDashboard/QualityDashboardUtil.java
new file mode 100644
index 0000000..e2dff06
--- /dev/null
+++ b/src/main/java/com/browserstack/automate/ci/jenkins/qualityDashboard/QualityDashboardUtil.java
@@ -0,0 +1,18 @@
+package com.browserstack.automate.ci.jenkins.qualityDashboard;
+
+import com.browserstack.automate.ci.jenkins.BrowserStackCredentials;
+import com.cloudbees.plugins.credentials.CredentialsProvider;
+import com.cloudbees.plugins.credentials.common.StandardCredentials;
+import jenkins.model.Jenkins;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class QualityDashboardUtil {
+ public static BrowserStackCredentials getBrowserStackCreds() {
+ Jenkins jenkins = Jenkins.getInstanceOrNull();
+ List creds = CredentialsProvider.lookupCredentials(StandardCredentials.class,jenkins,null,new ArrayList<>());
+ BrowserStackCredentials browserStackCredentials = (BrowserStackCredentials) creds.stream().filter(c -> c instanceof BrowserStackCredentials).findFirst().orElse(null);
+ return browserStackCredentials;
+ }
+}