Skip to content

Commit 1be5219

Browse files
authored
Merge pull request #92 from sanjay000001/QD-1713-Parent-Child-Job-Support
Adding parent-child job support to the pipeline job.
2 parents 1f36150 + 631cf69 commit 1be5219

File tree

6 files changed

+537
-104
lines changed

6 files changed

+537
-104
lines changed

src/main/java/com/browserstack/automate/ci/common/constants/Constants.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ public static final class SessionStatus {
7777
public static final class QualityDashboardAPI {
7878
public static final String QEI_DEFAULT_URL = "https://quality-engineering-insights.browserstack.com";
7979
public static String host = QEI_DEFAULT_URL;
80+
// Cache configuration
81+
public static final long CACHE_DURATION_MS = 60 * 60 * 1000L; // 1 hour in milliseconds
8082

8183
public static String getHost() {
8284
return host;

src/main/java/com/browserstack/automate/ci/jenkins/qualityDashboard/QualityDashboardInit.java

Lines changed: 79 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,29 @@
99
import hudson.Extension;
1010
import hudson.init.InitMilestone;
1111
import hudson.init.Initializer;
12+
import hudson.model.Job;
1213
import hudson.model.Result;
14+
import hudson.model.Run;
1315
import java.sql.Timestamp;
1416
import java.time.Instant;
17+
import java.io.Serializable;
1518
import jenkins.model.Jenkins;
1619
import okhttp3.*;
1720
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
18-
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
1921
import java.time.temporal.ChronoUnit;
2022
import java.io.IOException;
2123
import java.net.HttpURLConnection;
2224
import java.util.ArrayList;
2325
import java.util.List;
26+
import java.util.logging.Logger;
2427

2528
@Extension
2629
public class QualityDashboardInit {
2730

31+
private static final Logger LOGGER = Logger.getLogger(QualityDashboardInit.class.getName());
2832
static QualityDashboardAPIUtil apiUtil = new QualityDashboardAPIUtil();
2933

30-
@Initializer(after = InitMilestone.PLUGINS_PREPARED)
34+
@Initializer(after = InitMilestone.JOB_LOADED)
3135
public static void postInstall() {
3236
try {
3337
initQDSetupIfRequired();
@@ -51,13 +55,15 @@ private static String exceptionToString(Throwable throwable) {
5155
private static void initQDSetupIfRequired() throws JsonProcessingException {
5256
BrowserStackCredentials browserStackCredentials = QualityDashboardUtil.getBrowserStackCreds();
5357
try {
54-
if(browserStackCredentials!=null) {
58+
if(browserStackCredentials != null) {
5559
apiUtil.logToQD(browserStackCredentials,"Starting plugin data export to QD");
5660
checkQDIntegrationAndDumpMetaData(browserStackCredentials);
61+
} else {
62+
LOGGER.warning("BrowserStack credentials not found. Skipping Quality Dashboard initialization.");
5763
}
5864
} catch (Exception e) {
5965
try {
60-
apiUtil.logToQD(browserStackCredentials, "Global exception in data export is:");
66+
apiUtil.logToQD(browserStackCredentials, "Global exception in data export is:" + exceptionToString(e));
6167
} catch (Exception ex) {
6268
String exceptionString = exceptionToString(ex);
6369
apiUtil.logToQD(browserStackCredentials, "Global exception in exception data export is:" + exceptionString);
@@ -69,7 +75,7 @@ private static void initQDSetupIfRequired() throws JsonProcessingException {
6975

7076
private static void checkQDIntegrationAndDumpMetaData(BrowserStackCredentials browserStackCredentials) throws JsonProcessingException {
7177
if(initialQDSetupRequired(browserStackCredentials)) {
72-
List<String> allPipelines = getAllPipelines(browserStackCredentials);
78+
List<PipelineInfo> allPipelines = getAllPipelines(browserStackCredentials);
7379
if(!allPipelines.isEmpty()){
7480
boolean projectsSavedSuccessfully = sendPipelinesPaginated(browserStackCredentials, allPipelines);
7581
if(projectsSavedSuccessfully) {
@@ -105,41 +111,47 @@ private static boolean initialQDSetupRequired(BrowserStackCredentials browserSta
105111
return false;
106112
}
107113

108-
private static List<String> getAllPipelines(BrowserStackCredentials browserStackCredentials) {
109-
List<String> allPipelines = new ArrayList<>();
114+
private static List<PipelineInfo> getAllPipelines(BrowserStackCredentials browserStackCredentials) {
115+
List<PipelineInfo> allPipelines = new ArrayList<>();
110116
Jenkins jenkins = Jenkins.getInstanceOrNull();
111117
Integer totalPipelines = 0;
112118

113119
if (jenkins != null) {
114120
totalPipelines = jenkins.getAllItems().size();
115121
jenkins.getAllItems().forEach(job -> {
116122
try {
123+
String itemType = QualityDashboardUtil.getItemTypeModified(job);
124+
boolean isWorkflowJob = job instanceof WorkflowJob;
125+
117126
// Logging job details
118127
apiUtil.logToQD(
119128
browserStackCredentials,
120129
String.format(
121130
"Job name: %s, instance type: %s, and is_workflow_job: %s",
122131
job.getName(),
123-
job.getClass().getSimpleName(),
124-
(job instanceof WorkflowJob) ? "yes" : "no"
132+
itemType,
133+
isWorkflowJob ? "yes" : "no"
125134
)
126135
);
136+
if (itemType != null && !itemType.equals("FOLDER")) {
137+
String pipelineName = job.getFullName();
138+
allPipelines.add(new PipelineInfo(pipelineName, itemType));
139+
}
140+
else{
141+
apiUtil.logToQD(browserStackCredentials, "Skipping job or Folder: " + job.getName() + " as it is not a Job or Folder instance");
142+
}
143+
127144
} catch (JsonProcessingException e) {
128145
// Handling the exception and logging an error
129-
System.err.println("Error processing JSON for job: " + job.getName());
146+
LOGGER.warning("Error processing JSON for job: " + job.getName() + " - " + e.getMessage());
130147
e.printStackTrace();
131148
}
132-
133-
if (job instanceof WorkflowJob) {
134-
String pipelineName = job.getFullName(); // Getting pipeline name
135-
allPipelines.add(pipelineName);
136-
}
137149
});
138150
} else {
139151
try {
140152
apiUtil.logToQD(browserStackCredentials, "Issue getting Jenkins Instance");
141153
} catch (JsonProcessingException e) {
142-
System.err.println("Error logging issue with Jenkins instance.");
154+
LOGGER.warning("Error logging issue with Jenkins instance.");
143155
e.printStackTrace();
144156
}
145157
}
@@ -149,20 +161,20 @@ private static List<String> getAllPipelines(BrowserStackCredentials browserStack
149161
apiUtil.logToQD(browserStackCredentials,"Total Pipelines detected : " + allPipelines.size());
150162
} catch (JsonProcessingException e) {
151163
// Handling the exception and logging an error
152-
System.err.println("Error processing JSON for total pipelines: ");
164+
LOGGER.warning("Error processing JSON for total pipelines: " + e.getMessage());
153165
e.printStackTrace();
154166
}
155167
// Returning the list of filtered pipelines
156168
return allPipelines;
157169
}
158170

159-
private static boolean sendPipelinesPaginated(BrowserStackCredentials browserStackCredentials, List<String> allPipelines) {
171+
private static boolean sendPipelinesPaginated(BrowserStackCredentials browserStackCredentials, List<PipelineInfo> allPipelines) {
160172
boolean isSuccess = true;
161173
int pageSize = getProjectPageSize(browserStackCredentials);
162-
List<List<String>> pipelinesInSmallerBatches = Lists.partition(allPipelines, pageSize);
174+
List<List<PipelineInfo>> pipelinesInSmallerBatches = Lists.partition(allPipelines, pageSize);
163175
int totalPages = !pipelinesInSmallerBatches.isEmpty() ? pipelinesInSmallerBatches.size() : 0;
164176
int page = 0;
165-
for(List<String> singlePagePipelineList : pipelinesInSmallerBatches) {
177+
for(List<PipelineInfo> singlePagePipelineList : pipelinesInSmallerBatches) {
166178
try {
167179
page++;
168180
ObjectMapper objectMapper = new ObjectMapper();
@@ -188,10 +200,12 @@ private static List<PipelineDetails> getAllBuilds(BrowserStackCredentials browse
188200
Jenkins jenkins = Jenkins.getInstanceOrNull();
189201
Instant thresholdInstant = Instant.now().minus(getHistoryForDays(browserStackCredentials), ChronoUnit.DAYS);
190202
if (jenkins != null) {
191-
jenkins.getAllItems().forEach(job -> {
192-
if (job instanceof WorkflowJob) {
203+
jenkins.getAllItems().forEach(item -> {
204+
// Support both WorkflowJob and Matrix projects (and potentially other job types)
205+
if (item instanceof Job) {
206+
Job<?, ?> job = (Job<?, ?>) item;
193207
String pipelineName = job.getFullName();
194-
List<WorkflowRun> allBuilds = ((WorkflowJob) job).getBuilds();
208+
List<? extends Run<?, ?>> allBuilds = job.getBuilds();
195209
if(!allBuilds.isEmpty()) {
196210
allBuilds.stream().filter(build -> Instant.ofEpochMilli(build.getTimeInMillis()).isAfter(thresholdInstant)).forEach(
197211
build -> {
@@ -201,7 +215,19 @@ private static List<PipelineDetails> getAllBuilds(BrowserStackCredentials browse
201215
long endTimeInMillis = build.getTimeInMillis();
202216
Timestamp endTime = new Timestamp(endTimeInMillis);
203217
String result = overallResult != null ? overallResult.toString() : null;
204-
PipelineDetails pipelineDetail = new PipelineDetails(pipelineName, buildNumber, duration, result, endTime);
218+
219+
// Get root upstream project information for QEI with build number (returns in format "project#build")
220+
String rootUpstreamProject = "";
221+
String immediateParentProject = "";
222+
try {
223+
rootUpstreamProject = UpstreamPipelineResolver.resolveRootUpstreamProject(build, browserStackCredentials);
224+
immediateParentProject = UpstreamPipelineResolver.resolveImmediateUpstreamProjectForQEI(build, browserStackCredentials);
225+
} catch (Exception e) {
226+
LOGGER.warning("Error resolving upstream project for " + pipelineName + " build number " + buildNumber + ": " + e.getMessage());
227+
e.printStackTrace();
228+
}
229+
PipelineDetails pipelineDetail = new PipelineDetails(pipelineName, buildNumber, duration, result,
230+
endTime, rootUpstreamProject, immediateParentProject);
205231
allBuildResults.add(pipelineDetail);
206232
}
207233
);
@@ -249,9 +275,8 @@ private static int getHistoryForDays(BrowserStackCredentials browserStackCredent
249275
}
250276
} catch(IOException e) {
251277
e.printStackTrace();
252-
} finally {
253-
return no_of_days;
254278
}
279+
return no_of_days;
255280
}
256281

257282
private static int getProjectPageSize(BrowserStackCredentials browserStackCredentials) {
@@ -268,9 +293,8 @@ private static int getProjectPageSize(BrowserStackCredentials browserStackCreden
268293
}
269294
} catch(IOException e) {
270295
e.printStackTrace();
271-
} finally {
272-
return projectPageSize;
273296
}
297+
return projectPageSize;
274298
}
275299

276300
private static int getResultPageSize(BrowserStackCredentials browserStackCredentials) {
@@ -287,9 +311,8 @@ private static int getResultPageSize(BrowserStackCredentials browserStackCredent
287311
}
288312
} catch(IOException e) {
289313
e.printStackTrace();
290-
} finally {
291-
return resultPageSize;
292314
}
315+
return resultPageSize;
293316
}
294317
}
295318

@@ -307,13 +330,35 @@ class PipelineDetails {
307330

308331
@JsonProperty("endTime")
309332
private Timestamp endTime;
310-
311-
public PipelineDetails(String pipelineName, Integer buildNumber, Long buildDuration, String buildStatus, Timestamp endTime) {
333+
334+
@JsonProperty("rootProject")
335+
private String rootProject;
336+
337+
@JsonProperty("immediateParentProject")
338+
private String immediateParentProject;
339+
340+
public PipelineDetails(String pipelineName, Integer buildNumber, Long buildDuration, String buildStatus,
341+
Timestamp endTime, String rootProject, String immediateParentProject) {
312342
this.pipelineName = pipelineName;
313343
this.buildNumber = buildNumber;
314344
this.buildDuration = buildDuration;
315345
this.buildStatus = buildStatus;
316346
this.endTime = endTime;
347+
this.rootProject = rootProject;
348+
this.immediateParentProject = immediateParentProject;
349+
}
350+
}
351+
352+
class PipelineInfo implements Serializable {
353+
@JsonProperty("pipelineName")
354+
private String pipelineName;
355+
356+
@JsonProperty("jobType")
357+
private String jobType;
358+
359+
public PipelineInfo(String pipelineName, String jobType) {
360+
this.pipelineName = pipelineName;
361+
this.jobType = jobType;
317362
}
318363
}
319364

@@ -325,9 +370,9 @@ class PipelinesPaginated {
325370
private int totalPages;
326371

327372
@JsonProperty("pipelines")
328-
private List<String> pipelines;
373+
private List<PipelineInfo> pipelines;
329374

330-
public PipelinesPaginated(int page, int totalPages, List<String> pipelines) {
375+
public PipelinesPaginated(int page, int totalPages, List<PipelineInfo> pipelines) {
331376
this.page = page;
332377
this.totalPages = totalPages;
333378
this.pipelines = pipelines;

src/main/java/com/browserstack/automate/ci/jenkins/qualityDashboard/QualityDashboardInitItemListener.java

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,33 +11,39 @@
1111
import okhttp3.MediaType;
1212
import okhttp3.RequestBody;
1313
import okhttp3.Response;
14-
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
15-
import java.io.IOException;
1614
import java.io.Serializable;
15+
import java.util.logging.Logger;
1716

1817
@Extension
1918
public class QualityDashboardInitItemListener extends ItemListener {
2019

20+
private static final Logger LOGGER = Logger.getLogger(QualityDashboardInitItemListener.class.getName());
21+
2122
@Override
2223
public void onCreated(Item job) {
2324
try {
2425
BrowserStackCredentials browserStackCredentials = QualityDashboardUtil.getBrowserStackCreds();
26+
if(browserStackCredentials == null) {
27+
LOGGER.warning("BrowserStackCredentials not found. Please ensure they are configured correctly.");
28+
return;
29+
}
2530
QualityDashboardAPIUtil apiUtil = new QualityDashboardAPIUtil();
26-
apiUtil.logToQD(browserStackCredentials, "Item Created : " + job.getClass().getName()) ;
2731

2832
String itemName = job.getFullName();
29-
String itemType = getItemTypeModified(job);
33+
String itemType = QualityDashboardUtil.getItemTypeModified(job);
3034

31-
apiUtil.logToQD(browserStackCredentials, "Item Type : " + itemType + " : " + itemName + " : " + itemType.equals("PIPELINE"));
32-
if(itemType != null && itemType.equals("PIPELINE")) {
35+
apiUtil.logToQD(browserStackCredentials, "Item Created : " + itemName + " - " + "Item Type : " + itemType);
36+
if(itemType != null && !itemType.equals("FOLDER")) {
3337
try {
34-
apiUtil.logToQD(browserStackCredentials, "Item Type inside the Folder : " + itemType + " : " + itemName + " : " + itemType.equals("PIPELINE"));
3538
String jsonBody = getJsonReqBody(new ItemUpdate(itemName, itemType));
3639
syncItemListToQD(jsonBody, Constants.QualityDashboardAPI.getItemCrudEndpoint(), "POST");
3740

38-
} catch(IOException e) {
41+
} catch(Exception e) {
42+
LOGGER.warning("Error syncing item creation to Quality Dashboard: " + e.getMessage());
3943
e.printStackTrace();
4044
}
45+
} else {
46+
apiUtil.logToQD(browserStackCredentials, "Skipping item creation sync: " + itemName);
4147
}
4248
} catch(Exception e) {
4349
e.printStackTrace();
@@ -47,42 +53,34 @@ public void onCreated(Item job) {
4753
@Override
4854
public void onDeleted(Item job) {
4955
String itemName = job.getFullName();
50-
String itemType = getItemTypeModified(job);
56+
String itemType = QualityDashboardUtil.getItemTypeModified(job);
5157
if(itemType != null) {
5258
try {
5359
String jsonBody = getJsonReqBody(new ItemUpdate(itemName, itemType));
5460
syncItemListToQD(jsonBody, Constants.QualityDashboardAPI.getItemCrudEndpoint(), "DELETE");
55-
} catch(IOException e) {
61+
} catch(Exception e) {
62+
LOGGER.warning("Error syncing item deletion to Quality Dashboard: " + e.getMessage());
5663
e.printStackTrace();
5764
}
5865
}
5966
}
6067

6168
@Override
6269
public void onRenamed(Item job, String oldName, String newName) {
63-
String itemType = getItemTypeModified(job);
70+
String itemType = QualityDashboardUtil.getItemTypeModified(job);
6471
if(itemType != null) {
6572
try {
6673
oldName = job.getParent().getFullName() + "/" + oldName;
6774
newName = job.getParent().getFullName() + "/" + newName;
6875
String jsonBody = getJsonReqBody(new ItemRename(oldName, newName, itemType));
6976
syncItemListToQD(jsonBody, Constants.QualityDashboardAPI.getItemCrudEndpoint(), "PUT");
70-
} catch(IOException e) {
77+
} catch(Exception e) {
78+
LOGGER.warning("Error syncing item rename to Quality Dashboard: " + e.getMessage());
7179
e.printStackTrace();
7280
}
7381
}
7482
}
7583

76-
private String getItemTypeModified(Item job) {
77-
String itemType = null;
78-
boolean isFolderRenamed = job.getClass().getName().contains("Folder");
79-
boolean isPipelineRenamed = job instanceof WorkflowJob;
80-
if(isFolderRenamed || isPipelineRenamed) {
81-
itemType = isPipelineRenamed ? "PIPELINE" : "FOLDER";
82-
}
83-
return itemType;
84-
}
85-
8684
private <T> String getJsonReqBody( T item) throws JsonProcessingException {
8785
ObjectMapper objectMapper = new ObjectMapper();
8886
String jsonBody = objectMapper.writeValueAsString(item);
@@ -93,6 +91,10 @@ private Response syncItemListToQD(String jsonBody, String url, String typeOfRequ
9391
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json"), jsonBody);
9492
QualityDashboardAPIUtil apiUtil = new QualityDashboardAPIUtil();
9593
BrowserStackCredentials browserStackCredentials = QualityDashboardUtil.getBrowserStackCreds();
94+
if(browserStackCredentials == null) {
95+
LOGGER.warning("BrowserStack credentials not found. Please ensure they are configured correctly.");
96+
return null;
97+
}
9698
if(typeOfRequest.equals("PUT")) {
9799
apiUtil.logToQD(browserStackCredentials, "Syncing Item Update - PUT");
98100
return apiUtil.makePutRequestToQd(url, browserStackCredentials, requestBody);

0 commit comments

Comments
 (0)