Skip to content

Commit cd1fb7c

Browse files
committed
Parent child relationship
1 parent 691cdaa commit cd1fb7c

File tree

3 files changed

+318
-20
lines changed

3 files changed

+318
-20
lines changed

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

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,13 @@ private static List<PipelineDetails> getAllBuilds(BrowserStackCredentials browse
201201
long endTimeInMillis = build.getTimeInMillis();
202202
Timestamp endTime = new Timestamp(endTimeInMillis);
203203
String result = overallResult != null ? overallResult.toString() : null;
204-
PipelineDetails pipelineDetail = new PipelineDetails(pipelineName, buildNumber, duration, result, endTime);
204+
205+
// Get root upstream project information for QEI with build number (returns in format "project#build")
206+
String rootUpstreamProject = UpstreamPipelineResolver.resolveRootUpstreamProject(build, browserStackCredentials);
207+
// Get immediate parent project information for QEI (returns in format "project#build")
208+
String immediateParentProject = UpstreamPipelineResolver.resolveImmediateUpstreamProjectForQEI(build, browserStackCredentials);
209+
PipelineDetails pipelineDetail = new PipelineDetails(pipelineName, buildNumber, duration, result,
210+
endTime, rootUpstreamProject, immediateParentProject);
205211
allBuildResults.add(pipelineDetail);
206212
}
207213
);
@@ -307,13 +313,22 @@ class PipelineDetails {
307313

308314
@JsonProperty("endTime")
309315
private Timestamp endTime;
310-
311-
public PipelineDetails(String pipelineName, Integer buildNumber, Long buildDuration, String buildStatus, Timestamp endTime) {
316+
317+
@JsonProperty("rootProject")
318+
private String rootProject;
319+
320+
@JsonProperty("immediateParentProject")
321+
private String immediateParentProject;
322+
323+
public PipelineDetails(String pipelineName, Integer buildNumber, Long buildDuration, String buildStatus,
324+
Timestamp endTime, String rootProject, String immediateParentProject) {
312325
this.pipelineName = pipelineName;
313326
this.buildNumber = buildNumber;
314327
this.buildDuration = buildDuration;
315328
this.buildStatus = buildStatus;
316329
this.endTime = endTime;
330+
this.rootProject = rootProject;
331+
this.immediateParentProject = immediateParentProject;
317332
}
318333
}
319334

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

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import java.nio.file.Path;
2525
import java.nio.file.Paths;
2626
import java.sql.Timestamp;
27-
import java.util.List;
2827

2928
@Extension
3029
public class QualityDashboardPipelineTracker extends RunListener<Run> {
@@ -102,8 +101,14 @@ private void sendBuildDataToQD(Run run, Result overallResult, String finalZipPat
102101
jobUrl = rootUrl + run.getUrl();
103102
}
104103

104+
// Get root upstream project information for QEI with build number (returns in format "project#build")
105+
String rootUpstreamProject = UpstreamPipelineResolver.resolveRootUpstreamProject(run, browserStackCredentials);
106+
// Get immediate parent project information for QEI (returns in format "project#build")
107+
String immediateParentProject = UpstreamPipelineResolver.resolveImmediateUpstreamProjectForQEI(run, browserStackCredentials);
108+
105109
Timestamp endTime = new Timestamp(endTimeInMillis);
106-
PipelineResults pipelineResultsReqObj = new PipelineResults(buildNumber, pipelineDuration, overallResult.toString(), finalZipPath, jobName, endTime, jobUrl);
110+
PipelineResults pipelineResultsReqObj = new PipelineResults(buildNumber, pipelineDuration, overallResult.toString(),
111+
finalZipPath, jobName, endTime, jobUrl, rootUpstreamProject, immediateParentProject);
107112
ObjectMapper objectMapper = new ObjectMapper();
108113
String jsonBody = objectMapper.writeValueAsString(pipelineResultsReqObj);
109114

@@ -243,7 +248,7 @@ private String uploadZipToQd(String pathToZip, BrowserStackCredentials browserSt
243248

244249
private void copyDirectoryToParentIfRequired(Run run, String finalParentPathFrom, BrowserStackCredentials browserStackCredentials) throws IOException {
245250
String finalParentPathTo = null;
246-
String upStreamProj = upStreamPipelineUrl(run);
251+
String upStreamProj = UpstreamPipelineResolver.resolveImmediateUpstreamProject(run, browserStackCredentials);
247252
if(StringUtils.isNotEmpty(upStreamProj)) {
248253
String parentResultDir = getResultDirForPipeline(upStreamProj, browserStackCredentials, run.getNumber());
249254
if(StringUtils.isNotEmpty(parentResultDir) && checkIfPathIsFound(parentResultDir)) {
@@ -268,18 +273,6 @@ private void copyDirectoryToParentIfRequired(Run run, String finalParentPathFrom
268273
}
269274
}
270275
}
271-
272-
private String upStreamPipelineUrl(Run run) {
273-
String upstreamProjectName = null;
274-
List<Cause> causes = run.getCauses();
275-
for (Cause cause : causes) {
276-
if (cause instanceof Cause.UpstreamCause) {
277-
Cause.UpstreamCause upstreamCause = (Cause.UpstreamCause) cause;
278-
upstreamProjectName = upstreamCause.getUpstreamProject();
279-
}
280-
}
281-
return upstreamProjectName;
282-
}
283276
}
284277

285278
class QualityDashboardGetDetailsForPipeline implements Serializable {
@@ -310,14 +303,24 @@ class PipelineResults implements Serializable {
310303

311304
@JsonProperty("zipFile")
312305
private String zipFile;
313-
314-
public PipelineResults(Integer buildNumber, Long buildDuration, String buildStatus, String zipFile, String pipelineName, Timestamp endTime, String jobUrl) {
306+
307+
@JsonProperty("rootProject")
308+
private String rootProject;
309+
310+
@JsonProperty("immediateParentProject")
311+
private String immediateParentProject;
312+
313+
public PipelineResults(Integer buildNumber, Long buildDuration, String buildStatus, String zipFile,
314+
String pipelineName, Timestamp endTime, String jobUrl, String rootProject,
315+
String immediateParentProject) {
315316
this.buildNumber = buildNumber;
316317
this.buildDuration = buildDuration;
317318
this.buildStatus = buildStatus;
318319
this.zipFile = zipFile;
319320
this.pipelineName = pipelineName;
320321
this.endTime = endTime;
321322
this.jobUrl = jobUrl;
323+
this.rootProject = rootProject;
324+
this.immediateParentProject = immediateParentProject;
322325
}
323326
}
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
package com.browserstack.automate.ci.jenkins.qualityDashboard;
2+
3+
import com.browserstack.automate.ci.jenkins.BrowserStackCredentials;
4+
import com.fasterxml.jackson.core.JsonProcessingException;
5+
import hudson.model.*;
6+
import jenkins.model.Jenkins;
7+
8+
import java.util.HashSet;
9+
import java.util.List;
10+
import java.util.Set;
11+
12+
13+
public class UpstreamPipelineResolver {
14+
15+
private static final QualityDashboardAPIUtil apiUtil = new QualityDashboardAPIUtil();
16+
17+
/**
18+
* Centralized method to handle JsonProcessingException logging.
19+
* This reduces duplication of exception handling code throughout the class.
20+
*
21+
* @param browserStackCredentials The credentials required for logging
22+
* @param message The error message to log
23+
*/
24+
private static void logError(BrowserStackCredentials browserStackCredentials, String message) {
25+
try {
26+
apiUtil.logToQD(browserStackCredentials, message);
27+
} catch (JsonProcessingException ex) {
28+
throw new RuntimeException("Failed to log error: " + ex.getMessage(), ex);
29+
}
30+
}
31+
32+
/**
33+
* Resolves the root upstream project with its build number in the format "project#build".
34+
* This method returns both the project name and build number, which is useful for
35+
* identifying the root project in the pipeline chain.
36+
*
37+
* @param run The current build run
38+
* @param browserStackCredentials The BrowserStack credentials for API access
39+
* @return The root upstream project with build number in format "project#build", or null if none found
40+
*/
41+
public static String resolveRootUpstreamProject(Run<?, ?> run, BrowserStackCredentials browserStackCredentials) {
42+
try {
43+
Set<String> visitedProjects = new HashSet<>();
44+
return findRootUpstreamProject(run, browserStackCredentials, visitedProjects);
45+
} catch (Exception e) {
46+
logError(browserStackCredentials, "Error resolving root upstream project for " +
47+
getProjectName(run) + "#" + run.getNumber() + ": " + e.getMessage());
48+
return null;
49+
}
50+
}
51+
52+
private static String findRootUpstreamProject(Run<?, ?> run, BrowserStackCredentials browserStackCredentials,
53+
Set<String> visitedProjects) {
54+
if (run == null) {
55+
return null;
56+
}
57+
58+
String currentProject = getProjectName(run);
59+
60+
// Cycle detection - prevent infinite recursion
61+
if (visitedProjects.contains(currentProject)) {
62+
logError(browserStackCredentials, "Circular dependency detected in project: " + currentProject);
63+
return null;
64+
}
65+
66+
visitedProjects.add(currentProject);
67+
68+
List<Cause> causes = run.getCauses();
69+
if (causes == null || causes.isEmpty()) {
70+
// No causes found - this is likely a root project
71+
return null;
72+
}
73+
74+
String rootProject = null;
75+
76+
for (Cause cause : causes) {
77+
String upstreamProject = processUpstreamCause(cause, run, browserStackCredentials, visitedProjects);
78+
if (upstreamProject != null) {
79+
// If we found an upstream project, that becomes our root candidate
80+
rootProject = upstreamProject;
81+
break; // Use first valid upstream cause
82+
}
83+
}
84+
85+
return rootProject;
86+
}
87+
88+
private static String processUpstreamCause(Cause cause, Run<?, ?> run,
89+
BrowserStackCredentials browserStackCredentials,
90+
Set<String> visitedProjects) {
91+
try {
92+
if (cause instanceof Cause.UpstreamCause) {
93+
return handleUpstreamCause((Cause.UpstreamCause) cause, browserStackCredentials, visitedProjects);
94+
} else if (cause instanceof hudson.triggers.TimerTrigger.TimerTriggerCause) {
95+
apiUtil.logToQD(browserStackCredentials, "Build triggered by timer/schedule");
96+
return null;
97+
} else if (cause instanceof hudson.triggers.SCMTrigger.SCMTriggerCause) {
98+
apiUtil.logToQD(browserStackCredentials, "Build triggered by SCM change");
99+
return null;
100+
} else if (cause instanceof Cause.UserIdCause) {
101+
Cause.UserIdCause userCause = (Cause.UserIdCause) cause;
102+
apiUtil.logToQD(browserStackCredentials, "Build triggered manually by user: " +
103+
getUserDisplayName(userCause));
104+
return null;
105+
} else if (cause instanceof Cause.RemoteCause) {
106+
Cause.RemoteCause remoteCause = (Cause.RemoteCause) cause;
107+
apiUtil.logToQD(browserStackCredentials, "Build triggered remotely from: " +
108+
remoteCause.getAddr());
109+
return null;
110+
} else {
111+
// Handle unknown cause types
112+
apiUtil.logToQD(browserStackCredentials, "Unknown build cause type: " +
113+
cause.getClass().getSimpleName());
114+
return null;
115+
}
116+
} catch (JsonProcessingException e) {
117+
logError(browserStackCredentials, "Error processing cause: " + e.getMessage());
118+
return null;
119+
}
120+
}
121+
122+
/**
123+
* Handles upstream cause by recursively finding the root upstream project and its build number.
124+
* This method always returns the project name and build number in the format "project#build".
125+
*
126+
* @param upstreamCause The upstream cause to process
127+
* @param browserStackCredentials The BrowserStack credentials for API access
128+
* @param visitedProjects Set of already visited projects to detect cycles
129+
* @return The root upstream project with build number in format "project#build", or null if none found
130+
*/
131+
private static String handleUpstreamCause(Cause.UpstreamCause upstreamCause,
132+
BrowserStackCredentials browserStackCredentials,
133+
Set<String> visitedProjects) {
134+
try {
135+
String upstreamProjectName = upstreamCause.getUpstreamProject();
136+
int upstreamBuildNumber = upstreamCause.getUpstreamBuild();
137+
138+
if (upstreamProjectName == null || upstreamProjectName.trim().isEmpty()) {
139+
apiUtil.logToQD(browserStackCredentials, "Invalid upstream project name");
140+
return null;
141+
}
142+
143+
apiUtil.logToQD(browserStackCredentials, "Found upstream: " + upstreamProjectName +
144+
"#" + upstreamBuildNumber);
145+
146+
// Try to get the upstream run for recursive traversal
147+
Run<?, ?> upstreamRun = getUpstreamRun(upstreamProjectName, upstreamBuildNumber);
148+
if (upstreamRun != null) {
149+
// Recursively check if this upstream has its own upstream
150+
String rootProject = findRootUpstreamProject(upstreamRun, browserStackCredentials, visitedProjects);
151+
if (rootProject != null) {
152+
// Found a higher-level upstream, return that
153+
return rootProject;
154+
}
155+
}
156+
// Return in format projectName#buildNumber
157+
String formattedResult = upstreamProjectName + "#" + upstreamBuildNumber;
158+
apiUtil.logToQD(browserStackCredentials, "Resolved root upstream project: " + formattedResult);
159+
return formattedResult;
160+
} catch (JsonProcessingException e) {
161+
logError(browserStackCredentials, "Error processing upstream cause: " + e.getMessage());
162+
return null;
163+
} catch (Exception e) {
164+
logError(browserStackCredentials, "Unexpected error while handling upstream cause: " + e.getMessage());
165+
return null;
166+
}
167+
}
168+
169+
private static Run<?, ?> getUpstreamRun(String projectName, int buildNumber) {
170+
try {
171+
Jenkins jenkins = Jenkins.getInstanceOrNull();
172+
if (jenkins == null) {
173+
return null;
174+
}
175+
176+
Job<?, ?> job = jenkins.getItemByFullName(projectName, Job.class);
177+
if (job == null) {
178+
return null;
179+
}
180+
181+
return job.getBuildByNumber(buildNumber);
182+
} catch (Exception e) {
183+
// upstream build might not exist anymore
184+
return null;
185+
}
186+
}
187+
188+
private static String getProjectName(Run<?, ?> run) {
189+
try {
190+
return run.getParent().getFullName();
191+
} catch (Exception e) {
192+
return "unknown";
193+
}
194+
}
195+
196+
private static String getUserDisplayName(Cause.UserIdCause userCause) {
197+
try {
198+
String userName = userCause.getUserName();
199+
String userId = userCause.getUserId();
200+
return userName != null ? userName : userId;
201+
} catch (Exception e) {
202+
return "unknown user";
203+
}
204+
}
205+
206+
public static String resolveImmediateUpstreamProject(Run<?, ?> run, BrowserStackCredentials browserStackCredentials) {
207+
try {
208+
return findImmediateUpstreamProject(run, browserStackCredentials);
209+
} catch (Exception e) {
210+
logError(browserStackCredentials, "Error resolving immediate upstream project for " +
211+
getProjectName(run) + "#" + run.getNumber() + ": " + e.getMessage());
212+
return null;
213+
}
214+
}
215+
216+
public static String resolveImmediateUpstreamProjectForQEI(Run<?, ?> run, BrowserStackCredentials browserStackCredentials) {
217+
try {
218+
return findImmediateUpstreamProjectForQEI(run, browserStackCredentials);
219+
} catch (Exception e) {
220+
logError(browserStackCredentials, "Error resolving immediate upstream project for QEI for " +
221+
getProjectName(run) + "#" + run.getNumber() + ": " + e.getMessage());
222+
return null;
223+
}
224+
}
225+
226+
private static String findImmediateUpstreamProject(Run<?, ?> run, BrowserStackCredentials browserStackCredentials) throws JsonProcessingException {
227+
if (run == null) {
228+
return null;
229+
}
230+
231+
List<Cause> causes = run.getCauses();
232+
if (causes == null || causes.isEmpty()) {
233+
return null;
234+
}
235+
236+
for (Cause cause : causes) {
237+
if (cause instanceof Cause.UpstreamCause) {
238+
Cause.UpstreamCause upstreamCause = (Cause.UpstreamCause) cause;
239+
String upstreamProjectName = upstreamCause.getUpstreamProject();
240+
241+
if (upstreamProjectName != null && !upstreamProjectName.trim().isEmpty()) {
242+
apiUtil.logToQD(browserStackCredentials, "Found immediate upstream: " + upstreamProjectName);
243+
return upstreamProjectName;
244+
}
245+
}
246+
}
247+
248+
apiUtil.logToQD(browserStackCredentials, "No immediate upstream found for: " + getProjectName(run));
249+
return null;
250+
}
251+
252+
private static String findImmediateUpstreamProjectForQEI(Run<?, ?> run, BrowserStackCredentials browserStackCredentials) throws JsonProcessingException {
253+
if (run == null) {
254+
return null;
255+
}
256+
257+
List<Cause> causes = run.getCauses();
258+
if (causes == null || causes.isEmpty()) {
259+
return null;
260+
}
261+
262+
for (Cause cause : causes) {
263+
if (cause instanceof Cause.UpstreamCause) {
264+
Cause.UpstreamCause upstreamCause = (Cause.UpstreamCause) cause;
265+
String upstreamProjectName = upstreamCause.getUpstreamProject();
266+
int upstreamBuildNumber = upstreamCause.getUpstreamBuild();
267+
268+
if (upstreamProjectName != null && !upstreamProjectName.trim().isEmpty()) {
269+
String formattedResult = upstreamProjectName + "#" + upstreamBuildNumber;
270+
apiUtil.logToQD(browserStackCredentials, "Found immediate upstream for QEI: " + formattedResult);
271+
return formattedResult;
272+
}
273+
}
274+
}
275+
276+
apiUtil.logToQD(browserStackCredentials, "No immediate upstream found with BuildNumber: " + getProjectName(run));
277+
return null;
278+
}
279+
280+
}

0 commit comments

Comments
 (0)