Skip to content

Commit d7a2024

Browse files
aksOpsclaude
andcommitted
ci: SonarCloud quality gate + coverage on PR and main
Adds a SonarCloud scan that ingests Go + JS/TS coverage on every PR and on push to main. Coverage is collected fresh in this workflow rather than reused from CI — the existing ci.yml runs go test without -coverprofile and adding it there would slow every PR build. Setup required (manual, one-time): 1. Sign in to sonarcloud.io with GitHub. 2. Import RandomCodeSpace/ctm; project key auto-generated as RandomCodeSpace_ctm (matches sonar-project.properties). 3. Generate a user token; add as repo secret SONAR_TOKEN. Until SONAR_TOKEN is set, the workflow runs the test+coverage steps and emits a workflow warning instead of erroring on the scan step. Coverage layout: - Go: go test -coverprofile=coverage.out (atomic mode for race-safe) - UI: vitest run --coverage with provider:v8 → ui/coverage/lcov.info - Both: excluded from git via .gitignore Excluded from analysis: dist/, vendor/, node_modules/, _attic/, .claude/, .codeiq/, internal/serve/dist/ (generated UI bundle), ui/playwright-report/, ui/test-results/, docs/. Quality gate uses SonarCloud's default ('clean as you code', 80% new-code coverage) — adjustable in the SonarCloud UI later. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c8e1224 commit d7a2024

6 files changed

Lines changed: 438 additions & 0 deletions

File tree

.github/workflows/sonar.yml

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# SonarCloud quality gate + coverage upload.
2+
#
3+
# Triggers: push to main (full-history scan) and PRs (incremental scan
4+
# against the base branch). Concurrency-gated per-ref so reruns cancel
5+
# in-flight scans.
6+
#
7+
# Coverage is collected fresh in this workflow rather than reused from
8+
# CI — Sonar needs both go-coverprofile and lcov in the same workspace
9+
# pass, and the existing CI job runs `go test` without -coverprofile.
10+
# Adding coverage there would slow every PR build; keeping it here
11+
# isolates the cost to the Sonar job.
12+
13+
name: SonarCloud
14+
15+
on:
16+
push:
17+
branches: [main]
18+
pull_request:
19+
types: [opened, synchronize, reopened]
20+
21+
permissions:
22+
contents: read
23+
pull-requests: read
24+
25+
concurrency:
26+
group: sonar-${{ github.workflow }}-${{ github.ref }}
27+
cancel-in-progress: true
28+
29+
jobs:
30+
scan:
31+
runs-on: ubuntu-latest
32+
steps:
33+
- name: Checkout
34+
uses: actions/checkout@v4
35+
with:
36+
# Sonar uses git blame for new-code detection — shallow
37+
# checkouts (default fetch-depth=1) attribute every line to
38+
# the latest commit, breaking the quality-gate "new code"
39+
# signal. fetch-depth: 0 grabs the full history.
40+
fetch-depth: 0
41+
42+
- name: Set up Go
43+
uses: actions/setup-go@v5
44+
with:
45+
go-version-file: go.mod
46+
47+
- name: Set up pnpm
48+
uses: pnpm/action-setup@v4
49+
with:
50+
version: 10.33.0
51+
52+
- name: Set up Node
53+
uses: actions/setup-node@v4
54+
with:
55+
node-version: 22
56+
cache: pnpm
57+
cache-dependency-path: ui/pnpm-lock.yaml
58+
59+
- name: Build UI bundle
60+
# Required before any Go test run: //go:embed all:dist in
61+
# internal/serve/assets.go errors out if internal/serve/dist/
62+
# is empty. Same as ci.yml.
63+
run: make ui
64+
65+
- name: Go test with coverage
66+
run: |
67+
go test -tags sqlite_fts5 \
68+
-coverprofile=coverage.out \
69+
-covermode=atomic \
70+
./...
71+
72+
- name: UI install
73+
run: pnpm -C ui install --frozen-lockfile
74+
75+
- name: UI test with coverage
76+
run: pnpm -C ui exec vitest run --coverage
77+
78+
- name: Detect SONAR_TOKEN
79+
id: token
80+
env:
81+
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
82+
run: |
83+
if [ -n "$SONAR_TOKEN" ]; then
84+
echo "present=true" >> "$GITHUB_OUTPUT"
85+
else
86+
echo "present=false" >> "$GITHUB_OUTPUT"
87+
echo "::warning::SONAR_TOKEN secret is not set; skipping SonarCloud scan. Add the token in repo Settings → Secrets and variables → Actions."
88+
fi
89+
90+
- name: SonarCloud Scan
91+
if: steps.token.outputs.present == 'true'
92+
uses: SonarSource/sonarqube-scan-action@v5
93+
env:
94+
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,7 @@ docs/
2626

2727
# Local code-intelligence cache (codeiq) — runtime DB + neo4j store, not source
2828
.codeiq/
29+
30+
# Coverage reports (Go + vitest) — regenerated per-test, never committed
31+
coverage.out
32+
ui/coverage/

sonar-project.properties

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# SonarCloud project configuration. The scanner reads this file at the
2+
# repo root automatically; the GitHub Action only needs SONAR_TOKEN.
3+
#
4+
# Project + organization keys must match what SonarCloud generated when
5+
# the project was imported from GitHub — adjust if you renamed the org
6+
# or the project (Sonar UI → Administration → General Settings → Project
7+
# Key).
8+
9+
sonar.projectKey=RandomCodeSpace_ctm
10+
sonar.organization=randomcodespace
11+
sonar.projectName=ctm
12+
13+
# ── Sources ────────────────────────────────────────────────────────────
14+
# Scan everything under the repo root. Generated artifacts, vendored
15+
# code, the embedded UI dist, and agent worktree scratch never enter
16+
# the analysis — they're either machine-written or out of scope.
17+
sonar.sources=.
18+
sonar.exclusions=\
19+
**/dist/**,\
20+
**/node_modules/**,\
21+
**/vendor/**,\
22+
**/_attic/**,\
23+
**/.claude/**,\
24+
**/.codeiq/**,\
25+
internal/serve/dist/**,\
26+
ui/coverage/**,\
27+
ui/playwright-report/**,\
28+
ui/test-results/**,\
29+
coverage.out,\
30+
docs/**
31+
32+
# ── Tests ──────────────────────────────────────────────────────────────
33+
# Sonar separates "test code" from "production code" so coverage and
34+
# duplication metrics target the right files. Playwright e2e specs
35+
# stay out of the JS test set — they don't generate the unit-style
36+
# coverage Sonar expects.
37+
sonar.tests=.
38+
sonar.test.inclusions=\
39+
**/*_test.go,\
40+
ui/src/**/*.test.ts,\
41+
ui/src/**/*.test.tsx
42+
sonar.test.exclusions=ui/e2e/**
43+
44+
# ── Coverage report paths ──────────────────────────────────────────────
45+
# Go: `go test -coverprofile=coverage.out ./...` writes to repo root.
46+
# JS/TS: vitest with provider:v8 writes lcov to ui/coverage/lcov.info
47+
# (configured in ui/vitest.config.ts).
48+
sonar.go.coverage.reportPaths=coverage.out
49+
sonar.javascript.lcov.reportPaths=ui/coverage/lcov.info
50+
51+
# ── General ────────────────────────────────────────────────────────────
52+
sonar.sourceEncoding=UTF-8

ui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"@types/react": "^19.2.14",
3535
"@types/react-dom": "^19.2.3",
3636
"@vitejs/plugin-react": "^5.2.0",
37+
"@vitest/coverage-v8": "^3.2.4",
3738
"eslint": "^9.0.0",
3839
"eslint-plugin-react-hooks": "^5.2.0",
3940
"eslint-plugin-react-refresh": "^0.4.12",

0 commit comments

Comments
 (0)