Skip to content

Commit 523f634

Browse files
authored
build+ci: JaCoCo coverage, self-scan + SonarCloud parity workflows, fix all CRITICAL findings, drop spike
PR #4 — what landed: - **JaCoCo enabled** across all reactor modules with the 0.8.13 plugin (Java 25 class-file compatible). `mvn verify` now produces `<module>/target/site/jacoco/jacoco.xml` everywhere. - **77 → 0 CRITICAL findings** in the in-repo self-scan: every BUG fixed (volatile → AtomicReference across DaemonServer + AnalysisService); every security hotspot resolved (owner-only tempdir, bounded regex input, NOSONAR-with-justification on zip-slip-irrelevant entry reads, S2612/S4036 narrowings). - **Self-scan CI workflow** (`.github/workflows/sonar.yml`) — runs the in-repo daemon against the branch on every PR; fork-PR-safe (no SONAR_TOKEN dep); uploads scan.json artifact. - **SonarQube Cloud parity workflow** (`.github/workflows/parity.yml` + `scripts/scan_parity.py`) — runs both scans on the same commit, diffs issue lists and coverage measures, posts a structured summary to the GitHub job summary. - **CLI `--save PATH` flag** — writes the report to a file and emits a compact summary on stdout (no jq dependency in the plugin layer). - **Spike module deleted** — research artifact, never imported by production code, eliminated as a source of false-positive scan noise. - **CI workflow hardening** — `curl --proto =https --proto-redir =https` (S6506), `MAVEN_OPTS=-Xmx2g` for the dist-assembly heap, exit-code handling that treats `agent-scan`'s exit-1 (issues found) as a normal result. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
1 parent 060a2c2 commit 523f634

33 files changed

Lines changed: 1466 additions & 644 deletions

.github/workflows/parity.yml

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
name: Scan parity (self-scan ↔ SonarQube Cloud)
2+
3+
# Runs BOTH scans on the same commit and diffs their issue lists. Every PR
4+
# answers: "does our daemon find what SonarSource's own pipeline finds?".
5+
#
6+
# This workflow supersedes .github/workflows/sonarqube-cloud.yml — that file
7+
# was removed in the same commit; the Cloud scan now happens here, alongside
8+
# the self-scan and the parity comparison. The standalone self-scan
9+
# (sonar.yml) is kept because it works on fork PRs (no SONAR_TOKEN needed),
10+
# whereas this one requires the token and so skips for forks.
11+
#
12+
# Setup required (one-time, by the repo admin):
13+
# 1. Sign in to https://sonarcloud.io with the repo's GitHub org.
14+
# 2. Import this repo as a SonarQube Cloud project.
15+
# Project key: RandomCodeSpace_sonar-predict, organisation: randomcodespace
16+
# (adjust the env values below if SonarCloud assigns different ones).
17+
# 3. Configure "Analysis Method" → "With GitHub Actions"; copy SONAR_TOKEN.
18+
# 4. Add SONAR_TOKEN as a repo secret.
19+
20+
permissions:
21+
contents: read
22+
pull-requests: read
23+
24+
on:
25+
pull_request:
26+
branches: [main]
27+
push:
28+
branches: [main]
29+
workflow_dispatch:
30+
31+
env:
32+
SONAR_PROJECT_KEY: RandomCodeSpace_sonar-predict
33+
SONAR_ORGANIZATION: randomcodespace
34+
SONAR_HOST_URL: https://sonarcloud.io
35+
36+
jobs:
37+
parity:
38+
name: Scan parity
39+
runs-on: ubuntu-latest
40+
# Skip when the token isn't reachable (fork PRs) — the standalone
41+
# self-scan workflow still gates fork PRs on our daemon's findings.
42+
if: ${{ github.event_name == 'workflow_dispatch' || github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository }}
43+
env:
44+
# Same dist-assembly heap concern as sonar.yml — 2 GB to keep the
45+
# 150 MB skill-bundle zip step from OOMing maven-assembly-plugin.
46+
MAVEN_OPTS: -Xmx2g
47+
steps:
48+
- name: Checkout
49+
uses: actions/checkout@v4
50+
with:
51+
fetch-depth: 0
52+
53+
- name: Set up JDK 17
54+
uses: actions/setup-java@v4
55+
with:
56+
distribution: temurin
57+
java-version: '17'
58+
59+
- name: Set up Node.js 20
60+
uses: actions/setup-node@v4
61+
with:
62+
node-version: '20'
63+
64+
- name: Cache Maven repository
65+
uses: actions/cache@v4
66+
with:
67+
path: ~/.m2/repository
68+
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
69+
restore-keys: |
70+
${{ runner.os }}-maven-
71+
72+
- name: Cache SonarQube Cloud packages
73+
uses: actions/cache@v4
74+
with:
75+
path: ~/.sonar/cache
76+
key: ${{ runner.os }}-sonar
77+
restore-keys: |
78+
${{ runner.os }}-sonar
79+
80+
# Fail fast if the SONAR_TOKEN secret isn't configured. The Cloud step
81+
# below would just error obscurely; this is a clearer signal.
82+
- name: Verify SONAR_TOKEN
83+
env:
84+
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
85+
run: |
86+
if [ -z "${SONAR_TOKEN:-}" ]; then
87+
echo "::error::SONAR_TOKEN not set — parity workflow needs SonarQube Cloud access. See the header of this workflow for setup."
88+
exit 1
89+
fi
90+
echo "SONAR_TOKEN configured."
91+
92+
# Single build that both scans then consume. `verify` also produces
93+
# the per-module JaCoCo XMLs that both scans use as coverage evidence.
94+
- name: Build and test (generates JaCoCo XML)
95+
run: mvn -B -ntp -pl '!dist' verify -Dsurefire.failIfNoSpecifiedTests=false
96+
97+
- name: Build dist (skill bundle for self-scan)
98+
run: mvn -B -ntp -pl dist -am package -DskipTests
99+
100+
# --- (A) Self-scan ------------------------------------------------------
101+
102+
- name: Run self-scan
103+
run: |
104+
set +e
105+
export SONAR_PREDICTOR_HOME="$(pwd)/dist/target/skill/sonar-predictor"
106+
./plugin/skills/sonar-predictor/bin/sonar agent-scan analyze . \
107+
--coverage protocol/target/site/jacoco/jacoco.xml \
108+
--coverage daemon/target/site/jacoco/jacoco.xml \
109+
--coverage cli/target/site/jacoco/jacoco.xml
110+
rc=$?
111+
set -e
112+
echo "Self-scan exit code: $rc (0=clean, 1=issues found, 2+=tool error)"
113+
if [ "$rc" -ge 2 ]; then
114+
echo "::error::Self-scan tool error (exit $rc)"
115+
exit "$rc"
116+
fi
117+
118+
# --- (B) SonarQube Cloud scan ------------------------------------------
119+
120+
- name: Run SonarQube Cloud scan
121+
env:
122+
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
123+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
124+
run: |
125+
mvn -B -ntp -pl '!dist' \
126+
org.sonarsource.scanner.maven:sonar-maven-plugin:sonar \
127+
-Dsonar.projectKey="${SONAR_PROJECT_KEY}" \
128+
-Dsonar.organization="${SONAR_ORGANIZATION}" \
129+
-Dsonar.host.url="${SONAR_HOST_URL}" \
130+
-Dsonar.qualitygate.wait=false \
131+
-Dsonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xml,../protocol/target/site/jacoco/jacoco.xml,../daemon/target/site/jacoco/jacoco.xml,../cli/target/site/jacoco/jacoco.xml
132+
133+
# SonarQube Cloud processes the scan asynchronously after upload. Poll
134+
# the last completed analysis until the timestamp moves past the start
135+
# of this run, or give up after ~3 minutes (most analyses complete in
136+
# 30-60s; longer polls aren't worth holding the runner for).
137+
- name: Wait for SonarQube Cloud processing
138+
env:
139+
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
140+
run: |
141+
START_TS=$(date +%s)
142+
for attempt in $(seq 1 18); do
143+
# Constrain both the initial request (--proto =https) AND any redirect
144+
# (--proto-redir =https) to HTTPS. Without --proto, githubactions:S6506
145+
# still fires even when SONAR_HOST_URL is already https://; the rule
146+
# wants both belts explicit at the curl-call site.
147+
RESP=$(curl -fsSL --proto =https --proto-redir =https -u "$SONAR_TOKEN:" \
148+
"${SONAR_HOST_URL}/api/project_analyses/search?project=${SONAR_PROJECT_KEY}&ps=1" || echo '{}')
149+
ANALYSIS_TS=$(echo "$RESP" | jq -r '.analyses[0].date // empty' || true)
150+
if [ -n "$ANALYSIS_TS" ]; then
151+
ANALYSIS_EPOCH=$(date -d "$ANALYSIS_TS" +%s 2>/dev/null || echo 0)
152+
if [ "$ANALYSIS_EPOCH" -ge "$START_TS" ]; then
153+
echo "Latest analysis ($ANALYSIS_TS) is from this run."
154+
exit 0
155+
fi
156+
echo "Attempt $attempt: latest analysis is $ANALYSIS_TS (before run start), waiting…"
157+
else
158+
echo "Attempt $attempt: no analyses yet on SonarQube Cloud, waiting…"
159+
fi
160+
sleep 10
161+
done
162+
echo "::warning::Timed out waiting for SonarQube Cloud to publish this run's analysis. Parity diff will use the most-recent published state."
163+
164+
# --- (C) Parity diff ----------------------------------------------------
165+
166+
- name: Diff self-scan vs SonarQube Cloud
167+
env:
168+
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
169+
run: |
170+
PR_ARG=""
171+
BRANCH_ARG=""
172+
if [ "${{ github.event_name }}" = "pull_request" ]; then
173+
PR_ARG="--pull-request ${{ github.event.pull_request.number }}"
174+
else
175+
BRANCH_ARG="--branch ${{ github.ref_name }}"
176+
fi
177+
python3 scripts/scan_parity.py \
178+
--self-scan .sonar-predictor/scan.json \
179+
--project-key "${SONAR_PROJECT_KEY}" \
180+
--organization "${SONAR_ORGANIZATION}" \
181+
--host "${SONAR_HOST_URL}" \
182+
$PR_ARG $BRANCH_ARG \
183+
--out .sonar-predictor/parity.json
184+
185+
- name: Upload scan + parity artifacts
186+
if: always()
187+
uses: actions/upload-artifact@v4
188+
with:
189+
name: scan-parity-${{ github.run_id }}
190+
path: |
191+
.sonar-predictor/scan.json
192+
.sonar-predictor/parity.json
193+
retention-days: 14
194+
195+
- name: Link to SonarQube Cloud dashboard
196+
if: always()
197+
run: |
198+
{
199+
echo ""
200+
echo "---"
201+
echo ""
202+
echo "**SonarQube Cloud dashboard:** ${SONAR_HOST_URL}/project/overview?id=${SONAR_PROJECT_KEY}"
203+
if [ "${{ github.event_name }}" = "pull_request" ]; then
204+
echo ""
205+
echo "**PR-scoped view:** ${SONAR_HOST_URL}/project/issues?id=${SONAR_PROJECT_KEY}&pullRequest=${{ github.event.pull_request.number }}"
206+
fi
207+
} >> "$GITHUB_STEP_SUMMARY"

.github/workflows/sonar.yml

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
name: Self-scan (sonar-predictor against itself)
2+
3+
permissions:
4+
contents: read
5+
6+
# Runs the project's OWN scanner — the in-repo daemon, freshly built from this
7+
# branch — against the repository on every PR and on pushes to main. The point
8+
# is CI parity with the local self-scan we run during development: every change
9+
# passes through the same gate, so the bar we apply to others applies to us.
10+
#
11+
# We deliberately do NOT use the previously released bundle from Maven Central.
12+
# SONAR_PREDICTOR_HOME is repointed at dist/target/skill/sonar-predictor so the
13+
# scan exercises the branch's analyzer code, not yesterday's release.
14+
15+
on:
16+
pull_request:
17+
branches: [main]
18+
push:
19+
branches: [main]
20+
workflow_dispatch:
21+
22+
jobs:
23+
self-scan:
24+
name: Self-scan
25+
runs-on: ubuntu-latest
26+
env:
27+
# The dist-assembly step packages the ~150 MB skill bundle (CLI + daemon
28+
# fat jars + 10 analyzer plugins). Maven's default heap is too small for
29+
# that on ubuntu-latest — we'd get 'Execution exception: Java heap space'
30+
# from maven-assembly-plugin:single. 2 GB is plenty and well under the
31+
# runner's ~7 GB available memory.
32+
MAVEN_OPTS: -Xmx2g
33+
steps:
34+
# fetch-depth: 0 keeps the full history available so we can switch to
35+
# `--diff`-style semantics later without re-checking out the repo.
36+
- name: Checkout
37+
uses: actions/checkout@v4
38+
with:
39+
fetch-depth: 0
40+
41+
# JDK 17 is the project's build/runtime target. Temurin is the safe default.
42+
- name: Set up JDK 17
43+
uses: actions/setup-java@v4
44+
with:
45+
distribution: temurin
46+
java-version: '17'
47+
48+
# The JS/TS analyzer plugin spawns Node at runtime to lint JS/TS sources,
49+
# so Node must be on PATH when the scan runs (not just at build time).
50+
- name: Set up Node.js 20
51+
uses: actions/setup-node@v4
52+
with:
53+
node-version: '20'
54+
55+
# Cache the local Maven repo across runs. Keyed on every pom.xml in the
56+
# tree so a dependency change invalidates cleanly; restore-keys lets a
57+
# partial cache hit still seed most of ~/.m2.
58+
- name: Cache Maven repository
59+
uses: actions/cache@v4
60+
with:
61+
path: ~/.m2/repository
62+
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
63+
restore-keys: |
64+
${{ runner.os }}-maven-
65+
66+
# Build + test the whole reactor except `dist` (the bundle module is
67+
# packaged separately below). `verify` runs the integration tests AND
68+
# produces the JaCoCo XML reports we feed back into the scan as coverage
69+
# evidence. failIfNoSpecifiedTests=false keeps modules with no tests
70+
# from breaking the reactor.
71+
- name: Build and test (generates JaCoCo XML reports)
72+
run: mvn -B -ntp -pl '!dist' verify -Dsurefire.failIfNoSpecifiedTests=false
73+
74+
# Now build the skill bundle. -am pulls in upstream modules if they
75+
# weren't already installed by the previous step. -DskipTests because
76+
# the tests already ran — re-running them here just wastes minutes.
77+
- name: Build dist (skill bundle this scan will use)
78+
run: mvn -B -ntp -pl dist -am package -DskipTests
79+
80+
# The actual self-scan. We override SONAR_PREDICTOR_HOME so the wrapper
81+
# script picks up THIS branch's freshly-built daemon jar and analyzer
82+
# plugins, not whatever happens to be installed globally. Three JaCoCo
83+
# XMLs are passed in as coverage evidence — one per Java module that
84+
# produces coverage. agent-scan writes JSON to .sonar-predictor/scan.json
85+
# and prints a human summary on stdout; we want both.
86+
#
87+
# IMPORTANT: the CLI uses three-state exit codes
88+
# 0 = clean (no findings at the floor)
89+
# 1 = issues found (a normal scan outcome, not a failure)
90+
# 2 = tool error (broken input, daemon unreachable, no Java)
91+
# Step success means "the scanner ran". Whether the *result* should
92+
# fail the build is decided by the Quality gate step below. We must
93+
# not let `bash -e` propagate exit-1 from a healthy scan as a job
94+
# failure; we propagate exit code only when it's >= 2.
95+
- name: Run self-scan
96+
run: |
97+
set +e
98+
export SONAR_PREDICTOR_HOME="$(pwd)/dist/target/skill/sonar-predictor"
99+
./plugin/skills/sonar-predictor/bin/sonar agent-scan analyze . \
100+
--coverage protocol/target/site/jacoco/jacoco.xml \
101+
--coverage daemon/target/site/jacoco/jacoco.xml \
102+
--coverage cli/target/site/jacoco/jacoco.xml
103+
rc=$?
104+
set -e
105+
echo "Self-scan exit code: $rc (0=clean, 1=issues found, 2+=tool error)"
106+
if [ "$rc" -ge 2 ]; then
107+
echo "::error::Self-scan tool error (exit $rc) — see step log."
108+
exit "$rc"
109+
fi
110+
111+
# Render headline counts into the GitHub job summary so reviewers see
112+
# the scan result inline on the run page without downloading artifacts.
113+
# `// ([.files[]?.issues[]?]|length)` is the fallback path when an older
114+
# JSON shape doesn't carry a top-level issueCount.
115+
- name: Render scan summary
116+
if: always()
117+
run: |
118+
J=.sonar-predictor/scan.json
119+
if [ ! -f "$J" ]; then
120+
echo "## Sonar self-scan" >> "$GITHUB_STEP_SUMMARY"
121+
echo "" >> "$GITHUB_STEP_SUMMARY"
122+
echo "Scan JSON not produced — see the **Run self-scan** step log." >> "$GITHUB_STEP_SUMMARY"
123+
exit 0
124+
fi
125+
{
126+
echo "## Sonar self-scan"
127+
echo
128+
echo "| Metric | Value |"
129+
echo "| --- | --- |"
130+
echo "| Total issues | $(jq -r '.issueCount // ([.files[]?.issues[]?]|length)' "$J") |"
131+
echo "| Coverage (line, overall) | $(jq -r '.coverage.overallPercent // "n/a"' "$J")% |"
132+
echo "| Severity | $(jq -rc '[.files[]?.issues[]?.severity]|group_by(.)|map("\(.[0])=\(length)")|join(" ")' "$J") |"
133+
echo "| Type | $(jq -rc '[.files[]?.issues[]?.type]|group_by(.)|map("\(.[0])=\(length)")|join(" ")' "$J") |"
134+
echo "| Warnings | $(jq -r '.warnings // [] | length' "$J") |"
135+
} >> "$GITHUB_STEP_SUMMARY"
136+
137+
# Always upload the scan JSON, even on failure, so a broken scan is
138+
# still debuggable from the run page. 14-day retention is enough to
139+
# cover a typical PR review cycle without pinning storage forever.
140+
- name: Upload scan JSON
141+
if: always()
142+
uses: actions/upload-artifact@v4
143+
with:
144+
name: sonar-scan-${{ github.run_id }}
145+
path: .sonar-predictor/scan.json
146+
retention-days: 14
147+
148+
# Informational gate. We compute the count of CRITICAL bugs and security
149+
# hotspots and emit a `::warning::` so they show on the PR's Checks tab,
150+
# but we deliberately do NOT `exit 1` yet — first runs are baseline data
151+
# while we work the existing findings down to zero.
152+
#
153+
# TODO: once the backlog is clear, flip this to `exit 1` on any
154+
# CRITICAL bug or security hotspot and rename this step to "Quality gate".
155+
- name: Quality gate (informational only)
156+
if: always()
157+
run: |
158+
J=.sonar-predictor/scan.json
159+
if [ ! -f "$J" ]; then
160+
echo "No scan JSON found — skipping gate."
161+
exit 0
162+
fi
163+
CRIT=$(jq -r '[.files[]?.issues[]? | select(.severity=="CRITICAL" and .type=="BUG")] | length' "$J")
164+
HOT=$(jq -r '[.files[]?.issues[]? | select(.type=="SECURITY_HOTSPOT")] | length' "$J")
165+
echo "Critical bugs: $CRIT"
166+
echo "Security hotspots: $HOT"
167+
if [ "$CRIT" -gt 0 ] || [ "$HOT" -gt 0 ]; then
168+
echo "::warning::Self-scan found $CRIT critical bug(s) and $HOT security hotspot(s). Gate is informational for now; will enforce later."
169+
fi

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,8 @@ daemon/plugins/*.jar
1818

1919
# sonar-predictor scan output, written by `bin/sonar agent-scan`. Never commit.
2020
.sonar-predictor/
21+
22+
# Python bytecode (scan_parity.py compiles to scripts/__pycache__ when run locally).
23+
__pycache__/
24+
*.pyc
25+

0 commit comments

Comments
 (0)