Skip to content
Merged
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
30 changes: 0 additions & 30 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,36 +95,6 @@ jobs:
path: clients/passkeys-browser/reports/mutation/
if-no-files-found: ignore

# Run the full build/test, then submit coverage + static analysis to SonarQube Cloud
# (free open-source tier). The sonar task is not configuration-cache compatible, so it's
# invoked with --no-configuration-cache (gradle.properties enables the config cache globally).
sonar:
runs-on: ubuntu-latest
# SONAR_TOKEN is unavailable to PRs from forks; skip there to avoid a hard failure.
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
# Full history so Sonar can attribute new-code and SCM blame correctly.
fetch-depth: 0

- name: Set up JDK 21
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
with:
java-version: '21'
distribution: 'temurin'

- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@5e2ebd065dc2488b7a6ad670704656cbbe1e8f60 # v6

- name: Set up Gradle
uses: gradle/actions/setup-gradle@5e2ebd065dc2488b7a6ad670704656cbbe1e8f60 # v6

- name: Build, test, and analyze
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
run: ./gradlew build jacocoTestReport sonar --no-configuration-cache --stacktrace

# Block PRs that introduce dependencies with known vulnerabilities or incompatible
# licenses, before they merge. Uses the GitHub dependency graph; PR-only.
dependency-review:
Expand Down
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

![pk-auth CI](https://github.com/codeheadsystems/pk-auth/actions/workflows/ci.yml/badge.svg)
[![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)
[![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)
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=codeheadsystems_pk-auth&metric=coverage)](https://sonarcloud.io/summary/new_code?id=codeheadsystems_pk-auth)

A production-grade, **passkeys-first** authentication template for the JVM.
pk-auth ships as a reusable library set that can be dropped into a Spring
Expand Down
29 changes: 26 additions & 3 deletions build-logic/src/main/kotlin/pkauth.test-conventions.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,32 @@ tasks.named<JacocoReport>("jacocoTestReport") {
}
}

// Per-module coverage gates (≥80% on pk-auth-core, ≥70% on adapters) are wired in each module's
// build.gradle.kts via JacocoCoverageVerification, not centrally here.

tasks.named("check") {
dependsOn(tasks.named("jacocoTestReport"))
}

// Baseline coverage gate for published library modules: LINE ≥70%, BRANCH ≥55%. Keyed off
// `java-library` (applied by pkauth.library-conventions) so it covers every library module but
// NOT the example apps, which apply test-conventions only for the JaCoCo report. A module may
// layer on a STRICTER rule in its own build.gradle.kts — Gradle enforces all rules, so a module
// override need only state the limits it raises, never restate this floor. Floors are static:
// bump them as coverage improves (jacocoTestReport's HTML shows current numbers).
plugins.withId("java-library") {
tasks.named<JacocoCoverageVerification>("jacocoTestCoverageVerification") {
violationRules {
rule {
limit {
counter = "LINE"
minimum = "0.70".toBigDecimal()
}
limit {
counter = "BRANCH"
minimum = "0.55".toBigDecimal()
}
}
}
}
tasks.named("check") {
dependsOn(tasks.named("jacocoTestCoverageVerification"))
}
}
15 changes: 0 additions & 15 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,6 @@
// assemble).
plugins {
base
alias(libs.plugins.sonarqube)
}

// SonarQube Cloud analysis. Applied at the root for multi-module aggregation; the scanner
// auto-detects each module's JaCoCo XML report (build/reports/jacoco/test/jacocoTestReport.xml),
// produced by pkauth.test-conventions. Run via `./gradlew build jacocoTestReport sonar`.
// NOTE: the sonar task is not configuration-cache compatible, so CI invokes it with
// `--no-configuration-cache` (gradle.properties enables the config cache globally).
sonar {
properties {
property("sonar.projectKey", "codeheadsystems_pk-auth")
property("sonar.organization", "codeheadsystems")
property("sonar.host.url", "https://sonarcloud.io")
property("sonar.exclusions", "examples/**,clients/passkeys-browser/dist/**")
}
}

// Required for the nmcp aggregation plugin (auto-applied by `nmcp.settings` in settings.gradle.kts)
Expand Down
2 changes: 1 addition & 1 deletion docs/adr/0017-sonarqube-cloud-static-analysis.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Date: 2026-06-11

## Status

Accepted.
Superseded by [ADR 0018](0018-remove-sonarqube-cloud.md).

## Context

Expand Down
60 changes: 60 additions & 0 deletions docs/adr/0018-remove-sonarqube-cloud.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# 18. Remove SonarQube Cloud; enforce coverage with native JaCoCo line + branch gates

Date: 2026-06-11

## Status

Accepted. Supersedes [ADR 0017](0017-sonarqube-cloud-static-analysis.md).

## Context

ADR 0017 adopted SonarQube Cloud for trend visibility and a new-code quality gate. In practice the
gate produced low-signal friction rather than insight:

- Its findings largely **duplicate tooling we already run at build time, more precisely**: null
discipline is enforced by Error Prone + JSpecify, formatting/SPDX by Spotless, and test
effectiveness by per-module JaCoCo floors plus Stryker mutation testing on the browser SDK.
- Several rules **conflicted with deliberate project conventions** — e.g. `S4449` demands JSR-305
`javax.annotation.Nullable` while we standardize on JSpecify, and `S2583`/`S2589` flagged correct
defensive null-guards on framework-injected parameters as dead code.
- The **new-code gate fired on attribution noise**: editing one line of an existing file surfaced a
pre-existing, idiomatic pattern (`HttpResponse<?>` on a Micronaut `@Controller`) as a "new" issue,
failing the gate over a non-defect.

Keeping the gate green required suppressions that litter the build with rule exclusions — paying
maintenance cost to satisfy a tool that told us less than the gates we already trust. The one thing
ADR 0017 genuinely wanted — **coverage tracked at line and branch level** — JaCoCo already provides
natively and offline.

## Decision

Remove the SonarQube Cloud integration and enforce coverage directly with JaCoCo.

- Drop the `org.sonarqube` Gradle plugin and its `sonar { }` block from the root build, the version
catalog entry, the `sonar` CI job, and the SonarCloud README badges.
- Gate **both `LINE` and `BRANCH`** counters (previously line only), via a **hybrid** of one central
baseline plus a few per-module overrides:
- **Baseline** lives in `pkauth.test-conventions` and applies to every published library module:
`LINE` ≥70%, `BRANCH` ≥55%. It is keyed off the `java-library` plugin (applied by
`pkauth.library-conventions`) so it covers the libraries but **not** the `examples/*` demos,
which apply `test-conventions` only for the JaCoCo report and are intentionally ungated.
- **Overrides** stay in a module's own `build.gradle.kts` and raise only the limits above the
baseline (Gradle enforces all rules, so an override never restates the floor): `pk-auth-core` /
`pk-auth-jwt` / `pk-auth-admin-api` keep the ≥80% line bar from the brief §11; `pk-auth-jwt`,
`pk-auth-backup-codes`, `pk-auth-otp`, and `pk-auth-refresh-tokens` pin `BRANCH` near their
current (high) coverage. Every other module rides the baseline.
- Branch floors are static — set below current measured coverage to lock in today's level and fail
on regression; raise them as coverage improves. These gates run under `check`, which the `build`
CI job already invokes.

## Consequences

- **Positive — no external service, no attribution noise.** Coverage is enforced deterministically
from the build we already run; there is nothing to suppress.
- **Positive — branch coverage is now a hard gate**, not just a dashboard number, catching untested
conditionals that line coverage alone misses.
- **Negative — no hosted trend dashboard.** We lose cross-commit graphs, duplication tracking, and
the security-hotspot review surface. The first two were redundant with existing gates; if hotspot
review is wanted later, prefer a tool that doesn't gate on new-code attribution.
- **Negative — branch floors are static.** They must be raised by hand as coverage improves;
`jacocoTestReport` HTML shows current numbers when revising them.
5 changes: 0 additions & 5 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,6 @@ errorprone-plugin = "5.1.0"
errorprone-core = "2.50.0"
google-java-format = "1.25.2"
jspecify = "1.0.0"
# SonarScanner for Gradle — drives CI-based analysis on SonarQube Cloud (free open-source tier).
# Applied to the root project for multi-module aggregation; consumes the per-module JaCoCo XML
# reports already produced by pkauth.test-conventions.
sonarqube = "7.3.1.8318"

# Runtime — pk-auth-core
webauthn4j = "0.31.7.RELEASE"
Expand Down Expand Up @@ -180,4 +176,3 @@ jackson = ["jackson-databind", "jackson-annotations"]
[plugins]
spotless = { id = "com.diffplug.spotless", version.ref = "spotless" }
errorprone = { id = "net.ltgt.errorprone", version.ref = "errorprone-plugin" }
sonarqube = { id = "org.sonarqube", version.ref = "sonarqube" }
6 changes: 2 additions & 4 deletions pk-auth-admin-api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ dependencies {
testRuntimeOnly(libs.logback.classic)
}

// Stricter ≥80% line bar than the pkauth.test-conventions baseline (LINE ≥0.70, BRANCH ≥0.55);
// the baseline branch floor applies via the convention.
tasks.named<JacocoCoverageVerification>("jacocoTestCoverageVerification") {
violationRules {
rule {
Expand All @@ -34,7 +36,3 @@ tasks.named<JacocoCoverageVerification>("jacocoTestCoverageVerification") {
}
}
}

tasks.named("check") {
dependsOn("jacocoTestCoverageVerification")
}
10 changes: 4 additions & 6 deletions pk-auth-backup-codes/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,15 @@ dependencies {
testRuntimeOnly(libs.logback.classic)
}

// Raises only BRANCH above the pkauth.test-conventions baseline (LINE ≥0.70, BRANCH ≥0.55):
// recovery-code logic is branch-heavy and already well-covered, so the bar is pinned near current.
tasks.named<JacocoCoverageVerification>("jacocoTestCoverageVerification") {
violationRules {
rule {
limit {
counter = "LINE"
minimum = "0.70".toBigDecimal()
counter = "BRANCH"
minimum = "0.85".toBigDecimal()
}
}
}
}

tasks.named("check") {
dependsOn("jacocoTestCoverageVerification")
}
10 changes: 3 additions & 7 deletions pk-auth-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ dependencies {
testRuntimeOnly(libs.logback.classic)
}

// Phase 1 establishes the ≥80% line-coverage gate called for in the brief §11. The default in
// pkauth.test-conventions is intentionally permissive; this override turns the bar on for
// pk-auth-core specifically.
// pkauth.test-conventions sets the baseline gate (LINE ≥0.70, BRANCH ≥0.55) for every library
// module. pk-auth-core is held to the stricter ≥80% line bar from the brief §11; the baseline
// branch floor applies via the convention.
tasks.named<JacocoCoverageVerification>("jacocoTestCoverageVerification") {
violationRules {
rule {
Expand All @@ -42,7 +42,3 @@ tasks.named<JacocoCoverageVerification>("jacocoTestCoverageVerification") {
}
}
}

tasks.named("check") {
dependsOn("jacocoTestCoverageVerification")
}
12 changes: 0 additions & 12 deletions pk-auth-dropwizard/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -97,16 +97,4 @@ tasks.named<JacocoCoverageVerification>("jacocoTestCoverageVerification") {
}
}),
)
violationRules {
rule {
limit {
counter = "LINE"
minimum = "0.70".toBigDecimal()
}
}
}
}

tasks.named("check") {
dependsOn("jacocoTestCoverageVerification")
}
11 changes: 7 additions & 4 deletions pk-auth-jwt/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,20 @@ dependencies {
testRuntimeOnly(libs.logback.classic)
}

// Stricter than the pkauth.test-conventions baseline (LINE ≥0.70, BRANCH ≥0.55): JWT issuance +
// validation is security-critical and already well-covered, so the bar is pinned near its current
// level.
tasks.named<JacocoCoverageVerification>("jacocoTestCoverageVerification") {
violationRules {
rule {
limit {
counter = "LINE"
minimum = "0.80".toBigDecimal()
}
limit {
counter = "BRANCH"
minimum = "0.85".toBigDecimal()
}
}
}
}

tasks.named("check") {
dependsOn("jacocoTestCoverageVerification")
}
15 changes: 0 additions & 15 deletions pk-auth-magic-link/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,3 @@ dependencies {
testImplementation(project(":pk-auth-testkit"))
testRuntimeOnly(libs.logback.classic)
}

tasks.named<JacocoCoverageVerification>("jacocoTestCoverageVerification") {
violationRules {
rule {
limit {
counter = "LINE"
minimum = "0.70".toBigDecimal()
}
}
}
}

tasks.named("check") {
dependsOn("jacocoTestCoverageVerification")
}
15 changes: 0 additions & 15 deletions pk-auth-micronaut/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -60,18 +60,3 @@ dependencies {
testAnnotationProcessor(libs.micronaut.inject.java)
testRuntimeOnly(libs.logback.classic)
}

tasks.named<JacocoCoverageVerification>("jacocoTestCoverageVerification") {
violationRules {
rule {
limit {
counter = "LINE"
minimum = "0.70".toBigDecimal()
}
}
}
}

tasks.named("check") {
dependsOn("jacocoTestCoverageVerification")
}
10 changes: 4 additions & 6 deletions pk-auth-otp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,15 @@ dependencies {
testRuntimeOnly(libs.logback.classic)
}

// Raises only BRANCH above the pkauth.test-conventions baseline (LINE ≥0.70, BRANCH ≥0.55) to
// pin the OTP verification branches near their current coverage.
tasks.named<JacocoCoverageVerification>("jacocoTestCoverageVerification") {
violationRules {
rule {
limit {
counter = "LINE"
minimum = "0.70".toBigDecimal()
counter = "BRANCH"
minimum = "0.80".toBigDecimal()
}
}
}
}

tasks.named("check") {
dependsOn("jacocoTestCoverageVerification")
}
15 changes: 0 additions & 15 deletions pk-auth-persistence-dynamodb/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,3 @@ dependencies {
testImplementation(libs.testcontainers.core)
testRuntimeOnly(libs.logback.classic)
}

tasks.named<JacocoCoverageVerification>("jacocoTestCoverageVerification") {
violationRules {
rule {
limit {
counter = "LINE"
minimum = "0.70".toBigDecimal()
}
}
}
}

tasks.named("check") {
dependsOn("jacocoTestCoverageVerification")
}
15 changes: 0 additions & 15 deletions pk-auth-persistence-jdbi/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,3 @@ dependencies {
testImplementation(libs.testcontainers.postgresql)
testRuntimeOnly(libs.logback.classic)
}

tasks.named<JacocoCoverageVerification>("jacocoTestCoverageVerification") {
violationRules {
rule {
limit {
counter = "LINE"
minimum = "0.70".toBigDecimal()
}
}
}
}

tasks.named("check") {
dependsOn("jacocoTestCoverageVerification")
}
Loading
Loading