Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion integration-tests/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,14 @@ limitations under the License.
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<testcontainers.version>1.19.3</testcontainers.version>
<testcontainers.version>1.20.4</testcontainers.version>
<junit.version>5.10.1</junit.version>
<logback.version>1.2.13</logback.version>
<jackson.version>2.15.3</jackson.version>
<!-- Default SonarQube image; empty so the test resolver can fall back to the
SONARQUBE_IMAGE env var and then the oldest supported version. Override with
-Dsonarqube.image=<tag>. -->
<sonarqube.image></sonarqube.image>
</properties>

<dependencies>
Expand Down Expand Up @@ -106,6 +110,8 @@ limitations under the License.
<!-- Ensure Docker is available -->
<systemPropertyVariables>
<testcontainers.reuse.enable>false</testcontainers.reuse.enable>
<!-- SonarQube image to test against; override with -Dsonarqube.image=<tag> -->
<sonarqube.image>${sonarqube.image}</sonarqube.image>
</systemPropertyVariables>
</configuration>
<executions>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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=<tag> (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";
Expand All @@ -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")
Expand All @@ -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
Expand All @@ -136,6 +142,26 @@ static void setUp() {
}
}

/**
* Resolves the SonarQube Docker image tag to test against.
*
* <p>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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -243,6 +249,69 @@ private static void waitForSonarProcessing(
}
}

/**
* Generates a SonarQube user token via the Web API for scanner authentication.
*
* <p>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<String> 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.
*
Expand All @@ -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<String> 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) {
Expand All @@ -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<String> command = new ArrayList<>();

// Use mvn wrapper if available, otherwise system mvn
Expand All @@ -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
Expand Down
Loading