Skip to content

feat(protocol): expose analyzer quick fixes in scan output #29

feat(protocol): expose analyzer quick fixes in scan output

feat(protocol): expose analyzer quick fixes in scan output #29

Workflow file for this run

name: Self-scan (sonar-predictor against itself)
permissions:
contents: read
# Runs the project's OWN scanner — the in-repo daemon, freshly built from this
# branch — against the repository on every PR and on pushes to main. The point
# is CI parity with the local self-scan we run during development: every change
# passes through the same gate, so the bar we apply to others applies to us.
#
# We deliberately do NOT use the previously released bundle from Maven Central.
# SONAR_PREDICTOR_HOME is repointed at the freshly-built exploded distribution
# under target/ so the scan exercises this branch's analyzer code, not
# yesterday's release.
on:
pull_request:
branches: [main]
push:
branches: [main]
workflow_dispatch:
jobs:
self-scan:
name: Self-scan
runs-on: ubuntu-latest
env:
# The assembly step packages the ~150 MB distribution bundle (CLI +
# daemon fat jars + 10 analyzer plugins). Maven's default heap is too
# small for that on ubuntu-latest — we'd get 'Execution exception:
# Java heap space' from maven-assembly-plugin:single. 2 GB is plenty
# and well under the runner's ~7 GB available memory.
MAVEN_OPTS: -Xmx2g
steps:
# fetch-depth: 0 keeps the full history available so we can switch to
# `--diff`-style semantics later without re-checking out the repo.
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
# JDK 21 is the project's build/runtime target (required by
# sonarlint-analysis-engine 11.x / LTA 2026.1). Temurin is the safe default.
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
# The JS/TS analyzer plugin spawns Node at runtime to lint JS/TS sources,
# so Node must be on PATH when the scan runs (not just at build time).
- name: Set up Node.js 20
uses: actions/setup-node@v4
with:
node-version: '20'
# Cache the local Maven repo across runs. Keyed on the pom.xml so a
# dependency change invalidates cleanly; restore-keys lets a partial
# cache hit still seed most of ~/.m2.
- name: Cache Maven repository
uses: actions/cache@v4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
# Single-module build. `verify` runs the integration tests AND produces
# the JaCoCo XML report we feed back into the scan as coverage evidence,
# plus the distribution bundle the self-scan invokes below.
- name: Build and test (generates JaCoCo XML + distribution bundle)
run: mvn -B -ntp -Dmaven.test.skip=false clean verify -Dsurefire.failIfNoSpecifiedTests=false
# Derive the project version from pom.xml so SONAR_PREDICTOR_HOME below
# tracks pom bumps without anyone touching CI.
- name: Derive project version
id: version
run: |
set -euo pipefail
VERSION=$(mvn -B -ntp -q -DforceStdout help:evaluate -Dexpression=project.version)
if [ -z "${VERSION}" ]; then
echo "::error::could not derive project.version from pom.xml"
exit 1
fi
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "Project version: ${VERSION}"
# The actual self-scan. We point bin/sonar at THIS branch's
# freshly-built distribution under target/ (not whatever happens to
# be installed globally), and capture the scan as JSON to
# .sonar-predictor/scan.json via --save. One JaCoCo XML is passed
# in as coverage evidence (single module = one report).
#
# IMPORTANT: the CLI uses three-state exit codes
# 0 = clean (no findings at the floor)
# 1 = issues found (a normal scan outcome, not a failure)
# 2 = tool error (broken input, daemon unreachable, no Java)
# Step success means "the scanner ran". Whether the *result* should
# fail the build is decided by the Quality gate step below. We must
# not let `bash -e` propagate exit-1 from a healthy scan as a job
# failure; we propagate exit code only when it's >= 2.
- name: Run self-scan
run: |
set +e
export SONAR_PREDICTOR_HOME="$(pwd)/target/sonar-predictor-dist-${{ steps.version.outputs.version }}/sonar-predictor"
mkdir -p .sonar-predictor
"$SONAR_PREDICTOR_HOME/bin/sonar" \
--format json --save .sonar-predictor/scan.json \
analyze . --coverage target/site/jacoco/jacoco.xml
rc=$?
set -e
echo "Self-scan exit code: $rc (0=clean, 1=issues found, 2+=tool error)"
if [ "$rc" -ge 2 ]; then
echo "::error::Self-scan tool error (exit $rc) — see step log."
exit "$rc"
fi
# Render headline counts into the GitHub job summary so reviewers see
# the scan result inline on the run page without downloading artifacts.
# `// ([.files[]?.issues[]?]|length)` is the fallback path when an older
# JSON shape doesn't carry a top-level issueCount.
- name: Render scan summary
if: always()
run: |
J=.sonar-predictor/scan.json
if [ ! -f "$J" ]; then
echo "## Sonar self-scan" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "Scan JSON not produced — see the **Run self-scan** step log." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
{
echo "## Sonar self-scan"
echo
echo "| Metric | Value |"
echo "| --- | --- |"
echo "| Total issues | $(jq -r '.issueCount // ([.files[]?.issues[]?]|length)' "$J") |"
echo "| Coverage (line, overall) | $(jq -r '.coverage.overallPercent // "n/a"' "$J")% |"
echo "| Severity | $(jq -rc '[.files[]?.issues[]?.severity]|group_by(.)|map("\(.[0])=\(length)")|join(" ")' "$J") |"
echo "| Type | $(jq -rc '[.files[]?.issues[]?.type]|group_by(.)|map("\(.[0])=\(length)")|join(" ")' "$J") |"
echo "| Warnings | $(jq -r '.warnings // [] | length' "$J") |"
} >> "$GITHUB_STEP_SUMMARY"
# Always upload the scan JSON, even on failure, so a broken scan is
# still debuggable from the run page. 14-day retention is enough to
# cover a typical PR review cycle without pinning storage forever.
- name: Upload scan JSON
if: always()
uses: actions/upload-artifact@v4
with:
name: sonar-scan-${{ github.run_id }}
path: .sonar-predictor/scan.json
retention-days: 14
# Informational gate. We compute the count of CRITICAL bugs and security
# hotspots and emit a `::warning::` so they show on the PR's Checks tab,
# but we deliberately do NOT `exit 1` yet — first runs are baseline data
# while we work the existing findings down to zero.
#
# TODO: once the backlog is clear, flip this to `exit 1` on any
# CRITICAL bug or security hotspot and rename this step to "Quality gate".
- name: Quality gate (informational only)
if: always()
run: |
J=.sonar-predictor/scan.json
if [ ! -f "$J" ]; then
echo "No scan JSON found — skipping gate."
exit 0
fi
CRIT=$(jq -r '[.files[]?.issues[]? | select(.severity=="CRITICAL" and .type=="BUG")] | length' "$J")
HOT=$(jq -r '[.files[]?.issues[]? | select(.type=="SECURITY_HOTSPOT")] | length' "$J")
echo "Critical bugs: $CRIT"
echo "Security hotspots: $HOT"
if [ "$CRIT" -gt 0 ] || [ "$HOT" -gt 0 ]; then
echo "::warning::Self-scan found $CRIT critical bug(s) and $HOT security hotspot(s). Gate is informational for now; will enforce later."
fi