diff --git a/README.md b/README.md index ff941901..2e6b4222 100644 --- a/README.md +++ b/README.md @@ -8,15 +8,8 @@ ## Introduction -The plugin shares [SonarQube](http://www.sonarqube.org/) feedback with developers via [Gerrit Code Review](https://www.gerritcodereview.com/). - -## Incompatibility with Sonar 7.7 - -Starting with Sonarqube 7.7 the preview mode (-Dsonar.analysis.mode=preview) was removed, making it incompatible with the plugin. - -Sonarqube release notes say it now has "native support for short-living branches" https://www.sonarqube.org/sonarqube-7-7/, author will make an effort to integrate these features. Contributions are appreciated (wink) - -A place to start with (for getting a json report): https://community.sonarsource.com/t/sonar-report-json-is-this-file-still-available/5827/6 +The plugin shares [SonarQube](http://www.sonarqube.org/) feedback with developers +via [Gerrit Code Review](https://www.gerritcodereview.com/). ## Requirements @@ -26,12 +19,22 @@ Jenkins version 2.249.3 or newer is required. ### SonarQube -This plugin is intended to work with report provided by SonarQube running on a project in preview mode. That means SonarQube report generation should be included to build. +SonarQube report generation *must* happen within [SonarQube Scanner](https://plugins.jenkins.io/sonar/) wrapper. -If you use Maven, fill out "Goals and options" field in "Build" section of your Jenkins job: +On a non-pipeline job, you can enable [SonarQube Scanner](https://plugins.jenkins.io/sonar/) wrapper by +checking `Prepare SonarQube Scanner environment`: -```bash -clean verify sonar:sonar -Dsonar.analysis.mode=preview -Dsonar.report.export.path=sonar-report.json +![Sonar scanner wrapper](doc/sonar-scanner-wrapper.png) + +On a pipeline job, [SonarQube Scanner](https://plugins.jenkins.io/sonar/) wrapper is represented by `withSonarQubeEnv`. +For example: + +```groovy +withSonarQubeEnv('my-sonar-installation-name') { + withMaven(maven: 'my-maven-installation-name') { + sh "mvn clean verify sonar:sonar -Dsonar.pullrequest.key=${env.GERRIT_CHANGE_NUMBER}-${env.GERRIT_PATCHSET_NUMBER} -Dsonar.pullrequest.base=${env.GERRIT_BRANCH} -Dsonar.pullrequest.branch=${env.GERRIT_REFSPEC}" + } +} ``` ### Gerrit @@ -44,9 +47,10 @@ Rest API should be configured in the Advanced section of Gerrit Trigger settings HTTP authentication data should be set up. Enable Code-Review and Enable Verified checkboxes should be checked on. -For complete guidance please see [Gerrit Trigger Wiki page](https://plugins.jenkins.io/gerrit-trigger/#plugin-content-setup-requirements). +For complete guidance please +see [Gerrit Trigger Wiki page](https://plugins.jenkins.io/gerrit-trigger/#plugin-content-setup-requirements). -![alt text](doc/gerrit-trigger-conf.png "Gerrit Trigger configuration") +![Gerrit Trigger configuration](doc/gerrit-trigger-conf.png) #### Running out of Gerrit Trigger job @@ -74,22 +78,103 @@ There are the next sections: #### SonarQube Settings -##### Preview mode analysis +##### Pull request analysis (since SonarQube 7.2) + +This analysis strategy is based on https://docs.sonarqube.org/latest/analysis/pull-request/ . + +The SonarQube instance must either +have [sonarqube-community-branch-plugin](https://github.com/mc1arke/sonarqube-community-branch-plugin) +enabled or be of [developer edition](https://www.sonarqube.org/developer-edition/) type. + +In order to run a pull request scan, `Sonar` requires the following mandatory properties: + +| Key | Recommended value template | Example | +|--------------------------|---------------------------------------------------|---------------------| +| sonar.pullrequest.key | ${GERRIT_CHANGE_NUMBER}-${GERRIT_PATCHSET_NUMBER} | 250-1 | +| sonar.pullrequest.base | ${GERRIT_BRANCH} | master | +| sonar.pullrequest.branch | ${GERRIT_REFSPEC} | refs/changes/01/1/1 | + +Example of `Maven` target: + +``` +clean verify sonar:sonar -Dsonar.pullrequest.key=${GERRIT_CHANGE_NUMBER}-${GERRIT_PATCHSET_NUMBER} -Dsonar.pullrequest.base=${GERRIT_BRANCH} -Dsonar.pullrequest.branch=${GERRIT_REFSPEC} +``` + +###### Non pipeline + +![Gerrit Trigger configuration](doc/sonar-pull-request-analysis-radio.png) + +###### Pipeline + +```groovy +sonarToGerrit( + inspectionConfig: [ + analysisStrategy: pullRequest() + ] +) +``` + +##### Preview mode analysis (until SonarQube 7.6) + +This analysis strategy is intended to work with report provided by SonarQube running on a project in preview mode. That +means SonarQube report generation should be included to build. + +If you use Maven, fill out "Goals and options" field in "Build" section of your Jenkins job: + +```bash +clean verify sonar:sonar -Dsonar.analysis.mode=preview -Dsonar.report.export.path=sonar-report.json +``` + +###### Non pipeline -Use setting "Project configuration" if only one SonarQube report is generated and static code analysis of the whole project is required. +Use setting "Project configuration" if only one SonarQube report is generated and static code analysis of the whole +project is required. -![alt text](doc/sonar-settings-base.png "Sonar settings base") +![Sonar settings base](doc/sonar-settings-base.png) -Use setting "Sub-project configurations" to specify modules and paths for separate reports if modules are analysed separately or not every module needs to be analysed. +Use setting "Sub-project configurations" to specify modules and paths for separate reports if modules are analysed +separately or not every module needs to be analysed. -![alt text](doc/sonar-settings-multi.png "Sonar settings multi") +![Sonar settings multi](doc/sonar-settings-multi.png) Settings: -1. SonarQube installation - The SonarQube installation (see https://plugins.jenkins.io/sonar/) to be used for analysis. It is also used to provide a link to a SonarQube rule in Gerrit comments. -2. Project base directory - subdirectory for a case when Jenkins job is related to a specific module of a big project. The path is relative to a main project root directory. Default value is empty. -3. SonarQube report path - Path to a SonarQube report generated by SonarQube while a project was being built. The path is relative to a build working directory. Default value: `target/sonar/sonar-report.json` -4. Allow auto match - setting to allow automatically match SonarQube issues to Gerrit files in case if project consists of several sub-modules, but only one SonarQube report is generated for the whole project. +1. SonarQube installation - The SonarQube installation (see https://plugins.jenkins.io/sonar/) to be used for analysis. + It is also used to provide a link to a SonarQube rule in Gerrit comments. +2. Project base directory - subdirectory for a case when Jenkins job is related to a specific module of a big project. + The path is relative to a main project root directory. Default value is empty. +3. SonarQube report path - Path to a SonarQube report generated by SonarQube while a project was being built. The path + is relative to a build working directory. Default value: `target/sonar/sonar-report.json` +4. Allow auto match - setting to allow automatically match SonarQube issues to Gerrit files in case if project consists + of several sub-modules, but only one SonarQube report is generated for the whole project. + +###### Pipeline + +```groovy +sonarToGerrit( + inspectionConfig: [ + analysisStrategy: previewMode( + sonarQubeInstallationName: 'My SonarQube Installation', + baseConfig: [ + projectPath : '', + sonarReportPath: 'target/sonar/sonar-report.json', + autoMatch : true + ] + // OR + //subJobConfigs : [ + // [ + // projectPath: 'module0', + // sonarReportPath: 'target/sonar/sonar-report.json' + // ], + // [ + // projectPath: 'module1', + // sonarReportPath: 'target/module1/sonar/sonar-report.json' + // ] + //] + ) + ] +) +``` #### Filter @@ -101,54 +186,60 @@ Filter is used to specify what issues will be affected in the output: It is possible to filter issues by: -1. Severity - SonarQube issue severity. If user doesn't want issues with low severity to be reported to Gerrit, he (or she) can choose the lowest severity level to be reported. For example if "Major" level is selected, information about issues with "Major", "Critical" and "Blocker" will be included to Gerrit review. Default value: Info. -2. New issues only - reflects SonarQube issue "new" property. If issue is not marked as new that may be a sign that it is not created by processing commit and this issue is not supposed to be included to review. -3. Changed lines only - when only several lines are changed in a commit user may not want other lines to be commented by Gerrit. With "Add comments to changed lines only" unchanged in the commit lines will not be commented in Gerrit. +1. Severity - SonarQube issue severity. If user doesn't want issues with low severity to be reported to Gerrit, he (or + she) can choose the lowest severity level to be reported. For example if "Major" level is selected, information about + issues with "Major", "Critical" and "Blocker" will be included to Gerrit review. Default value: Info. +2. New issues only - reflects SonarQube issue "new" property. If issue is not marked as new that may be a sign that it + is not created by processing commit and this issue is not supposed to be included to review. +3. Changed lines only - when only several lines are changed in a commit user may not want other lines to be commented by + Gerrit. With "Add comments to changed lines only" unchanged in the commit lines will not be commented in Gerrit. -![alt text](doc/filter-settings.png "Filter settings") +![Filter settings](doc/filter-settings.png) #### Review Settings Review settings contains of issue filter to specify issues to be commented and review template. -![alt text](doc/review-settings.png "Review settings") +![Filter settings](doc/review-settings.png) #### Report Formatting This section allows user to customise text, intended to use as review title and issue comment. -1. Title - Review title settings allow customization of Gerrit review titles for both cases (violations found or not) separately. There are several tags to be replaced by real values allowed in this context: - 1. \ - will be replaced with count of issues having INFO severity level; - 2. \ - will be replaced with count of issues having MINOR severity level; - 3. \ - will be replaced with count of issues having MAJOR severity level; - 4. \ - will be replaced with count of issues having CRITICAL severity level; - 5. \ - will be replaced with count of issues having BLOCKER severity level; - 6. \ - will be replaced with count of issues having MINOR severity level or higher; - 7. \ - will be replaced with count of issues having MAJOR severity level or higher; - 8. \ - will be replaced with count of issues having CRITICAL severity level or higher; - 9. \ - will be replaced with total count of issues. +1. Title - Review title settings allow customization of Gerrit review titles for both cases (violations found or not) + separately. There are several tags to be replaced by real values allowed in this context: + 1. \ - will be replaced with count of issues having INFO severity level; + 2. \ - will be replaced with count of issues having MINOR severity level; + 3. \ - will be replaced with count of issues having MAJOR severity level; + 4. \ - will be replaced with count of issues having CRITICAL severity level; + 5. \ - will be replaced with count of issues having BLOCKER severity level; + 6. \ - will be replaced with count of issues having MINOR severity level or higher; + 7. \ - will be replaced with count of issues having MAJOR severity level or higher; + 8. \ - will be replaced with count of issues having CRITICAL severity level or higher; + 9. \ - will be replaced with total count of issues. 2. Comment - Issue comment pattern. Available tags: - 1. \ - will be replaced with issue key; - 2. \ - will be replaced with issue component info; - 3. \ - will be replaced with issue message; - 4. \ - will be replaced with issue severity; - 5. \ - will be replaced with issue rule name; - 6. \ - will be replaced with link to rule description on SonarQube; - 7. \ - will be replaced with issue status; - 8. \ - will be replaced with issue creation date. + 1. \ - will be replaced with issue key; + 2. \ - will be replaced with issue component info; + 3. \ - will be replaced with issue message; + 4. \ - will be replaced with issue severity; + 5. \ - will be replaced with issue rule name; + 6. \ - will be replaced with link to rule description on SonarQube; + 7. \ - will be replaced with issue status; + 8. \ - will be replaced with issue creation date. #### Score Settings Starting with v. 2.1 it's become possible to specify a separate filter for score settings. -![alt text](doc/score-settings.png "Score settings") +![Score settings](doc/score-settings.png) 1. Post score - This setting describes whether it is necessary to post score to Gerrit or not. 2. Category - Gerrit category used for score posting. Default value: Code-Review. 3. Score for no SonarQube violation found case - Score to be posted to Gerrit. Default value: +1 4. Score for SonarQube violations found case - Score to be posted to Gerrit. Default value: -1 -Please note: to use Gerrit category other than Default it is necessary to configure it in Gerrit. See details in [Gerrit Documentation](https://gerrit-review.googlesource.com/Documentation/config-labels.html). +Please note: to use Gerrit category other than Default it is necessary to configure it in Gerrit. See details +in [Gerrit Documentation](https://gerrit-review.googlesource.com/Documentation/config-labels.html). An example of settings to be added to the project.config for creating Sonar-Verified category: @@ -171,13 +262,16 @@ And access rights: #### Credentials override -To override the credentials used to post comments on the job level set up section "Override default HTTP credentials". (Global credentials on the Gerrit Trigger Server level should be set up as well for Gerrit Trigger needs.) +To override the credentials used to post comments on the job level set up section "Override default HTTP credentials". ( +Global credentials on the Gerrit Trigger Server level should be set up as well for Gerrit Trigger needs.) -![alt text](doc/credentials-settings.png "Credentials settings") +![Credentials settings](doc/credentials-settings.png) -1. Override default HTTP credentials? - This setting describes whether it is necessary to override Gerrit credentials from the Gerrit Trigger Server settings or not. +1. Override default HTTP credentials? - This setting describes whether it is necessary to override Gerrit credentials + from the Gerrit Trigger Server settings or not. 2. HTTP credentials - Credentials to be used to post review result to Gerrit. -3. Gerrit Server - The server used to check connection with overridden credentials. The value *does not* affect plugin settings and only used to verify credentials. +3. Gerrit Server - The server used to check connection with overridden credentials. The value *does not* affect plugin + settings and only used to verify credentials. #### Notification Settings @@ -191,111 +285,160 @@ Options : * None - No notification regarding particular review will be sent. * Owner - Notification with review results will be sent to a change owner. -* Owner & Reviewers - Notification with review results will be sent to an owner and to all the change reviewers added to the change. +* Owner & Reviewers - Notification with review results will be sent to an owner and to all the change reviewers added to + the change. * All - Everyone in Gerrit project will receive notification. -![alt text](doc/notification-settings.png "Notification settings") +![Notification settings](doc/notification-settings.png) -## Pipelines support +## Pipeline full examples -Basic support for pipelines is added in 2.0 - -### Pipeline with default settings example +### Pull request analysis (since SonarQube 7.2) ```groovy node { - // trigger build - git url: 'ssh://your_project_repo' - // Fetch the changeset to a local branch using the build parameters provided to the build by the Gerrit Trigger... - def changeBranch = "change-${GERRIT_CHANGE_NUMBER}-${GERRIT_PATCHSET_NUMBER}" - sh "git fetch origin ${GERRIT_REFSPEC}:${changeBranch}" - sh "git checkout ${changeBranch}" - - - // Get the maven tool. - def mvnHome = tool 'M3' - // Mark the code build 'stage'.... - stage 'Build' - // Run the maven build - sh "${mvnHome}/bin/mvn clean verify sonar:sonar -Dmaven.test.skip=true -Dsonar.analysis.mode=preview -Dsonar.report.export.path=sonar-report.json" - - - // to run plugin with default settings - stage 'Review' - sonarToGerrit() - + stage('Build') { + // trigger build + git url: 'ssh://your_project_repo' + // Fetch the changeset to a local branch using the build parameters provided to the build by the Gerrit Trigger... + def changeBranch = "change-${GERRIT_CHANGE_NUMBER}-${GERRIT_PATCHSET_NUMBER}" + sh "git fetch origin ${GERRIT_REFSPEC}:${changeBranch}" + sh "git checkout ${changeBranch}" + try { + withSonarQubeEnv('my-sonar-installation') { + withMaven(maven: 'my-maven-installation') { + sh "mvn clean verify sonar:sonar -Dsonar.pullrequest.key=${GERRIT_CHANGE_NUMBER}-${GERRIT_PATCHSET_NUMBER} -Dsonar.pullrequest.base=${GERRIT_BRANCH} -Dsonar.pullrequest.branch=${GERRIT_REFSPEC}" + } + } + } finally { + sonarToGerrit( + inspectionConfig: [ + analysisStrategy: pullRequest() + ] + /* Optional parameters + , reviewConfig: [ + issueFilterConfig : [ + severity : 'INFO', + newIssuesOnly : false, + changedLinesOnly: false + ], + noIssuesTitleTemplate : 'SonarQube violations have not been found.', + someIssuesTitleTemplate: ' SonarQube violations have been found.', + issueCommentTemplate : ' SonarQube violation:\n\n\n\n\n\nRead more: ' + ], + scoreConfig: [ + issueFilterConfig: [ + severity : 'INFO', + newIssuesOnly : false, + changedLinesOnly: false + ], + category : 'Code-Review', + noIssuesScore : 0, + issuesScore : -1 + ], + notificationConfig: [ + noIssuesNotificationRecipient : 'NONE', + commentedIssuesNotificationRecipient: 'OWNER', + negativeScoreNotificationRecipient : 'OWNER' + ], + authConfig: [ + httpCredentialsId: 'b948c0ba-51a2-4eb7-b42b-71e6a77d7d34' + ]*/ + ) + } + } } + ``` -### Pipeline overridden settings example +### Preview mode analysis (until SonarQube 7.6) ```groovy -sonarToGerrit ( - inspectionConfig: [ - analysisStrategy: previewMode( - sonarQubeInstallationName: 'My SonarQube Installation', - baseConfig: [ - projectPath: '', - sonarReportPath: 'target/sonar/sonar-report.json', - autoMatch: true - ] - // OR - //subJobConfigs : [ - // [ - // projectPath: 'module0', - // sonarReportPath: 'target/sonar/sonar-report.json' - // ], - // [ - // projectPath: 'module1', - // sonarReportPath: 'target/module1/sonar/sonar-report.json' - // ] - //] - ) - ], - reviewConfig: [ - issueFilterConfig: [ - severity: 'INFO', - newIssuesOnly: false, - changedLinesOnly: false - ], - noIssuesTitleTemplate: 'SonarQube violations have not been found.', - someIssuesTitleTemplate: ' SonarQube violations have been found.', - issueCommentTemplate: ' SonarQube violation:\n\n\n\n\n\nRead more: ' - ], - scoreConfig: [ - issueFilterConfig: [ - severity: 'INFO', - newIssuesOnly: false, - changedLinesOnly: false - ], - category: 'Code-Review', - noIssuesScore: 0, - issuesScore: -1 - ], - notificationConfig: [ - noIssuesNotificationRecipient: 'NONE', - commentedIssuesNotificationRecipient: 'OWNER', - negativeScoreNotificationRecipient: 'OWNER' - ], - authConfig: [ - httpCredentialsId: 'b948c0ba-51a2-4eb7-b42b-71e6a77d7d34' - ] - ) +node { + stage('Build') { + // trigger build + git url: 'ssh://your_project_repo' + // Fetch the changeset to a local branch using the build parameters provided to the build by the Gerrit Trigger... + def changeBranch = "change-${GERRIT_CHANGE_NUMBER}-${GERRIT_PATCHSET_NUMBER}" + sh "git fetch origin ${GERRIT_REFSPEC}:${changeBranch}" + sh "git checkout ${changeBranch}" + try { + withSonarQubeEnv('my-sonar-installation') { + withMaven(maven: 'my-maven-installation') { + sh "mvn clean verify sonar:sonar -Dsonar.analysis.mode=preview -Dsonar.report.export.path=sonar-report.json" + } + } + } finally { + sonarToGerrit( + inspectionConfig: [ + analysisStrategy: previewMode( + sonarQubeInstallationName: 'My SonarQube Installation', + baseConfig: [ + projectPath : '', + sonarReportPath: 'target/sonar/sonar-report.json', + autoMatch : true + ] + // OR + //subJobConfigs : [ + // [ + // projectPath: 'module0', + // sonarReportPath: 'target/sonar/sonar-report.json' + // ], + // [ + // projectPath: 'module1', + // sonarReportPath: 'target/module1/sonar/sonar-report.json' + // ] + //] + ) + ] + /* Optional parameters + , reviewConfig: [ + issueFilterConfig : [ + severity : 'INFO', + newIssuesOnly : false, + changedLinesOnly: false + ], + noIssuesTitleTemplate : 'SonarQube violations have not been found.', + someIssuesTitleTemplate: ' SonarQube violations have been found.', + issueCommentTemplate : ' SonarQube violation:\n\n\n\n\n\nRead more: ' + ], + scoreConfig: [ + issueFilterConfig: [ + severity : 'INFO', + newIssuesOnly : false, + changedLinesOnly: false + ], + category : 'Code-Review', + noIssuesScore : 0, + issuesScore : -1 + ], + notificationConfig: [ + noIssuesNotificationRecipient : 'NONE', + commentedIssuesNotificationRecipient: 'OWNER', + negativeScoreNotificationRecipient : 'OWNER' + ], + authConfig: [ + httpCredentialsId: 'b948c0ba-51a2-4eb7-b42b-71e6a77d7d34' + ]*/ + ) + } + } +} ``` ## Result example ### Result of plugin work in Gerrit history: -![alt text](doc/gerrit-history-example.png "Gerrit history example") +![Gerrit history example](doc/gerrit-history-example.png) ### Gerrit commit -![alt text](doc/gerrit-commit-example.png "Gerrit commit example") +![Gerrit commit example](doc/gerrit-commit-example.png) ### Score posted -![alt text](doc/score-posted-example.png "Score posted example") +![Score posted example](doc/score-posted-example.png) ## Troubleshooting @@ -303,23 +446,26 @@ sonarToGerrit ( This message occurres when RestAPIException is thrown by Gerrit API on attempt to post request. -Since version 1.0.7 it is possible to obtain a full stacktrace of the exception using a logger for class `org.jenkinsci.plugins.sonargerrit.SonarToGerritPublisher` +Since version 1.0.7 it is possible to obtain a full stacktrace of the exception using a logger for +class `org.jenkinsci.plugins.sonargerrit.SonarToGerritPublisher` -![alt text](doc/plugin-fails.png "Plugin failure") +![Plugin failure](doc/plugin-fails.png) The log will contain necessary information about the exception as follows: -![alt text](doc/sonar-gerrit-log.png "Sonar Gerrit log") +![Sonar Gerrit log](doc/sonar-gerrit-log.png) ## Version incompatibilities ### Version 1.0.5 -In this version plugin settings has moved from Build Steps to Post Build Actions. User needs to reconfigure jobs, or settings will be erased to default. +In this version plugin settings has moved from Build Steps to Post Build Actions. User needs to reconfigure jobs, or +settings will be erased to default. ## Issues -Report issues and enhancements in the [Issue tracker](https://issues.jenkins.io/issues/?jql=resolution%20is%20EMPTY%20and%20component%3D20853). +Report issues and enhancements in +the [Issue tracker](https://issues.jenkins.io/issues/?jql=resolution%20is%20EMPTY%20and%20component%3D20853). ## Contributing diff --git a/doc/sonar-pull-request-analysis-radio.png b/doc/sonar-pull-request-analysis-radio.png new file mode 100644 index 00000000..77191950 Binary files /dev/null and b/doc/sonar-pull-request-analysis-radio.png differ diff --git a/doc/sonar-scanner-wrapper.png b/doc/sonar-scanner-wrapper.png new file mode 100644 index 00000000..e758d400 Binary files /dev/null and b/doc/sonar-scanner-wrapper.png differ diff --git a/pom.xml b/pom.xml index 680bc815..0892faad 100644 --- a/pom.xml +++ b/pom.xml @@ -32,13 +32,13 @@ 0.9.4.0 5.8.2 1.16.2 - 4.9.2 2.13.0 2.13.1 2.7 3.21.0 1.2.3 + 7.7 Sonar Gerrit Plugin @@ -88,6 +88,11 @@ commons-lang3 3.9 + + org.json + json + 20190722 + @@ -124,6 +129,12 @@ gerrit-rest-java-client-shaded ${gerrit-rest-java-client.version} + + org.sonarsource.sonarqube + sonar-ws + ${sonar-ws.version} + + org.junit.jupiter junit-jupiter @@ -185,12 +196,6 @@ durable-task test - - com.squareup.okhttp3 - okhttp - ${okhttp.version} - test - com.fasterxml.jackson.core jackson-databind diff --git a/src/main/java/org/jenkinsci/plugins/sonargerrit/SonarToGerritPublisher.java b/src/main/java/org/jenkinsci/plugins/sonargerrit/SonarToGerritPublisher.java index b98deb61..8adb4ee3 100644 --- a/src/main/java/org/jenkinsci/plugins/sonargerrit/SonarToGerritPublisher.java +++ b/src/main/java/org/jenkinsci/plugins/sonargerrit/SonarToGerritPublisher.java @@ -92,7 +92,7 @@ public void perform( GerritRevision revision = GerritConnector.connect(connectionInfo).fetchRevision(); // load inspection report - InspectionReport report = inspectionConfig.analyse(listener, revision, filePath); + InspectionReport report = inspectionConfig.analyse(run, listener, revision, filePath); Map> fileToChangedLines = revision.getFileToChangedLines(); diff --git a/src/main/java/org/jenkinsci/plugins/sonargerrit/TaskListenerLogger.java b/src/main/java/org/jenkinsci/plugins/sonargerrit/TaskListenerLogger.java index ff0c1a2d..3d9ec985 100644 --- a/src/main/java/org/jenkinsci/plugins/sonargerrit/TaskListenerLogger.java +++ b/src/main/java/org/jenkinsci/plugins/sonargerrit/TaskListenerLogger.java @@ -16,6 +16,11 @@ @Restricted(NoExternalUse.class) public class TaskListenerLogger { + public static void log(TaskListener listener, String message, Object... params) { + listener.getLogger().printf(message, params); + listener.getLogger().println(); + } + public static void logMessage( TaskListener listener, Logger logger, Level level, String message, Object... params) { message = getLocalized(message, params); diff --git a/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/AnalysisStrategy.java b/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/AnalysisStrategy.java index 60764eb4..34d03820 100644 --- a/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/AnalysisStrategy.java +++ b/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/AnalysisStrategy.java @@ -1,6 +1,7 @@ package org.jenkinsci.plugins.sonargerrit.sonar; import hudson.FilePath; +import hudson.model.Run; import hudson.model.TaskListener; import java.io.IOException; import org.jenkinsci.plugins.sonargerrit.gerrit.Revision; @@ -8,6 +9,7 @@ /** @author Réda Housni Alaoui */ public interface AnalysisStrategy { - InspectionReport analyse(TaskListener listener, Revision revision, FilePath workspace) + InspectionReport analyse( + Run run, TaskListener listener, Revision revision, FilePath workspace) throws IOException, InterruptedException; } diff --git a/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/Component.java b/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/Component.java new file mode 100644 index 00000000..d8f39ed4 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/Component.java @@ -0,0 +1,14 @@ +package org.jenkinsci.plugins.sonargerrit.sonar; + +import javax.annotation.Nullable; + +/** @author Réda Housni Alaoui */ +public interface Component { + String getKey(); + + @Nullable + String getPath(); + + @Nullable + String getModuleKey(); +} diff --git a/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/preview_mode_analysis/ComponentPathBuilder.java b/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/Components.java similarity index 92% rename from src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/preview_mode_analysis/ComponentPathBuilder.java rename to src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/Components.java index 69ce0369..2f047eea 100644 --- a/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/preview_mode_analysis/ComponentPathBuilder.java +++ b/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/Components.java @@ -1,4 +1,4 @@ -package org.jenkinsci.plugins.sonargerrit.sonar.preview_mode_analysis; +package org.jenkinsci.plugins.sonargerrit.sonar; import static com.google.common.base.Preconditions.checkNotNull; @@ -28,13 +28,13 @@ * */ @Restricted(NoExternalUse.class) -class ComponentPathBuilder { +public class Components { private final Map nodes = Maps.newHashMap(); - public ComponentPathBuilder(final List components) { + public Components(final List components) { checkNotNull(components); - for (final ComponentRepresentation c : components) { + for (final Component c : components) { nodes.put(c.getKey(), new Node(c)); } } @@ -109,11 +109,11 @@ private Node getNodeByComponentKey(final String componentKey) { private static class Node { private static final String GERRIT_FILE_DELIMITER = "/"; - private final ComponentRepresentation component; + private final Component component; private Node parent; - public Node(final ComponentRepresentation c) { + public Node(final Component c) { checkNotNull(c); this.component = c; } @@ -154,7 +154,7 @@ private void buildPath(final StringBuilder path) { } } - public ComponentRepresentation getComponent() { + public Component getComponent() { return component; } diff --git a/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/Inspection.java b/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/Inspection.java index 5dba6563..133e81d3 100644 --- a/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/Inspection.java +++ b/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/Inspection.java @@ -5,6 +5,7 @@ import hudson.FilePath; import hudson.model.AbstractDescribableImpl; import hudson.model.Descriptor; +import hudson.model.Run; import hudson.model.TaskListener; import hudson.plugins.sonar.SonarInstallation; import java.io.IOException; @@ -19,6 +20,7 @@ import org.jenkinsci.plugins.sonargerrit.sonar.preview_mode_analysis.PreviewModeAnalysisStrategy; import org.jenkinsci.plugins.sonargerrit.sonar.preview_mode_analysis.SonarQubeInstallations; import org.jenkinsci.plugins.sonargerrit.sonar.preview_mode_analysis.SubJobConfig; +import org.jenkinsci.plugins.sonargerrit.sonar.pull_request_analysis.PullRequestAnalysisStrategy; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; @@ -70,9 +72,10 @@ protected Object readResolve() { @DataBoundConstructor public Inspection() {} - public InspectionReport analyse(TaskListener listener, Revision revision, FilePath workspace) + public InspectionReport analyse( + Run run, TaskListener listener, Revision revision, FilePath workspace) throws IOException, InterruptedException { - return analysisStrategy.analyse(listener, revision, workspace); + return analysisStrategy.analyse(run, listener, revision, workspace); } public AnalysisStrategy getAnalysisStrategy() { @@ -198,7 +201,9 @@ public static class DescriptorImpl extends Descriptor { @SuppressWarnings("unused") public List> getAnalysisStrategyDescriptors() { Jenkins jenkins = Jenkins.get(); - return ImmutableList.of(jenkins.getDescriptorOrDie(PreviewModeAnalysisStrategy.class)); + return ImmutableList.of( + jenkins.getDescriptorOrDie(PullRequestAnalysisStrategy.class), + jenkins.getDescriptorOrDie(PreviewModeAnalysisStrategy.class)); } @Override diff --git a/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/Rule.java b/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/Rule.java new file mode 100644 index 00000000..be86ec60 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/Rule.java @@ -0,0 +1,47 @@ +package org.jenkinsci.plugins.sonargerrit.sonar; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +/** @author Réda Housni Alaoui */ +@Restricted(NoExternalUse.class) +public class Rule { + + private final String id; + + public Rule(String id) { + this.id = id; + } + + public String id() { + return id; + } + + public String createLink(String sonarQubeUrl) { + if (sonarQubeUrl == null) { + return id; + } + StringBuilder sb = new StringBuilder(); + String url = sonarQubeUrl.trim(); + if (!(url.startsWith("http://") || sonarQubeUrl.startsWith("https://"))) { + sb.append("http://"); + } + sb.append(url); + if (!(url.endsWith("/"))) { + sb.append("/"); + } + sb.append("coding_rules#rule_key="); + sb.append(escapeHttp(id)); // squid%3AS1319 + return sb.toString(); + } + + private String escapeHttp(String query) { + try { + return URLEncoder.encode(query, "UTF-8"); + } catch (UnsupportedEncodingException e) { + return query; + } + } +} diff --git a/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/preview_mode_analysis/ComponentRepresentation.java b/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/preview_mode_analysis/ComponentRepresentation.java index 84af0526..0c7b7764 100644 --- a/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/preview_mode_analysis/ComponentRepresentation.java +++ b/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/preview_mode_analysis/ComponentRepresentation.java @@ -2,12 +2,13 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import javax.annotation.Nullable; +import org.jenkinsci.plugins.sonargerrit.sonar.Component; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; /** Project: Sonar-Gerrit Plugin Author: Tatiana Didik */ @Restricted(NoExternalUse.class) -class ComponentRepresentation { +class ComponentRepresentation implements Component { @SuppressWarnings("unused") @SuppressFBWarnings("UWF_UNWRITTEN_FIELD") private String key; @@ -24,15 +25,18 @@ class ComponentRepresentation { @SuppressFBWarnings("UWF_UNWRITTEN_FIELD") private String status; + @Override public String getKey() { return key; } + @Override @Nullable public String getPath() { return path; } + @Override @Nullable public String getModuleKey() { return moduleKey; diff --git a/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/preview_mode_analysis/PreviewModeAnalysisStrategy.java b/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/preview_mode_analysis/PreviewModeAnalysisStrategy.java index 21c10568..3c871237 100644 --- a/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/preview_mode_analysis/PreviewModeAnalysisStrategy.java +++ b/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/preview_mode_analysis/PreviewModeAnalysisStrategy.java @@ -5,6 +5,7 @@ import hudson.FilePath; import hudson.model.AbstractDescribableImpl; import hudson.model.Descriptor; +import hudson.model.Run; import hudson.model.TaskListener; import hudson.plugins.sonar.SonarGlobalConfiguration; import hudson.plugins.sonar.SonarInstallation; @@ -127,7 +128,8 @@ public boolean isPathCorrectionNeeded() { } @Override - public InspectionReport analyse(TaskListener listener, Revision revision, FilePath workspace) + public InspectionReport analyse( + Run run, TaskListener listener, Revision revision, FilePath workspace) throws IOException, InterruptedException { return new SonarConnector(listener, this, revision, getSonarQubeInstallation().orElse(null)) .readSonarReports(workspace); @@ -162,7 +164,7 @@ public ListBoxModel doFillSonarQubeInstallationNameItems() { @Override public String getDisplayName() { - return "Preview mode analysis"; + return "Preview mode analysis (until SonarQube 7.6)"; } } } diff --git a/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/preview_mode_analysis/SimpleIssue.java b/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/preview_mode_analysis/SimpleIssue.java index 9d06a1a8..e42b4ebc 100644 --- a/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/preview_mode_analysis/SimpleIssue.java +++ b/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/preview_mode_analysis/SimpleIssue.java @@ -1,9 +1,9 @@ package org.jenkinsci.plugins.sonargerrit.sonar.preview_mode_analysis; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; import java.util.Date; +import org.jenkinsci.plugins.sonargerrit.sonar.Components; import org.jenkinsci.plugins.sonargerrit.sonar.Issue; +import org.jenkinsci.plugins.sonargerrit.sonar.Rule; import org.jenkinsci.plugins.sonargerrit.sonar.Severity; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; @@ -13,7 +13,7 @@ class SimpleIssue implements Issue { private final IssueRepresentation representation; - private final ComponentPathBuilder pathBuilder; + private final Components components; private final SubJobConfig config; private final String sonarQubeUrl; @@ -21,48 +21,26 @@ class SimpleIssue implements Issue { public SimpleIssue( IssueRepresentation representation, - ComponentPathBuilder pathBuilder, + Components components, SubJobConfig config, String sonarQubeUrl) { this.representation = representation; - this.pathBuilder = pathBuilder; + this.components = components; this.config = config; this.sonarQubeUrl = sonarQubeUrl; } @Override public String getRuleLink() { - if (sonarQubeUrl == null) { - return getRule(); - } - StringBuilder sb = new StringBuilder(); - String url = sonarQubeUrl.trim(); - if (!(url.startsWith("http://") || sonarQubeUrl.startsWith("https://"))) { - sb.append("http://"); - } - sb.append(url); - if (!(url.endsWith("/"))) { - sb.append("/"); - } - sb.append("coding_rules#rule_key="); - sb.append(escapeHttp(getRule())); // squid%3AS1319 - return sb.toString(); - } - - private String escapeHttp(String query) { - try { - return URLEncoder.encode(query, "UTF-8"); - } catch (UnsupportedEncodingException e) { - return query; - } + return new Rule(getRule()).createLink(sonarQubeUrl); } @Override public String getFilepath() { if (filepath == null) { - if (pathBuilder != null) { + if (components != null) { filepath = - pathBuilder + components .buildPrefixedPathForComponentWithKey(getComponent(), config.getProjectPath()) .or(getComponent()); } else { diff --git a/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/preview_mode_analysis/SonarConnector.java b/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/preview_mode_analysis/SonarConnector.java index 37fe78ae..072f65c7 100644 --- a/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/preview_mode_analysis/SonarConnector.java +++ b/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/preview_mode_analysis/SonarConnector.java @@ -15,6 +15,7 @@ import java.util.logging.Logger; import org.jenkinsci.plugins.sonargerrit.TaskListenerLogger; import org.jenkinsci.plugins.sonargerrit.gerrit.Revision; +import org.jenkinsci.plugins.sonargerrit.sonar.Components; import org.jenkinsci.plugins.sonargerrit.sonar.InspectionReport; import org.jenkinsci.plugins.sonargerrit.sonar.Issue; import org.kohsuke.accmod.Restricted; @@ -93,10 +94,10 @@ public void readSonarReports(List reports, ReportRecorder reportReco // multimap file-to-issues generation for each report for (ReportInfo info : reports) { ReportRepresentation report = info.report; - final ComponentPathBuilder pathBuilder = new ComponentPathBuilder(report.getComponents()); + final Components components = new Components(report.getComponents()); for (IssueRepresentation issue : report.getIssues()) { Issue issueToRecord = - decorator.decorate(new SimpleIssue(issue, pathBuilder, info.config, sonarQubeUrl)); + decorator.decorate(new SimpleIssue(issue, components, info.config, sonarQubeUrl)); reportRecorder.recordIssue(issueToRecord); } } diff --git a/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/pull_request_analysis/PullRequestAnalysisStrategy.java b/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/pull_request_analysis/PullRequestAnalysisStrategy.java new file mode 100644 index 00000000..318c19e5 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/pull_request_analysis/PullRequestAnalysisStrategy.java @@ -0,0 +1,42 @@ +package org.jenkinsci.plugins.sonargerrit.sonar.pull_request_analysis; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.FilePath; +import hudson.model.AbstractDescribableImpl; +import hudson.model.Descriptor; +import hudson.model.Run; +import hudson.model.TaskListener; +import org.jenkinsci.Symbol; +import org.jenkinsci.plugins.sonargerrit.gerrit.Revision; +import org.jenkinsci.plugins.sonargerrit.sonar.AnalysisStrategy; +import org.jenkinsci.plugins.sonargerrit.sonar.InspectionReport; +import org.kohsuke.stapler.DataBoundConstructor; + +/** @author Réda Housni Alaoui */ +public class PullRequestAnalysisStrategy + extends AbstractDescribableImpl implements AnalysisStrategy { + + @DataBoundConstructor + public PullRequestAnalysisStrategy() {} + + @Override + public InspectionReport analyse( + Run run, TaskListener listener, Revision revision, FilePath workspace) + throws InterruptedException { + + return new InspectionReport( + PullRequestAnalysisTask.parseLastAnalysis(run).fetchIssues(listener)); + } + + @Symbol("pullRequest") + @Extension + public static class DescriptorImpl extends Descriptor { + + @NonNull + @Override + public String getDisplayName() { + return "Pull request analysis (since SonarQube 7.2)"; + } + } +} diff --git a/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/pull_request_analysis/PullRequestAnalysisTask.java b/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/pull_request_analysis/PullRequestAnalysisTask.java new file mode 100644 index 00000000..616e2ad2 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/pull_request_analysis/PullRequestAnalysisTask.java @@ -0,0 +1,179 @@ +package org.jenkinsci.plugins.sonargerrit.sonar.pull_request_analysis; + +import static java.util.Objects.requireNonNull; + +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.plugins.sonar.SonarInstallation; +import hudson.plugins.sonar.action.SonarAnalysisAction; +import hudson.plugins.sonar.utils.SonarUtils; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; +import org.jenkinsci.plugins.sonargerrit.TaskListenerLogger; +import org.jenkinsci.plugins.sonargerrit.sonar.Components; +import org.jenkinsci.plugins.sonargerrit.sonar.Issue; +import org.sonarqube.ws.Ce; +import org.sonarqube.ws.Issues; +import org.sonarqube.ws.client.HttpConnector; +import org.sonarqube.ws.client.WsClient; +import org.sonarqube.ws.client.WsClientFactories; +import org.sonarqube.ws.client.ce.CeService; +import org.sonarqube.ws.client.ce.TaskRequest; +import org.sonarqube.ws.client.issues.SearchRequest; + +/** @author Réda Housni Alaoui */ +class PullRequestAnalysisTask { + + private static final String PLEASE_USE_THE_WITH_SONAR_QUBE_ENV_WRAPPER_TO_RUN_YOUR_ANALYSIS = + "Please use the 'withSonarQubeEnv' wrapper to run your analysis."; + private static final Duration CE_TASK_COMPLETION_CHECK_INTERVAL = Duration.ofSeconds(5); + + private final WsClient sonarClient; + private final String serverUrl; + private final String taskId; + private final String componentKey; + + private PullRequestAnalysisTask( + WsClient sonarClient, String serverUrl, String taskId, String componentKey) { + this.sonarClient = requireNonNull(sonarClient); + this.serverUrl = requireNonNull(StringUtils.trimToNull(serverUrl)); + this.taskId = requireNonNull(StringUtils.trimToNull(taskId)); + this.componentKey = requireNonNull(StringUtils.trimToNull(componentKey)); + } + + public static PullRequestAnalysisTask parseLastAnalysis(Run run) { + List actions = run.getActions(SonarAnalysisAction.class); + if (actions.isEmpty()) { + throw new IllegalStateException( + String.format( + "No previous SonarQube analysis found on this build. %s", + PLEASE_USE_THE_WITH_SONAR_QUBE_ENV_WRAPPER_TO_RUN_YOUR_ANALYSIS)); + } + + // Consider last analysis first + List reversedActions = new ArrayList<>(actions); + Collections.reverse(reversedActions); + return reversedActions.stream() + .map(sonarAnalysisAction -> tryCreate(run, sonarAnalysisAction)) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst() + .orElseThrow( + () -> + new IllegalStateException( + String.format( + "No previous SonarQube pull request analysis found on this build. %s", + PLEASE_USE_THE_WITH_SONAR_QUBE_ENV_WRAPPER_TO_RUN_YOUR_ANALYSIS))); + } + + private static Optional tryCreate( + Run run, SonarAnalysisAction action) { + String ceTaskId = action.getCeTaskId(); + if (ceTaskId == null) { + return Optional.empty(); + } + String serverUrl = action.getServerUrl(); + if (serverUrl == null) { + return Optional.empty(); + } + String installationName = action.getInstallationName(); + SonarInstallation sonarInstallation = + Optional.ofNullable(SonarInstallation.get(installationName)) + .orElseThrow( + () -> + new IllegalStateException( + String.format("Invalid installation name: %s", installationName))); + + String authenticationToken = + SonarUtils.getAuthenticationToken( + run, sonarInstallation, sonarInstallation.getCredentialsId()); + + WsClient sonarClient = + WsClientFactories.getDefault() + .newClient( + HttpConnector.newBuilder().url(serverUrl).token(authenticationToken).build()); + + Ce.Task ceTask = sonarClient.ce().task(new TaskRequest().setId(ceTaskId)).getTask(); + String componentKey = ceTask.getComponentKey(); + if (StringUtils.isBlank(componentKey)) { + return Optional.empty(); + } + + return Optional.of(new PullRequestAnalysisTask(sonarClient, serverUrl, ceTaskId, componentKey)); + } + + public List fetchIssues(TaskListener listener) throws InterruptedException { + CeService ceService = sonarClient.ce(); + Ce.Task ceTask; + while (true) { + ceTask = ceService.task(new TaskRequest().setId(taskId)).getTask(); + if (isComplete(listener, ceTask)) { + break; + } + TaskListenerLogger.log( + listener, + "Waiting %s before re-checking SonarQube task '%s' status ...", + CE_TASK_COMPLETION_CHECK_INTERVAL, + taskId); + Thread.sleep(CE_TASK_COMPLETION_CHECK_INTERVAL.toMillis()); + } + + String pullRequest = ceTask.getPullRequest(); + if (StringUtils.isBlank(pullRequest)) { + // Sometimes, when the task fails very early, the pull request attribute is blank. + // This is why, we can't control the presence of this attribute before creating the + // PullRequestAnalysisTask. + throw new IllegalStateException( + String.format("No pull request found for SonarQube task '%s'", taskId)); + } + + SearchRequest issueSearchRequest = + new SearchRequest() + .setComponentKeys(Collections.singletonList(componentKey)) + .setPullRequest(pullRequest); + + Issues.SearchWsResponse issueSearchResponse = sonarClient.issues().search(issueSearchRequest); + + Components components = + new Components( + issueSearchResponse.getComponentsList().stream() + .map(PullRequestComponent::new) + .collect(Collectors.toList())); + + return issueSearchResponse.getIssuesList().stream() + .map(issue -> new PullRequestIssue(components, issue, serverUrl)) + .collect(Collectors.toList()); + } + + private boolean isComplete(TaskListener listener, Ce.Task ceTask) { + Ce.TaskStatus taskStatus = ceTask.getStatus(); + switch (taskStatus) { + case SUCCESS: + TaskListenerLogger.log(listener, "SonarQube task '%s' completed.", taskId); + return true; + case PENDING: + TaskListenerLogger.log(listener, "SonarQube task '%s' is pending.", taskId); + return false; + case IN_PROGRESS: + TaskListenerLogger.log(listener, "SonarQube task '%s' is in progress.", taskId); + return false; + case FAILED: + throw new IllegalStateException( + String.format( + "SonarQube analysis '%s' failed with message: %s.", + taskId, ceTask.getErrorMessage())); + case CANCELED: + throw new IllegalStateException( + String.format("SonarQube analysis '%s' was canceled.", taskId)); + default: + throw new IllegalStateException( + String.format( + "SonarQube analysis '%s' returned unexpected status '%s'", taskId, taskStatus)); + } + } +} diff --git a/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/pull_request_analysis/PullRequestComponent.java b/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/pull_request_analysis/PullRequestComponent.java new file mode 100644 index 00000000..c9b97a9c --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/pull_request_analysis/PullRequestComponent.java @@ -0,0 +1,32 @@ +package org.jenkinsci.plugins.sonargerrit.sonar.pull_request_analysis; + +import javax.annotation.Nullable; +import org.jenkinsci.plugins.sonargerrit.sonar.Component; +import org.sonarqube.ws.Issues; + +/** @author Réda Housni Alaoui */ +class PullRequestComponent implements Component { + + private final Issues.Component component; + + public PullRequestComponent(Issues.Component component) { + this.component = component; + } + + @Override + public String getKey() { + return component.getKey(); + } + + @Nullable + @Override + public String getPath() { + return component.getPath(); + } + + @Nullable + @Override + public String getModuleKey() { + return null; + } +} diff --git a/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/pull_request_analysis/PullRequestIssue.java b/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/pull_request_analysis/PullRequestIssue.java new file mode 100644 index 00000000..cbefb206 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/sonargerrit/sonar/pull_request_analysis/PullRequestIssue.java @@ -0,0 +1,83 @@ +package org.jenkinsci.plugins.sonargerrit.sonar.pull_request_analysis; + +import static java.util.Objects.requireNonNull; + +import java.time.ZonedDateTime; +import java.util.Date; +import org.jenkinsci.plugins.sonargerrit.sonar.Components; +import org.jenkinsci.plugins.sonargerrit.sonar.Issue; +import org.jenkinsci.plugins.sonargerrit.sonar.Rule; +import org.jenkinsci.plugins.sonargerrit.sonar.Severity; +import org.sonarqube.ws.Issues; + +/** @author Réda Housni Alaoui */ +class PullRequestIssue implements Issue { + + private final Issues.Issue issue; + private final String filePath; + private final String sonarQubeUrl; + + public PullRequestIssue(Components components, Issues.Issue issue, String sonarQubeUrl) { + this.issue = requireNonNull(issue); + this.filePath = + components + .buildPrefixedPathForComponentWithKey(issue.getComponent(), "") + .or(issue.getComponent()); + this.sonarQubeUrl = requireNonNull(sonarQubeUrl); + } + + @Override + public String getFilepath() { + return filePath; + } + + @Override + public String getKey() { + return issue.getKey(); + } + + @Override + public String getComponent() { + return issue.getComponent(); + } + + @Override + public Integer getLine() { + return issue.getLine(); + } + + @Override + public String getMessage() { + return issue.getMessage(); + } + + @Override + public Severity getSeverity() { + return Severity.valueOf(issue.getSeverity().name()); + } + + @Override + public String getRule() { + return issue.getRule(); + } + + @Override + public String getRuleLink() { + return new Rule(issue.getRule()).createLink(sonarQubeUrl); + } + + @Override + public String getStatus() { + return issue.getStatus(); + } + + @Override + public boolean isNew() { + return true; + } + + @Override + public Date getCreationDate() { + return Date.from(ZonedDateTime.parse(issue.getCreationDate()).toInstant()); + } +} diff --git a/src/main/resources/org/jenkinsci/plugins/sonargerrit/sonar/IssueFilterConfig/config.properties b/src/main/resources/org/jenkinsci/plugins/sonargerrit/sonar/IssueFilterConfig/config.properties index 1cfd16c1..52577cfa 100644 --- a/src/main/resources/org/jenkinsci/plugins/sonargerrit/sonar/IssueFilterConfig/config.properties +++ b/src/main/resources/org/jenkinsci/plugins/sonargerrit/sonar/IssueFilterConfig/config.properties @@ -1,6 +1,6 @@ jenkins.plugin.settings.gerrit.filter.severity=Report issues having severity level higher or equal to: jenkins.plugin.settings.gerrit.filter.new=Report new issues only? -jenkins.plugin.settings.gerrit.filter.new.description=Reports new as well as existing issues when unchecked +jenkins.plugin.settings.gerrit.filter.new.description=Reports new as well as existing issues when unchecked. Note that pull request analysis only reports new issues and is therefore unaffected by this parameter. jenkins.plugin.settings.gerrit.filter.lines.changed=Affect changed lines only jenkins.plugin.settings.gerrit.filter.lines.changed.description=Reports issues for changed as well as not changed lines in files affected by patchset when unchecked diff --git a/src/test/java/org/jenkinsci/plugins/sonargerrit/ConfigRoundTripTest.java b/src/test/java/org/jenkinsci/plugins/sonargerrit/ConfigRoundTripTest.java index c8eb7700..94f5c570 100644 --- a/src/test/java/org/jenkinsci/plugins/sonargerrit/ConfigRoundTripTest.java +++ b/src/test/java/org/jenkinsci/plugins/sonargerrit/ConfigRoundTripTest.java @@ -7,6 +7,7 @@ import org.jenkinsci.plugins.sonargerrit.gerrit.ScoreConfig; import org.jenkinsci.plugins.sonargerrit.sonar.preview_mode_analysis.PreviewModeAnalysisStrategy; import org.jenkinsci.plugins.sonargerrit.sonar.preview_mode_analysis.SubJobConfig; +import org.jenkinsci.plugins.sonargerrit.sonar.pull_request_analysis.PullRequestAnalysisStrategy; import org.jenkinsci.plugins.sonargerrit.test_infrastructure.jenkins.EnableJenkinsRule; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -207,7 +208,7 @@ void test3(JenkinsRule jenkinsRule) throws Exception { } @Test - @DisplayName("next") + @DisplayName("Preview mode analysis next") void test4(JenkinsRule jenkinsRule) throws Exception { FreeStyleProject project = jenkinsRule.createFreeStyleProject(); @@ -269,4 +270,59 @@ void test4(JenkinsRule jenkinsRule) throws Exception { "notificationConfig.negativeScoreNotificationRecipient", "authConfig.httpCredentialsId")); } + + @Test + @DisplayName("Pull request analysis next") + void test5(JenkinsRule jenkinsRule) throws Exception { + FreeStyleProject project = jenkinsRule.createFreeStyleProject(); + + SonarToGerritPublisher before = new SonarToGerritPublisher(); + project.getPublishersList().add(before); + + before.getInspectionConfig().setAnalysisStrategy(new PullRequestAnalysisStrategy()); + + before.getReviewConfig().getIssueFilterConfig().setChangedLinesOnly(true); + before.getReviewConfig().getIssueFilterConfig().setNewIssuesOnly(true); + before.getReviewConfig().getIssueFilterConfig().setSeverity("BLOCKER"); + + before.setScoreConfig(new ScoreConfig()); + before.getScoreConfig().getIssueFilterConfig().setChangedLinesOnly(true); + before.getScoreConfig().getIssueFilterConfig().setNewIssuesOnly(true); + before.getScoreConfig().getIssueFilterConfig().setSeverity("BLOCKER"); + before.getScoreConfig().setNoIssuesScore(20); + before.getScoreConfig().setIssuesScore(-20); + before.getScoreConfig().setCategory("Foo-Label"); + + before.getNotificationConfig().setNoIssuesNotificationRecipient("foo"); + before.getNotificationConfig().setCommentedIssuesNotificationRecipient("bar"); + before.getNotificationConfig().setNegativeScoreNotificationRecipient("baz"); + + GerritAuthenticationConfig authenticationConfig = new GerritAuthenticationConfig(); + authenticationConfig.setHttpCredentialsId(UUID.randomUUID().toString()); + before.setAuthConfig(authenticationConfig); + + jenkinsRule.submit( + jenkinsRule.createWebClient().getPage(project, "configure").getFormByName("config")); + + SonarToGerritPublisher after = project.getPublishersList().get(SonarToGerritPublisher.class); + jenkinsRule.assertEqualBeans( + before, + after, + String.join( + ",", + "inspectionConfig.analysisStrategy", + "reviewConfig.issueFilterConfig.changedLinesOnly", + "reviewConfig.issueFilterConfig.newIssuesOnly", + "reviewConfig.issueFilterConfig.severity", + "scoreConfig.issueFilterConfig.changedLinesOnly", + "scoreConfig.issueFilterConfig.newIssuesOnly", + "scoreConfig.issueFilterConfig.severity", + "scoreConfig.noIssuesScore", + "scoreConfig.issuesScore", + "scoreConfig.category", + "notificationConfig.noIssuesNotificationRecipient", + "notificationConfig.commentedIssuesNotificationRecipient", + "notificationConfig.negativeScoreNotificationRecipient", + "authConfig.httpCredentialsId")); + } } diff --git a/src/test/java/org/jenkinsci/plugins/sonargerrit/sonar/preview_mode_analysis/ComponentPathBuilderTest.java b/src/test/java/org/jenkinsci/plugins/sonargerrit/sonar/preview_mode_analysis/ComponentPathBuilderTest.java index 8ee8a77f..b6f93bdf 100644 --- a/src/test/java/org/jenkinsci/plugins/sonargerrit/sonar/preview_mode_analysis/ComponentPathBuilderTest.java +++ b/src/test/java/org/jenkinsci/plugins/sonargerrit/sonar/preview_mode_analysis/ComponentPathBuilderTest.java @@ -6,6 +6,7 @@ import java.io.IOException; import java.util.Arrays; import java.util.List; +import org.jenkinsci.plugins.sonargerrit.sonar.Components; import org.jenkinsci.plugins.sonargerrit.sonar.InspectionReport; import org.jenkinsci.plugins.sonargerrit.sonar.Issue; import org.junit.jupiter.api.Assertions; @@ -29,7 +30,7 @@ public void testNestedComponent() throws IOException, InterruptedException { Issue issue = issues.get(0); ReportRepresentation report = recordedReports.getRawReport(config); - ComponentPathBuilder builder = new ComponentPathBuilder(report.getComponents()); + Components builder = new Components(report.getComponents()); String issueComponent = issue.getComponent(); String realFileName = builder @@ -54,7 +55,7 @@ public void testSuperNestedComponent() throws IOException, InterruptedException Assertions.assertEquals(8, issues.size()); ReportRepresentation report = recordedReports.getRawReport(config); - ComponentPathBuilder builder = new ComponentPathBuilder(report.getComponents()); + Components builder = new Components(report.getComponents()); testIssue( builder, issues.get(0), @@ -298,7 +299,7 @@ public void testTwoProjectPaths() throws IOException, InterruptedException { } private void testIssue( - ComponentPathBuilder builder, Issue issue, String projectPath, String expectedFilename) { + Components builder, Issue issue, String projectPath, String expectedFilename) { String issueComponent = issue.getComponent(); String realFileName = builder diff --git a/src/test/java/org/jenkinsci/plugins/sonargerrit/PreviewModeAnalysisTest.java b/src/test/java/org/jenkinsci/plugins/sonargerrit/sonar/preview_mode_analysis/PreviewModeAnalysisTest.java similarity index 97% rename from src/test/java/org/jenkinsci/plugins/sonargerrit/PreviewModeAnalysisTest.java rename to src/test/java/org/jenkinsci/plugins/sonargerrit/sonar/preview_mode_analysis/PreviewModeAnalysisTest.java index 8eb3cd6f..9e81903c 100644 --- a/src/test/java/org/jenkinsci/plugins/sonargerrit/PreviewModeAnalysisTest.java +++ b/src/test/java/org/jenkinsci/plugins/sonargerrit/sonar/preview_mode_analysis/PreviewModeAnalysisTest.java @@ -1,4 +1,4 @@ -package org.jenkinsci.plugins.sonargerrit; +package org.jenkinsci.plugins.sonargerrit.sonar.preview_mode_analysis; import static org.assertj.core.api.Assertions.assertThat; @@ -18,10 +18,10 @@ import jenkins.model.ParameterizedJobMixIn; import me.redaalaoui.gerrit_rest_java_client.thirdparty.com.google.gerrit.extensions.common.ChangeInfo; import org.eclipse.jgit.api.errors.GitAPIException; +import org.jenkinsci.plugins.sonargerrit.SonarToGerritPublisher; import org.jenkinsci.plugins.sonargerrit.gerrit.ScoreConfig; import org.jenkinsci.plugins.sonargerrit.sonar.Inspection; import org.jenkinsci.plugins.sonargerrit.sonar.IssueFilterConfig; -import org.jenkinsci.plugins.sonargerrit.sonar.preview_mode_analysis.PreviewModeAnalysisStrategy; import org.jenkinsci.plugins.sonargerrit.test_infrastructure.cluster.Cluster; import org.jenkinsci.plugins.sonargerrit.test_infrastructure.cluster.EnableCluster; import org.jenkinsci.plugins.sonargerrit.test_infrastructure.gerrit.GerritChange; @@ -143,7 +143,7 @@ private Job createFreestyleJob(GerritChange change) throws IOException { job.setScm(createGitSCM(change, patchSetNumber)); job.getBuildWrappersList() - .add(new SonarBuildWrapper(cluster.jenkinsSonarqubeInstallationName())); + .add(new SonarBuildWrapper(cluster.jenkinsSonarqube7InstallationName())); job.getBuildWrappersList() .add( @@ -157,7 +157,7 @@ private Job createFreestyleJob(GerritChange change) throws IOException { SonarToGerritPublisher sonarToGerrit = new SonarToGerritPublisher(); Inspection inspectionConfig = sonarToGerrit.getInspectionConfig(); PreviewModeAnalysisStrategy analysisStrategy = new PreviewModeAnalysisStrategy(); - analysisStrategy.setSonarQubeInstallationName(cluster.jenkinsSonarqubeInstallationName()); + analysisStrategy.setSonarQubeInstallationName(cluster.jenkinsSonarqube7InstallationName()); analysisStrategy.setAutoMatch(true); inspectionConfig.setAnalysisStrategy(analysisStrategy); IssueFilterConfig issueFilterConfig = sonarToGerrit.getReviewConfig().getIssueFilterConfig(); @@ -195,7 +195,7 @@ private Job createPipelineJob(GerritChange change) throws IOException { + "branches: [[name: 'FETCH_HEAD']]\n" + "])\n" + String.format( - "withSonarQubeEnv('%s') {\n", cluster.jenkinsSonarqubeInstallationName()) + "withSonarQubeEnv('%s') {\n", cluster.jenkinsSonarqube7InstallationName()) + String.format( "withMaven(jdk: '%s', maven: '%s') {\n", cluster.jenkinsJdk8InstallationName(), cluster.jenkinsMavenInstallationName()) @@ -207,7 +207,7 @@ private Job createPipelineJob(GerritChange change) throws IOException { + "inspectionConfig: [\n" + "analysisStrategy: previewMode(\n" + String.format( - "sonarQubeInstallationName: '%s',\n", cluster.jenkinsSonarqubeInstallationName()) + "sonarQubeInstallationName: '%s',\n", cluster.jenkinsSonarqube7InstallationName()) + "baseConfig: [autoMatch: true]\n" + ")\n" + "],\n" // inspectionConfig diff --git a/src/test/java/org/jenkinsci/plugins/sonargerrit/sonar/pull_request_analysis/PullRequestAnalysisTest.java b/src/test/java/org/jenkinsci/plugins/sonargerrit/sonar/pull_request_analysis/PullRequestAnalysisTest.java new file mode 100644 index 00000000..71988783 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/sonargerrit/sonar/pull_request_analysis/PullRequestAnalysisTest.java @@ -0,0 +1,298 @@ +package org.jenkinsci.plugins.sonargerrit.sonar.pull_request_analysis; + +import static org.assertj.core.api.Assertions.assertThat; + +import hudson.model.FreeStyleProject; +import hudson.model.Job; +import hudson.model.queue.QueueTaskFuture; +import hudson.plugins.git.BranchSpec; +import hudson.plugins.git.GitSCM; +import hudson.plugins.git.UserRemoteConfig; +import hudson.plugins.sonar.SonarBuildWrapper; +import hudson.tasks.Maven; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import jenkins.model.Jenkins; +import jenkins.model.ParameterizedJobMixIn; +import me.redaalaoui.gerrit_rest_java_client.thirdparty.com.google.gerrit.extensions.common.ChangeInfo; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.jenkinsci.plugins.sonargerrit.SonarToGerritPublisher; +import org.jenkinsci.plugins.sonargerrit.gerrit.ScoreConfig; +import org.jenkinsci.plugins.sonargerrit.sonar.Inspection; +import org.jenkinsci.plugins.sonargerrit.sonar.IssueFilterConfig; +import org.jenkinsci.plugins.sonargerrit.test_infrastructure.cluster.Cluster; +import org.jenkinsci.plugins.sonargerrit.test_infrastructure.cluster.EnableCluster; +import org.jenkinsci.plugins.sonargerrit.test_infrastructure.gerrit.GerritChange; +import org.jenkinsci.plugins.sonargerrit.test_infrastructure.gerrit.GerritGit; +import org.jenkinsci.plugins.sonargerrit.test_infrastructure.gerrit.GerritServer; +import org.jenkinsci.plugins.sonargerrit.test_infrastructure.jenkins.EnvironmentVariableBuildWrapper; +import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** @author Réda Housni Alaoui */ +@EnableCluster +class PullRequestAnalysisTest { + + private static final String MAVEN_FREESTYLE_TARGET = + "clean verify sonar:sonar " + + "-Dsonar.pullrequest.key=${GERRIT_CHANGE_NUMBER}-${GERRIT_PATCHSET_NUMBER} " + + "-Dsonar.pullrequest.base=${GERRIT_BRANCH} " + + "-Dsonar.pullrequest.branch=${GERRIT_REFSPEC}"; + + private static final String MAVEN_PIPELINE_TARGET = + "clean verify sonar:sonar " + + "-Dsonar.pullrequest.key=${env.GERRIT_CHANGE_NUMBER}-${env.GERRIT_PATCHSET_NUMBER} " + + "-Dsonar.pullrequest.base=${env.GERRIT_BRANCH} " + + "-Dsonar.pullrequest.branch=${env.GERRIT_REFSPEC}"; + + private static Cluster cluster; + private static GerritGit git; + + @BeforeAll + static void beforeAll(Cluster cluster, @TempDir Path workTree) throws Exception { + + PullRequestAnalysisTest.cluster = cluster; + + git = GerritGit.createAndCloneRepository(cluster.gerrit(), workTree); + + git.addAndCommitFile( + "pom.xml", + "\n" + + "\n" + + " 4.0.0\n" + + "\n" + + " org.example\n" + + " example\n" + + " 1.0-SNAPSHOT\n" + + ""); + + git.push(); + + FreeStyleProject masterJob = cluster.jenkinsRule().createFreeStyleProject(); + masterJob.setJDK(Jenkins.get().getJDK(cluster.jenkinsJdk8InstallationName())); + masterJob.setScm(createGitSCM()); + masterJob + .getBuildWrappersList() + .add(new SonarBuildWrapper(cluster.jenkinsSonarqubeInstallationName())); + masterJob + .getBuildersList() + .add( + new Maven( + "clean verify sonar:sonar -Dsonar.branch.name=master", + cluster.jenkinsMavenInstallationName())); + triggerAndAssertSuccess(masterJob); + } + + @BeforeEach + void beforeEach() throws GitAPIException { + git.resetToOriginMaster(); + } + + @Test + @DisplayName("Bad quality freestyle build") + void test1() throws Exception { + testWithBadQualityCode(this::createFreestyleJob); + } + + @Test + @DisplayName("Good quality freestyle build") + void test2() throws Exception { + testWithGoodQualityCode(this::createFreestyleJob); + } + + @Test + @DisplayName("Bad quality pipeline build") + void test3() throws Exception { + testWithBadQualityCode(this::createPipelineJob); + } + + @Test + @DisplayName("Good quality pipeline build") + void test4() throws Exception { + testWithGoodQualityCode(this::createPipelineJob); + } + + private void testWithBadQualityCode(JobFactory jobFactory) throws Exception { + git.addAndCommitFile( + "src/main/java/org/example/UselessConstructorDeclaration.java", + "package org.example; " + + "public class UselessConstructorDeclaration { " + + "public UselessConstructorDeclaration() {} " + + "}"); + GerritChange change = git.createGerritChangeForMaster(); + + triggerAndAssertSuccess(jobFactory.build(change)); + + ChangeInfo changeDetail = change.getDetail(); + assertThat(changeDetail.labels.get(GerritServer.CODE_QUALITY_LABEL).all) + .hasSize(1) + .map(approvalInfo -> approvalInfo.value) + .containsExactly(-1); + assertThat(change.listComments()) + .map(commentInfo -> commentInfo.message) + .filteredOn(message -> message.contains("S1186")) + .hasSize(1); + } + + private void testWithGoodQualityCode(JobFactory jobFactory) throws Exception { + git.addAndCommitFile( + "src/main/java/org/example/Foo.java", "package org.example; public interface Foo {}"); + GerritChange change = git.createGerritChangeForMaster(); + + triggerAndAssertSuccess(jobFactory.build(change)); + + ChangeInfo changeDetail = change.getDetail(); + assertThat(changeDetail.labels.get(GerritServer.CODE_QUALITY_LABEL).all) + .hasSize(1) + .map(approvalInfo -> approvalInfo.value) + .containsExactly(1); + assertThat(change.listComments()).isEmpty(); + } + + @SuppressWarnings("rawtypes") + private Job createFreestyleJob(GerritChange change) throws IOException { + FreeStyleProject job = cluster.jenkinsRule().createFreeStyleProject(); + job.setJDK(Jenkins.get().getJDK(cluster.jenkinsJdk8InstallationName())); + + int patchSetNumber = 1; + job.setScm(createGitSCM(change, patchSetNumber)); + + job.getBuildWrappersList() + .add(new SonarBuildWrapper(cluster.jenkinsSonarqubeInstallationName())); + + job.getBuildWrappersList() + .add( + new EnvironmentVariableBuildWrapper() + .add("GERRIT_NAME", cluster.jenkinsGerritTriggerServerName()) + .add("GERRIT_CHANGE_NUMBER", change.changeNumericId()) + .add("GERRIT_PATCHSET_NUMBER", String.valueOf(patchSetNumber)) + .add("GERRIT_BRANCH", "master") + .add("GERRIT_REFSPEC", change.refName(patchSetNumber))); + + job.getBuildersList() + .add(new Maven(MAVEN_FREESTYLE_TARGET, cluster.jenkinsMavenInstallationName())); + + SonarToGerritPublisher sonarToGerrit = new SonarToGerritPublisher(); + Inspection inspectionConfig = sonarToGerrit.getInspectionConfig(); + inspectionConfig.setAnalysisStrategy(new PullRequestAnalysisStrategy()); + IssueFilterConfig issueFilterConfig = sonarToGerrit.getReviewConfig().getIssueFilterConfig(); + issueFilterConfig.setSeverity("MINOR"); + issueFilterConfig.setChangedLinesOnly(true); + + ScoreConfig scoreConfig = new ScoreConfig(); + scoreConfig.getIssueFilterConfig().setSeverity("MINOR"); + scoreConfig.getIssueFilterConfig().setNewIssuesOnly(false); + scoreConfig.getIssueFilterConfig().setChangedLinesOnly(true); + scoreConfig.setCategory(GerritServer.CODE_QUALITY_LABEL); + scoreConfig.setNoIssuesScore(1); + scoreConfig.setIssuesScore(-1); + sonarToGerrit.setScoreConfig(scoreConfig); + job.getPublishersList().add(sonarToGerrit); + return job; + } + + @SuppressWarnings("rawtypes") + private Job createPipelineJob(GerritChange change) throws IOException { + WorkflowJob job = cluster.jenkinsRule().createProject(WorkflowJob.class); + int patchSetNumber = 1; + String script = + "node {\n" + + "stage('Build') {\n" + + "try {\n" + + String.format("env.GERRIT_NAME = '%s'\n", cluster.jenkinsGerritTriggerServerName()) + + String.format("env.GERRIT_CHANGE_NUMBER = '%s'\n", change.changeNumericId()) + + String.format("env.GERRIT_PATCHSET_NUMBER = '%s'\n", patchSetNumber) + + String.format("env.GERRIT_BRANCH = '%s'\n", "master") + + String.format("env.GERRIT_REFSPEC = '%s'\n", change.refName(patchSetNumber)) + + "checkout scm: ([\n" + + "$class: 'GitSCM',\n" + + String.format( + "userRemoteConfigs: [[url: '%s', refspec: '%s', credentialsId: '%s']],\n", + git.httpUrl(), change.refName(patchSetNumber), cluster.jenkinsGerritCredentialsId()) + + "branches: [[name: 'FETCH_HEAD']]\n" + + "])\n" + + String.format( + "withSonarQubeEnv('%s') {\n", cluster.jenkinsSonarqubeInstallationName()) + + String.format( + "withMaven(jdk: '%s', maven: '%s') {\n", + cluster.jenkinsJdk8InstallationName(), cluster.jenkinsMavenInstallationName()) + + String.format("sh \"mvn %s\"\n", MAVEN_PIPELINE_TARGET) + + "}\n" // withMaven + + "}\n" // withSonarQubeEnv + + "} finally {\n" + + "sonarToGerrit(\n" + + "inspectionConfig: [\n" + + "analysisStrategy: pullRequest()\n" + + "],\n" // inspectionConfig + + "reviewConfig: [\n" + + "issueFilterConfig: [\n" + + "severity: 'MINOR',\n" + + "newIssuesOnly: false,\n" + + "changedLinesOnly: true\n" + + "]\n" // issueFilterConfig + + "],\n" // reviewConfig + + "scoreConfig: [\n" + + "issueFilterConfig: [\n" + + "severity: 'MINOR'," + + "newIssuesOnly: false," + + "changedLinesOnly: true" + + "],\n" // issueFilterConfig + + String.format("category: '%s',\n", GerritServer.CODE_QUALITY_LABEL) + + "noIssuesScore: 1,\n" + + "issuesScore: -1,\n" + + "]\n" // scoreConfig + + ")\n" // sonarToGerrit + + "}\n" // finally + + "}\n" // stage('Build') + + "}"; + job.setDefinition(new CpsFlowDefinition(script, true)); + return job; + } + + private GitSCM createGitSCM(GerritChange change, int patchSetNumber) { + String refName = change.refName(patchSetNumber); + List remoteConfigs = + Collections.singletonList( + new UserRemoteConfig( + git.httpUrl(), null, refName, cluster.jenkinsGerritCredentialsId())); + return new GitSCM( + remoteConfigs, + Collections.singletonList(new BranchSpec("FETCH_HEAD")), + null, + null, + Collections.emptyList()); + } + + private static GitSCM createGitSCM() { + return new GitSCM( + GitSCM.createRepoList(git.httpUrl(), cluster.jenkinsGerritCredentialsId()), + Collections.emptyList(), + null, + null, + Collections.emptyList()); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private static void triggerAndAssertSuccess(Job job) throws Exception { + final QueueTaskFuture future = + new ParameterizedJobMixIn() { + @Override + protected Job asJob() { + return job; + } + }.scheduleBuild2(0); + cluster.jenkinsRule().assertBuildStatusSuccess(future); + } + + private interface JobFactory { + @SuppressWarnings("rawtypes") + Job build(GerritChange change) throws Exception; + } +} diff --git a/src/test/java/org/jenkinsci/plugins/sonargerrit/test_infrastructure/cluster/Cluster.java b/src/test/java/org/jenkinsci/plugins/sonargerrit/test_infrastructure/cluster/Cluster.java index 1f0d28f2..05958385 100644 --- a/src/test/java/org/jenkinsci/plugins/sonargerrit/test_infrastructure/cluster/Cluster.java +++ b/src/test/java/org/jenkinsci/plugins/sonargerrit/test_infrastructure/cluster/Cluster.java @@ -13,6 +13,7 @@ import org.jenkinsci.plugins.sonargerrit.test_infrastructure.jenkins.JDKs; import org.jenkinsci.plugins.sonargerrit.test_infrastructure.jenkins.MavenConfiguration; import org.jenkinsci.plugins.sonargerrit.test_infrastructure.jenkins.SonarqubeConfiguration; +import org.jenkinsci.plugins.sonargerrit.test_infrastructure.sonarqube.Sonarqube7Server; import org.jenkinsci.plugins.sonargerrit.test_infrastructure.sonarqube.SonarqubeAccessTokens; import org.jenkinsci.plugins.sonargerrit.test_infrastructure.sonarqube.SonarqubeServer; import org.jvnet.hudson.test.JenkinsRule; @@ -21,19 +22,22 @@ public class Cluster { private final GerritServer gerrit; - private final SonarqubeServer sonarqube; private final JenkinsRule jenkinsRule; private final String jenkinsGerritCredentialsId; private final String jenkinsJdk8InstallationName; private final String jenkinsMavenInstallationName; + private final String jenkinsSonarqube7InstallationName; private final String jenkinsSonarqubeInstallationName; private final String jenkinsGerritTriggerServerName; - private Cluster(GerritServer gerrit, SonarqubeServer sonarqube, JenkinsRule jenkinsRule) + private Cluster( + GerritServer gerrit, + Sonarqube7Server sonarqube7, + SonarqubeServer sonarqube, + JenkinsRule jenkinsRule) throws IOException { this.gerrit = requireNonNull(gerrit); - this.sonarqube = requireNonNull(sonarqube); this.jenkinsRule = requireNonNull(jenkinsRule); SystemCredentialsProvider credentialsProvider = SystemCredentialsProvider.getInstance(); @@ -55,16 +59,27 @@ private Cluster(GerritServer gerrit, SonarqubeServer sonarqube, JenkinsRule jenk new GerritTriggerConfiguration(jenkinsRule.jenkins) .addServer(gerrit.url(), gerrit.adminUsername(), gerrit.adminPassword()); - String sonarqubeToken = - new SonarqubeAccessTokens(sonarqube).createAdminAccessToken(UUID.randomUUID().toString()); + SonarqubeConfiguration sonarqubeConfiguration = new SonarqubeConfiguration(jenkinsRule.jenkins); + + jenkinsSonarqube7InstallationName = + sonarqubeConfiguration.addInstallation( + sonarqube7.url(), + new SonarqubeAccessTokens(sonarqube7.url(), sonarqube7.adminAuthorization()) + .createAdminAccessToken(UUID.randomUUID().toString())); jenkinsSonarqubeInstallationName = - new SonarqubeConfiguration(jenkinsRule.jenkins) - .addInstallation(sonarqube.url(), sonarqubeToken); + sonarqubeConfiguration.addInstallation( + sonarqube.url(), + new SonarqubeAccessTokens(sonarqube.url(), sonarqube.adminAuthorization()) + .createAdminAccessToken(UUID.randomUUID().toString())); } public static Cluster configure( - GerritServer gerrit, SonarqubeServer sonarqube, JenkinsRule jenkinsRule) throws IOException { - return new Cluster(gerrit, sonarqube, jenkinsRule); + GerritServer gerrit, + Sonarqube7Server sonarqube7, + SonarqubeServer sonarqube, + JenkinsRule jenkinsRule) + throws IOException { + return new Cluster(gerrit, sonarqube7, sonarqube, jenkinsRule); } public String jenkinsGerritCredentialsId() { @@ -75,10 +90,6 @@ public GerritServer gerrit() { return gerrit; } - public SonarqubeServer sonarqube() { - return sonarqube; - } - public JenkinsRule jenkinsRule() { return jenkinsRule; } @@ -87,6 +98,10 @@ public String jenkinsMavenInstallationName() { return jenkinsMavenInstallationName; } + public String jenkinsSonarqube7InstallationName() { + return jenkinsSonarqube7InstallationName; + } + public String jenkinsSonarqubeInstallationName() { return jenkinsSonarqubeInstallationName; } diff --git a/src/test/java/org/jenkinsci/plugins/sonargerrit/test_infrastructure/cluster/ClusterTestExtension.java b/src/test/java/org/jenkinsci/plugins/sonargerrit/test_infrastructure/cluster/ClusterTestExtension.java index fc06ba78..fd4abdf0 100644 --- a/src/test/java/org/jenkinsci/plugins/sonargerrit/test_infrastructure/cluster/ClusterTestExtension.java +++ b/src/test/java/org/jenkinsci/plugins/sonargerrit/test_infrastructure/cluster/ClusterTestExtension.java @@ -1,6 +1,7 @@ package org.jenkinsci.plugins.sonargerrit.test_infrastructure.cluster; import org.jenkinsci.plugins.sonargerrit.test_infrastructure.gerrit.GerritServer; +import org.jenkinsci.plugins.sonargerrit.test_infrastructure.sonarqube.Sonarqube7Server; import org.jenkinsci.plugins.sonargerrit.test_infrastructure.sonarqube.SonarqubeServer; import org.junit.jupiter.api.extension.BeforeAllCallback; import org.junit.jupiter.api.extension.ExtensionContext; @@ -19,10 +20,13 @@ public void beforeAll(ExtensionContext context) throws Exception { } GerritServer gerritServer = store.get(GerritServer.class, GerritServer.class); + Sonarqube7Server sonarqube7Server = store.get(Sonarqube7Server.class, Sonarqube7Server.class); SonarqubeServer sonarqubeServer = store.get(SonarqubeServer.class, SonarqubeServer.class); JenkinsRule jenkinsRule = store.get(JenkinsRule.class, JenkinsRule.class); - store.put(Cluster.class, Cluster.configure(gerritServer, sonarqubeServer, jenkinsRule)); + store.put( + Cluster.class, + Cluster.configure(gerritServer, sonarqube7Server, sonarqubeServer, jenkinsRule)); } @Override diff --git a/src/test/java/org/jenkinsci/plugins/sonargerrit/test_infrastructure/cluster/EnableCluster.java b/src/test/java/org/jenkinsci/plugins/sonargerrit/test_infrastructure/cluster/EnableCluster.java index 7ed47ae4..175fd02a 100644 --- a/src/test/java/org/jenkinsci/plugins/sonargerrit/test_infrastructure/cluster/EnableCluster.java +++ b/src/test/java/org/jenkinsci/plugins/sonargerrit/test_infrastructure/cluster/EnableCluster.java @@ -8,6 +8,7 @@ import java.lang.annotation.Target; import org.jenkinsci.plugins.sonargerrit.test_infrastructure.gerrit.EnableGerritServer; import org.jenkinsci.plugins.sonargerrit.test_infrastructure.jenkins.EnableJenkinsRule; +import org.jenkinsci.plugins.sonargerrit.test_infrastructure.sonarqube.EnableSonarqube7Server; import org.jenkinsci.plugins.sonargerrit.test_infrastructure.sonarqube.EnableSonarqubeServer; import org.junit.jupiter.api.extension.ExtendWith; @@ -15,6 +16,7 @@ @Target({TYPE, ANNOTATION_TYPE}) @Retention(RUNTIME) @EnableGerritServer +@EnableSonarqube7Server @EnableSonarqubeServer @EnableJenkinsRule @ExtendWith(ClusterTestExtension.class) diff --git a/src/test/java/org/jenkinsci/plugins/sonargerrit/test_infrastructure/sonarqube/EnableSonarqube7Server.java b/src/test/java/org/jenkinsci/plugins/sonargerrit/test_infrastructure/sonarqube/EnableSonarqube7Server.java new file mode 100644 index 00000000..55098781 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/sonargerrit/test_infrastructure/sonarqube/EnableSonarqube7Server.java @@ -0,0 +1,17 @@ +package org.jenkinsci.plugins.sonargerrit.test_infrastructure.sonarqube; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import org.jenkinsci.plugins.sonargerrit.test_infrastructure.docker_network.EnableDockerNetwork; +import org.junit.jupiter.api.extension.ExtendWith; + +/** @author Réda Housni Alaoui */ +@Target({TYPE, ANNOTATION_TYPE}) +@Retention(RUNTIME) +@EnableDockerNetwork +@ExtendWith(Sonarqube7TestExtension.class) +public @interface EnableSonarqube7Server {} diff --git a/src/test/java/org/jenkinsci/plugins/sonargerrit/test_infrastructure/sonarqube/Sonarqube7Server.java b/src/test/java/org/jenkinsci/plugins/sonargerrit/test_infrastructure/sonarqube/Sonarqube7Server.java new file mode 100644 index 00000000..e7067533 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/sonargerrit/test_infrastructure/sonarqube/Sonarqube7Server.java @@ -0,0 +1,139 @@ +package org.jenkinsci.plugins.sonargerrit.test_infrastructure.sonarqube; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import org.jenkinsci.plugins.sonargerrit.test_infrastructure.CloseableResource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.output.Slf4jLogConsumer; + +/** @author Réda Housni Alaoui */ +public class Sonarqube7Server { + + private static final Logger LOG = LoggerFactory.getLogger(Sonarqube7Server.class); + + private static final int HTTP_PORT = 9000; + + private static final String NETWORK_ALIAS = "sonarqube"; + private static final String ADMIN_USERNAME = "admin"; + + private final GenericContainer container; + private final String url; + private final String adminPassword; + + public static CloseableResource start(Network network) { + Sonarqube7Server sonarqubeServer = new Sonarqube7Server(network); + return new CloseableResource() { + @Override + public Sonarqube7Server resource() { + return sonarqubeServer; + } + + @Override + public void close() { + sonarqubeServer.stop(); + } + }; + } + + private Sonarqube7Server(Network network) { + container = + new GenericContainer<>("sonarqube:7.6-community") + .withLogConsumer(new Slf4jLogConsumer(LOG)) + .withExposedPorts(HTTP_PORT) + .withNetwork(network) + .withNetworkAliases(NETWORK_ALIAS); + container.start(); + url = "http://localhost:" + container.getMappedPort(HTTP_PORT); + + adminPassword = "1234"; + changeOwnPassword(url, ADMIN_USERNAME, "admin", adminPassword); + authorizeThirdPartyPlugins(url, adminPassword); + } + + public String url() { + return url; + } + + public String adminAuthorization() { + return createAuthorization(ADMIN_USERNAME, adminPassword); + } + + private void stop() { + container.stop(); + } + + private static void changeOwnPassword( + String sonarqubeUrl, String username, String currentPassword, String newPassword) { + OkHttpClient httpClient = SonarqubeOkHttpClients.get(); + RequestBody requestBody = + RequestBody.create( + MediaType.parse("application/x-www-form-urlencoded"), + "login=" + + username + + "&previousPassword=" + + currentPassword + + "&password=" + + newPassword); + Request request = + new Request.Builder() + .url(sonarqubeUrl + "/api/users/change_password") + .header("Authorization", createAuthorization(username, currentPassword)) + .post(requestBody) + .build(); + try (Response response = httpClient.newCall(request).execute()) { + if (response.isSuccessful()) { + return; + } + throw new IllegalStateException( + "Own password change failed with code " + + response.code() + + " and message '" + + response.message() + + "'"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static void authorizeThirdPartyPlugins(String sonarqubeUrl, String adminPassword) { + OkHttpClient httpClient = SonarqubeOkHttpClients.get(); + RequestBody requestBody = + RequestBody.create( + MediaType.parse("application/x-www-form-urlencoded"), + "key=sonar.plugins.risk.consent&value=ACCEPTED"); + Request request = + new Request.Builder() + .url(sonarqubeUrl + "/api/settings/set") + .header("Authorization", createAuthorization(ADMIN_USERNAME, adminPassword)) + .post(requestBody) + .build(); + try (Response response = httpClient.newCall(request).execute()) { + if (response.isSuccessful()) { + return; + } + throw new IllegalStateException( + "Third party plugins authorization failed with code " + + response.code() + + " and message '" + + response.message() + + "'"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static String createAuthorization(String username, String password) { + return "Basic " + + Base64.getEncoder() + .encodeToString((username + ":" + password).getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/src/test/java/org/jenkinsci/plugins/sonargerrit/test_infrastructure/sonarqube/Sonarqube7TestExtension.java b/src/test/java/org/jenkinsci/plugins/sonargerrit/test_infrastructure/sonarqube/Sonarqube7TestExtension.java new file mode 100644 index 00000000..96631527 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/sonargerrit/test_infrastructure/sonarqube/Sonarqube7TestExtension.java @@ -0,0 +1,10 @@ +package org.jenkinsci.plugins.sonargerrit.test_infrastructure.sonarqube; + +import org.jenkinsci.plugins.sonargerrit.test_infrastructure.docker_network.DockerDependentTestExtension; + +class Sonarqube7TestExtension extends DockerDependentTestExtension { + + protected Sonarqube7TestExtension() { + super(Sonarqube7Server.class, Sonarqube7Server::start); + } +} diff --git a/src/test/java/org/jenkinsci/plugins/sonargerrit/test_infrastructure/sonarqube/SonarqubeAccessTokens.java b/src/test/java/org/jenkinsci/plugins/sonargerrit/test_infrastructure/sonarqube/SonarqubeAccessTokens.java index a5438073..19c18a93 100644 --- a/src/test/java/org/jenkinsci/plugins/sonargerrit/test_infrastructure/sonarqube/SonarqubeAccessTokens.java +++ b/src/test/java/org/jenkinsci/plugins/sonargerrit/test_infrastructure/sonarqube/SonarqubeAccessTokens.java @@ -12,19 +12,21 @@ /** @author Réda Housni Alaoui */ public class SonarqubeAccessTokens { - private final SonarqubeServer sonarqubeServer; + private final String sonarqubeUrl; + private final String adminAuthorization; - public SonarqubeAccessTokens(SonarqubeServer sonarqubeServer) { - this.sonarqubeServer = sonarqubeServer; + public SonarqubeAccessTokens(String sonarqubeUrl, String adminAuthorization) { + this.sonarqubeUrl = sonarqubeUrl; + this.adminAuthorization = adminAuthorization; } public String createAdminAccessToken(String name) { RequestBody requestBody = - RequestBody.create("", MediaType.get("application/x-www-form-urlencoded")); + RequestBody.create(MediaType.parse("application/x-www-form-urlencoded"), ""); Request request = new Request.Builder() - .url(sonarqubeServer.url() + "/api/user_tokens/generate?name=" + name) - .header("Authorization", sonarqubeServer.adminAuthorization()) + .url(sonarqubeUrl + "/api/user_tokens/generate?name=" + name) + .header("Authorization", adminAuthorization) .post(requestBody) .build(); diff --git a/src/test/java/org/jenkinsci/plugins/sonargerrit/test_infrastructure/sonarqube/SonarqubeServer.java b/src/test/java/org/jenkinsci/plugins/sonargerrit/test_infrastructure/sonarqube/SonarqubeServer.java index d2a6dc99..659da2c9 100644 --- a/src/test/java/org/jenkinsci/plugins/sonargerrit/test_infrastructure/sonarqube/SonarqubeServer.java +++ b/src/test/java/org/jenkinsci/plugins/sonargerrit/test_infrastructure/sonarqube/SonarqubeServer.java @@ -14,6 +14,7 @@ import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.images.builder.ImageFromDockerfile; /** @author Réda Housni Alaoui */ public class SonarqubeServer { @@ -46,7 +47,21 @@ public void close() { private SonarqubeServer(Network network) { container = - new GenericContainer<>("sonarqube:7.6-community") + new GenericContainer<>( + new ImageFromDockerfile() + .withDockerfileFromBuilder( + builder -> + builder + .from("docker.cosium.dev/sonarqube:8.9.2-community") + .add( + "https://github.com/mc1arke/sonarqube-community-branch-plugin/releases/download/1.8.1/sonarqube-community-branch-plugin-1.8.1.jar", + "/opt/sonarqube/extensions/plugins/sonarqube-community-branch-plugin.jar"))) + .withEnv( + "SONAR_WEB_JAVAADDITIONALOPTS", + "-javaagent:./extensions/plugins/sonarqube-community-branch-plugin.jar=web") + .withEnv( + "SONAR_CE_JAVAADDITIONALOPTS", + "-javaagent:./extensions/plugins/sonarqube-community-branch-plugin.jar=ce") .withLogConsumer(new Slf4jLogConsumer(LOG)) .withExposedPorts(HTTP_PORT) .withNetwork(network) @@ -76,13 +91,13 @@ private static void changeOwnPassword( OkHttpClient httpClient = SonarqubeOkHttpClients.get(); RequestBody requestBody = RequestBody.create( + MediaType.parse("application/x-www-form-urlencoded"), "login=" + username + "&previousPassword=" + currentPassword + "&password=" - + newPassword, - MediaType.get("application/x-www-form-urlencoded")); + + newPassword); Request request = new Request.Builder() .url(sonarqubeUrl + "/api/users/change_password") @@ -108,8 +123,8 @@ private static void authorizeThirdPartyPlugins(String sonarqubeUrl, String admin OkHttpClient httpClient = SonarqubeOkHttpClients.get(); RequestBody requestBody = RequestBody.create( - "key=sonar.plugins.risk.consent&value=ACCEPTED", - MediaType.get("application/x-www-form-urlencoded")); + MediaType.parse("application/x-www-form-urlencoded"), + "key=sonar.plugins.risk.consent&value=ACCEPTED"); Request request = new Request.Builder() .url(sonarqubeUrl + "/api/settings/set")