SonarQube locally¶
The local SonarQube Community Build rates product code before the push with the same
profile and gate as SonarQube Cloud (ADR 0040). It runs from
tools/sonarqube/compose.yaml: web interface only on 127.0.0.1, PostgreSQL only in
an internal network without a host port.
Prerequisites¶
- A Docker-compatible runtime with Compose.
- Memory for the container VM according to the vendor's figures: the Community Build names 4 GB for up to one million lines (https://docs.sonarsource.com/sonarqube-community-build/server-installation/server-host-requirements/, retrieved 2026-10-10) [verified].
- The port
quality.sonar.local_port(environment variableSONARQUBE_PORT) is free on the host. If it is taken, setSONARQUBE_PORTand chooseSONAR_HOST_URLto match. The examples below use the default port.
1. Start¶
make sonar-up
On the first run, the target creates tools/sonarqube/.env with a random
database password (file permissions for the owner only, git-ignored), starts both
containers and waits until api/system/status reports the status UP, at most
quality.sonar.startup_timeout_s. The data is in named volumes; make sonar-down
stops without deleting them.
The built-in search engine otherwise requires kernel settings of the host
(vm.max_map_count); for local single use, the Compose file switches off this
startup check, as the vendor's quick start guide does
(https://docs.sonarsource.com/sonarqube-community-build/try-out-sonarqube/, retrieved
2026-10-10) [verified].
2. Change the admin password¶
Open http://127.0.0.1:9000, log in with admin/admin (vendor default, same
source) and change the password right away. The new password belongs in the password manager,
never in a file in the repository.
3. Create tokens without displaying them¶
Two tokens with separate permissions:
| Token | Type | For what |
|---|---|---|
| Configuration | USER_TOKEN of the admin, with expiry date |
once for make sonar-config, revoke afterwards |
| Analysis | GLOBAL_ANALYSIS_TOKEN |
make sonar and make check |
The types are named by the web API (api/user_tokens/generate, parameter type) [verified on the
vendor's public server, https://next.sonarqube.com/sonarqube/api/webservices/list,
retrieved 2026-10-10]. The token goes directly into a file outside the repository and
never appears in the terminal:
read -rs -p "SonarQube admin password: " SQ_ADMIN; echo
umask 077; mkdir -p ~/.config/bothy
curl -fsS -u "admin:${SQ_ADMIN}" -X POST "http://127.0.0.1:9000/api/user_tokens/generate" \
--data-urlencode "name=bothy-analysis" --data-urlencode "type=GLOBAL_ANALYSIS_TOKEN" \
| python3 -c 'import json, sys; print(json.load(sys.stdin)["token"])' > ~/.config/bothy/sonar-token
unset SQ_ADMIN
For the configuration, the same with type=USER_TOKEN, an expirationDate and a
separate file. In the shell that calls make:
export SONAR_TOKEN="$(cat ~/.config/bothy/sonar-token)"
Instead of a file, the keychain of the operating system also works.
4. Import the profile and gate¶
SONAR_TOKEN="$(cat ~/.config/bothy/sonar-config-token)" make sonar-config
SONAR_TOKEN="$(cat ~/.config/bothy/sonar-token)" make sonar-config-check
sonar-config creates the gate "Bothy way" (or repairs it) and makes it the
default, creates the profile "Bothy way" per language, sets the parent profile, imports the
additional rules from quality/sonar/quality-profile-<language>.xml and makes the profile
the default. Languages that the Community Build does not know (Dart, Swift) are
skipped and named. Afterwards, revoke the configuration token.
If the parent profile is missing (built-in profile names can change with a new
SonarQube version), sonar-config stops. Then check the built-in profiles,
adjust quality/sonar/profiles.json by pull request and import again.
5. Analyze¶
make sonar # or make check
The scanner (@sonar/scan, without a local Java installation) reads
sonar-project.properties and waits for the gate result; a red gate makes
make sonar fail. As long as there is no product code, the target skips.
6. Alignment with SonarQube Cloud¶
In the public state (public-switch.md), against the cloud:
SONAR_HOST_URL=https://sonarcloud.io SONAR_ORGANIZATION="<organization>" SONAR_PROJECT_KEY="<project-key>" \
SONAR_TOKEN="<token from the password manager>" make sonar-config # once
SONAR_HOST_URL=https://sonarcloud.io SONAR_ORGANIZATION="<organization>" SONAR_PROJECT_KEY="<project-key>" \
SONAR_TOKEN="<token>" make sonar-config-check
The analysis of main (ci.yml on every push to main) needs the main branch of the cloud
project to be named main. Until it is, the scanner reports Branch name: main, type: short
and the cloud fails the analysis ("CE Task finished abnormally with status: FAILED"). The job
sonar therefore runs on main only when the repository variable SONAR_MAIN_ANALYSIS is
true. Once: make sonar-main-branch (same variables as above) renames the main branch of the
project to main through the Web API (api/project_branches/list, then
api/project_branches/rename only when it carries another name, after deleting a short-lived
branch main that the failed analyses left, because the rename refuses a name in use); then
gh variable set SONAR_MAIN_ANALYSIS --body true. Pull requests are analysed either way.
The analysis of main also needs the project to be public: the free plan caps the lines
of code of a private project and refuses a larger analysis ("maximum allowed lines limit"),
a public one has no such cap. make sonar-visibility (same variables) sets the visibility to
public through api/projects/update_visibility when api/projects/search reports it private.
The gate of main needs a new code definition: without one of the project's own the cloud falls
back to the previous version, which needs a sonar.projectVersion the repository does not send,
and the gate has no status. make sonar-new-code (same variables) sets the last 30 days through
api/settings/set (sonar.leak.period, then sonar.leak.period.type; the cloud has no
api/new_code_periods) when api/settings/values shows no value of the project's own. The scan
of main does not wait for the gate (SONAR_QUALITYGATE_WAIT=false in ci.yml): it records the
overall state and the baseline and must not block main; pull requests wait and fail with it.
Where the token exists only as the GitHub Actions secret SONAR_TOKEN, the manual workflow
.github/workflows/sonar-config.yml runs these targets against the cloud
(gh workflow run sonar-config.yml --ref main, variable SONAR_ORGANIZATION). The cloud
addresses the gate by its id in create_condition and set_as_default
(https://sonarcloud.io/api/webservices/list, retrieved 2026-10-13) [verified], the Community
Build by its name, and it has no built-in profile "Sonar way": there the profiles inherit from
cloud_parent of quality/sonar/profiles.json (ADR 0040). quality_sync.py chooses both by
--organization.
SonarQube Cloud makes a custom quality gate or quality profile the organization default only
on plans that include it; otherwise api/qualitygates/set_as_default answers HTTP 403 and
api/qualityprofiles/set_default HTTP 400 ("Organization ... is not allowed to perform this
action"). In cloud mode quality_sync.py therefore never makes the profiles the organization
default. It assigns each profile to the project (api/qualityprofiles/add_project) and the
gate as well (api/qualitygates/select); the project key comes from --project or
SONAR_PROJECT_KEY (the workflow takes the repository variable of that name), and cloud mode
stops without it. It still tries to make the gate the organization default.
A call the plan refuses (HTTP 400 or 403 with "Organization ... is not allowed to ...", such as
"... to perform this action" or "... to modify Quality gates") is a named gap, not a failure:
apply prints it with the server's reason and continues with the next language; it selects
the gate after the profiles. Any other refusal, such as HTTP 403
"Insufficient privileges" for a token without the permission, still stops apply. Examples:
quality_sync: gap: profile go: not assigned to project: plan does not allow it (...),
quality_sync: gap: gate not assigned to project: plan does not allow it (...) and
quality_sync: gap: gate not default: plan does not allow it. check reads what the project
uses (api/qualityprofiles/search?project=..., api/qualitygates/get_by_project): a
built-in profile or gate there is reported as the same gap, any other profile or gate fails
the check. The gate "Bothy way", its conditions and the profiles are still compared; every
other difference (a condition, a parent, an activated rule) still fails the check, and the
exit code is 0 when only gaps remain. On the Community Build nothing is assigned to a project:
the gate and the profiles become the defaults, a refused set_as_default stops apply, and a
gate that is not the default fails check.
Both checks (local and cloud) pass against the same files, so the same rules
apply. What is compared is the addition on top of the parent profile and every gate condition. The
built-in parent profile itself can differ between the Community Build (monthly releases)
and the cloud (continuous); that is the known residual deviation. When to check:
after every update of the local image, after every change under quality/sonar/ and
monthly.
Add a rule¶
- Name the rule key and the rationale in the pull request (which error, which occurrence).
- Enter the rule in
quality/sonar/quality-profile-<language>.xmlunder<rules>(format of the SonarQube backup file:repositoryKey,key, optionallypriorityandparameters). make sonar-configlocally, after the merge against the cloud;sonar-config-checkin both places.