From f7aabefb064a94c110015b127c1f4a54c2eb917f Mon Sep 17 00:00:00 2001 From: Thibaud Lepretre Date: Fri, 4 Oct 2024 11:36:39 +0200 Subject: [PATCH] Add new endpoint /api/project_pull_requests/gitlab_report for GitLab codeclimate The plugin [sonar-gitlab-plugin](https://github.com/javamachr/sonar-gitlab-plugin) is relying on `/api/issues/search` with _query-parameter_ `pullRequest` to retrieve information and generate json. Unfortunately the `pullRequest` parameter isn't available for CE edition. Thus I've created a new endpoint called `/api/project_pull_requests/gitlab_report` with 2 mands parameters 1. `project` 2. `pullRequest` This endpoint will transform all `OPEN` issue to `GitLab codeclimate` json format. Thus simply add following inside your `.gitlab-ci.yml` after the analysis to get report (adapt your variables name, only `CI_MERGE_REQUEST_IID` is predefined) ``` - | curl -o gl-code-quality-report.json --header "Authorization: Bearer ${SONAR_TOKEN}" "${SONAR_HOST_URL}/api/project_pull_requests/gitlab_report?project=${SONAR_PROJECT_KEY}&pullRequest=${CI_MERGE_REQUEST_IID}" artifacts: reports: codequality: gl-code-quality-report.json ``` And tada is working --- .../plugin/CommunityBranchPlugin.java | 2 + .../action/GitLabReportAction.java | 107 ++++++++++++++++++ .../ws/pullrequest/action/ListAction.java | 2 +- .../plugin/CommunityBranchPluginTest.java | 2 + 4 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/pullrequest/action/GitLabReportAction.java diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPlugin.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPlugin.java index be1822654..7081f1f27 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPlugin.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPlugin.java @@ -58,6 +58,7 @@ import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.pullrequest.action.DeleteAction; import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.pullrequest.action.ListAction; +import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.pullrequest.action.GitLabReportAction; import org.sonar.api.CoreProperties; import org.sonar.api.Plugin; import org.sonar.api.PropertyType; @@ -94,6 +95,7 @@ public void load(CoreExtension.Context context) { ValidateBindingAction.class, DeleteAction.class, ListAction.class, + GitLabReportAction.class, PullRequestWs.class, GithubValidator.class, diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/pullrequest/action/GitLabReportAction.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/pullrequest/action/GitLabReportAction.java new file mode 100644 index 000000000..9a8d41565 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/pullrequest/action/GitLabReportAction.java @@ -0,0 +1,107 @@ +package com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.pullrequest.action; + +import org.apache.commons.codec.digest.DigestUtils; +import org.sonar.api.issue.IssueStatus; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService; +import org.sonar.api.utils.text.JsonWriter; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.component.BranchDto; +import org.sonar.db.component.BranchType; +import org.sonar.db.issue.IssueDao; +import org.sonar.db.issue.IssueDto; +import org.sonar.db.project.ProjectDto; +import org.sonar.db.rule.RuleDao; +import org.sonar.server.component.ComponentFinder; +import org.sonar.server.exceptions.NotFoundException; +import org.sonar.server.user.UserSession; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import static com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.pullrequest.action.ListAction.checkPermission; + +public class GitLabReportAction extends ProjectWsAction { + + private static final String PULL_REQUEST_PARAMETER = "pullRequest"; + + private static final Map ruleKeyToCheckName = new HashMap<>(); + + private final UserSession userSession; + + @Autowired + public GitLabReportAction(DbClient dbClient, ComponentFinder componentFinder, UserSession userSession) { + super("gitlab_report", dbClient, componentFinder); + this.userSession = userSession; + } + + @Override + protected void configureAction(WebService.NewAction action) { + action.createParam(PULL_REQUEST_PARAMETER).setRequired(true); + } + + @Override + protected void handleProjectRequest(ProjectDto project, Request request, Response response, DbSession dbSession) { + checkPermission(project, userSession); + + String pullRequestId = request.mandatoryParam(PULL_REQUEST_PARAMETER); + + BranchDto pullRequest = getDbClient() + .branchDao() + .selectByPullRequestKey(dbSession, project.getUuid(), pullRequestId) + .filter(branch -> branch.getBranchType() == BranchType.PULL_REQUEST) + .orElseThrow(() -> new NotFoundException( + String.format("Pull request '%s' is not found for project '%s'", pullRequestId, + project.getKey()))); + + IssueDao issueDao = getDbClient().issueDao(); + RuleDao ruleDao = getDbClient().ruleDao(); + // selectOpenByComponentUuids seems to not work as expected + Set issueKeys = issueDao.selectIssueKeysByComponentUuid(dbSession, pullRequest.getUuid()); + + JsonWriter writer = response.newJsonWriter(); + writer.beginArray(); + issueDao + .selectByKeys(dbSession, issueKeys) + .stream() + .filter(i -> i.getIssueStatus() == IssueStatus.OPEN) + .forEach(i -> buildGitLabIssue(dbSession, i, ruleDao, writer)); + writer.endArray(); + writer.close(); + } + + private void buildGitLabIssue(DbSession dbSession, IssueDto issue, RuleDao ruleDao, JsonWriter writer) { + writer.beginObject(); + // https://docs.gitlab.com/ee/ci/testing/code_quality.html#implement-a-custom-tool + writer.prop("check_name", getCheckName(dbSession, ruleDao, issue.getRuleKey())); + writer.prop("fingerprint", DigestUtils.md5Hex(issue.getKey())); + writer.prop("description", issue.getMessage()); + writer.prop("severity", issue.getSeverity().toLowerCase()); + // Location + buildLocation(issue, writer); + writer.endObject(); + } + + private void buildLocation(IssueDto issue, JsonWriter jsonWriter) { + jsonWriter.name("location").beginObject(); + jsonWriter.prop("path", issue.getFilePath()); + jsonWriter.name("lines").beginObject(); + jsonWriter.prop("begin", issue.getLine()); + jsonWriter.prop("end", issue.getLine()); + jsonWriter.endObject(); + jsonWriter.endObject(); + } + + private String getCheckName(DbSession dbSession, RuleDao ruleDao, RuleKey ruleKey) { + return ruleKeyToCheckName.computeIfAbsent(ruleKey.rule(), k -> ruleDao + .selectByKey(dbSession, ruleKey) + .map(r -> r.getName().toLowerCase().replaceAll("[^a-zA-Z ]", "").replaceAll("\\s+", "-")) + .orElse(ruleKey.toString())); + } + +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/pullrequest/action/ListAction.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/pullrequest/action/ListAction.java index a84dc477f..20d5b44eb 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/pullrequest/action/ListAction.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/pullrequest/action/ListAction.java @@ -104,7 +104,7 @@ public void handleProjectRequest(ProjectDto project, Request request, Response r protoBufWriter.write(protobufResponse.build(), request, response); } - private static void checkPermission(ProjectDto project, UserSession userSession) { + static void checkPermission(ProjectDto project, UserSession userSession) { if (userSession.hasEntityPermission(UserRole.USER, project) || userSession.hasEntityPermission(UserRole.SCAN, project) || userSession.hasPermission(GlobalPermission.SCAN)) { diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPluginTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPluginTest.java index 22fd3a21b..97dab66f8 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPluginTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPluginTest.java @@ -57,6 +57,7 @@ import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.pullrequest.PullRequestWs; import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.pullrequest.action.DeleteAction; import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.pullrequest.action.ListAction; +import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.pullrequest.action.GitLabReportAction; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.sonar.api.Plugin; @@ -142,6 +143,7 @@ void shouldAddExtensionsForServerSideLoad() { eq(ValidateBindingAction.class), eq(DeleteAction.class), eq(ListAction.class), + eq(GitLabReportAction.class), eq(PullRequestWs.class), eq(GithubValidator.class), eq(DefaultGraphqlProvider.class),