sonarqube-setup
Wires the project to a SonarQube server: the scanner in the build file, and a CI step or a local container.
sonarqube-setup, section by section
The sections of the skill's own SKILL.md, excerpted. Hover or click one to see which project rules, templates and references/ files it cites. The sections are also chips: click one to jump to it.
## Why this is a skill (Form 1) and not Form 2
Form 1: project-bootstrap and arch-adopt chain it by name, and a skill with disable-model-invocation: true cannot be called through the Skill tool — the same reason git-publish and docker-architect stay model-invocable. Axis 2 of the interview — two callers, one of which runs inside a project where project-bootstrap does not exist — is what made it a skill of its own instead of a step inside the bootstrap. The closest rejected form was that step: arch-adopt would then have to write pom.xml and docker-compose.yml itself, widening its territory and giving the compose file a second owner. Record: @.claude/decisions/0091-sonarqube-setup-skill.md.
Runs on sonnet with effort: low: a fixed procedure behind one question, every version resolved by curl (@.claude/decisions/0080-procedural-skills-on-sonnet.md).
## Procedure
Project directory. When the caller's one-line context names project: <absolute path>, that directory is the project root, not the session's: read and write every path of this procedure under it, and run every shell command as (cd "<path>" && …). When this skill chains docker-architect, pass project: <path> on in its context.
### 1 · Detect the build tool and whether Sonar is already there
pom.xml at the root → maven. build.gradle at the root → gradle. Neither → stop: there is no project to configure. Both → stop and say so; this repo never generates both.
Grep the root build file for sonar-maven-plugin / org.sonarqube. Found → entry rule above: report and stop.
### 2 · Ask about the server — one `AskUserQuestion`
Two questions in the same call:
- Server — "Is there already a SonarQube server or SonarCloud organization for this
- Authentication — "How does the analysis authenticate?"
When a caller already passed an answer in its one-line context, don't ask it again.
### 3 · Write the build configuration
Resolve the scanner version on the repository itself — never from memory, same rule project-bootstrap/references/build-maven.md § 4.6 applies to every plugin:
# maven
curl -sS 'https://repo1.maven.org/maven2/org/sonarsource/scanner/maven/sonar-maven-plugin/maven-metadata.xml' | grep -o '<release>[^<]*</release>'
# gradle — the plugin lives on the Gradle Plugin Portal, not on Maven Central
curl -sS 'https://plugins.gradle.org/m2/org/sonarqube/org.sonarqube.gradle.plugin/maven-metadata.xml' | grep -o '<release>[^<]*</release>'
No network → don't write a version: say which one could not be resolved and let the user supply it.
Then merge — never replace what the root build file already has:
| Tool | Exemplar | Where |
|---|---|---|
| maven | templates/pom-sonar.xml.example | properties into the root POM's <properties>, the plugin into <build><pluginManagement><plugins> |
| gradle | templates/build-gradle-sonar.example | the plugin into the root plugins {}, the sonar {} block at the root level — not inside subprojects |
sonar.projectKey is <groupId>:<artifactId> from the root build file. sonar.host.url is the URL from step 2, or http://localhost:9000 for the local container. sonar.organization only for SonarCloud. The coverage report path needs no property: the scanner's default report paths are exactly where the JaCoCo setup project-bootstrap writes puts them (target/site/jacoco/jacoco.xml, build/reports/jacoco/test/jacocoTestReport.xml).
… cut here, the rest is in the file
### 4 · Local server — only when step 2 answered *None*
Invoke docker-architect through the Skill tool with a one-line context: "service sonarqube, chained from sonarqube-setup — no engine question". It owns every service block in docker-compose.yml, and its template for this one is templates/sonarqube-service.yml.example there. Don't write the block here, not even when it looks like a copy: that is the ownership split, not a style choice.
No docker-compose.yml at the root → docker-architect stops on its own entry rule. Report that the build points at http://localhost:9000 and nothing runs there yet.
### 5 · CI — only when the server is external
A local container is unreachable from a CI runner, so this step runs only for an existing server or SonarCloud. Merge templates/ci-sonar-step.yml.example into .github/workflows/build.yml (the file project-bootstrap step 6.5 writes): job-level env, fetch-depth: 0 on the checkout, and the analysis step after verify. The step is skipped while the SONAR_TOKEN secret is absent — a fork's pull request has none, and a red build there would say nothing about the code.
No .github/workflows/build.yml → skip, and say it in the report.
## Report
✅ SonarQube configured — <existing server <url> | SonarCloud <org> | local container>
Build ....... <pom.xml | build.gradle> — scanner <version> (resolved from <repository>)
Project key . <groupId:artifactId>
Coverage .... sonar.coverage.exclusions = <patterns> (from the JaCoCo excludes in <file>:<line>) | not set — no JaCoCo excludes in <file>
Server ...... <url> · auth: <SONAR_TOKEN | anonymous>
Compose ..... <sonarqube added by docker-architect, tag <tag> | not touched — external server>
CI .......... <step added, runs when secret SONAR_TOKEN is set | skipped — local server | skipped — no workflow>
Access check (before any scan):
curl -s <url>/api/authentication/validate → {"valid":true} when anonymous is allowed
(or SONAR_TOKEN is exported — the scanner reads it)
First run: <./mvnw -B verify sonar:sonar | ./gradlew build sonar>
Rerun: <./mvnw -B sonar:sonar | ./gradlew sonar> — same build output, no tests again
The access check moves an authentication failure from the end of the scan to its start: validate answers in milliseconds, the scan fails only after verify has run every test. The rerun line is for a change on the server side — a setting, a permission, a quality profile — where the code and target/ (or build/) are unchanged.
… cut here, the rest is in the file
What it reads, writes and refuses
The skill's own contract, from SKILL.md, in the original wording.
Class
build — the territory is skill_classes.build's override for this skill in @.claude/schemas/extensions.json: the root build file and .github/workflows/**, nothing else. ArchHook.java guard enforces it. docker-compose.yml is deliberately outside it.
Reads
the root pom.xml or build.gradle (coordinates, existing plugins, the JaCoCo excludes that sonar.coverage.exclusions is derived from), and .github/workflows/build.yml when it exists.
Writes
the scanner plugin and the sonar.* properties into the root build file — the only change to that file outside a feature round besides project-bootstrap's generation and the two installers' own setup — and the analysis step into the workflow when the server is external.
Never writes
a token, a password, or any credential value, in any file.
Delegates
the sonarqube compose service to docker-architect, the single owner of every service block.
Chained by
project-bootstrap (step 8.4, after Verify) and arch-adopt (step 7), and invocable by hand. Travels into the generated project (export.skills.include): arch-adopt runs inside the project, where project-bootstrap does not exist, and needs this skill there.
Source: SKILL.md § Contract, at nerviz e4cae27.
What the guard does with it
build is the skill's class, and each skill of it carries its own territory. Who calls it decides which territory applies: under project-initializer the agent's, from the main thread no call at all. The text on the right is the hook's own.
write_allow of class sonarqube-setup
- ✓
pom.xml - ✓
build.gradle - ✓
.github/workflows/** any path outside this list, when the skill opens the phase itselfa call from the main thread while /init-project is open
A refusal exits with code 2 and the message goes back to the model.
Chained by project-initializer, its writes carry the agent's agent_type and are judged by agent_classes.driver, **. This list applies when the skill runs on a prompt of its own.
PreToolUse Skill(sonarqube-setup) # agent_type: project-initializer (class driver) guard call exit 0 # a classed agent's call leaves the phase as it was PreToolUse Write(../bank-app/pom.xml) guard write exit 0 # judged by agent_classes.driver, write_allow: **
# phase open: init-project (class orchestrator) PreToolUse Skill(sonarqube-setup) guard call exit 2 ❌ `sonarqube-setup` is class `build` and `init-project` (class `orchestrator`) is open — a design run does not materialize files. Record what is missing in the partial and in the consolidated spec, finish the run, then invoke `/sonarqube-setup` from a prompt of its own.
# in the generated project, on a prompt of its own > /sonarqube-setup … UserPromptSubmit audit prompt exit 0 guard prompt exit 0 # phase opened: sonarqube-setup (class build) PreToolUse Write(src/main/resources/application.yml) guard write exit 2 ❌ `sonarqube-setup` is class `build` — src/main/resources/application.yml is outside its territory. write_allow: pom.xml, build.gradle, .github/workflows/** Put the content in the spec as a code block, or finish the run and invoke the skill that owns this path. If the path is legitimately this skill's, widen skill_classes.build in .claude/schemas/extensions.json — retrying will not help.
Where it sits on the rail
Each card is a step of /init-project. The interview runs in the main session; the tinted steps run inside project-initializer; git-publish comes back to the main session, after audit genesis. Dashed steps run only when they apply.