Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,10 @@ public static URI create(String baseUri, String commit) {
String uri = String.join("/", tidyBase, "rest/build-status/1.0/commits", commit);
return URI.create(uri);
}

public static URI create(String baseUri, String projectKey, String repoSlug, String commit) {
String tidyBase = StringUtils.removeEnd(baseUri, "/");
String uri = String.join("/", tidyBase, "rest/api/latest/projects", projectKey, "repos", repoSlug, "commits", commit, "builds");
return URI.create(uri);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,11 @@ class DefaultApacheHttpNotifier implements HttpNotifier {
try (CloseableHttpClient client = getHttpClient(logger, uri, settings.isIgnoreUnverifiedSSL())) {
HttpPost req = createRequest(uri, payload, settings.getCredentials(), context);
HttpResponse res = client.execute(req);
if (res.getStatusLine().getStatusCode() != 204) {
return NotificationResult.newFailure(EntityUtils.toString(res.getEntity()));
} else {
int statusCode = res.getStatusLine().getStatusCode();
if (statusCode >= 200 && statusCode < 300) {
return NotificationResult.newSuccess();
} else {
return NotificationResult.newFailure(EntityUtils.toString(res.getEntity()));
}
} catch (Exception e) {
LOGGER.warn("{} failed to send {} to Bitbucket Server at {}", context.getRunId(), payload, uri, e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -862,7 +862,15 @@ protected NotificationResult notifyStash(
Credentials stringCredentials
= getCredentials(StringCredentials.class, run.getParent());

URI uri = BuildStatusUriFactory.create(stashURL, commitSha1);
String[] repoInfo = resolveBitbucketRepo(run, logger);
URI uri;
if (repoInfo != null) {
uri = BuildStatusUriFactory.create(stashURL, repoInfo[0], repoInfo[1], commitSha1);
logger.println("Using repo-scoped builds API: " + uri);
} else {
uri = BuildStatusUriFactory.create(stashURL, commitSha1);
logger.println("Using legacy build-status API: " + uri);
}
NotificationSettings settings = new NotificationSettings(
ignoreUnverifiedSSLPeer || getDescriptor().isIgnoreUnverifiedSsl(),
stringCredentials != null ? stringCredentials : usernamePasswordCredentials
Expand Down Expand Up @@ -1023,11 +1031,13 @@ private JSONObject createNotificationPayload(
TaskListener listener) {

JSONObject json = new JSONObject();
String buildKey = abbreviate(getBuildKey(run, listener), MAX_FIELD_LENGTH);
json.put("state", state.name());
json.put("key", abbreviate(getBuildKey(run, listener), MAX_FIELD_LENGTH));
json.put("key", buildKey);
json.put("name", abbreviate(getBuildName(run), MAX_FIELD_LENGTH));
json.put("description", abbreviate(getBuildDescription(run, state), MAX_FIELD_LENGTH));
json.put("url", abbreviate(getBuildUrl(run), MAX_URL_FIELD_LENGTH));
json.put("parent", buildKey);
return json;
}

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

/**
* Extracts the Bitbucket project key and repository slug from a Git remote URL.
* Supports common Bitbucket Server URL formats:
* <ul>
* <li>HTTPS: {@code https://host/scm/PROJECT/repo.git}</li>
* <li>SSH: {@code ssh://git@host:7999/PROJECT/repo.git}</li>
* <li>SCP-style: {@code git@host:PROJECT/repo.git}</li>
* <li>Personal repos: {@code https://host/scm/~user/repo.git}</li>
* </ul>
*
* @param remoteUrl the Git remote URL
* @return a two-element array [projectKey, repoSlug], or null if parsing fails
*/
static String[] parseBitbucketRemoteUrl(String remoteUrl) {
if (remoteUrl == null || remoteUrl.isEmpty()) {
return null;
}

// Remove trailing .git
String url = remoteUrl.replaceAll("\\.git$", "");

// Handle SCP-style: git@host:PROJECT/repo
if (url.matches("^[^/]+@[^:]+:.+/.+$") && !url.startsWith("ssh://")) {
String path = url.substring(url.indexOf(':') + 1);
String[] parts = path.split("/");
if (parts.length >= 2) {
return new String[]{parts[parts.length - 2], parts[parts.length - 1]};
}
return null;
}

// Handle URL-style (https://, ssh://)
// Extract path and get last two segments
String path;
try {
URI uri = URI.create(url);
path = uri.getPath();
} catch (Exception e) {
return null;
}

if (path == null || path.isEmpty()) {
return null;
}

// Remove /scm/ prefix if present (Bitbucket Server HTTPS clone URLs use /scm/)
path = path.replaceFirst("^/scm/", "/");

// Remove leading slash and split
path = path.replaceFirst("^/", "");
String[] segments = path.split("/");
if (segments.length >= 2) {
return new String[]{segments[segments.length - 2], segments[segments.length - 1]};
}

return null;
}

/**
* Resolves the Bitbucket project key and repository slug for the current build.
* Uses manually configured values if available, otherwise auto-detects from Git remote URL.
*
* @param run the current build run
* @param logger the logger for output
* @return a two-element array [projectKey, repoSlug], or null if not resolvable
*/
private String[] resolveBitbucketRepo(Run<?, ?> run, PrintStream logger) {
for (BuildData buildData : run.getActions(BuildData.class)) {
for (String remoteUrl : buildData.getRemoteUrls()) {
String[] parsed = parseBitbucketRemoteUrl(remoteUrl);
if (parsed != null) {
logger.println("Auto-detected Bitbucket project: " + parsed[0] + ", repo: " + parsed[1] + " from " + remoteUrl);
return parsed;
}
}
}

logger.println("Could not determine Bitbucket project/repo - falling back to legacy build-status API");
return null;
}

/**
* Returns the build name to be pushed. This will select the specifically overwritten build name
* or get the build name from the {@link Run}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,34 +10,58 @@
class BuildStatusUriFactoryTest {

@Test
void shouldHandleTrailingSlash() {
void shouldCreateLegacyUriWithTrailingSlash() {
String baseUri = "http://localhost:12345/";
URI expected = URI.create("http://localhost:12345/rest/build-status/1.0/commits/25a4b3c9b494fc7ac65b80e3b0ecce63f235f20d");
URI actual = BuildStatusUriFactory.create(baseUri, "25a4b3c9b494fc7ac65b80e3b0ecce63f235f20d");
assertThat(actual, equalTo(expected));
}

@Test
void shouldHandleNoTrailingSlash() {
void shouldCreateLegacyUriWithNoTrailingSlash() {
String baseUri = "http://localhost:12345";
URI expected = URI.create("http://localhost:12345/rest/build-status/1.0/commits/25a4b3c9b494fc7ac65b80e3b0ecce63f235f20d");
URI actual = BuildStatusUriFactory.create(baseUri, "25a4b3c9b494fc7ac65b80e3b0ecce63f235f20d");
assertThat(actual, equalTo(expected));
}

@Test
void shouldHandleBasePathTrailingSlash() {
void shouldCreateLegacyUriWithBasePathTrailingSlash() {
String baseUri = "http://localhost:12345/some-path/";
URI expected = URI.create("http://localhost:12345/some-path/rest/build-status/1.0/commits/25a4b3c9b494fc7ac65b80e3b0ecce63f235f20d");
URI actual = BuildStatusUriFactory.create(baseUri, "25a4b3c9b494fc7ac65b80e3b0ecce63f235f20d");
assertThat(actual, equalTo(expected));
}

@Test
void shouldHandleBasePathNoTrailingSlash() {
void shouldCreateLegacyUriWithBasePathNoTrailingSlash() {
String baseUri = "http://localhost:12345/some-path";
URI expected = URI.create("http://localhost:12345/some-path/rest/build-status/1.0/commits/25a4b3c9b494fc7ac65b80e3b0ecce63f235f20d");
URI actual = BuildStatusUriFactory.create(baseUri, "25a4b3c9b494fc7ac65b80e3b0ecce63f235f20d");
assertThat(actual, equalTo(expected));
}

@Test
void shouldCreateRepoScopedUri() {
String baseUri = "http://localhost:12345";
URI expected = URI.create("http://localhost:12345/rest/api/latest/projects/PROJ/repos/my-repo/commits/abc123/builds");
URI actual = BuildStatusUriFactory.create(baseUri, "PROJ", "my-repo", "abc123");
assertThat(actual, equalTo(expected));
}

@Test
void shouldCreateRepoScopedUriWithTrailingSlash() {
String baseUri = "http://localhost:12345/";
URI expected = URI.create("http://localhost:12345/rest/api/latest/projects/PROJ/repos/my-repo/commits/abc123/builds");
URI actual = BuildStatusUriFactory.create(baseUri, "PROJ", "my-repo", "abc123");
assertThat(actual, equalTo(expected));
}

@Test
void shouldCreateRepoScopedUriWithBasePath() {
String baseUri = "http://localhost:12345/bitbucket";
URI expected = URI.create("http://localhost:12345/bitbucket/rest/api/latest/projects/PROJ/repos/my-repo/commits/25a4b3c9/builds");
URI actual = BuildStatusUriFactory.create(baseUri, "PROJ", "my-repo", "25a4b3c9");
assertThat(actual, equalTo(expected));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -156,17 +156,35 @@ private NotificationResult notifyStash(int statusCode) throws Exception {
}

@Test
void notifyStash_success() throws Exception {
void notifyStash_success204() throws Exception {
NotificationResult notificationResult = notifyStash(204);
assertThat(notificationResult.indicatesSuccess, is(true));
}

@Test
void notifyStash_success200() throws Exception {
NotificationResult notificationResult = notifyStash(200);
assertThat(notificationResult.indicatesSuccess, is(true));
}

@Test
void notifyStash_success201() throws Exception {
NotificationResult notificationResult = notifyStash(201);
assertThat(notificationResult.indicatesSuccess, is(true));
}

@Test
void notifyStash_fail() throws Exception {
NotificationResult notificationResult = notifyStash(400);
assertThat(notificationResult.indicatesSuccess, is(false));
}

@Test
void notifyStash_fail500() throws Exception {
NotificationResult notificationResult = notifyStash(500);
assertThat(notificationResult.indicatesSuccess, is(false));
}

@Test
void notifyStashUsesRequestParameters() throws Exception {
notifyStash(204);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1014,4 +1014,49 @@ void setBuildStatus_null() {
sn.setBuildStatus(null);
assertThat(sn.getBuildStatus(), equalTo(StashBuildState.SUCCESSFUL));
}

@Test
void parseBitbucketRemoteUrl_https_with_scm() {
String[] result = StashNotifier.parseBitbucketRemoteUrl("https://bitbucket.example.com/scm/PROJ/my-repo.git");
assertThat(result[0], equalTo("PROJ"));
assertThat(result[1], equalTo("my-repo"));
}

@Test
void parseBitbucketRemoteUrl_ssh() {
String[] result = StashNotifier.parseBitbucketRemoteUrl("ssh://[email protected]:7999/PROJ/my-repo.git");
assertThat(result[0], equalTo("PROJ"));
assertThat(result[1], equalTo("my-repo"));
}

@Test
void parseBitbucketRemoteUrl_scp_style() {
String[] result = StashNotifier.parseBitbucketRemoteUrl("[email protected]:PROJ/my-repo.git");
assertThat(result[0], equalTo("PROJ"));
assertThat(result[1], equalTo("my-repo"));
}

@Test
void parseBitbucketRemoteUrl_personal_repo() {
String[] result = StashNotifier.parseBitbucketRemoteUrl("https://bitbucket.example.com/scm/~username/my-repo.git");
assertThat(result[0], equalTo("~username"));
assertThat(result[1], equalTo("my-repo"));
}

@Test
void parseBitbucketRemoteUrl_no_dotgit_suffix() {
String[] result = StashNotifier.parseBitbucketRemoteUrl("https://bitbucket.example.com/scm/PROJ/repo");
assertThat(result[0], equalTo("PROJ"));
assertThat(result[1], equalTo("repo"));
}

@Test
void parseBitbucketRemoteUrl_null() {
assertThat(StashNotifier.parseBitbucketRemoteUrl(null), nullValue());
}

@Test
void parseBitbucketRemoteUrl_empty() {
assertThat(StashNotifier.parseBitbucketRemoteUrl(""), nullValue());
}
}
Loading