Skip to content

Commit 491e0df

Browse files
Ildar AsmandiyarovIldar Asmandiyarov
authored andcommitted
Fix Required Builds merge check by switching to repo-scoped Bitbucket builds API
1 parent 8937f80 commit 491e0df

6 files changed

Lines changed: 191 additions & 6 deletions

File tree

src/main/java/org/jenkinsci/plugins/stashNotifier/BuildStatusUriFactory.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,10 @@ public static URI create(String baseUri, String commit) {
1313
String uri = String.join("/", tidyBase, "rest/build-status/1.0/commits", commit);
1414
return URI.create(uri);
1515
}
16+
17+
public static URI create(String baseUri, String projectKey, String repoSlug, String commit) {
18+
String tidyBase = StringUtils.removeEnd(baseUri, "/");
19+
String uri = String.join("/", tidyBase, "rest/api/latest/projects", projectKey, "repos", repoSlug, "commits", commit, "builds");
20+
return URI.create(uri);
21+
}
1622
}

src/main/java/org/jenkinsci/plugins/stashNotifier/DefaultApacheHttpNotifier.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,11 @@ class DefaultApacheHttpNotifier implements HttpNotifier {
6060
try (CloseableHttpClient client = getHttpClient(logger, uri, settings.isIgnoreUnverifiedSSL())) {
6161
HttpPost req = createRequest(uri, payload, settings.getCredentials(), context);
6262
HttpResponse res = client.execute(req);
63-
if (res.getStatusLine().getStatusCode() != 204) {
64-
return NotificationResult.newFailure(EntityUtils.toString(res.getEntity()));
65-
} else {
63+
int statusCode = res.getStatusLine().getStatusCode();
64+
if (statusCode >= 200 && statusCode < 300) {
6665
return NotificationResult.newSuccess();
66+
} else {
67+
return NotificationResult.newFailure(EntityUtils.toString(res.getEntity()));
6768
}
6869
} catch (Exception e) {
6970
LOGGER.warn("{} failed to send {} to Bitbucket Server at {}", context.getRunId(), payload, uri, e);

src/main/java/org/jenkinsci/plugins/stashNotifier/StashNotifier.java

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -862,7 +862,15 @@ protected NotificationResult notifyStash(
862862
Credentials stringCredentials
863863
= getCredentials(StringCredentials.class, run.getParent());
864864

865-
URI uri = BuildStatusUriFactory.create(stashURL, commitSha1);
865+
String[] repoInfo = resolveBitbucketRepo(run, logger);
866+
URI uri;
867+
if (repoInfo != null) {
868+
uri = BuildStatusUriFactory.create(stashURL, repoInfo[0], repoInfo[1], commitSha1);
869+
logger.println("Using repo-scoped builds API: " + uri);
870+
} else {
871+
uri = BuildStatusUriFactory.create(stashURL, commitSha1);
872+
logger.println("Using legacy build-status API: " + uri);
873+
}
866874
NotificationSettings settings = new NotificationSettings(
867875
ignoreUnverifiedSSLPeer || getDescriptor().isIgnoreUnverifiedSsl(),
868876
stringCredentials != null ? stringCredentials : usernamePasswordCredentials
@@ -1023,11 +1031,13 @@ private JSONObject createNotificationPayload(
10231031
TaskListener listener) {
10241032

10251033
JSONObject json = new JSONObject();
1034+
String buildKey = abbreviate(getBuildKey(run, listener), MAX_FIELD_LENGTH);
10261035
json.put("state", state.name());
1027-
json.put("key", abbreviate(getBuildKey(run, listener), MAX_FIELD_LENGTH));
1036+
json.put("key", buildKey);
10281037
json.put("name", abbreviate(getBuildName(run), MAX_FIELD_LENGTH));
10291038
json.put("description", abbreviate(getBuildDescription(run, state), MAX_FIELD_LENGTH));
10301039
json.put("url", abbreviate(getBuildUrl(run), MAX_URL_FIELD_LENGTH));
1040+
json.put("parent", buildKey);
10311041
return json;
10321042
}
10331043

@@ -1109,6 +1119,87 @@ private static String idOf(Run<?, ?> run) {
11091119
return run != null ? run.getExternalizableId() : "(absent run)";
11101120
}
11111121

1122+
/**
1123+
* Extracts the Bitbucket project key and repository slug from a Git remote URL.
1124+
* Supports common Bitbucket Server URL formats:
1125+
* <ul>
1126+
* <li>HTTPS: {@code https://host/scm/PROJECT/repo.git}</li>
1127+
* <li>SSH: {@code ssh://git@host:7999/PROJECT/repo.git}</li>
1128+
* <li>SCP-style: {@code git@host:PROJECT/repo.git}</li>
1129+
* <li>Personal repos: {@code https://host/scm/~user/repo.git}</li>
1130+
* </ul>
1131+
*
1132+
* @param remoteUrl the Git remote URL
1133+
* @return a two-element array [projectKey, repoSlug], or null if parsing fails
1134+
*/
1135+
static String[] parseBitbucketRemoteUrl(String remoteUrl) {
1136+
if (remoteUrl == null || remoteUrl.isEmpty()) {
1137+
return null;
1138+
}
1139+
1140+
// Remove trailing .git
1141+
String url = remoteUrl.replaceAll("\\.git$", "");
1142+
1143+
// Handle SCP-style: git@host:PROJECT/repo
1144+
if (url.matches("^[^/]+@[^:]+:.+/.+$") && !url.startsWith("ssh://")) {
1145+
String path = url.substring(url.indexOf(':') + 1);
1146+
String[] parts = path.split("/");
1147+
if (parts.length >= 2) {
1148+
return new String[]{parts[parts.length - 2], parts[parts.length - 1]};
1149+
}
1150+
return null;
1151+
}
1152+
1153+
// Handle URL-style (https://, ssh://)
1154+
// Extract path and get last two segments
1155+
String path;
1156+
try {
1157+
URI uri = URI.create(url);
1158+
path = uri.getPath();
1159+
} catch (Exception e) {
1160+
return null;
1161+
}
1162+
1163+
if (path == null || path.isEmpty()) {
1164+
return null;
1165+
}
1166+
1167+
// Remove /scm/ prefix if present (Bitbucket Server HTTPS clone URLs use /scm/)
1168+
path = path.replaceFirst("^/scm/", "/");
1169+
1170+
// Remove leading slash and split
1171+
path = path.replaceFirst("^/", "");
1172+
String[] segments = path.split("/");
1173+
if (segments.length >= 2) {
1174+
return new String[]{segments[segments.length - 2], segments[segments.length - 1]};
1175+
}
1176+
1177+
return null;
1178+
}
1179+
1180+
/**
1181+
* Resolves the Bitbucket project key and repository slug for the current build.
1182+
* Uses manually configured values if available, otherwise auto-detects from Git remote URL.
1183+
*
1184+
* @param run the current build run
1185+
* @param logger the logger for output
1186+
* @return a two-element array [projectKey, repoSlug], or null if not resolvable
1187+
*/
1188+
private String[] resolveBitbucketRepo(Run<?, ?> run, PrintStream logger) {
1189+
for (BuildData buildData : run.getActions(BuildData.class)) {
1190+
for (String remoteUrl : buildData.getRemoteUrls()) {
1191+
String[] parsed = parseBitbucketRemoteUrl(remoteUrl);
1192+
if (parsed != null) {
1193+
logger.println("Auto-detected Bitbucket project: " + parsed[0] + ", repo: " + parsed[1] + " from " + remoteUrl);
1194+
return parsed;
1195+
}
1196+
}
1197+
}
1198+
1199+
logger.println("Could not determine Bitbucket project/repo - falling back to legacy build-status API");
1200+
return null;
1201+
}
1202+
11121203
/**
11131204
* Returns the build name to be pushed. This will select the specifically overwritten build name
11141205
* or get the build name from the {@link Run}.

src/test/java/org/jenkinsci/plugins/stashNotifier/BuildStatusUriFactoryTest.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,28 @@ void shouldHandleBasePathNoTrailingSlash() {
4040
URI actual = BuildStatusUriFactory.create(baseUri, "25a4b3c9b494fc7ac65b80e3b0ecce63f235f20d");
4141
assertThat(actual, equalTo(expected));
4242
}
43+
44+
@Test
45+
void shouldCreateRepoScopedUri() {
46+
String baseUri = "http://localhost:12345";
47+
URI expected = URI.create("http://localhost:12345/rest/api/latest/projects/PROJ/repos/my-repo/commits/abc123/builds");
48+
URI actual = BuildStatusUriFactory.create(baseUri, "PROJ", "my-repo", "abc123");
49+
assertThat(actual, equalTo(expected));
50+
}
51+
52+
@Test
53+
void shouldCreateRepoScopedUriWithTrailingSlash() {
54+
String baseUri = "http://localhost:12345/";
55+
URI expected = URI.create("http://localhost:12345/rest/api/latest/projects/PROJ/repos/my-repo/commits/abc123/builds");
56+
URI actual = BuildStatusUriFactory.create(baseUri, "PROJ", "my-repo", "abc123");
57+
assertThat(actual, equalTo(expected));
58+
}
59+
60+
@Test
61+
void shouldCreateRepoScopedUriWithBasePath() {
62+
String baseUri = "http://localhost:12345/bitbucket";
63+
URI expected = URI.create("http://localhost:12345/bitbucket/rest/api/latest/projects/PROJ/repos/my-repo/commits/25a4b3c9/builds");
64+
URI actual = BuildStatusUriFactory.create(baseUri, "PROJ", "my-repo", "25a4b3c9");
65+
assertThat(actual, equalTo(expected));
66+
}
4367
}

src/test/java/org/jenkinsci/plugins/stashNotifier/DefaultApacheHttpNotifierTest.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,17 +156,35 @@ private NotificationResult notifyStash(int statusCode) throws Exception {
156156
}
157157

158158
@Test
159-
void notifyStash_success() throws Exception {
159+
void notifyStash_success204() throws Exception {
160160
NotificationResult notificationResult = notifyStash(204);
161161
assertThat(notificationResult.indicatesSuccess, is(true));
162162
}
163163

164+
@Test
165+
void notifyStash_success200() throws Exception {
166+
NotificationResult notificationResult = notifyStash(200);
167+
assertThat(notificationResult.indicatesSuccess, is(true));
168+
}
169+
170+
@Test
171+
void notifyStash_success201() throws Exception {
172+
NotificationResult notificationResult = notifyStash(201);
173+
assertThat(notificationResult.indicatesSuccess, is(true));
174+
}
175+
164176
@Test
165177
void notifyStash_fail() throws Exception {
166178
NotificationResult notificationResult = notifyStash(400);
167179
assertThat(notificationResult.indicatesSuccess, is(false));
168180
}
169181

182+
@Test
183+
void notifyStash_fail500() throws Exception {
184+
NotificationResult notificationResult = notifyStash(500);
185+
assertThat(notificationResult.indicatesSuccess, is(false));
186+
}
187+
170188
@Test
171189
void notifyStashUsesRequestParameters() throws Exception {
172190
notifyStash(204);

src/test/java/org/jenkinsci/plugins/stashNotifier/StashNotifierTest.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1014,4 +1014,49 @@ void setBuildStatus_null() {
10141014
sn.setBuildStatus(null);
10151015
assertThat(sn.getBuildStatus(), equalTo(StashBuildState.SUCCESSFUL));
10161016
}
1017+
1018+
@Test
1019+
void parseBitbucketRemoteUrl_https_with_scm() {
1020+
String[] result = StashNotifier.parseBitbucketRemoteUrl("https://bitbucket.example.com/scm/PROJ/my-repo.git");
1021+
assertThat(result[0], equalTo("PROJ"));
1022+
assertThat(result[1], equalTo("my-repo"));
1023+
}
1024+
1025+
@Test
1026+
void parseBitbucketRemoteUrl_ssh() {
1027+
String[] result = StashNotifier.parseBitbucketRemoteUrl("ssh://[email protected]:7999/PROJ/my-repo.git");
1028+
assertThat(result[0], equalTo("PROJ"));
1029+
assertThat(result[1], equalTo("my-repo"));
1030+
}
1031+
1032+
@Test
1033+
void parseBitbucketRemoteUrl_scp_style() {
1034+
String[] result = StashNotifier.parseBitbucketRemoteUrl("[email protected]:PROJ/my-repo.git");
1035+
assertThat(result[0], equalTo("PROJ"));
1036+
assertThat(result[1], equalTo("my-repo"));
1037+
}
1038+
1039+
@Test
1040+
void parseBitbucketRemoteUrl_personal_repo() {
1041+
String[] result = StashNotifier.parseBitbucketRemoteUrl("https://bitbucket.example.com/scm/~username/my-repo.git");
1042+
assertThat(result[0], equalTo("~username"));
1043+
assertThat(result[1], equalTo("my-repo"));
1044+
}
1045+
1046+
@Test
1047+
void parseBitbucketRemoteUrl_no_dotgit_suffix() {
1048+
String[] result = StashNotifier.parseBitbucketRemoteUrl("https://bitbucket.example.com/scm/PROJ/repo");
1049+
assertThat(result[0], equalTo("PROJ"));
1050+
assertThat(result[1], equalTo("repo"));
1051+
}
1052+
1053+
@Test
1054+
void parseBitbucketRemoteUrl_null() {
1055+
assertThat(StashNotifier.parseBitbucketRemoteUrl(null), nullValue());
1056+
}
1057+
1058+
@Test
1059+
void parseBitbucketRemoteUrl_empty() {
1060+
assertThat(StashNotifier.parseBitbucketRemoteUrl(""), nullValue());
1061+
}
10171062
}

0 commit comments

Comments
 (0)