From a8dca4515b253e60c8990003f15148f4bb023d8c Mon Sep 17 00:00:00 2001 From: Julien Roy Date: Thu, 11 Mar 2021 22:16:38 +0100 Subject: [PATCH] Add support of summaryComment on GitHub --- .../ce/pullrequest/github/v4/AddComment.java | 40 ++++++++++++ .../pullrequest/github/v4/GetPullRequest.java | 61 +++++++++++++++++++ .../github/v4/GraphqlCheckRunProvider.java | 53 +++++++++++++++- .../ws/action/SetGithubBindingAction.java | 3 + .../v4/GraphqlCheckRunProviderTest.java | 53 +++++++++++++--- .../ws/action/SetGithubBindingActionTest.java | 8 ++- 6 files changed, 209 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v4/AddComment.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v4/GetPullRequest.java diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v4/AddComment.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v4/AddComment.java new file mode 100644 index 000000000..9f4d2c71a --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v4/AddComment.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2021 Julien Roy + * + * 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.ce.pullrequest.github.v4; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.aexp.nodes.graphql.annotations.GraphQLArgument; +import io.aexp.nodes.graphql.annotations.GraphQLProperty; + +@GraphQLProperty(name = "addComment", arguments = {@GraphQLArgument(name = "input")}) +public class AddComment { + + private final String clientMutationId; + + @JsonCreator + public AddComment(@JsonProperty("clientMutationId") String clientMutationId) { + this.clientMutationId = clientMutationId; + } + + public String getClientMutationId() { + return clientMutationId; + } + +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v4/GetPullRequest.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v4/GetPullRequest.java new file mode 100644 index 000000000..5f469b683 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v4/GetPullRequest.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2021 Julien Roy + * + * 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.ce.pullrequest.github.v4; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.aexp.nodes.graphql.annotations.GraphQLArgument; +import io.aexp.nodes.graphql.annotations.GraphQLProperty; + +@GraphQLProperty(name = "repository", arguments = {@GraphQLArgument(name = "owner"), @GraphQLArgument(name = "name")}) +public class GetPullRequest { + + private final String url; + + @GraphQLProperty(name = "pullRequest", arguments = {@GraphQLArgument(name = "number")}) + private final PullRequest pullRequest; + + @JsonCreator + public GetPullRequest(@JsonProperty("url") String url, @JsonProperty("pullRequest") PullRequest pullRequest) { + this.url = url; + this.pullRequest = pullRequest; + } + + public String getUrl() { + return url; + } + + public PullRequest getPullRequest() { + return pullRequest; + } + + public static class PullRequest { + + private final String id; + + @JsonCreator + public PullRequest(@JsonProperty("id") String id) { + this.id = id; + } + + public String getId() { + return id; + } + } +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v4/GraphqlCheckRunProvider.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v4/GraphqlCheckRunProvider.java index 42e0c2688..2236eb5d6 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v4/GraphqlCheckRunProvider.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v4/GraphqlCheckRunProvider.java @@ -109,6 +109,8 @@ public DecorationResult createCheckRun(AnalysisDetails analysisDetails, AlmSetti headers.put("Accept", "application/vnd.github.antiope-preview+json"); + String summary = analysisDetails.createAnalysisSummary(new MarkdownFormatterFactory()); + List issues = analysisDetails.getPostAnalysisIssueVisitor().getIssues(); List> annotations = createAnnotations(issues); @@ -119,7 +121,7 @@ public DecorationResult createCheckRun(AnalysisDetails analysisDetails, AlmSetti QualityGate.Status.OK ? "success" : "failed")) - .put("summary", analysisDetails.createAnalysisSummary(new MarkdownFormatterFactory())) + .put("summary", summary) .put("annotations", annotations); SimpleDateFormat startedDateFormat = new SimpleDateFormat(DATE_TIME_PATTERN); @@ -165,12 +167,61 @@ public DecorationResult createCheckRun(AnalysisDetails analysisDetails, AlmSetti inputObjectArguments, checkRunOutputContentBuilder, graphQLRequestEntityBuilder); + Optional oPullRequestKey = analysisDetails.getPullRequestKey(); + if (Optional.ofNullable(projectAlmSettingDto.getSummaryCommentEnabled()).orElse(true) && oPullRequestKey.isPresent()) { + postSummaryComment(apiUrl, headers, projectPath, oPullRequestKey.get(), summary); + } + return DecorationResult.builder() .withPullRequestUrl(repositoryAuthenticationToken.getRepositoryUrl() + "/pull/" + analysisDetails.getBranchName()) .build(); } + private void postSummaryComment(String apiUrl, Map headers, String projectPath, String pullRequestKey, String summary) throws IOException { + + String[] paths = projectPath.split("/", 2); + String owner = paths[0]; + String projectName = paths[1]; + + GraphQLRequestEntity getPullRequest = + graphqlProvider.createRequestBuilder() + .url(getGraphqlUrl(apiUrl)) + .headers(headers) + .request(GetPullRequest.class) + .arguments( + new Arguments("repository", new Argument<>("owner", owner), new Argument<>("name", projectName)), + new Arguments("repository.pullRequest", new Argument<>("number", Integer.valueOf(pullRequestKey))) + ) + .requestMethod(GraphQLTemplate.GraphQLMethod.QUERY) + .build(); + + + GraphQLResponseEntity response = + executeRequest((r, t) -> graphqlProvider.createGraphQLTemplate().execute(r, t), getPullRequest, GetPullRequest.class); + + String pullRequestId = response.getResponse().getPullRequest().getId(); + + InputObject.Builder repositoryInputObjectBuilder = graphqlProvider.createInputObject(); + + InputObject input = repositoryInputObjectBuilder + .put("body", summary) + .put("subjectId", pullRequestId) + .build(); + + GraphQLRequestEntity graphQLRequestEntity = + graphqlProvider.createRequestBuilder() + .url(getGraphqlUrl(apiUrl)) + .headers(headers) + .request(AddComment.class) + .arguments(new Arguments("addComment", new Argument<>("input", input))) + .requestMethod(GraphQLTemplate.GraphQLMethod.MUTATE) + .build(); + + executeRequest((r, t) -> graphqlProvider.createGraphQLTemplate().mutate(r, t), graphQLRequestEntity, AddComment.class); + + } + private static GraphQLResponseEntity executeRequest( BiFunction, GraphQLResponseEntity> executor, GraphQLRequestEntity graphQLRequestEntity, Class responseType) { LOGGER.debug("Using request: " + graphQLRequestEntity.getRequest()); diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/SetGithubBindingAction.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/SetGithubBindingAction.java index 6c5cf5976..b532d256b 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/SetGithubBindingAction.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/SetGithubBindingAction.java @@ -28,6 +28,7 @@ public class SetGithubBindingAction extends SetBindingAction { private static final String REPOSITORY_PARAMETER = "repository"; + private static final String SUMMARY_COMMENT_PARAMETER = "summaryCommentEnabled"; public SetGithubBindingAction(DbClient dbClient, ComponentFinder componentFinder, UserSession userSession) { super(dbClient, componentFinder, userSession, "set_github_binding"); @@ -37,6 +38,7 @@ public SetGithubBindingAction(DbClient dbClient, ComponentFinder componentFinder protected void configureAction(WebService.NewAction action) { super.configureAction(action); action.createParam(REPOSITORY_PARAMETER).setRequired(true).setMaximumLength(256); + action.createParam(SUMMARY_COMMENT_PARAMETER).setRequired(false).setBooleanPossibleValues(); } @Override @@ -45,6 +47,7 @@ protected ProjectAlmSettingDto createProjectAlmSettingDto(String projectUuid, St .setProjectUuid(projectUuid) .setAlmSettingUuid(settingsUuid) .setAlmRepo(request.mandatoryParam(REPOSITORY_PARAMETER)) + .setSummaryCommentEnabled(request.paramAsBoolean(SUMMARY_COMMENT_PARAMETER)) .setMonorepo(false); } diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v4/GraphqlCheckRunProviderTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v4/GraphqlCheckRunProviderTest.java index 5e600ae82..0d9c16df3 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v4/GraphqlCheckRunProviderTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v4/GraphqlCheckRunProviderTest.java @@ -357,6 +357,7 @@ private void createCheckRunHappyPath(QualityGate.Status status, String basePath, when(analysisDetails.getBranchName()).thenReturn("branchName"); when(analysisDetails.getAnalysisDate()).thenReturn(new Date(1234567890)); when(analysisDetails.getAnalysisId()).thenReturn("analysis ID"); + when(analysisDetails.getPullRequestKey()).thenReturn(Optional.of("1")); when(analysisDetails.getPostAnalysisIssueVisitor()).thenReturn(postAnalysisIssueVisitor); ArgumentCaptor authenticationProviderArgumentCaptor = ArgumentCaptor.forClass(String.class); @@ -398,18 +399,37 @@ private void createCheckRunHappyPath(QualityGate.Status status, String basePath, ObjectMapper objectMapper = new ObjectMapper(); GraphQLResponseEntity graphQLResponseEntity = - new ObjectMapper().readValue("{\"response\": {\"checkRun\": {\"id\": \"ABC\"}}}", objectMapper.getTypeFactory().constructParametricType(GraphQLResponseEntity.class, CreateCheckRun.class)); + objectMapper.readValue("{\"response\": {\"checkRun\": {\"id\": \"ABC\"}}}", objectMapper.getTypeFactory().constructParametricType(GraphQLResponseEntity.class, CreateCheckRun.class)); - ArgumentCaptor requestEntityArgumentCaptor = - ArgumentCaptor.forClass(GraphQLRequestEntity.class); + ArgumentCaptor requestEntityArgumentCaptor = ArgumentCaptor.forClass(GraphQLRequestEntity.class); GraphQLTemplate graphQLTemplate = mock(GraphQLTemplate.class); - when(graphQLTemplate.mutate(requestEntityArgumentCaptor.capture(), eq(CreateCheckRun.class))) - .thenReturn(graphQLResponseEntity); + when(graphQLTemplate.mutate(requestEntityArgumentCaptor.capture(), eq(CreateCheckRun.class))).thenReturn(graphQLResponseEntity); + + GraphQLResponseEntity getPullRequestResponseEntity = + objectMapper.readValue("{" + + "\"response\": " + + " {\n" + + " \"pullRequest\": {\n" + + " \"id\": \"MDExOlB1bGxSZXF1ZXN0MzUzNDc=\"\n" + + " }\n" + + " }\n" + + "}", objectMapper.getTypeFactory().constructParametricType(GraphQLResponseEntity.class, GetPullRequest.class)); + + ArgumentCaptor getPullRequestRequestEntityArgumentCaptor = ArgumentCaptor.forClass(GraphQLRequestEntity.class); + when(graphQLTemplate.execute(getPullRequestRequestEntityArgumentCaptor.capture(), eq(GetPullRequest.class))).thenReturn(getPullRequestResponseEntity); + + GraphQLResponseEntity addCommentResponseEntity = + objectMapper.readValue("{\"response\":{}}", objectMapper.getTypeFactory().constructParametricType(GraphQLResponseEntity.class, AddComment.class)); + + ArgumentCaptor addCommentRequestEntityArgumentCaptor = ArgumentCaptor.forClass(GraphQLRequestEntity.class); + when(graphQLTemplate.mutate(addCommentRequestEntityArgumentCaptor.capture(), eq(AddComment.class))).thenReturn(addCommentResponseEntity); + when(graphqlProvider.createGraphQLTemplate()).thenReturn(graphQLTemplate); ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class); when(projectAlmSettingDto.getAlmRepo()).thenReturn("dummy/repo"); + when(projectAlmSettingDto.getSummaryCommentEnabled()).thenReturn(true); AlmSettingDto almSettingDto = mock(AlmSettingDto.class); when(almSettingDto.getUrl()).thenReturn(basePath); when(almSettingDto.getAppId()).thenReturn("app id"); @@ -419,7 +439,7 @@ private void createCheckRunHappyPath(QualityGate.Status status, String basePath, new GraphqlCheckRunProvider(graphqlProvider, clock, githubApplicationAuthenticationProvider, server); testCase.createCheckRun(analysisDetails, almSettingDto, projectAlmSettingDto); - assertEquals(1, requestBuilders.size()); + assertEquals(3, requestBuilders.size()); Map headers = new HashMap<>(); headers.put("Authorization", "Bearer dummyAuthToken"); @@ -473,7 +493,7 @@ private void createCheckRunHappyPath(QualityGate.Status status, String basePath, position++; } - assertEquals(2 + position, inputObjectBuilders.size()); + assertEquals(3 + position, inputObjectBuilders.size()); ArgumentCaptor>> annotationArgumentCaptor = ArgumentCaptor.forClass(List.class); @@ -499,6 +519,25 @@ private void createCheckRunHappyPath(QualityGate.Status status, String basePath, verify(inputObjectBuilders.get(position + 1)).put(eq("externalId"), eq("analysis ID")); verify(inputObjectBuilders.get(position + 1)).put(eq("output"), eq(inputObjects.get(position))); verify(inputObjectBuilders.get(position + 1)).build(); + + // Verify getPullRequest requestEntity + assertEquals( + "query { repository (owner:\"dummy\",name:\"repo\") { url pullRequest : pullRequest (number:1) { id } } } ", + getPullRequestRequestEntityArgumentCaptor.getValue().getRequest() + ); + + // Validate AddComment + verify(requestBuilders.get(2)).url(fullPath); + verify(requestBuilders.get(2)).headers(headers); + verify(requestBuilders.get(2)).requestMethod(GraphQLTemplate.GraphQLMethod.MUTATE); + verify(requestBuilders.get(2)).build(); + assertEquals(requestEntities.get(2), addCommentRequestEntityArgumentCaptor.getValue()); + + assertEquals( + "mutation { addComment (input:{body:\"dummy summary\",subjectId:\"MDExOlB1bGxSZXF1ZXN0MzUzNDc=\"}) { clientMutationId } } ", + addCommentRequestEntityArgumentCaptor.getValue().getRequest() + ); + } @Test diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/SetGithubBindingActionTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/SetGithubBindingActionTest.java index 491f2c00d..d38868fc4 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/SetGithubBindingActionTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/SetGithubBindingActionTest.java @@ -48,6 +48,11 @@ public void testConfigureAction() { when(repositoryParameter.setRequired(anyBoolean())).thenReturn(repositoryParameter); when(newAction.createParam("repository")).thenReturn(repositoryParameter); + WebService.NewParam commentEnabledParameter = mock(WebService.NewParam.class); + when(commentEnabledParameter.setBooleanPossibleValues()).thenReturn(commentEnabledParameter); + when(commentEnabledParameter.setRequired(anyBoolean())).thenReturn(commentEnabledParameter); + when(newAction.createParam("summaryCommentEnabled")).thenReturn(commentEnabledParameter); + WebService.NewParam almSettingParameter = mock(WebService.NewParam.class); when(almSettingParameter.setMaximumLength(any(Integer.class))).thenReturn(almSettingParameter); when(almSettingParameter.setRequired(anyBoolean())).thenReturn(almSettingParameter); @@ -68,11 +73,12 @@ public void testCreateProjectAlmSettingDto() { Request request = mock(Request.class); when(request.mandatoryParam("repository")).thenReturn("repository"); + when(request.paramAsBoolean("summaryCommentEnabled")).thenReturn(true); SetGithubBindingAction testCase = new SetGithubBindingAction(dbClient, componentFinder, userSession); ProjectAlmSettingDto result = testCase.createProjectAlmSettingDto("projectUuid", "settingsUuid", request); - assertThat(result).isEqualToComparingFieldByField(new ProjectAlmSettingDto().setProjectUuid("projectUuid").setAlmSettingUuid("settingsUuid").setAlmRepo("repository").setMonorepo(false)); + assertThat(result).isEqualToComparingFieldByField(new ProjectAlmSettingDto().setProjectUuid("projectUuid").setAlmSettingUuid("settingsUuid").setAlmRepo("repository").setSummaryCommentEnabled(true).setMonorepo(false)); } }