Skip to content

Commit 0a78e32

Browse files
wolpertclaude
andcommitted
ci: add SonarQube Cloud static analysis and coverage tracking
Apply the org.sonarqube Gradle plugin at the root for multi-module aggregation, consuming the per-module JaCoCo XML already produced by pkauth.test-conventions. A dedicated `sonar` CI job runs the build and submits coverage + analysis, gated to pushes and same-repo PRs (fork PRs can't read SONAR_TOKEN) with fetch-depth: 0 for SCM blame. The sonar task is not configuration-cache compatible, so CI invokes it with --no-configuration-cache (the config cache stays on globally). Adds ADR 0017 documenting the decision and Quality Gate + Coverage badges to the README. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 53bc8f7 commit 0a78e32

5 files changed

Lines changed: 98 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,36 @@ jobs:
9595
path: clients/passkeys-browser/reports/mutation/
9696
if-no-files-found: ignore
9797

98+
# Run the full build/test, then submit coverage + static analysis to SonarQube Cloud
99+
# (free open-source tier). The sonar task is not configuration-cache compatible, so it's
100+
# invoked with --no-configuration-cache (gradle.properties enables the config cache globally).
101+
sonar:
102+
runs-on: ubuntu-latest
103+
# SONAR_TOKEN is unavailable to PRs from forks; skip there to avoid a hard failure.
104+
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
105+
steps:
106+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
107+
with:
108+
# Full history so Sonar can attribute new-code and SCM blame correctly.
109+
fetch-depth: 0
110+
111+
- name: Set up JDK 21
112+
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
113+
with:
114+
java-version: '21'
115+
distribution: 'temurin'
116+
117+
- name: Validate Gradle wrapper
118+
uses: gradle/actions/wrapper-validation@5e2ebd065dc2488b7a6ad670704656cbbe1e8f60 # v6
119+
120+
- name: Set up Gradle
121+
uses: gradle/actions/setup-gradle@5e2ebd065dc2488b7a6ad670704656cbbe1e8f60 # v6
122+
123+
- name: Build, test, and analyze
124+
env:
125+
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
126+
run: ./gradlew build jacocoTestReport sonar --no-configuration-cache --stacktrace
127+
98128
# Block PRs that introduce dependencies with known vulnerabilities or incompatible
99129
# licenses, before they merge. Uses the GitHub dependency graph; PR-only.
100130
dependency-review:

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
![pk-auth CI](https://github.com/codeheadsystems/pk-auth/actions/workflows/ci.yml/badge.svg)
44
[![passkeys-browser mutation score](https://img.shields.io/endpoint?style=flat&label=mutation%20(passkeys-browser)&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fcodeheadsystems%2Fpk-auth%2Fmain%3Fmodule%3Dpasskeys-browser)](https://dashboard.stryker-mutator.io/reports/github.com/codeheadsystems/pk-auth/main?module=passkeys-browser)
5+
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=codeheadsystems_pk-auth&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=codeheadsystems_pk-auth)
6+
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=codeheadsystems_pk-auth&metric=coverage)](https://sonarcloud.io/summary/new_code?id=codeheadsystems_pk-auth)
57

68
A production-grade, **passkeys-first** authentication template for the JVM.
79
pk-auth ships as a reusable library set that can be dropped into a Spring

build.gradle.kts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,21 @@
33
// assemble).
44
plugins {
55
base
6+
alias(libs.plugins.sonarqube)
7+
}
8+
9+
// SonarQube Cloud analysis. Applied at the root for multi-module aggregation; the scanner
10+
// auto-detects each module's JaCoCo XML report (build/reports/jacoco/test/jacocoTestReport.xml),
11+
// produced by pkauth.test-conventions. Run via `./gradlew build jacocoTestReport sonar`.
12+
// NOTE: the sonar task is not configuration-cache compatible, so CI invokes it with
13+
// `--no-configuration-cache` (gradle.properties enables the config cache globally).
14+
sonar {
15+
properties {
16+
property("sonar.projectKey", "codeheadsystems_pk-auth")
17+
property("sonar.organization", "codeheadsystems")
18+
property("sonar.host.url", "https://sonarcloud.io")
19+
property("sonar.exclusions", "examples/**,clients/passkeys-browser/dist/**")
20+
}
621
}
722

823
// Required for the nmcp aggregation plugin (auto-applied by `nmcp.settings` in settings.gradle.kts)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# 17. SonarQube Cloud for static analysis and coverage tracking
2+
3+
Date: 2026-06-11
4+
5+
## Status
6+
7+
Accepted.
8+
9+
## Context
10+
11+
pk-auth already gates quality at build time: Spotless (formatting + SPDX), Error Prone with strict JSpecify null discipline, per-module JaCoCo coverage floors (≥80% on `pk-auth-core`, ≥70% on adapters), and Stryker mutation testing on the browser SDK. What's missing is a *trend* view of the JVM code — coverage over time, code smells, security hotspots, and duplication tracked across commits rather than only pass/fail at HEAD.
12+
13+
We want a hosted dashboard that:
14+
15+
- ingests the JaCoCo XML we already produce, so no new coverage machinery is needed;
16+
- understands the multi-module Gradle build as one project;
17+
- is free for a public/open-source repository;
18+
- runs from CI rather than requiring a separate scan service that can't see our build.
19+
20+
Options considered:
21+
22+
- **SonarQube Cloud (formerly SonarCloud)** — hosted, free for public repos, first-class Gradle scanner (`org.sonarqube`), auto-detects JaCoCo XML, integrates with GitHub PR decoration. No infrastructure to run.
23+
- **Self-hosted SonarQube Community Edition** — same analyzer, but we'd host and maintain a server + database. Overkill for an open-source library; nobody owns the ops.
24+
- **Codecov / Coveralls** — coverage trend only; no static analysis, hotspots, or duplication. Narrower than what we want.
25+
26+
## Decision
27+
28+
Adopt **SonarQube Cloud** via the `org.sonarqube` Gradle plugin, driven from CI.
29+
30+
- The plugin (version pinned in `gradle/libs.versions.toml`) is applied to the **root** project only; multi-module aggregation is built in. The `sonar` block in the root `build.gradle.kts` carries `projectKey`, `organization`, host URL, and `sonar.exclusions` (the example apps and the generated browser SDK `dist/`).
31+
- Coverage is **not** reconfigured — the scanner auto-detects each module's `build/reports/jacoco/test/jacocoTestReport.xml`, already emitted by `pkauth.test-conventions`. The CI step runs `./gradlew build jacocoTestReport sonar`.
32+
- Analysis runs as a dedicated `sonar` job in `ci.yml`, gated to pushes and same-repo PRs (fork PRs can't read `SONAR_TOKEN`), with `fetch-depth: 0` so Sonar gets full SCM blame for new-code attribution.
33+
- We use **CI-based analysis**, with Automatic Analysis turned off on the Sonar side — Automatic can't run our Gradle build or see JaCoCo coverage.
34+
35+
## Consequences
36+
37+
- **Positive — trend visibility.** Coverage, code smells, security hotspots, and duplication are tracked per commit with a quality gate on *new code*, complementing (not replacing) the existing per-module JaCoCo floors.
38+
- **Positive — no new coverage plumbing.** Sonar consumes the JaCoCo XML we already generate.
39+
- **Negative — config cache caveat.** The `sonar` task is not Gradle configuration-cache compatible, and `gradle.properties` enables the config cache globally. CI invokes the analysis with `--no-configuration-cache`; this is a documented, scoped exception, not a global change.
40+
- **Negative — fork PRs get no analysis.** GitHub withholds secrets from fork PRs, so the `sonar` job is skipped there. External contributors' branches are covered once merged to `main`; the standard trade-off for an open-source repo.
41+
- **Negative — heavier CI job.** The `sonar` job runs the full `build` (including the Docker-backed Testcontainers integration tests) so coverage exists when the scanner reads it. Acceptable on `ubuntu-latest`; can be optimized later by sharing coverage artifacts with the existing `build` job.
42+
43+
## Open follow-ups
44+
45+
- If we want the quality gate to block merges, wire the Sonar status check into branch protection.
46+
- If CI time becomes a concern, have the `sonar` job download JaCoCo artifacts from the `build` job instead of rebuilding.

gradle/libs.versions.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ errorprone-plugin = "5.1.0"
1818
errorprone-core = "2.50.0"
1919
google-java-format = "1.25.2"
2020
jspecify = "1.0.0"
21+
# SonarScanner for Gradle — drives CI-based analysis on SonarQube Cloud (free open-source tier).
22+
# Applied to the root project for multi-module aggregation; consumes the per-module JaCoCo XML
23+
# reports already produced by pkauth.test-conventions.
24+
sonarqube = "7.3.1.8318"
2125

2226
# Runtime — pk-auth-core
2327
webauthn4j = "0.31.7.RELEASE"
@@ -176,3 +180,4 @@ jackson = ["jackson-databind", "jackson-annotations"]
176180
[plugins]
177181
spotless = { id = "com.diffplug.spotless", version.ref = "spotless" }
178182
errorprone = { id = "net.ltgt.errorprone", version.ref = "errorprone-plugin" }
183+
sonarqube = { id = "org.sonarqube", version.ref = "sonarqube" }

0 commit comments

Comments
 (0)