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 a9f6d90..1eca695 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 @@ -65,6 +65,8 @@ public static final class SessionStatus { public static final class QualityDashboardAPI { public static final String QEI_DEFAULT_URL = "https://quality-engineering-insights.browserstack.com"; public static String host = QEI_DEFAULT_URL; + // Cache configuration + public static final long CACHE_DURATION_MS = 60 * 60 * 1000L; // 1 hour in milliseconds public static String getHost() { return host; 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 index 48499af..cb18f33 100644 --- a/src/main/java/com/browserstack/automate/ci/jenkins/qualityDashboard/QualityDashboardInit.java +++ b/src/main/java/com/browserstack/automate/ci/jenkins/qualityDashboard/QualityDashboardInit.java @@ -9,25 +9,29 @@ import hudson.Extension; import hudson.init.InitMilestone; import hudson.init.Initializer; +import hudson.model.Job; import hudson.model.Result; +import hudson.model.Run; import java.sql.Timestamp; import java.time.Instant; +import java.io.Serializable; 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; +import java.util.logging.Logger; @Extension public class QualityDashboardInit { + private static final Logger LOGGER = Logger.getLogger(QualityDashboardInit.class.getName()); static QualityDashboardAPIUtil apiUtil = new QualityDashboardAPIUtil(); - @Initializer(after = InitMilestone.PLUGINS_PREPARED) + @Initializer(after = InitMilestone.JOB_LOADED) public static void postInstall() { try { initQDSetupIfRequired(); @@ -51,13 +55,15 @@ private static String exceptionToString(Throwable throwable) { private static void initQDSetupIfRequired() throws JsonProcessingException { BrowserStackCredentials browserStackCredentials = QualityDashboardUtil.getBrowserStackCreds(); try { - if(browserStackCredentials!=null) { + if(browserStackCredentials != null) { apiUtil.logToQD(browserStackCredentials,"Starting plugin data export to QD"); checkQDIntegrationAndDumpMetaData(browserStackCredentials); + } else { + LOGGER.warning("BrowserStack credentials not found. Skipping Quality Dashboard initialization."); } } catch (Exception e) { try { - apiUtil.logToQD(browserStackCredentials, "Global exception in data export is:"); + apiUtil.logToQD(browserStackCredentials, "Global exception in data export is:" + exceptionToString(e)); } catch (Exception ex) { String exceptionString = exceptionToString(ex); apiUtil.logToQD(browserStackCredentials, "Global exception in exception data export is:" + exceptionString); @@ -69,7 +75,7 @@ private static void initQDSetupIfRequired() throws JsonProcessingException { private static void checkQDIntegrationAndDumpMetaData(BrowserStackCredentials browserStackCredentials) throws JsonProcessingException { if(initialQDSetupRequired(browserStackCredentials)) { - List allPipelines = getAllPipelines(browserStackCredentials); + List allPipelines = getAllPipelines(browserStackCredentials); if(!allPipelines.isEmpty()){ boolean projectsSavedSuccessfully = sendPipelinesPaginated(browserStackCredentials, allPipelines); if(projectsSavedSuccessfully) { @@ -105,8 +111,8 @@ private static boolean initialQDSetupRequired(BrowserStackCredentials browserSta return false; } - private static List getAllPipelines(BrowserStackCredentials browserStackCredentials) { - List allPipelines = new ArrayList<>(); + private static List getAllPipelines(BrowserStackCredentials browserStackCredentials) { + List allPipelines = new ArrayList<>(); Jenkins jenkins = Jenkins.getInstanceOrNull(); Integer totalPipelines = 0; @@ -114,32 +120,38 @@ private static List getAllPipelines(BrowserStackCredentials browserStack totalPipelines = jenkins.getAllItems().size(); jenkins.getAllItems().forEach(job -> { try { + String itemType = QualityDashboardUtil.getItemTypeModified(job); + boolean isWorkflowJob = job instanceof WorkflowJob; + // Logging job details apiUtil.logToQD( browserStackCredentials, String.format( "Job name: %s, instance type: %s, and is_workflow_job: %s", job.getName(), - job.getClass().getSimpleName(), - (job instanceof WorkflowJob) ? "yes" : "no" + itemType, + isWorkflowJob ? "yes" : "no" ) ); + if (itemType != null && !itemType.equals("FOLDER")) { + String pipelineName = job.getFullName(); + allPipelines.add(new PipelineInfo(pipelineName, itemType)); + } + else{ + apiUtil.logToQD(browserStackCredentials, "Skipping job or Folder: " + job.getName() + " as it is not a Job or Folder instance"); + } + } catch (JsonProcessingException e) { // Handling the exception and logging an error - System.err.println("Error processing JSON for job: " + job.getName()); + LOGGER.warning("Error processing JSON for job: " + job.getName() + " - " + e.getMessage()); e.printStackTrace(); } - - if (job instanceof WorkflowJob) { - String pipelineName = job.getFullName(); // Getting pipeline name - allPipelines.add(pipelineName); - } }); } else { try { apiUtil.logToQD(browserStackCredentials, "Issue getting Jenkins Instance"); } catch (JsonProcessingException e) { - System.err.println("Error logging issue with Jenkins instance."); + LOGGER.warning("Error logging issue with Jenkins instance."); e.printStackTrace(); } } @@ -149,20 +161,20 @@ private static List getAllPipelines(BrowserStackCredentials browserStack apiUtil.logToQD(browserStackCredentials,"Total Pipelines detected : " + allPipelines.size()); } catch (JsonProcessingException e) { // Handling the exception and logging an error - System.err.println("Error processing JSON for total pipelines: "); + LOGGER.warning("Error processing JSON for total pipelines: " + e.getMessage()); e.printStackTrace(); } // Returning the list of filtered pipelines return allPipelines; } - private static boolean sendPipelinesPaginated(BrowserStackCredentials browserStackCredentials, List allPipelines) { + private static boolean sendPipelinesPaginated(BrowserStackCredentials browserStackCredentials, List allPipelines) { boolean isSuccess = true; int pageSize = getProjectPageSize(browserStackCredentials); - List> pipelinesInSmallerBatches = Lists.partition(allPipelines, pageSize); + List> pipelinesInSmallerBatches = Lists.partition(allPipelines, pageSize); int totalPages = !pipelinesInSmallerBatches.isEmpty() ? pipelinesInSmallerBatches.size() : 0; int page = 0; - for(List singlePagePipelineList : pipelinesInSmallerBatches) { + for(List singlePagePipelineList : pipelinesInSmallerBatches) { try { page++; ObjectMapper objectMapper = new ObjectMapper(); @@ -188,10 +200,12 @@ private static List getAllBuilds(BrowserStackCredentials browse Jenkins jenkins = Jenkins.getInstanceOrNull(); Instant thresholdInstant = Instant.now().minus(getHistoryForDays(browserStackCredentials), ChronoUnit.DAYS); if (jenkins != null) { - jenkins.getAllItems().forEach(job -> { - if (job instanceof WorkflowJob) { + jenkins.getAllItems().forEach(item -> { + // Support both WorkflowJob and Matrix projects (and potentially other job types) + if (item instanceof Job) { + Job job = (Job) item; String pipelineName = job.getFullName(); - List allBuilds = ((WorkflowJob) job).getBuilds(); + List> allBuilds = job.getBuilds(); if(!allBuilds.isEmpty()) { allBuilds.stream().filter(build -> Instant.ofEpochMilli(build.getTimeInMillis()).isAfter(thresholdInstant)).forEach( build -> { @@ -201,7 +215,19 @@ private static List getAllBuilds(BrowserStackCredentials browse 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); + + // Get root upstream project information for QEI with build number (returns in format "project#build") + String rootUpstreamProject = ""; + String immediateParentProject = ""; + try { + rootUpstreamProject = UpstreamPipelineResolver.resolveRootUpstreamProject(build, browserStackCredentials); + immediateParentProject = UpstreamPipelineResolver.resolveImmediateUpstreamProjectForQEI(build, browserStackCredentials); + } catch (Exception e) { + LOGGER.warning("Error resolving upstream project for " + pipelineName + " build number " + buildNumber + ": " + e.getMessage()); + e.printStackTrace(); + } + PipelineDetails pipelineDetail = new PipelineDetails(pipelineName, buildNumber, duration, result, + endTime, rootUpstreamProject, immediateParentProject); allBuildResults.add(pipelineDetail); } ); @@ -249,9 +275,8 @@ private static int getHistoryForDays(BrowserStackCredentials browserStackCredent } } catch(IOException e) { e.printStackTrace(); - } finally { - return no_of_days; } + return no_of_days; } private static int getProjectPageSize(BrowserStackCredentials browserStackCredentials) { @@ -268,9 +293,8 @@ private static int getProjectPageSize(BrowserStackCredentials browserStackCreden } } catch(IOException e) { e.printStackTrace(); - } finally { - return projectPageSize; } + return projectPageSize; } private static int getResultPageSize(BrowserStackCredentials browserStackCredentials) { @@ -287,9 +311,8 @@ private static int getResultPageSize(BrowserStackCredentials browserStackCredent } } catch(IOException e) { e.printStackTrace(); - } finally { - return resultPageSize; } + return resultPageSize; } } @@ -307,13 +330,35 @@ class PipelineDetails { @JsonProperty("endTime") private Timestamp endTime; - - public PipelineDetails(String pipelineName, Integer buildNumber, Long buildDuration, String buildStatus, Timestamp endTime) { + + @JsonProperty("rootProject") + private String rootProject; + + @JsonProperty("immediateParentProject") + private String immediateParentProject; + + public PipelineDetails(String pipelineName, Integer buildNumber, Long buildDuration, String buildStatus, + Timestamp endTime, String rootProject, String immediateParentProject) { this.pipelineName = pipelineName; this.buildNumber = buildNumber; this.buildDuration = buildDuration; this.buildStatus = buildStatus; this.endTime = endTime; + this.rootProject = rootProject; + this.immediateParentProject = immediateParentProject; + } +} + +class PipelineInfo implements Serializable { + @JsonProperty("pipelineName") + private String pipelineName; + + @JsonProperty("jobType") + private String jobType; + + public PipelineInfo(String pipelineName, String jobType) { + this.pipelineName = pipelineName; + this.jobType = jobType; } } @@ -325,9 +370,9 @@ class PipelinesPaginated { private int totalPages; @JsonProperty("pipelines") - private List pipelines; + private List pipelines; - public PipelinesPaginated(int page, int totalPages, List pipelines) { + public PipelinesPaginated(int page, int totalPages, List pipelines) { this.page = page; this.totalPages = totalPages; this.pipelines = pipelines; 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 index fd4259f..e00adba 100644 --- a/src/main/java/com/browserstack/automate/ci/jenkins/qualityDashboard/QualityDashboardInitItemListener.java +++ b/src/main/java/com/browserstack/automate/ci/jenkins/qualityDashboard/QualityDashboardInitItemListener.java @@ -11,33 +11,39 @@ import okhttp3.MediaType; import okhttp3.RequestBody; import okhttp3.Response; -import org.jenkinsci.plugins.workflow.job.WorkflowJob; -import java.io.IOException; import java.io.Serializable; +import java.util.logging.Logger; @Extension public class QualityDashboardInitItemListener extends ItemListener { + private static final Logger LOGGER = Logger.getLogger(QualityDashboardInitItemListener.class.getName()); + @Override public void onCreated(Item job) { try { BrowserStackCredentials browserStackCredentials = QualityDashboardUtil.getBrowserStackCreds(); + if(browserStackCredentials == null) { + LOGGER.warning("BrowserStackCredentials not found. Please ensure they are configured correctly."); + return; + } QualityDashboardAPIUtil apiUtil = new QualityDashboardAPIUtil(); - apiUtil.logToQD(browserStackCredentials, "Item Created : " + job.getClass().getName()) ; String itemName = job.getFullName(); - String itemType = getItemTypeModified(job); + String itemType = QualityDashboardUtil.getItemTypeModified(job); - apiUtil.logToQD(browserStackCredentials, "Item Type : " + itemType + " : " + itemName + " : " + itemType.equals("PIPELINE")); - if(itemType != null && itemType.equals("PIPELINE")) { + apiUtil.logToQD(browserStackCredentials, "Item Created : " + itemName + " - " + "Item Type : " + itemType); + if(itemType != null && !itemType.equals("FOLDER")) { try { - apiUtil.logToQD(browserStackCredentials, "Item Type inside the Folder : " + itemType + " : " + itemName + " : " + itemType.equals("PIPELINE")); String jsonBody = getJsonReqBody(new ItemUpdate(itemName, itemType)); syncItemListToQD(jsonBody, Constants.QualityDashboardAPI.getItemCrudEndpoint(), "POST"); - } catch(IOException e) { + } catch(Exception e) { + LOGGER.warning("Error syncing item creation to Quality Dashboard: " + e.getMessage()); e.printStackTrace(); } + } else { + apiUtil.logToQD(browserStackCredentials, "Skipping item creation sync: " + itemName); } } catch(Exception e) { e.printStackTrace(); @@ -47,12 +53,13 @@ public void onCreated(Item job) { @Override public void onDeleted(Item job) { String itemName = job.getFullName(); - String itemType = getItemTypeModified(job); + String itemType = QualityDashboardUtil.getItemTypeModified(job); if(itemType != null) { try { String jsonBody = getJsonReqBody(new ItemUpdate(itemName, itemType)); syncItemListToQD(jsonBody, Constants.QualityDashboardAPI.getItemCrudEndpoint(), "DELETE"); - } catch(IOException e) { + } catch(Exception e) { + LOGGER.warning("Error syncing item deletion to Quality Dashboard: " + e.getMessage()); e.printStackTrace(); } } @@ -60,29 +67,20 @@ public void onDeleted(Item job) { @Override public void onRenamed(Item job, String oldName, String newName) { - String itemType = getItemTypeModified(job); + String itemType = QualityDashboardUtil.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.getItemCrudEndpoint(), "PUT"); - } catch(IOException e) { + } catch(Exception e) { + LOGGER.warning("Error syncing item rename to Quality Dashboard: " + e.getMessage()); 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); @@ -93,6 +91,10 @@ private Response syncItemListToQD(String jsonBody, String url, String typeOfRequ RequestBody requestBody = RequestBody.create(MediaType.parse("application/json"), jsonBody); QualityDashboardAPIUtil apiUtil = new QualityDashboardAPIUtil(); BrowserStackCredentials browserStackCredentials = QualityDashboardUtil.getBrowserStackCreds(); + if(browserStackCredentials == null) { + LOGGER.warning("BrowserStack credentials not found. Please ensure they are configured correctly."); + return null; + } if(typeOfRequest.equals("PUT")) { apiUtil.logToQD(browserStackCredentials, "Syncing Item Update - PUT"); return apiUtil.makePutRequestToQd(url, browserStackCredentials, requestBody); 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 index ae8c90f..ca77738 100644 --- a/src/main/java/com/browserstack/automate/ci/jenkins/qualityDashboard/QualityDashboardPipelineTracker.java +++ b/src/main/java/com/browserstack/automate/ci/jenkins/qualityDashboard/QualityDashboardPipelineTracker.java @@ -12,8 +12,6 @@ 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; @@ -24,42 +22,27 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.sql.Timestamp; -import java.util.List; +import java.time.Instant; +import java.util.logging.Logger; @Extension -public class QualityDashboardPipelineTracker extends RunListener { +public class QualityDashboardPipelineTracker extends RunListener> { + private static final Logger LOGGER = Logger.getLogger(QualityDashboardPipelineTracker.class.getName()); QualityDashboardAPIUtil apiUtil = new QualityDashboardAPIUtil(); @Override - public void onCompleted(Run run, TaskListener listener) { + 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(); + if(browserStackCredentials != null) { + String jobName = getJobNameFromRun(run, browserStackCredentials); 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); + processArtifactsAndSendData(run, overallResult, browserStackCredentials, jobName, buildNumber); } else { apiUtil.logToQD(browserStackCredentials, "Null Result Captured for jobName: " + jobName + " and buildNumber: " + buildNumber); } @@ -73,6 +56,48 @@ public void onCompleted(Run run, TaskListener listener) { throw new RuntimeException(e); } } + else { + LOGGER.warning("BrowserStack credentials not found. Please ensure they are configured correctly."); + } + } + + + private void processArtifactsAndSendData(Run run, Result overallResult, BrowserStackCredentials browserStackCredentials, + String jobName, int buildNumber) throws IOException { + 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()) { + File artifactsDir = new File(run.getRootDir(), "archive"); + if (artifactsDir.exists() && artifactsDir.isDirectory()) { + finalPathToZip = artifactsDir.getAbsolutePath(); + } + if (finalPathToZip == null || !Files.exists(Paths.get(finalPathToZip))) { + Jenkins jenkins = Jenkins.getInstanceOrNull(); + if (jenkins != null) { + finalPathToZip = jenkins.getRootDir().getAbsolutePath() + "/archive/" + jobName + "/" + buildNumber; + } else { + apiUtil.logToQD(browserStackCredentials, "Jenkins instance is null, cannot access archived artifacts for jobName: " + jobName + " and buildNumber: " + buildNumber); + finalPathToZip = null; + } + } + if (StringUtils.isNotEmpty(finalPathToZip) && Files.exists(Paths.get(finalPathToZip))) { + apiUtil.logToQD(browserStackCredentials, "Got artifact path for jobName: " + jobName + " and buildNumber: " + buildNumber + " as: " + finalPathToZip); + qdS3Url = zipArtifactsAndUploadToQD(finalPathToZip, browserStackCredentials, jobName, buildNumber); + } else { + apiUtil.logToQD(browserStackCredentials, "Archive artifacts not found at expected path for jobName: " + jobName + " and buildNumber: " + buildNumber); + finalPathToZip = null; + } + } else { + apiUtil.logToQD(browserStackCredentials, "Finally no artifacts found for jobName: " + jobName + " and buildNumber: " + buildNumber); + } + sendBuildDataToQD(run, overallResult, qdS3Url, browserStackCredentials); } private String zipArtifactsAndUploadToQD (String finalPathToZip, BrowserStackCredentials browserStackCredentials, String jobName, int buildNumber) throws IOException { @@ -88,7 +113,7 @@ private String zipArtifactsAndUploadToQD (String finalPathToZip, BrowserStackCre return qdS3Url; } - private void sendBuildDataToQD(Run run, Result overallResult, String finalZipPath, BrowserStackCredentials browserStackCredentials) { + private void sendBuildDataToQD(Run run, Result overallResult, String finalZipPath, BrowserStackCredentials browserStackCredentials) { Long pipelineDuration = getPipelineDuration(run); try { String jobName = run.getParent().getFullName(); @@ -101,9 +126,19 @@ private void sendBuildDataToQD(Run run, Result overallResult, String finalZipPat if(rootUrl != null) { jobUrl = rootUrl + run.getUrl(); } - + // Get root upstream project information for QEI with build number (returns in format "project#build") + String rootUpstreamProject = ""; + String immediateParentProject = ""; + try { + rootUpstreamProject = UpstreamPipelineResolver.resolveRootUpstreamProject(run, browserStackCredentials); + immediateParentProject = UpstreamPipelineResolver.resolveImmediateUpstreamProjectForQEI(run, browserStackCredentials); + } catch (Exception e) { + LOGGER.warning("Error resolving upstream project for jobName: " + jobName + " and buildNumber: " + buildNumber + ". Exception: " + e.getMessage()); + e.printStackTrace(); + } Timestamp endTime = new Timestamp(endTimeInMillis); - PipelineResults pipelineResultsReqObj = new PipelineResults(buildNumber, pipelineDuration, overallResult.toString(), finalZipPath, jobName, endTime, jobUrl); + PipelineResults pipelineResultsReqObj = new PipelineResults(buildNumber, pipelineDuration, overallResult.toString(), + finalZipPath, jobName, endTime, jobUrl, rootUpstreamProject, immediateParentProject); ObjectMapper objectMapper = new ObjectMapper(); String jsonBody = objectMapper.writeValueAsString(pipelineResultsReqObj); @@ -115,7 +150,7 @@ private void sendBuildDataToQD(Run run, Result overallResult, String finalZipPat } } - private Long getPipelineDuration(Run build) { + private Long getPipelineDuration(Run build) { long startTime = build.getStartTimeInMillis(); long endTime = System.currentTimeMillis(); long duration = (endTime - startTime) / 1000; @@ -126,7 +161,7 @@ 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 { + 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)) { @@ -142,7 +177,7 @@ private String getFinalZipPath(Run run, BrowserStackCredentials browserStackCred return finalZipPath; } - private String getDefaultWorkspaceDirectory(Run run) { + 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; @@ -153,16 +188,31 @@ private String getUrlForPipeline(Run build) { } private boolean isQDEnabled(BrowserStackCredentials browserStackCredentials) throws IOException { + // Check if we have a valid cached value + Boolean cachedResult = QDEnabledCache.getCachedValue(); + if (cachedResult != null) { + return cachedResult; + } Response response = apiUtil.makeGetRequestToQd(Constants.QualityDashboardAPI.getIsQdEnabledEndpoint(), browserStackCredentials); - if (response != null && response.code() == HttpURLConnection.HTTP_OK) { + boolean isEnabled = false; + + if (response != null && response.code() == HttpURLConnection.HTTP_OK) { ResponseBody responseBody = response.body(); - if(responseBody != null && Boolean.parseBoolean(response.body().string())) { + if (responseBody != null && Boolean.parseBoolean(response.body().string())) { + isEnabled = true; apiUtil.logToQD(browserStackCredentials, "QD enabled check passed"); - return true; } } - apiUtil.logToQD(browserStackCredentials, "QD enabled check failed"); - return false; + + if (!isEnabled) { + apiUtil.logToQD(browserStackCredentials, "QD enabled check failed"); + } + + // Cache the result for 1 hour + QDEnabledCache.setCachedValue(isEnabled); + LOGGER.info("Cached QD enabled status: " + isEnabled + " for 1 hour"); + + return isEnabled; } private boolean isPipelineEnabledForQD(BrowserStackCredentials browserStackCredentials, String pipelineName) throws IOException { @@ -241,9 +291,9 @@ private String uploadZipToQd(String pathToZip, BrowserStackCredentials browserSt return qdS3Url; } - private void copyDirectoryToParentIfRequired(Run run, String finalParentPathFrom, BrowserStackCredentials browserStackCredentials) throws IOException { + private void copyDirectoryToParentIfRequired(Run run, String finalParentPathFrom, BrowserStackCredentials browserStackCredentials) throws IOException { String finalParentPathTo = null; - String upStreamProj = upStreamPipelineUrl(run); + String upStreamProj = UpstreamPipelineResolver.resolveImmediateUpstreamProject(run, browserStackCredentials); if(StringUtils.isNotEmpty(upStreamProj)) { String parentResultDir = getResultDirForPipeline(upStreamProj, browserStackCredentials, run.getNumber()); if(StringUtils.isNotEmpty(parentResultDir) && checkIfPathIsFound(parentResultDir)) { @@ -269,17 +319,42 @@ private void copyDirectoryToParentIfRequired(Run run, String finalParentPathFrom } } - 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(); + private String getJobNameFromRun(Run run, BrowserStackCredentials browserStackCredentials) { + try { + // Check if parent is a Job (covers all job types) + if (run.getParent() instanceof Job) { + Job job = (Job) run.getParent(); + String jobName = job.getFullName(); + return jobName; + } else { + // Fallback for any other parent types + String fallbackName = run.getParent().getFullName(); + return fallbackName; + } + } catch (Exception e) { + try { + apiUtil.logToQD(browserStackCredentials, "Error getting job name from run: " + e.getMessage()); + } catch (JsonProcessingException jsonEx) { + jsonEx.printStackTrace(); } } - return upstreamProjectName; + return null; } + private static class QDEnabledCache { + private static volatile Boolean qdEnabled = null; + private static volatile Instant expiryTime = Instant.EPOCH; + + public static Boolean getCachedValue() { + if (qdEnabled != null && Instant.now().isBefore(expiryTime)) { + return qdEnabled; + } + return null; // Cache expired or not set + } + public static void setCachedValue(boolean value) { + qdEnabled = value; + expiryTime = Instant.now().plusMillis(Constants.QualityDashboardAPI.CACHE_DURATION_MS); + } + } } class QualityDashboardGetDetailsForPipeline implements Serializable { @@ -310,8 +385,16 @@ class PipelineResults implements Serializable { @JsonProperty("zipFile") private String zipFile; - - public PipelineResults(Integer buildNumber, Long buildDuration, String buildStatus, String zipFile, String pipelineName, Timestamp endTime, String jobUrl) { + + @JsonProperty("rootProject") + private String rootProject; + + @JsonProperty("immediateParentProject") + private String immediateParentProject; + + public PipelineResults(Integer buildNumber, Long buildDuration, String buildStatus, String zipFile, + String pipelineName, Timestamp endTime, String jobUrl, String rootProject, + String immediateParentProject) { this.buildNumber = buildNumber; this.buildDuration = buildDuration; this.buildStatus = buildStatus; @@ -319,5 +402,7 @@ public PipelineResults(Integer buildNumber, Long buildDuration, String buildStat this.pipelineName = pipelineName; this.endTime = endTime; this.jobUrl = jobUrl; + this.rootProject = rootProject; + this.immediateParentProject = immediateParentProject; } } 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 index e2dff06..0e08874 100644 --- a/src/main/java/com/browserstack/automate/ci/jenkins/qualityDashboard/QualityDashboardUtil.java +++ b/src/main/java/com/browserstack/automate/ci/jenkins/qualityDashboard/QualityDashboardUtil.java @@ -3,6 +3,8 @@ import com.browserstack.automate.ci.jenkins.BrowserStackCredentials; import com.cloudbees.plugins.credentials.CredentialsProvider; import com.cloudbees.plugins.credentials.common.StandardCredentials; +import hudson.model.Item; +import hudson.model.Job; import jenkins.model.Jenkins; import java.util.ArrayList; @@ -15,4 +17,21 @@ public static BrowserStackCredentials getBrowserStackCreds() { BrowserStackCredentials browserStackCredentials = (BrowserStackCredentials) creds.stream().filter(c -> c instanceof BrowserStackCredentials).findFirst().orElse(null); return browserStackCredentials; } + + public static String getItemTypeModified(Item job) { + try { + if (job instanceof Job) { + return job.getClass().getSimpleName().toUpperCase(); + } + else if (job.getClass().getName().contains("Folder")) { + return "FOLDER"; + } + else { + return null; + } + } catch (Exception e) { + // Return null in case of any error during job type determination + return null; + } + } } diff --git a/src/main/java/com/browserstack/automate/ci/jenkins/qualityDashboard/UpstreamPipelineResolver.java b/src/main/java/com/browserstack/automate/ci/jenkins/qualityDashboard/UpstreamPipelineResolver.java new file mode 100644 index 0000000..00d0b1a --- /dev/null +++ b/src/main/java/com/browserstack/automate/ci/jenkins/qualityDashboard/UpstreamPipelineResolver.java @@ -0,0 +1,280 @@ +package com.browserstack.automate.ci.jenkins.qualityDashboard; + +import com.browserstack.automate.ci.jenkins.BrowserStackCredentials; +import com.fasterxml.jackson.core.JsonProcessingException; +import hudson.model.*; +import jenkins.model.Jenkins; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + + +public class UpstreamPipelineResolver { + + private static final QualityDashboardAPIUtil apiUtil = new QualityDashboardAPIUtil(); + + /** + * Centralized method to handle JsonProcessingException logging. + * This reduces duplication of exception handling code throughout the class. + * + * @param browserStackCredentials The credentials required for logging + * @param message The error message to log + */ + private static void logError(BrowserStackCredentials browserStackCredentials, String message) { + try { + apiUtil.logToQD(browserStackCredentials, message); + } catch (JsonProcessingException ex) { + throw new RuntimeException("Failed to log error: " + ex.getMessage(), ex); + } + } + + /** + * Resolves the root upstream project with its build number in the format "project#build". + * This method returns both the project name and build number, which is useful for + * identifying the root project in the pipeline chain. + * + * @param run The current build run + * @param browserStackCredentials The BrowserStack credentials for API access + * @return The root upstream project with build number in format "project#build", or null if none found + */ + public static String resolveRootUpstreamProject(Run run, BrowserStackCredentials browserStackCredentials) { + try { + Set visitedProjects = new HashSet<>(); + return findRootUpstreamProject(run, browserStackCredentials, visitedProjects); + } catch (Exception e) { + logError(browserStackCredentials, "Error resolving root upstream project for " + + getProjectName(run) + "#" + run.getNumber() + ": " + e.getMessage()); + return null; + } + } + + private static String findRootUpstreamProject(Run run, BrowserStackCredentials browserStackCredentials, + Set visitedProjects) { + if (run == null) { + return null; + } + + String currentProject = getProjectName(run); + + // Cycle detection - prevent infinite recursion + if (visitedProjects.contains(currentProject)) { + logError(browserStackCredentials, "Circular dependency detected in project: " + currentProject); + return null; + } + + visitedProjects.add(currentProject); + + List causes = run.getCauses(); + if (causes == null || causes.isEmpty()) { + // No causes found - this is likely a root project + return null; + } + + String rootProject = null; + + for (Cause cause : causes) { + String upstreamProject = processUpstreamCause(cause, run, browserStackCredentials, visitedProjects); + if (upstreamProject != null) { + // If we found an upstream project, that becomes our root candidate + rootProject = upstreamProject; + break; // Use first valid upstream cause + } + } + + return rootProject; + } + + private static String processUpstreamCause(Cause cause, Run run, + BrowserStackCredentials browserStackCredentials, + Set visitedProjects) { + try { + if (cause instanceof Cause.UpstreamCause) { + return handleUpstreamCause((Cause.UpstreamCause) cause, browserStackCredentials, visitedProjects); + } else if (cause instanceof hudson.triggers.TimerTrigger.TimerTriggerCause) { + apiUtil.logToQD(browserStackCredentials, "Build triggered by timer/schedule"); + return null; + } else if (cause instanceof hudson.triggers.SCMTrigger.SCMTriggerCause) { + apiUtil.logToQD(browserStackCredentials, "Build triggered by SCM change"); + return null; + } else if (cause instanceof Cause.UserIdCause) { + Cause.UserIdCause userCause = (Cause.UserIdCause) cause; + apiUtil.logToQD(browserStackCredentials, "Build triggered manually by user: " + + getUserDisplayName(userCause)); + return null; + } else if (cause instanceof Cause.RemoteCause) { + Cause.RemoteCause remoteCause = (Cause.RemoteCause) cause; + apiUtil.logToQD(browserStackCredentials, "Build triggered remotely from: " + + remoteCause.getAddr()); + return null; + } else { + // Handle unknown cause types + apiUtil.logToQD(browserStackCredentials, "Unknown build cause type: " + + cause.getClass().getSimpleName()); + return null; + } + } catch (JsonProcessingException e) { + logError(browserStackCredentials, "Error processing cause: " + e.getMessage()); + return null; + } + } + + /** + * Handles upstream cause by recursively finding the root upstream project and its build number. + * This method always returns the project name and build number in the format "project#build". + * + * @param upstreamCause The upstream cause to process + * @param browserStackCredentials The BrowserStack credentials for API access + * @param visitedProjects Set of already visited projects to detect cycles + * @return The root upstream project with build number in format "project#build", or null if none found + */ + private static String handleUpstreamCause(Cause.UpstreamCause upstreamCause, + BrowserStackCredentials browserStackCredentials, + Set visitedProjects) { + try { + String upstreamProjectName = upstreamCause.getUpstreamProject(); + int upstreamBuildNumber = upstreamCause.getUpstreamBuild(); + + if (upstreamProjectName == null || upstreamProjectName.trim().isEmpty()) { + apiUtil.logToQD(browserStackCredentials, "Invalid upstream project name"); + return null; + } + + apiUtil.logToQD(browserStackCredentials, "Found upstream: " + upstreamProjectName + + "#" + upstreamBuildNumber); + + // Try to get the upstream run for recursive traversal + Run upstreamRun = getUpstreamRun(upstreamProjectName, upstreamBuildNumber); + if (upstreamRun != null) { + // Recursively check if this upstream has its own upstream + String rootProject = findRootUpstreamProject(upstreamRun, browserStackCredentials, visitedProjects); + if (rootProject != null) { + // Found a higher-level upstream, return that + return rootProject; + } + } + // Return in format projectName#buildNumber + String formattedResult = upstreamProjectName + "#" + upstreamBuildNumber; + apiUtil.logToQD(browserStackCredentials, "Resolved root upstream project: " + formattedResult); + return formattedResult; + } catch (JsonProcessingException e) { + logError(browserStackCredentials, "Error processing upstream cause: " + e.getMessage()); + return null; + } catch (Exception e) { + logError(browserStackCredentials, "Unexpected error while handling upstream cause: " + e.getMessage()); + return null; + } + } + + private static Run getUpstreamRun(String projectName, int buildNumber) { + try { + Jenkins jenkins = Jenkins.getInstanceOrNull(); + if (jenkins == null) { + return null; + } + + Job job = jenkins.getItemByFullName(projectName, Job.class); + if (job == null) { + return null; + } + + return job.getBuildByNumber(buildNumber); + } catch (Exception e) { + // upstream build might not exist anymore + return null; + } + } + + private static String getProjectName(Run run) { + try { + return run.getParent().getFullName(); + } catch (Exception e) { + return "unknown"; + } + } + + private static String getUserDisplayName(Cause.UserIdCause userCause) { + try { + String userName = userCause.getUserName(); + String userId = userCause.getUserId(); + return userName != null ? userName : userId; + } catch (Exception e) { + return "unknown user"; + } + } + + public static String resolveImmediateUpstreamProject(Run run, BrowserStackCredentials browserStackCredentials) { + try { + return findImmediateUpstreamProject(run, browserStackCredentials); + } catch (Exception e) { + logError(browserStackCredentials, "Error resolving immediate upstream project for " + + getProjectName(run) + "#" + run.getNumber() + ": " + e.getMessage()); + return null; + } + } + + public static String resolveImmediateUpstreamProjectForQEI(Run run, BrowserStackCredentials browserStackCredentials) { + try { + return findImmediateUpstreamProjectForQEI(run, browserStackCredentials); + } catch (Exception e) { + logError(browserStackCredentials, "Error resolving immediate upstream project for QEI for " + + getProjectName(run) + "#" + run.getNumber() + ": " + e.getMessage()); + return null; + } + } + + private static String findImmediateUpstreamProject(Run run, BrowserStackCredentials browserStackCredentials) throws JsonProcessingException { + if (run == null) { + return null; + } + + List causes = run.getCauses(); + if (causes == null || causes.isEmpty()) { + return null; + } + + for (Cause cause : causes) { + if (cause instanceof Cause.UpstreamCause) { + Cause.UpstreamCause upstreamCause = (Cause.UpstreamCause) cause; + String upstreamProjectName = upstreamCause.getUpstreamProject(); + + if (upstreamProjectName != null && !upstreamProjectName.trim().isEmpty()) { + apiUtil.logToQD(browserStackCredentials, "Found immediate upstream: " + upstreamProjectName); + return upstreamProjectName; + } + } + } + + apiUtil.logToQD(browserStackCredentials, "No immediate upstream found for: " + getProjectName(run)); + return null; + } + + private static String findImmediateUpstreamProjectForQEI(Run run, BrowserStackCredentials browserStackCredentials) throws JsonProcessingException { + if (run == null) { + return null; + } + + List causes = run.getCauses(); + if (causes == null || causes.isEmpty()) { + return null; + } + + for (Cause cause : causes) { + if (cause instanceof Cause.UpstreamCause) { + Cause.UpstreamCause upstreamCause = (Cause.UpstreamCause) cause; + String upstreamProjectName = upstreamCause.getUpstreamProject(); + int upstreamBuildNumber = upstreamCause.getUpstreamBuild(); + + if (upstreamProjectName != null && !upstreamProjectName.trim().isEmpty()) { + String formattedResult = upstreamProjectName + "#" + upstreamBuildNumber; + apiUtil.logToQD(browserStackCredentials, "Found immediate upstream for QEI: " + formattedResult); + return formattedResult; + } + } + } + + apiUtil.logToQD(browserStackCredentials, "No immediate upstream found with BuildNumber: " + getProjectName(run)); + return null; + } + +}