diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ca949e8..a8ffc99 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,6 +32,43 @@ jobs: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} run: mvn install org.sonarsource.scanner.maven:sonar-maven-plugin:sonar + compatibility-matrix: + # Verify the plugin loads and produces correct coverage metrics across the range of + # SonarQube Community Build versions we claim to support in the Marketplace properties. + # Note: commercial SonarQube Server (sqs) images require a license and cannot be run here. + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + sonarqube-image: + - 'sonarqube:9.9-community' # oldest tested version (previous minimum) + - 'sonarqube:25.1.0.102122-community' # sqcb lower bound + - 'sonarqube:26.7.0.124771-community' # latest Community Build (sqcb upper bound) + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: 'adopt' + java-version: '17' + cache: 'maven' + + # SonarQube's bundled Elasticsearch requires a higher vm.max_map_count than the + # runner default (65530); without this newer images fail to start. + - name: Raise vm.max_map_count for Elasticsearch + run: sudo sysctl -w vm.max_map_count=262144 + + - name: Build plugin JAR + run: mvn -pl sonar-plugin -am install -DskipTests + + - name: Run integration tests against ${{ matrix.sonarqube-image }} + run: mvn -pl integration-tests verify -Dsonarqube.image=${{ matrix.sonarqube-image }} + release-check: if: github.event_name == 'pull_request' && github.base_ref == 'main' runs-on: ubuntu-latest diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 2a26a90..c4147a7 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -31,10 +31,14 @@ limitations under the License. 17 17 - 1.19.3 + 1.20.4 5.10.1 1.2.13 2.15.3 + + @@ -106,6 +110,8 @@ limitations under the License. false + + ${sonarqube.image} diff --git a/integration-tests/src/test/java/com/diffblue/sonar/integration/DiffbluePluginIntegrationTest.java b/integration-tests/src/test/java/com/diffblue/sonar/integration/DiffbluePluginIntegrationTest.java index 58bdcb6..0cff591 100644 --- a/integration-tests/src/test/java/com/diffblue/sonar/integration/DiffbluePluginIntegrationTest.java +++ b/integration-tests/src/test/java/com/diffblue/sonar/integration/DiffbluePluginIntegrationTest.java @@ -76,6 +76,11 @@ class DiffbluePluginIntegrationTest { // Path to the built plugin JAR (relative to this module) private static final Path PLUGIN_JAR = resolvePluginJar(); + // SonarQube Docker image to test against. Override with -Dsonarqube.image= (or the + // SONARQUBE_IMAGE env var) to validate compatibility with other versions. Defaults to + // 9.9-community, the oldest version we support (first with ARM64 support). + private static final String SONARQUBE_IMAGE = resolveSonarqubeImage(); + // SonarQube credentials - default credentials for test container // Note: These are only used in isolated test containers and never in production private static final String SONAR_USER = "admin"; @@ -85,13 +90,13 @@ class DiffbluePluginIntegrationTest { private static final String TEST_CALCULATOR_FILE = "src/main/java/com/example/Calculator.java"; /** - * SonarQube container configured with: - SonarQube 9.9 Community Edition (first version with - * ARM64 support) - Diffblue Coverage plugin mounted into extensions/plugins/ - Health check - * waiting for API to be ready + * SonarQube container configured with: - SonarQube image from {@link #SONARQUBE_IMAGE} (defaults + * to 9.9 Community Edition, the oldest supported version) - Diffblue Coverage plugin mounted into + * extensions/plugins/ - Health check waiting for API to be ready */ @Container static GenericContainer sonarqube = - new GenericContainer<>("sonarqube:9.9-community") + new GenericContainer<>(SONARQUBE_IMAGE) .withExposedPorts(9000) .withFileSystemBind( PLUGIN_JAR.toString(), "/opt/sonarqube/extensions/plugins/diffblue-coverage.jar") @@ -115,6 +120,7 @@ static void setUp() { // Get the dynamically assigned host and port baseUrl = "http://" + sonarqube.getHost() + ":" + sonarqube.getMappedPort(9000); + logger.info("SonarQube image under test: {}", SONARQUBE_IMAGE); logger.info("SonarQube is running at: {}", baseUrl); // Verify the Diffblue Coverage plugin is installed @@ -136,6 +142,26 @@ static void setUp() { } } + /** + * Resolves the SonarQube Docker image tag to test against. + * + *

Reads the {@code sonarqube.image} system property first, then the {@code SONARQUBE_IMAGE} + * environment variable, falling back to the oldest supported version. This lets CI run the same + * suite across a matrix of supported SonarQube versions. + * + * @return the Docker image coordinate (e.g. {@code sonarqube:26.7-community}) + */ + private static String resolveSonarqubeImage() { + String image = System.getProperty("sonarqube.image"); + if (image == null || image.isBlank()) { + image = System.getenv("SONARQUBE_IMAGE"); + } + if (image == null || image.isBlank()) { + image = "sonarqube:9.9-community"; + } + return image; + } + /** * Dynamically resolves the plugin JAR path by searching for any diffblue-sonar-plugin-*.jar file. * diff --git a/integration-tests/src/test/java/com/diffblue/sonar/integration/SonarAnalysisRunner.java b/integration-tests/src/test/java/com/diffblue/sonar/integration/SonarAnalysisRunner.java index a86696b..471d631 100644 --- a/integration-tests/src/test/java/com/diffblue/sonar/integration/SonarAnalysisRunner.java +++ b/integration-tests/src/test/java/com/diffblue/sonar/integration/SonarAnalysisRunner.java @@ -111,8 +111,14 @@ private static void runAnalysis( } logger.info("Detected Maven project"); + + // Generate a user token for the scanner. Recent SonarQube versions reject the deprecated + // sonar.login/sonar.password (username/password) authentication for analysis and require a + // token instead. The token is generated via the Web API, which still accepts basic auth. + String token = generateToken(sonarUrl, projectKey, username, password); + ProcessBuilder pb = - buildMavenCommand(projectDir, sonarUrl, projectKey, username, password, enableDiffblue); + buildMavenCommand(projectDir, sonarUrl, projectKey, token, enableDiffblue); pb.directory(projectDir.toFile()); pb.redirectErrorStream(true); @@ -243,6 +249,69 @@ private static void waitForSonarProcessing( } } + /** + * Generates a SonarQube user token via the Web API for scanner authentication. + * + *

Recent SonarQube versions no longer accept username/password (sonar.login/sonar.password) + * for analysis and require a token. The token is created using basic auth, which the Web API + * still supports. The token name is derived from the project key so parallel/repeated runs use + * distinct names; any existing token with the same name is revoked first to avoid a conflict. + * + * @param sonarUrl Base URL of the SonarQube instance + * @param projectKey Project key, used to build a unique token name + * @param username SonarQube username + * @param password SonarQube password + * @return the generated token value + */ + private static String generateToken( + String sonarUrl, String projectKey, String username, String password) { + String tokenName = "it-token-" + projectKey; + String authHeader = createAuthHeader(username, password); + HttpClient httpClient = HttpClient.newHttpClient(); + try { + // Revoke any pre-existing token with this name so regeneration does not fail on conflict. + String revokeBody = "name=" + URLEncoder.encode(tokenName, StandardCharsets.UTF_8); + HttpRequest revokeRequest = + HttpRequest.newBuilder() + .uri(URI.create(sonarUrl + "/api/user_tokens/revoke")) + .header("Authorization", authHeader) + .header("Content-Type", "application/x-www-form-urlencoded") + .POST(HttpRequest.BodyPublishers.ofString(revokeBody)) + .build(); + httpClient.send(revokeRequest, HttpResponse.BodyHandlers.ofString()); + + // Generate a fresh token. + String generateBody = "name=" + URLEncoder.encode(tokenName, StandardCharsets.UTF_8); + HttpRequest generateRequest = + HttpRequest.newBuilder() + .uri(URI.create(sonarUrl + "/api/user_tokens/generate")) + .header("Authorization", authHeader) + .header("Content-Type", "application/x-www-form-urlencoded") + .POST(HttpRequest.BodyPublishers.ofString(generateBody)) + .build(); + HttpResponse response = + httpClient.send(generateRequest, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + throw new RuntimeException( + "Failed to generate SonarQube token: HTTP " + + response.statusCode() + + " - " + + response.body()); + } + + String token = objectMapper.readTree(response.body()).path("token").asText(); + if (token == null || token.isBlank()) { + throw new RuntimeException( + "SonarQube token generation returned no token: " + response.body()); + } + logger.info("Generated SonarQube analysis token '{}'", tokenName); + return token; + } catch (IOException | InterruptedException e) { + throw new RuntimeException("Failed to generate SonarQube token", e); + } + } + /** * Creates a basic authentication header value. * @@ -261,20 +330,21 @@ private static String createAuthHeader(String username, String password) { * @param command The command list to add properties to * @param sonarUrl SonarQube URL * @param projectKey Project key - * @param username SonarQube username - * @param password SonarQube password + * @param token SonarQube user token used for scanner authentication * @param enableDiffblue Whether to enable Diffblue plugin */ private static void addCommonSonarProperties( List command, String sonarUrl, String projectKey, - String username, - String password, + String token, boolean enableDiffblue) { command.add("-Dsonar.host.url=" + sonarUrl); - command.add("-Dsonar.login=" + username); - command.add("-Dsonar.password=" + password); + // Pass the token via both properties for cross-version compatibility: newer SonarQube uses + // sonar.token, while 9.9 only recognises a token supplied in sonar.login. Setting both to the + // same token value is accepted by every version in the supported range. + command.add("-Dsonar.token=" + token); + command.add("-Dsonar.login=" + token); command.add("-Dsonar.projectKey=" + projectKey); if (enableDiffblue) { @@ -288,18 +358,12 @@ private static void addCommonSonarProperties( * @param projectDir Project directory path * @param sonarUrl SonarQube URL * @param projectKey Project key - * @param username SonarQube username - * @param password SonarQube password + * @param token SonarQube user token used for scanner authentication * @param enableDiffblue Whether to enable Diffblue plugin * @return ProcessBuilder configured for Maven */ private static ProcessBuilder buildMavenCommand( - Path projectDir, - String sonarUrl, - String projectKey, - String username, - String password, - boolean enableDiffblue) { + Path projectDir, String sonarUrl, String projectKey, String token, boolean enableDiffblue) { List command = new ArrayList<>(); // Use mvn wrapper if available, otherwise system mvn @@ -308,7 +372,7 @@ private static ProcessBuilder buildMavenCommand( command.add("verify"); command.add("sonar:sonar"); - addCommonSonarProperties(command, sonarUrl, projectKey, username, password, enableDiffblue); + addCommonSonarProperties(command, sonarUrl, projectKey, token, enableDiffblue); // Skip tests if they would fail (we're only interested in analysis) command.add("-DskipTests=false"); // Run tests to generate coverage