build: opt-in local SonarQube analysis via local.properties

Adds a `sonar` Gradle target that activates only when `sonar.host.url`
is present in local.properties (gitignored). Developers who don't opt
in are unaffected: the scanner plugin is neither resolved nor applied,
so no dependency downloads, no extra tasks, no config-time cost
This commit is contained in:
davotoula
2026-07-08 22:33:06 +01:00
parent ff27720aff
commit cf43e6e435
3 changed files with 123 additions and 0 deletions
+62
View File
@@ -213,6 +213,68 @@ toolchain drifted — file it before publishing.
---
## Local SonarQube analysis (opt-in)
The build supports running a [SonarQube](https://www.sonarsource.com/products/sonarqube/)
analysis against a locally hosted server. It is **off by default**: unless you
opt in, the scanner plugin is neither downloaded nor applied and the build is
unaffected.
### 1. Install and start a local SonarQube server
Either run the official Docker image:
```bash
docker run -d --name sonarqube -p 9000:9000 sonarqube:community
```
or download the [Community Build zip](https://www.sonarsource.com/products/sonarqube/downloads/),
unzip it, and start it (requires a JDK 17+ on `PATH`):
```bash
cd sonarqube-<version>
bin/macosx-universal-64/sonar.sh console # pick the folder matching your OS
```
Once it reports up, open <http://localhost:9000> (first login `admin`/`admin`,
you'll be asked to change it), create a **local project** named `Amethyst` with
project key `Amethyst`, and generate a **project analysis token** for it
(*Project Settings → Analysis Method → With Gradle*, or
*My Account → Security → Generate token*). The token looks like `sqp_…`.
### 2. Point the build at your server
Add the server and token to `local.properties` (gitignored — the token never
lands in the repo):
```properties
sonar.host.url=http://localhost:9000
sonar.token=sqp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```
### 3. Run the analysis
```bash
./gradlew sonar
```
When it finishes, browse the results at
<http://localhost:9000/dashboard?id=Amethyst>.
Every `sonar.*` entry in `local.properties` is forwarded to the scanner, so any
[analysis parameter](https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/analysis-parameters/)
can be set there. `sonar.projectKey` / `sonar.projectName` default to the root
project name (`Amethyst`).
Even when opted in, the scanner plugin only loads on invocations that actually
request the `sonar` task — ordinary builds and IDE syncs are unaffected (which
is also why `./gradlew tasks` doesn't list it).
Note: the SonarQube Gradle scanner plugin is LGPL-3.0. It is a build-time-only
tool fetched after explicit opt-in; it is never linked into shipped artifacts.
---
## Release runbook
The release flow is driven by a tag push. Every cut ships Android + Desktop +
+58
View File
@@ -1,4 +1,36 @@
import com.android.build.gradle.tasks.GenerateResValues
import com.diffplug.gradle.spotless.SpotlessExtensionPredeclare
import java.util.Properties
// Local SonarQube analysis is opt-in: it activates only when `sonar.host.url`
// is present in local.properties (gitignored) AND a sonar task was requested,
// so neither developers who haven't opted in nor ordinary builds/IDE syncs of
// opted-in developers resolve or apply the scanner plugin. The Kotlin DSL
// compiles this buildscript {} section in an earlier stage that can't see the
// file's imports (hence the qualified Properties) or share code with the body,
// but it can publish values — the gate is computed once here and read below
// via `by extra`.
buildscript {
val localProperties = File(rootDir, "local.properties")
val sonarProperties by extra(
java.util.Properties().apply {
if (localProperties.exists()) localProperties.inputStream().use { load(it) }
},
)
val sonarEnabled by extra(
sonarProperties.getProperty("sonar.host.url") != null &&
gradle.startParameter.taskNames.any { it.substringAfterLast(":") in setOf("sonar", "sonarqube") },
)
if (sonarEnabled) {
repositories {
gradlePluginPortal()
}
dependencies {
// LGPL-3.0, build-time only — never linked into shipped artifacts.
classpath(libs.sonarqube.gradle.plugin)
}
}
}
plugins {
alias(libs.plugins.androidApplication) apply false
@@ -73,6 +105,32 @@ subprojects {
}
}
// Second half of the opt-in local SonarQube support gated above in buildscript {}.
// All sonar.* entries in local.properties are forwarded as system properties, so
// `./gradlew sonar` behaves exactly like passing them via -Dsonar.xxx=... on the
// command line. sonar.projectKey/projectName default to the root project name
// ("Amethyst") and only need overriding in local.properties if desired.
val sonarEnabled: Boolean by extra
if (sonarEnabled) {
val sonarProperties: Properties by extra
apply(plugin = "org.sonarqube")
sonarProperties
.stringPropertyNames()
.filter { it.startsWith("sonar.") }
.forEach { System.setProperty(it, sonarProperties.getProperty(it)) }
// The scanner's sonarResolver task reads AGP's generated-res-values provider
// but doesn't depend on the task that produces it — wire it up in every
// module that has both (today only :amethyst enables resValues, but the
// scanner defect is module-agnostic).
subprojects {
tasks.named { it == "sonarResolver" }.configureEach {
dependsOn(tasks.withType<GenerateResValues>())
}
}
}
val installGitHook = tasks.register<Copy>("installGitHook") {
val dotGit = File(rootProject.rootDir, ".git")
val hooksDir: File = if (dotGit.isFile) {
+3
View File
@@ -62,6 +62,7 @@ secp256k1KmpJniAndroid = "0.23.0"
schnorr256k1Kmp = "1.0.5"
securityCryptoKtx = "1.1.0"
slf4j = "2.0.18"
sonarqubeGradlePlugin = "7.3.1.8318"
spotless = "8.8.0"
streamWebrtcAndroid = "1.3.10"
translate = "17.0.3"
@@ -210,6 +211,8 @@ secp256k1-kmp-common = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp", v
secp256k1-kmp-jni-android = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp-jni-android", version.ref = "secp256k1KmpJniAndroid" }
secp256k1-kmp-jni-jvm = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp-jni-jvm", version.ref = "secp256k1KmpJniAndroid" }
schnorr256k1-kmp = { group = "com.vitorpamplona.schnorr256k1", name = "schnorr256k1-kmp", version.ref = "schnorr256k1Kmp" }
# Build-time only, gated behind the local.properties sonar opt-in in the root build script (LGPL-3.0).
sonarqube-gradle-plugin = { group = "org.sonarsource.scanner.gradle", name = "sonarqube-gradle-plugin", version.ref = "sonarqubeGradlePlugin" }
stream-webrtc-android = { group = "io.getstream", name = "stream-webrtc-android", version.ref = "streamWebrtcAndroid" }
unifiedpush = { group = "com.github.UnifiedPush", name = "android-connector", version.ref = "unifiedpush" }
play-services-cast-framework = { group = "com.google.android.gms", name = "play-services-cast-framework", version.ref = "playServicesCast" }