Skip to content

Commit 9c40d6e

Browse files
aksOpsclaude
andcommitted
ci: parity workflow — self-scan vs SonarQube Cloud, diffed per PR
Adds .github/workflows/parity.yml and scripts/scan_parity.py. Replaces the standalone .github/workflows/sonarqube-cloud.yml — the parity job runs the SonarQube Cloud scan AND our self-scan in the same job, then diffs the issue lists, so drift between our daemon and SonarSource's own pipeline shows up directly on every PR. What it computes (stdlib Python, no third-party deps): - total issues per source (self vs cloud) - per-rule cardinality with deltas (self count, cloud count, delta) - issues present in only one source, keyed on (ruleKey, file, line) - parity score = common / union (Jaccard on issue identity) Output: - Markdown report rendered into \$GITHUB_STEP_SUMMARY on every run - Machine-readable .sonar-predictor/parity.json uploaded as an artifact (14-day retention) alongside scan.json Workflow shape: 1. Single build + test (generates the JaCoCo XMLs both scans consume) 2. (A) self-scan via the in-repo CLI's --save flag 3. (B) SonarQube Cloud scan via mvn sonar:sonar 4. Poll SonarQube Cloud's analysis API until the just-published run shows up (~30-60s typical, 3-min cap) 5. (C) Pull Cloud issues via /api/issues/search, diff against self-scan, write report Skips on fork PRs (no SONAR_TOKEN reachable); the standalone sonar.yml self-scan still gates them on our daemon's findings. The standalone sonarqube-cloud.yml is removed — its work is a strict subset of what parity.yml does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 18877ad commit 9c40d6e

3 files changed

Lines changed: 471 additions & 143 deletions

File tree

.github/workflows/parity.yml

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
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+
steps:
44+
- name: Checkout
45+
uses: actions/checkout@v4
46+
with:
47+
fetch-depth: 0
48+
49+
- name: Set up JDK 17
50+
uses: actions/setup-java@v4
51+
with:
52+
distribution: temurin
53+
java-version: '17'
54+
55+
- name: Set up Node.js 20
56+
uses: actions/setup-node@v4
57+
with:
58+
node-version: '20'
59+
60+
- name: Cache Maven repository
61+
uses: actions/cache@v4
62+
with:
63+
path: ~/.m2/repository
64+
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
65+
restore-keys: |
66+
${{ runner.os }}-maven-
67+
68+
- name: Cache SonarQube Cloud packages
69+
uses: actions/cache@v4
70+
with:
71+
path: ~/.sonar/cache
72+
key: ${{ runner.os }}-sonar
73+
restore-keys: |
74+
${{ runner.os }}-sonar
75+
76+
# Fail fast if the SONAR_TOKEN secret isn't configured. The Cloud step
77+
# below would just error obscurely; this is a clearer signal.
78+
- name: Verify SONAR_TOKEN
79+
env:
80+
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
81+
run: |
82+
if [ -z "${SONAR_TOKEN:-}" ]; then
83+
echo "::error::SONAR_TOKEN not set — parity workflow needs SonarQube Cloud access. See the header of this workflow for setup."
84+
exit 1
85+
fi
86+
echo "SONAR_TOKEN configured."
87+
88+
# Single build that both scans then consume. `verify` also produces
89+
# the per-module JaCoCo XMLs that both scans use as coverage evidence.
90+
- name: Build and test (generates JaCoCo XML)
91+
run: mvn -B -ntp -pl '!dist' verify -Dsurefire.failIfNoSpecifiedTests=false
92+
93+
- name: Build dist (skill bundle for self-scan)
94+
run: mvn -B -ntp -pl dist -am package -DskipTests
95+
96+
# --- (A) Self-scan ------------------------------------------------------
97+
98+
- name: Run self-scan
99+
run: |
100+
set +e
101+
export SONAR_PREDICTOR_HOME="$(pwd)/dist/target/skill/sonar-predictor"
102+
./plugin/skills/sonar-predictor/bin/sonar agent-scan analyze . \
103+
--coverage protocol/target/site/jacoco/jacoco.xml \
104+
--coverage daemon/target/site/jacoco/jacoco.xml \
105+
--coverage cli/target/site/jacoco/jacoco.xml
106+
rc=$?
107+
set -e
108+
echo "Self-scan exit code: $rc (0=clean, 1=issues found, 2+=tool error)"
109+
if [ "$rc" -ge 2 ]; then
110+
echo "::error::Self-scan tool error (exit $rc)"
111+
exit "$rc"
112+
fi
113+
114+
# --- (B) SonarQube Cloud scan ------------------------------------------
115+
116+
- name: Run SonarQube Cloud scan
117+
env:
118+
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
119+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
120+
run: |
121+
mvn -B -ntp -pl '!dist' \
122+
org.sonarsource.scanner.maven:sonar-maven-plugin:sonar \
123+
-Dsonar.projectKey="${SONAR_PROJECT_KEY}" \
124+
-Dsonar.organization="${SONAR_ORGANIZATION}" \
125+
-Dsonar.host.url="${SONAR_HOST_URL}" \
126+
-Dsonar.qualitygate.wait=false \
127+
-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
128+
129+
# SonarQube Cloud processes the scan asynchronously after upload. Poll
130+
# the last completed analysis until the timestamp moves past the start
131+
# of this run, or give up after ~3 minutes (most analyses complete in
132+
# 30-60s; longer polls aren't worth holding the runner for).
133+
- name: Wait for SonarQube Cloud processing
134+
env:
135+
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
136+
run: |
137+
START_TS=$(date +%s)
138+
for attempt in $(seq 1 18); do
139+
RESP=$(curl -fsSL -u "$SONAR_TOKEN:" \
140+
"${SONAR_HOST_URL}/api/project_analyses/search?project=${SONAR_PROJECT_KEY}&ps=1" || echo '{}')
141+
ANALYSIS_TS=$(echo "$RESP" | jq -r '.analyses[0].date // empty' || true)
142+
if [ -n "$ANALYSIS_TS" ]; then
143+
ANALYSIS_EPOCH=$(date -d "$ANALYSIS_TS" +%s 2>/dev/null || echo 0)
144+
if [ "$ANALYSIS_EPOCH" -ge "$START_TS" ]; then
145+
echo "Latest analysis ($ANALYSIS_TS) is from this run."
146+
exit 0
147+
fi
148+
echo "Attempt $attempt: latest analysis is $ANALYSIS_TS (before run start), waiting…"
149+
else
150+
echo "Attempt $attempt: no analyses yet on SonarQube Cloud, waiting…"
151+
fi
152+
sleep 10
153+
done
154+
echo "::warning::Timed out waiting for SonarQube Cloud to publish this run's analysis. Parity diff will use the most-recent published state."
155+
156+
# --- (C) Parity diff ----------------------------------------------------
157+
158+
- name: Diff self-scan vs SonarQube Cloud
159+
env:
160+
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
161+
run: |
162+
PR_ARG=""
163+
BRANCH_ARG=""
164+
if [ "${{ github.event_name }}" = "pull_request" ]; then
165+
PR_ARG="--pull-request ${{ github.event.pull_request.number }}"
166+
else
167+
BRANCH_ARG="--branch ${{ github.ref_name }}"
168+
fi
169+
python3 scripts/scan_parity.py \
170+
--self-scan .sonar-predictor/scan.json \
171+
--project-key "${SONAR_PROJECT_KEY}" \
172+
--organization "${SONAR_ORGANIZATION}" \
173+
--host "${SONAR_HOST_URL}" \
174+
$PR_ARG $BRANCH_ARG \
175+
--out .sonar-predictor/parity.json
176+
177+
- name: Upload scan + parity artifacts
178+
if: always()
179+
uses: actions/upload-artifact@v4
180+
with:
181+
name: scan-parity-${{ github.run_id }}
182+
path: |
183+
.sonar-predictor/scan.json
184+
.sonar-predictor/parity.json
185+
retention-days: 14
186+
187+
- name: Link to SonarQube Cloud dashboard
188+
if: always()
189+
run: |
190+
{
191+
echo ""
192+
echo "---"
193+
echo ""
194+
echo "**SonarQube Cloud dashboard:** ${SONAR_HOST_URL}/project/overview?id=${SONAR_PROJECT_KEY}"
195+
if [ "${{ github.event_name }}" = "pull_request" ]; then
196+
echo ""
197+
echo "**PR-scoped view:** ${SONAR_HOST_URL}/project/issues?id=${SONAR_PROJECT_KEY}&pullRequest=${{ github.event.pull_request.number }}"
198+
fi
199+
} >> "$GITHUB_STEP_SUMMARY"

.github/workflows/sonarqube-cloud.yml

Lines changed: 0 additions & 143 deletions
This file was deleted.

0 commit comments

Comments
 (0)