Skip to main content

SonarCloud analysis in CI

Pull requests are analysed by GitHub Actions, not by SonarCloud's Automatic Analysis (INF-805). This page is the operator's reference: what runs, what it needs, how to switch it off, and how to recover a PR that has no result. The design and its trust boundary are in SPEC-inf-805-ci-sonar-analysis.md.

What runs​

  1. Test · Coverage, a job in CI, runs whenever CI runs for a pull request: PRs into develop, epic/** and main, when opened, pushed, reopened, edited or marked ready. It tests the PR's exact head without the remote cache: every workspace package's test:ci, then the root scripts/ suite, which turbo never runs. It uploads each lcov.info as the sonar-coverage artifact. Quality Gates is what gates the scripts/ suite, so here it only measures: a failing test still writes the report and costs only that test's lines, while a run stopped at its 25-minute bound leaves the root report out and the package reports still go up. It holds no secret and is not a required check.
  2. Sonar · Analysis starts when that CI run completes, always from develop's copy of the workflow:
    • Sonar · Resolve, with no secret, decides from the GitHub API whether the PR is analysed: same-repository, open, not a draft, based on develop, epic/* or main, head unchanged, and a Test · Coverage job that succeeded;
    • Sonar · Analysis holds the token. It checks the PR out as data, imports the coverage through a trusted normaliser, and runs the scanner with configuration checkpoints. The result is the SonarCloud Code Analysis check on the PR's head.

Pull requests are analysed, and one scoped branch analysis of main. The plan's 400,000-line limit is charged on the largest analysed branch, so develop and epic/* are never analysed here, and main only through the scoped baseline below, which an operator dispatches. A PR analysis counts only the files it changes.

A release PR's stale main baseline (INF-870)​

A release PR (release/* or hotfix/* into main) is judged against main's own analysis. When that analysis is stale, every old issue in the files the release changes counts as new: PR #3260 showed 241 new issues, 209 of them on lines it did not change. A whole-repository main analysis is about 1.6 million lines, far over the plan. The main-baseline mode analyses main at its current tip, restricted to the files the release PR changes that exist on main. SonarQube Cloud ignores sonar.inclusions, so the scope reaches the scanner as a generated copy of the trusted settings whose sonar.exclusions also cover every other file; the checkout itself stays whole. The design is in SPEC-inf-870-scoped-main-baseline.md.

  1. Dispatch the baseline from develop. head_sha is optional here: when given, it must be the release PR's current head.

    gh workflow run sonar-analysis.yml --ref develop -f mode=main-baseline -f pr_number=<release PR>
  2. When that run succeeds, re-analyse the release PR at its head, as for any PR (below).

What the baseline refuses, and why:

  • More than 350,000 code lines. The Compute the baseline scope step estimates every scoped file's code lines at main's tip, which is how SonarQube Cloud bills, and stops the job before the token-holding step. Blank lines, comment-only lines, Markdown, plain JSON and lockfiles don't count. It is a billing ceiling, not a tuning knob: do not raise it to make a release fit.
  • A scope list over 100,000 bytes, or a generated settings file over 4 MiB.
  • An exclusion list that does not leave exactly the scope, checked against SonarQube's own pattern reading before the scanner starts.
  • A path SonarQube could read as more than one file, such as one containing a comma or *. One such path refuses the whole scope: a release file left out would re-count all of its issues as new.
  • A scan that indexes more files than the scope. The scanner is stopped within seconds of preprocessing, before upload, so a scope the scanner did not apply never becomes a whole-repository analysis.
  • main or the release head moving before or during the scan, a PR that is closed, not into main or not from release/* or hotfix/*, and any dispatch from a branch other than develop.

Afterwards, main's own branch quality gate on SonarCloud shows red: it carries every old issue of the files analysed, with no coverage. That is expected and is not the release PR's check; the run reports it as completed-gate-red and succeeds. Its summary also shows main's ncloc as SonarCloud now counts it. An empty scope (no changed file exists on main) skips the scan.

What it needs​

ItemWhereNotes
SONAR_CI_ANALYSIS = enabledRepository variableAny other value, or none, skips every job above.
sonar-analysisRepository environmentDeployment branches: exactly develop.
SONAR_ANALYSIS_TOKENSecret of the sonar-analysis environment onlyA scoped organisation token limited to this project, never a repository secret and never the repository's SONAR_TOKEN. Rotation: secret-rotation runbook.
Automatic Analysis OFFSonarCloud → Administration → Analysis MethodWith it on, every CI analysis fails.

The token is not a JWT, so the CI credential expiry check cannot watch it.

Token expiry: 2027-01-26 (recorded 2026-10-28). Renew it before that date: nothing alerts on it. Whoever rotates the token updates this date.

Switching it off​

Set SONAR_CI_ANALYSIS to anything other than enabled. Every job that has not started yet skips, including an analysis whose resolve already ran. An analysis job that has already started runs to the end, scan included: the switch is checked when the job starts, not before the scanner. Re-enabling Automatic Analysis is a separate SonarCloud setting and is not needed for rollback.

A PR with no SonarCloud result​

Open Actions → Sonar · Analysis and find the run for the PR's last CI run. The Sonar · Resolve job summary names why it skipped:

  • a head that moved on: expected; the newer head's own CI run analyses it;
  • a draft: mark the PR ready for review, which runs CI and then the analysis, or dispatch it (below);
  • a fork PR: not supported. Fork PRs are never analysed, since the analysis job holds the token;
  • Test · Coverage failed or was cancelled: gh run rerun <run-id> --failed on the CI run re-runs it, and the analysis starts again when CI completes;
  • Test · Coverage skipped: --failed does not re-run a skipped job, so re-run the whole CI run (gh run rerun <run-id>);
  • Test · Coverage absent, as on a CI run whose workflow predates the job: a re-run keeps that run's definition, so start a fresh CI run with a push or another PR event;
  • the analysis job failed: read its log. A runner shutdown signal is a spot reclaim, and gh run rerun <run-id> --failed recovers it.

To analyse a head again without pushing, dispatch it from develop. It needs a successful Test · Coverage run for that head whose sonar-coverage artifact has not expired; CI keeps it for 3 days, so for an older head re-run CI first:

gh workflow run sonar-analysis.yml --ref develop -f mode=pull-request -f pr_number=<N> -f head_sha=<full head SHA>

mode defaults to pull-request, and head_sha is still required in that mode, although the form no longer marks it required because a main-baseline dispatch can omit it.

Measured on enablement (2026-10-28)​

JobDurationTimeout
Test · Coverage18.6 and 20.0 minutes, before the scripts/ suite was added (about 13 minutes more locally)75 minutes
Sonar · Analysisabout 9 minutes, of which the scan about 7.5120 minutes

The scanner runs with an 8 GB Java heap and an 8,192 MB SonarJS Node heap on a 16 GB runner.