diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ec7e8f4..d9a1e56 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: diff --git a/README.md b/README.md index 8284a13..0cb06a3 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/build-logic/src/main/kotlin/pkauth.test-conventions.gradle.kts b/build-logic/src/main/kotlin/pkauth.test-conventions.gradle.kts index 85fad9b..84ce9ea 100644 --- a/build-logic/src/main/kotlin/pkauth.test-conventions.gradle.kts +++ b/build-logic/src/main/kotlin/pkauth.test-conventions.gradle.kts @@ -31,9 +31,32 @@ tasks.named("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("jacocoTestCoverageVerification") { + violationRules { + rule { + limit { + counter = "LINE" + minimum = "0.70".toBigDecimal() + } + limit { + counter = "BRANCH" + minimum = "0.55".toBigDecimal() + } + } + } + } + tasks.named("check") { + dependsOn(tasks.named("jacocoTestCoverageVerification")) + } +} diff --git a/build.gradle.kts b/build.gradle.kts index 62a6d1d..4df096d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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) diff --git a/docs/adr/0017-sonarqube-cloud-static-analysis.md b/docs/adr/0017-sonarqube-cloud-static-analysis.md index cb3284f..4f8305a 100644 --- a/docs/adr/0017-sonarqube-cloud-static-analysis.md +++ b/docs/adr/0017-sonarqube-cloud-static-analysis.md @@ -4,7 +4,7 @@ Date: 2026-06-11 ## Status -Accepted. +Superseded by [ADR 0018](0018-remove-sonarqube-cloud.md). ## Context diff --git a/docs/adr/0018-remove-sonarqube-cloud.md b/docs/adr/0018-remove-sonarqube-cloud.md new file mode 100644 index 0000000..d5ff6de --- /dev/null +++ b/docs/adr/0018-remove-sonarqube-cloud.md @@ -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. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bccf249..a2c1eff 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" @@ -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" } diff --git a/pk-auth-admin-api/build.gradle.kts b/pk-auth-admin-api/build.gradle.kts index 2547a3e..818b1e8 100644 --- a/pk-auth-admin-api/build.gradle.kts +++ b/pk-auth-admin-api/build.gradle.kts @@ -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("jacocoTestCoverageVerification") { violationRules { rule { @@ -34,7 +36,3 @@ tasks.named("jacocoTestCoverageVerification") { } } } - -tasks.named("check") { - dependsOn("jacocoTestCoverageVerification") -} diff --git a/pk-auth-backup-codes/build.gradle.kts b/pk-auth-backup-codes/build.gradle.kts index 5565895..6357986 100644 --- a/pk-auth-backup-codes/build.gradle.kts +++ b/pk-auth-backup-codes/build.gradle.kts @@ -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("jacocoTestCoverageVerification") { violationRules { rule { limit { - counter = "LINE" - minimum = "0.70".toBigDecimal() + counter = "BRANCH" + minimum = "0.85".toBigDecimal() } } } } - -tasks.named("check") { - dependsOn("jacocoTestCoverageVerification") -} diff --git a/pk-auth-core/build.gradle.kts b/pk-auth-core/build.gradle.kts index 9a48b3e..3685a7e 100644 --- a/pk-auth-core/build.gradle.kts +++ b/pk-auth-core/build.gradle.kts @@ -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("jacocoTestCoverageVerification") { violationRules { rule { @@ -42,7 +42,3 @@ tasks.named("jacocoTestCoverageVerification") { } } } - -tasks.named("check") { - dependsOn("jacocoTestCoverageVerification") -} diff --git a/pk-auth-dropwizard/build.gradle.kts b/pk-auth-dropwizard/build.gradle.kts index ed5ea35..f8e4644 100644 --- a/pk-auth-dropwizard/build.gradle.kts +++ b/pk-auth-dropwizard/build.gradle.kts @@ -97,16 +97,4 @@ tasks.named("jacocoTestCoverageVerification") { } }), ) - violationRules { - rule { - limit { - counter = "LINE" - minimum = "0.70".toBigDecimal() - } - } - } -} - -tasks.named("check") { - dependsOn("jacocoTestCoverageVerification") } diff --git a/pk-auth-jwt/build.gradle.kts b/pk-auth-jwt/build.gradle.kts index 11c740b..5c754a2 100644 --- a/pk-auth-jwt/build.gradle.kts +++ b/pk-auth-jwt/build.gradle.kts @@ -20,6 +20,9 @@ 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("jacocoTestCoverageVerification") { violationRules { rule { @@ -27,10 +30,10 @@ tasks.named("jacocoTestCoverageVerification") { counter = "LINE" minimum = "0.80".toBigDecimal() } + limit { + counter = "BRANCH" + minimum = "0.85".toBigDecimal() + } } } } - -tasks.named("check") { - dependsOn("jacocoTestCoverageVerification") -} diff --git a/pk-auth-magic-link/build.gradle.kts b/pk-auth-magic-link/build.gradle.kts index e4b8781..629d3f0 100644 --- a/pk-auth-magic-link/build.gradle.kts +++ b/pk-auth-magic-link/build.gradle.kts @@ -21,18 +21,3 @@ dependencies { testImplementation(project(":pk-auth-testkit")) testRuntimeOnly(libs.logback.classic) } - -tasks.named("jacocoTestCoverageVerification") { - violationRules { - rule { - limit { - counter = "LINE" - minimum = "0.70".toBigDecimal() - } - } - } -} - -tasks.named("check") { - dependsOn("jacocoTestCoverageVerification") -} diff --git a/pk-auth-micronaut/build.gradle.kts b/pk-auth-micronaut/build.gradle.kts index 3806dbe..4b8c8c0 100644 --- a/pk-auth-micronaut/build.gradle.kts +++ b/pk-auth-micronaut/build.gradle.kts @@ -60,18 +60,3 @@ dependencies { testAnnotationProcessor(libs.micronaut.inject.java) testRuntimeOnly(libs.logback.classic) } - -tasks.named("jacocoTestCoverageVerification") { - violationRules { - rule { - limit { - counter = "LINE" - minimum = "0.70".toBigDecimal() - } - } - } -} - -tasks.named("check") { - dependsOn("jacocoTestCoverageVerification") -} diff --git a/pk-auth-otp/build.gradle.kts b/pk-auth-otp/build.gradle.kts index fa3c421..c41ab1d 100644 --- a/pk-auth-otp/build.gradle.kts +++ b/pk-auth-otp/build.gradle.kts @@ -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("jacocoTestCoverageVerification") { violationRules { rule { limit { - counter = "LINE" - minimum = "0.70".toBigDecimal() + counter = "BRANCH" + minimum = "0.80".toBigDecimal() } } } } - -tasks.named("check") { - dependsOn("jacocoTestCoverageVerification") -} diff --git a/pk-auth-persistence-dynamodb/build.gradle.kts b/pk-auth-persistence-dynamodb/build.gradle.kts index 98a89af..2416638 100644 --- a/pk-auth-persistence-dynamodb/build.gradle.kts +++ b/pk-auth-persistence-dynamodb/build.gradle.kts @@ -28,18 +28,3 @@ dependencies { testImplementation(libs.testcontainers.core) testRuntimeOnly(libs.logback.classic) } - -tasks.named("jacocoTestCoverageVerification") { - violationRules { - rule { - limit { - counter = "LINE" - minimum = "0.70".toBigDecimal() - } - } - } -} - -tasks.named("check") { - dependsOn("jacocoTestCoverageVerification") -} diff --git a/pk-auth-persistence-jdbi/build.gradle.kts b/pk-auth-persistence-jdbi/build.gradle.kts index c6d0454..295189b 100644 --- a/pk-auth-persistence-jdbi/build.gradle.kts +++ b/pk-auth-persistence-jdbi/build.gradle.kts @@ -36,18 +36,3 @@ dependencies { testImplementation(libs.testcontainers.postgresql) testRuntimeOnly(libs.logback.classic) } - -tasks.named("jacocoTestCoverageVerification") { - violationRules { - rule { - limit { - counter = "LINE" - minimum = "0.70".toBigDecimal() - } - } - } -} - -tasks.named("check") { - dependsOn("jacocoTestCoverageVerification") -} diff --git a/pk-auth-refresh-tokens/build.gradle.kts b/pk-auth-refresh-tokens/build.gradle.kts index 9a6c985..c3dfbe7 100644 --- a/pk-auth-refresh-tokens/build.gradle.kts +++ b/pk-auth-refresh-tokens/build.gradle.kts @@ -25,17 +25,16 @@ dependencies { testRuntimeOnly(libs.logback.classic) } +// Raises only BRANCH above the pkauth.test-conventions baseline (LINE ≥0.70, BRANCH ≥0.55): +// family-based replay defense is branch-heavy and already well-covered, so the bar is pinned near +// current. tasks.named("jacocoTestCoverageVerification") { violationRules { rule { limit { - counter = "LINE" - minimum = "0.70".toBigDecimal() + counter = "BRANCH" + minimum = "0.80".toBigDecimal() } } } } - -tasks.named("check") { - dependsOn("jacocoTestCoverageVerification") -} diff --git a/pk-auth-spring-boot-starter/build.gradle.kts b/pk-auth-spring-boot-starter/build.gradle.kts index a76c2c8..a0d7c1e 100644 --- a/pk-auth-spring-boot-starter/build.gradle.kts +++ b/pk-auth-spring-boot-starter/build.gradle.kts @@ -68,18 +68,3 @@ dependencies { testImplementation(libs.h2) testRuntimeOnly(libs.logback.classic) } - -tasks.named("jacocoTestCoverageVerification") { - violationRules { - rule { - limit { - counter = "LINE" - minimum = "0.70".toBigDecimal() - } - } - } -} - -tasks.named("check") { - dependsOn("jacocoTestCoverageVerification") -} diff --git a/pk-auth-testkit/build.gradle.kts b/pk-auth-testkit/build.gradle.kts index 51614ed..fee5a64 100644 --- a/pk-auth-testkit/build.gradle.kts +++ b/pk-auth-testkit/build.gradle.kts @@ -34,20 +34,3 @@ dependencies { testRuntimeOnly(libs.logback.classic) } - -tasks.named("jacocoTestCoverageVerification") { - violationRules { - rule { - limit { - counter = "LINE" - // Adapter-tier threshold from brief §11 — testkit is adapter-flavored: complex - // crypto wiring that's mostly exercised via the end-to-end ceremony test. - minimum = "0.70".toBigDecimal() - } - } - } -} - -tasks.named("check") { - dependsOn("jacocoTestCoverageVerification") -}