Skip to content

Commit

Permalink
#893: Report build status to Bitbucket
Browse files Browse the repository at this point in the history
The Bitbucket decorators submit a report to Bitbucket containing the
quality gate summary, but don't submit a report that influences the
build status. A second call is being made to submit abuild status that
is either successful is the Quality Gate has passed, or failed is the
Quality Gate did not pass.
  • Loading branch information
mc1arke committed Nov 17, 2024
1 parent 6259650 commit 8ce5825
Show file tree
Hide file tree
Showing 8 changed files with 228 additions and 84 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020-2022 Marvin Wichmann, Michael Clarke
* Copyright (C) 2020-2024 Marvin Wichmann, Michael Clarke
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
Expand All @@ -19,6 +19,7 @@
package com.github.mc1arke.sonarqube.plugin.almclient.bitbucket;

import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.AnnotationUploadLimit;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.BuildStatus;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.CodeInsightsAnnotation;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.CodeInsightsReport;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.DataValue;
Expand Down Expand Up @@ -108,4 +109,11 @@ CodeInsightsReport createCodeInsightsReport(List<ReportData> reportData,
*/
Repository retrieveRepository() throws IOException;

/**
* Submit the build status for the given commit.
* @param commitSha the Git commit hash
* @param buildStatus the build status containing the state, URL, and identifiers
* @throws IOException on any issue submitting the build status
*/
void submitBuildStatus(String commitSha, BuildStatus buildStatus) throws IOException;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020-2023 Marvin Wichmann, Michael Clarke
* Copyright (C) 2020-2024 Marvin Wichmann, Michael Clarke
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
Expand All @@ -23,6 +23,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.AnnotationUploadLimit;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.BitbucketConfiguration;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.BuildStatus;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.CodeInsightsAnnotation;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.CodeInsightsReport;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.DataValue;
Expand Down Expand Up @@ -198,6 +199,20 @@ public Repository retrieveRepository() throws IOException {
}
}

@Override
public void submitBuildStatus(String commitSha, BuildStatus buildStatus) throws IOException {
Request req = new Request.Builder()
.post(RequestBody.create(objectMapper.writeValueAsString(buildStatus), APPLICATION_JSON_MEDIA_TYPE))
.url(format("https://api.bitbucket.org/2.0/repositories/%s/%s/commit/%s/statuses/build", bitbucketConfiguration.getProject(), bitbucketConfiguration.getRepository(), commitSha))
.build();

LOGGER.info("Submitting build status to bitbucket cloud");

try (Response response = okHttpClient.newCall(req).execute()) {
validate(response);
}
}

void deleteExistingReport(String commit, String reportKey) throws IOException {
Request req = new Request.Builder()
.delete()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020-2023 Mathias Åhsberg, Michael Clarke
* Copyright (C) 2020-2024 Mathias Åhsberg, Michael Clarke
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
Expand All @@ -20,6 +20,7 @@

import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.AnnotationUploadLimit;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.BuildStatus;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.CodeInsightsAnnotation;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.CodeInsightsReport;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.DataValue;
Expand Down Expand Up @@ -179,6 +180,18 @@ public Repository retrieveRepository() throws IOException {
}
}

@Override
public void submitBuildStatus(String commitSha, BuildStatus buildStatus) throws IOException {
Request req = new Request.Builder()
.post(RequestBody.create(objectMapper.writeValueAsString(buildStatus), APPLICATION_JSON_MEDIA_TYPE))
.url(format("%s/rest/api/1.0/projects/%s/repos/%s/commits/%s/builds", config.getUrl(), config.getProject(), config.getRepository(), commitSha))
.build();

try (Response response = okHttpClient.newCall(req).execute()) {
validate(response);
}
}

public ServerProperties getServerProperties() throws IOException {
Request req = new Request.Builder()
.get()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright (C) 2024 Michael Clarke
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
*/
package com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model;

import com.fasterxml.jackson.annotation.JsonProperty;

public class BuildStatus {

private final State state;
private final String key;
private final String name;
private final String url;

public BuildStatus(@JsonProperty("state") State state,
@JsonProperty("key") String key,
@JsonProperty("name") String name,
@JsonProperty("url") String url) {
this.state = state;
this.key = key;
this.name = name;
this.url = url;
}

public State getState() {
return state;
}

public String getKey() {
return key;
}

public String getName() {
return name;
}

public String getUrl() {
return url;
}

public enum State {
INPROGRESS,
SUCCESSFUL,
FAILED,
CANCELLED,
UNKNOWN
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.BitbucketClientFactory;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.BitbucketException;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.AnnotationUploadLimit;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.BuildStatus;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.CodeInsightsAnnotation;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.CodeInsightsReport;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.DataValue;
Expand Down Expand Up @@ -98,6 +99,9 @@ public DecorationResult decorateQualityGateStatus(AnalysisDetails analysisDetail
client.uploadReport(analysisDetails.getCommitSha(), codeInsightsReport, reportKey);

updateAnnotations(client, analysisDetails, reportKey);

BuildStatus buildStatus = new BuildStatus(analysisDetails.getQualityGateStatus() == QualityGate.Status.OK ? BuildStatus.State.SUCCESSFUL : BuildStatus.State.FAILED, reportKey, "SonarQube", analysisSummary.getDashboardUrl());
client.submitBuildStatus(analysisDetails.getCommitSha(),buildStatus);
} catch (IOException e) {
LOGGER.error("Could not decorate pull request for project {}", analysisDetails.getAnalysisProjectKey(), e);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021-2022 Michael Clarke
* Copyright (C) 2021-2024 Michael Clarke
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
Expand All @@ -21,6 +21,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.AnnotationUploadLimit;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.BitbucketConfiguration;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.BuildStatus;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.CodeInsightsAnnotation;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.CodeInsightsReport;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.DataValue;
Expand All @@ -33,49 +34,42 @@
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

import java.io.IOException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Set;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.internal.verification.VerificationModeFactory.times;

@RunWith(MockitoJUnitRunner.class)
public class BitbucketCloudClientUnitTest {

private BitbucketCloudClient underTest;

@Mock
private ObjectMapper mapper;
class BitbucketCloudClientUnitTest {

@Mock
private OkHttpClient client;
private final ObjectMapper mapper = mock();
private final OkHttpClient client = mock();
private final BitbucketCloudClient underTest = new BitbucketCloudClient(mapper, client, new BitbucketConfiguration("project", "repository"));

@Before
public void before() {
BitbucketConfiguration bitbucketConfiguration = new BitbucketConfiguration("project", "repository");
Call call = mock(Call.class);
@BeforeEach
void setup() {
Call call = mock();
when(client.newCall(any())).thenReturn(call);
underTest = new BitbucketCloudClient(mapper, client, bitbucketConfiguration);
}

@Test
public void testUploadReport() throws IOException {
void testUploadReport() throws IOException {
// given
CodeInsightsReport report = mock(CodeInsightsReport.class);
Call call = mock(Call.class);
Expand All @@ -99,7 +93,7 @@ public void testUploadReport() throws IOException {
}

@Test
public void testDeleteReport() throws IOException {
void testDeleteReport() throws IOException {
// given
Call call = mock(Call.class);
Response response = mock(Response.class);
Expand All @@ -119,7 +113,7 @@ public void testDeleteReport() throws IOException {
}

@Test
public void testUploadAnnotations() throws IOException {
void testUploadAnnotations() throws IOException {
// given
CodeInsightsAnnotation annotation = mock(CloudAnnotation.class);
Set<CodeInsightsAnnotation> annotations = Sets.newHashSet(annotation);
Expand All @@ -144,7 +138,7 @@ public void testUploadAnnotations() throws IOException {
}

@Test
public void testUploadLimit() {
void testUploadLimit() {
// given
// when
AnnotationUploadLimit annotationUploadLimit = underTest.getAnnotationUploadLimit();
Expand All @@ -155,7 +149,7 @@ public void testUploadLimit() {
}

@Test
public void testUploadReportFailsWithMessage() throws IOException {
void testUploadReportFailsWithMessage() throws IOException {
// given
CodeInsightsReport report = mock(CodeInsightsReport.class);
Call call = mock(Call.class);
Expand All @@ -180,7 +174,7 @@ public void testUploadReportFailsWithMessage() throws IOException {
}

@Test
public void testUploadAnnotationsWithEmptyAnnotations() throws IOException {
void testUploadAnnotationsWithEmptyAnnotations() throws IOException {
// given
Set<CodeInsightsAnnotation> annotations = Sets.newHashSet();

Expand All @@ -192,14 +186,14 @@ public void testUploadAnnotationsWithEmptyAnnotations() throws IOException {
}

@Test
public void testCreateAnnotationForCloud() {
void testCreateAnnotationForCloud() {
// given

// when
CodeInsightsAnnotation annotation = underTest.createCodeInsightsAnnotation("issueKey", 12, "http://localhost:9000/dashboard", "Failed", "/path/to/file", "MAJOR", "BUG");

// then
assertTrue(annotation instanceof CloudAnnotation);
assertInstanceOf(CloudAnnotation.class, annotation);
assertEquals("issueKey", ((CloudAnnotation) annotation).getExternalId());
assertEquals(12, annotation.getLine());
assertEquals("http://localhost:9000/dashboard", ((CloudAnnotation) annotation).getLink());
Expand All @@ -209,19 +203,19 @@ public void testCreateAnnotationForCloud() {
}

@Test
public void testCreateDataLinkForCloud() {
void testCreateDataLinkForCloud() {
// given

// when
DataValue data = underTest.createLinkDataValue("https://localhost:9000/any/project");

// then
assertTrue(data instanceof DataValue.CloudLink);
assertInstanceOf(DataValue.CloudLink.class, data);
assertEquals("https://localhost:9000/any/project", ((DataValue.CloudLink) data).getHref());
}

@Test
public void testCloudAlwaysSupportsCodeInsights() {
void testCloudAlwaysSupportsCodeInsights() {
// given

// when
Expand All @@ -232,19 +226,46 @@ public void testCloudAlwaysSupportsCodeInsights() {
}

@Test
public void testCreateCloudReport() {
void testCreateCloudReport() {
// given

// when
CodeInsightsReport result = underTest.createCodeInsightsReport(new ArrayList<>(), "reportDescription", Instant.now(), "dashboardUrl", "logoUrl", ReportStatus.FAILED);

// then
assertTrue(result instanceof CloudCreateReportRequest);
assertInstanceOf(CloudCreateReportRequest.class, result);
assertEquals(0, result.getData().size());
assertEquals("reportDescription", result.getDetails());
assertEquals("dashboardUrl", result.getLink());
assertEquals("logoUrl", ((CloudCreateReportRequest) result).getLogoUrl());
assertEquals("FAILED", result.getResult());

}

@Test
void shouldSubmitBuildStatusToServer() throws IOException {
// given
Call call = mock();
Response response = mock();
ArgumentCaptor<Request> captor = ArgumentCaptor.captor();

when(client.newCall(any())).thenReturn(call);
when(call.execute()).thenReturn(response);
when(response.isSuccessful()).thenReturn(true);

when(mapper.writeValueAsString(any())).thenReturn("{payload}");

BuildStatus buildStatus = new BuildStatus(BuildStatus.State.INPROGRESS, "key", "name", "url");

// when
underTest.submitBuildStatus("commit", buildStatus);

// then
verify(client).newCall(captor.capture());
Request request = captor.getValue();
assertThat(request.method()).isEqualTo("POST");
assertThat(request.url()).hasToString("https://api.bitbucket.org/2.0/repositories/project/repository/commit/commit/statuses/build");

verify(mapper).writeValueAsString(buildStatus);
}
}
Loading

0 comments on commit 8ce5825

Please sign in to comment.