Getting SonarQube Community Edition to Comment on GitHub PRs

SonarQube's free Community Build cannot decorate pull requests — no inline comments, no Quality Gate summary on the PR. That's officially gated to Developer Edition and above (paid). There's an unofficial, community-maintained plugin that unlocks it anyway: mc1arke/sonarqube-community-branch-plugin. Not supported by SonarSource, no upgrade path to a commercial edition afterward if you go this route — but it works.

(This runs inside a self-hosted OneDev CI pipeline in my case — see how I set that up without Docker if that part's relevant to you too.)

Installing the plugin

Plugin version must match the SonarQube minor version exactly (a 26.4.0 plugin release for a 26.4.0.xxxxxx server, etc.) — check the plugin's GitHub releases page for the matching build.

  1. Back up before touching anything: conf/sonar.properties → a copy, and the whole web/ directory → a copy.
  2. Download from the release: the plugin jar and the sonarqube-webapp.zip.
  3. Copy the jar into extensions/plugins/.
  4. Replace the entire web/ directory contents with the extracted sonarqube-webapp.zip — the plugin ships a patched frontend, this isn't optional.
  5. Add to conf/sonar.properties (needs a full restart, not just the API restart endpoint):
sonar.web.javaAdditionalOpts=-javaagent:./extensions/plugins/sonarqube-community-branch-plugin-<version>.jar=web
sonar.ce.javaAdditionalOpts=-javaagent:./extensions/plugins/sonarqube-community-branch-plugin-<version>.jar=ce
  1. Restart. Verify in logs/web.log:
INFO web[][o.s.c.e.CoreExtensionsLoader] Loaded core extensions: Community Branch Plugin

Only touch web/ while SonarQube is stopped — overwriting it under a running process is asking for trouble. Rollback is just restoring the two backups and deleting the plugin jar.

GitHub wiring is a GitHub App, not a token

I assumed GitHub worked like GitLab/Bitbucket/Azure DevOps, which authenticate PR decoration with a personal access token. It doesn't — SonarQube's "DevOps Platform Integrations → GitHub" screen only has GitHub App fields (App ID, Client ID, Client Secret, Private Key), no PAT field anywhere, and the community plugin reuses that exact flow.

Creating the App (GitHub → Settings → Developer settings → GitHub Apps → New GitHub App):

  • Homepage URL can be anything, e.g. the SonarQube URL.
  • Uncheck "Active" on the webhook — not needed for basic PR decoration. SonarQube pushes results out from its own compute-engine process; it doesn't need GitHub calling back in.
  • Repository permissions, minimal set: Checks (Read & write), Pull requests (Read & write), Contents (Read-only, needed for private repos), Metadata (Read-only, auto-set). Skip Org/Account permissions unless you're also doing SSO/provisioning through the same App.
  • Install the app on the target repo(s) — creating the App is not the same step as installing it, more on that below.
  • Generate a Private Key (.pem), note the App ID, generate a Client Secret.

Wiring into SonarQube: Admin → Configuration → General → DevOps Platform Integrations → GitHub → Add configuration (API URL https://api.github.com/ for github.com, App ID, Client ID, Client Secret, the full .pem contents). Then per-project: Project Settings → General Settings → DevOps Platform Integration → select the config → set the repository identifier as owner/repo.

Scanner params for a PR run:

sonar-scanner \
  -Dsonar.pullrequest.key=<PR_NUMBER> \
  -Dsonar.pullrequest.branch=<HEAD_BRANCH> \
  -Dsonar.pullrequest.base=<BASE_BRANCH> \
  -Dsonar.token=<project token>

A plain feature-branch scan uses -Dsonar.branch.name=<branch> instead. GitHub Actions with sonarsource/sonarqube-scan-action on a pull_request trigger usually auto-detects these for you.

One more requirement that's easy to miss: the SonarQube server itself needs outbound internet access to api.github.com to post the decoration. This is server-side, not CI-side — a fully offline/localhost-only box will silently fail decoration even with everything else configured correctly.

The real gotcha: "status=SUCCESS" lies

This is the part that cost the most time. The compute-engine log line Pull Request Decoration | status=SUCCESS in ce.log does not mean anything was actually posted to GitHub. It just means the internal step didn't throw an exception. If the SonarQube project was never actually bound to the DevOps Platform config, the step silently no-ops and still logs SUCCESS.

The tell is the time= value on that same log line:

  • time=8ms → no-op, nothing happened (no real HTTP round-trip completes that fast)
  • time=5000-6000ms → a real network call to GitHub actually happened

Two separate things both have to be true, and misconfiguring either is completely invisible from the scan output:

  1. The project is bound to the DevOps Platform config in SonarQube (Project Settings → DevOps Platform Integration, or the api/alm_settings/set_github_binding endpoint).
  2. The GitHub App is actually installed on that specific repo — Settings → Developer settings → GitHub Apps → your App → Install App → select the repo. Creating/configuring the App is not the same as installing it: a freshly created App has zero installations until you do this explicitly. And a brand-new App doesn't inherit installations from an old one, even if it's meant to replace it — they're fully separate identities.

Fast verification — skip the rescan

Don't re-run a full PR scan just to check whether the GitHub wiring works. Client-side indexing takes the same time regardless of how small the PR diff is (more on that below), so using a rescan to test config is 10-30+ minutes wasted per attempt. Use these instead — near-instant:

# 1. Is the App's own identity (ID/secret/private key) valid?
curl -u "$SONAR_TOKEN:" "http://localhost:9000/api/alm_settings/validate?key=<config-name>"
# HTTP 204 = the App identity itself authenticates fine

# 2. Is THIS project's binding actually reachable on GitHub — the real test
curl -u "$SONAR_TOKEN:" "http://localhost:9000/api/alm_settings/validate_binding?project=<projectKey>"
# HTTP 200 = fully wired, will decorate
# HTTP 400 "Could not create Github client - .../installation ... 404" = App not installed on this repo

validate_binding's 404 is the single most useful diagnostic in this whole setup. It tells you definitively whether the App is installed on that repo, without touching GitHub's UI or trusting the scanner log at all.

One more one-way trap: api/alm_settings/update_github lets you swap credentials on an existing config, but SonarQube never exposes the clientSecret/privateKey back out via the list-definitions endpoint — write-only by design. Overwrite a config with new credentials and the old secret/key are gone unless you'd saved the original .pem yourself. You can still regenerate a new secret/key on the *original* GitHub App (its App ID/Client ID stay the same) — you just can't recover the old secret's value.

Scan timing reality check

Language indexing (at least for PHP) is not scoped to the diff — it re-parses the whole codebase every time, because cross-file symbol resolution needs the full picture, whether it's a baseline scan or a one-line PR. On a few-thousand-file codebase this took 15-30+ minutes client-side, every single time. Only the server-side comparison step is fast once a baseline exists (tens of seconds). Budget CI time for this as a real per-PR cost once it's wired into your pipeline, not a one-time setup cost.

If this becomes a real team gate, size for it properly

Community Edition running as an eval on a laptop and Community Edition running as an actual team gate across many repos want very different specs:

  • 16GB RAM / 8 cores minimum if multiple PRs might scan concurrently — more cores matter more than clock speed here, since the compute engine runs concurrent workers.
  • Move off the embedded H2 database to PostgreSQL — H2 is explicitly eval-only per SonarQube's own config comments.
  • Elasticsearch heap = 50% of RAM, capped at 32GB even with more available (the other half is intentionally left for OS disk cache).
  • SSD, 100GB+ disk, keep at least 10% free — Elasticsearch refuses to run past a 90% disk watermark.
  • On Linux (recommended over Windows for a real deployment): set vm.max_map_count=262144 at the OS level, or Elasticsearch won't start. This is invisible on Windows and will bite on the first Linux deploy if forgotten.
  • Run the scanner client and the SonarQube server on separate hosts if you can — running both on one machine measurably slows indexing down.

Source

Subscribe to Building software. Writing what I learn.

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
[email protected]
Subscribe