この記事の対象と、書いた理由
Vue3 + Django REST Framework の構成でアプリを作る案件によく入るのですが、
静的解析は ESLint / ruff で止まっていて、SonarQube は名前しか知らない状態でした。
先行記事を読むと、次のところまでは分かります。
-
SonarQube をオンボーディングする(Zenn)
…docker runでの起動、GitHub App 連携、Secrets 名(SONAR_TOKEN/SONAR_HOST_URL) -
SonarQube とは(Qiita)
… 製品ラインナップ、Community Build / Developer / Enterprise の機能差、日本語化プラグイン
一方で、自分のプロジェクトに当てはめるときに必要な情報が見つかりませんでした。
-
sonar-project.propertiesに Vue3 / DRF それぞれ何を書くのか -
vitestとcoverage.pyのカバレッジをどう食わせるのか - 出てきた指摘をどう読み、Quality Gate をどう運用するのか
そこで、ホストに Node / Python / Java / sonar-scanner を一切入れず、
Docker だけで検証環境を作って一通り動かしました。この記事はその一次情報です。
数値・ログ・エンドポイントはすべて実際に動かした結果を載せています。
検証環境: SonarQube Community Build 26.9.0.129388 / Docker 28.2.2 / Compose v2.37.1 / macOS (x86_64)
ソースコード
前提:SonarQube とは何をするツールか
一言でいうと 「コードを読んで問題を指摘し、品質を数値にしてサーバーに溜めるツール」 です。
ESLint / ruff と比べると役割の違いが見えます。
| ESLint / ruff | SonarQube | |
|---|---|---|
| 動く場所 | 手元(保存時・コミット時) | サーバー(解析結果を送信して蓄積) |
| 見るもの | 文法・スタイル・一部のバグ | 左記+カバレッジ・重複・複雑度・セキュリティ設定 |
| 結果の形 | その場のエラー一覧 | 時系列のグラフ、プロジェクト横断の比較 |
| 強み | 即時、自動修正できる | 推移が見える、CI で合否判定、チームで共有 |
| 弱み | 履歴が残らない | 即時性がない、サーバーが必要 |
置き換えではなく併用です。実際この検証でも、v-html の XSS は SonarQube では
拾えず ESLint の担当でした(後編で詳述)。逆に「認知的複雑度が 28 で上限 15 を超えている」
「重複が 10.9%」のような定量化と推移の可視化は SonarQube の得意分野です。
覚えておく用語はこれだけ
Issue(指摘) は 3 種類に分かれます。
| 種別 | 意味 | 今回出た例 |
|---|---|---|
| Bug | 動作が壊れる・意図通りでない |
return 後の到達不能コード、自己代入 |
| Vulnerability | セキュリティ上の問題 | MD5 でハッシュ化、SECRET_KEY のハードコード |
| Code Smell | 動くが保守しづらい | 複雑度が高い、TODO の放置、コピペ関数 |
Rating は A〜E の 5 段階評価で、上の 3 種類にそれぞれ対応します
(Reliability ← Bug、Security ← Vulnerability、Maintainability ← Code Smell)。
Quality Gate は合否判定のルールセットで、CI を止める根拠になります。
New Code は「最近書いたコード」。SonarQube の設計思想は Clean as You Code
(既存の負債は据え置き、これから書く分だけきれいに保つ)で、
既定の判定条件は全部この New Code 側を見ています。 ここが後編の山場になります。
結論(先に成果)
同一リポジトリに 2 プロジェクトを登録し、片方は指摘を修正、もう片方は未修正のまま
並べました。最終的にこうなりました。
| backend (DRF・修正後) | frontend (Vue3・未修正) | |
|---|---|---|
| Quality Gate | OK | ERROR |
| Bugs | 0 | 3 |
| Vulnerabilities | 0 | 1 |
| Code Smells | 0 | 22 |
| Coverage | 78.9% | 52.0% |
| Duplications | 0.0% | 10.9% |
| Reliability / Security | A / A | C / C |
そして、つまずいた点が 5 つありました。ここが本題です。
| # | つまずき | 扱う記事 |
|---|---|---|
| 1 | ディスク使用率 95% で SonarQube が起動しない(Elasticsearch の watermark) | 前編(この記事) |
| 2 | パスワード変更 API のパスが先行記事と違う(/api/users/change_password) |
前編 |
| 3 |
カバレッジが 0% になる罠(レポートのパスと projectBaseDir の不一致) |
前編 |
| 4 | 初回スキャンでは Quality Gate が必ず素通りする(条件が全部 New Code 側) | 後編 |
| 5 | 仕込んだのに検出されない指摘がある(v-html、console.log、eval など) |
後編 |
この記事の構成
前編(この記事)で「動いて数字が出る状態」まで到達します。
必要なところだけ読んでも分かるように書いています。
| 章 | 内容 |
|---|---|
| 1 |
環境構築 — Compose 全文、PostgreSQL が必要な理由、scanner / api の役割 |
| 2 | 初期セットアップの自動化 — パスワード変更 API のパスに注意 |
| 3 |
解析設定 — Vue3 / DRF それぞれの sonar-project.properties、スキャンの中身 |
| 4 | カバレッジ連携 — 0% になる罠、手元の数字と合わない理由 |
後編では「出てきた結果をどう読み、どう運用するか」を扱います。
| 章 | 内容 |
|---|---|
| 5 | 検出結果の一覧 — 実際に何が出たか |
| 6 | 出なかった指摘の原因 — 3 パターンの切り分けと Quality Profile のカスタマイズ |
| 7 | Quality Gate が初回素通りする正体 — New Code とは何か |
| 8 | Fail → 修正 → Pass の実演 — 誤検知(False Positive)の扱いも |
| 9 | GitHub Actions 連携 |
| 10 | 静的解析の深さ — どこまで見るのか、脆弱性検出の限界 |
| 11 | Community Build の線引き — 有償版が必要になる境界 |
S106 のような個別ルールの解説(何を問題としているのか・どう直すのか)は、
分量が多いので別記事「SonarQube ルール早見表(Vue3 + DRF 編)」にまとめています。
1. 環境構築(Docker Compose)
イメージタグは community を明示する
先行記事では sonarqube:latest を使っていますが、今は latest を避けた方が安全です。
調べた時点で latest と community は同じイメージ(26.9.0.129388-community、LGPL v3)を
指していましたが、バージョン体系が分岐しています。
- Community Build(無償):
26.x系 →sonarqube:community - 商用エディション:
2026.x系 →sonarqube:developer/enterprise
将来 latest がどちらを指すか保証がないので、community と書いておきます。
docker-compose.yml
name: qube-test
services:
db:
image: postgres:17-alpine
environment:
POSTGRES_USER: sonar
POSTGRES_PASSWORD: sonar
POSTGRES_DB: sonar
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U sonar -d sonar"]
interval: 10s
timeout: 5s
retries: 5
sonarqube:
image: sonarqube:community
depends_on:
db:
condition: service_healthy
environment:
SONAR_JDBC_URL: jdbc:postgresql://db:5432/sonar
SONAR_JDBC_USERNAME: sonar
SONAR_JDBC_PASSWORD: sonar
SONAR_ES_BOOTSTRAP_CHECKS_DISABLE: "true"
ports:
- "127.0.0.1:9000:9000" # LAN に晒さない
volumes:
- sonarqube_data:/opt/sonarqube/data
- sonarqube_extensions:/opt/sonarqube/extensions
- sonarqube_logs:/opt/sonarqube/logs
- sonarqube_temp:/opt/sonarqube/temp
tmpfs:
- /tmp:rw,noexec,nosuid,size=256m
read_only: true
ulimits:
nofile: { soft: 65536, hard: 65536 }
# 以下は profiles 付き。`up` では起動せず `run --rm` で使い捨て実行する
scanner:
image: sonarsource/sonar-scanner-cli:latest
profiles: ["tools"]
environment:
SONAR_HOST_URL: http://sonarqube:9000
SONAR_TOKEN: ${SONAR_TOKEN:-}
volumes:
- .:/usr/src
- scanner_cache:/opt/sonar-scanner/.sonar/cache
working_dir: /usr/src
frontend:
image: node:22-alpine
profiles: ["tools"]
volumes:
- .:/usr/src
- frontend_node_modules:/usr/src/frontend/node_modules # ホストに出さない
- npm_cache:/root/.npm
working_dir: /usr/src/frontend
ポイントは 3 つです。
-
read_only: true+tmpfsで書き込み先を限定(公式の Compose 例に準拠) - 公開ポートを
127.0.0.1に固定 -
全サービスでリポジトリルートを
/usr/srcにマウント(後述のカバレッジ問題対策)
Q. なぜ PostgreSQL が必要なのか(docker run 単発では駄目なのか)
先行記事のように docker run だけで起動すると、組み込みの H2 データベースが使われます。
動くことは動きますが、公式が「評価目的のみ」としています。
SonarQube は 2 種類のデータストアを使います。
| データストア | 何を持つか | どこにあるか |
|---|---|---|
| リレーショナル DB | プロジェクト、指摘の一覧、メトリクス、解析履歴、ユーザー、Quality Gate/Profile の設定 | 外部に用意が必要 |
| Elasticsearch | 検索・集計用インデックス | コンテナに同梱(用意不要) |
前述のディスク不足で落ちたのが Elasticsearch だったのは、後者がコンテナ内で動いているからです。
H2 で運用した場合の問題はこれです。
- バージョンアップ時にデータを引き継げない — 解析履歴が全部消える
- 性能が出ない(プロジェクトが増えると顕著)
「とりあえず触る」なら H2 で十分。「社内に置いて履歴を溜める」なら PostgreSQL が必須です。
繋ぎ方は環境変数 3 つだけで、対応 DB は PostgreSQL / Oracle / SQL Server です
(MySQL は廃止済み)。
ボリューム 5 つの役割も押さえておくと、どれを消すと何を失うか分かります。
| ボリューム | 中身 | 消すと失うもの |
|---|---|---|
postgres_data |
解析結果・履歴・全設定 | 全部(これが本体) |
sonarqube_data |
Elasticsearch のインデックス | 再構築される |
sonarqube_extensions |
プラグイン(日本語 Pack を入れるならここ) | プラグイン |
sonarqube_logs |
ログ | ログ |
sonarqube_temp |
一時ファイル | なし |
Q. scanner と api は何をするコンテナなのか
ここは SonarQube の構造理解に直結します。
SonarQube は 「サーバー」と「スキャナ」に分かれています。
ソースコード
│ 読む
▼
┌──────────────┐ 解析結果を送信 ┌──────────────┐ ┌──────┐
│ scanner │ ─────────────────> │ sonarqube │ ─────> │ db │
│ (解析する) │ HTTP + Token │ (保存・表示) │ └──────┘
└──────────────┘ └──────────────┘
解析処理はスキャナ側で走ります。サーバーはソースコードを解析しません。
これが分かると CI 連携が繋がります。GitHub Actions で sonarqube-scan-action を
動かすのは、CI ランナー上でスキャナを実行してサーバーに送っているだけで、
ローカルの make scan と同じことです。
sonar-scanner-cli は Java 製です。コンテナにする最大の理由がこれで、
ホストに JDK を入れずに済みます。
一方 api(curl コンテナ)は SonarQube の仕組みとは無関係で、こちらの便宜のための箱です。
Web API を叩くのに使っており、ホストに curl があるかどうかにも依存させないためのものです。
サービスは 2 種類に分かれています。
| サービス | 役割 | 種類 |
|---|---|---|
db / sonarqube
|
サーバーとして動き続ける | 常駐 |
scanner / backend / frontend / api
|
コマンドを 1 回実行して消える | 使い捨て(profiles: ["tools"]) |
profiles: ["tools"] が付いていると docker compose up では起動しません。
docker compose run --rm scanner ... と明示的に呼んだときだけ動きます。
SonarQube はサーバーだが、スキャナやテスト実行はコマンドだからです。
後片付けの注意:
profiles付きサービスのボリュームはdocker compose down -vの
対象外です。node_modulesや scanner キャッシュまで消すには
docker compose --profile tools down -vが必要です。
つまずき① ディスク 95% で起動しない
起動したのにコンテナが Exited (0) になりました。ログを追うとこれです。
flood stage disk watermark [95%] exceeded on [.../opt/sonarqube/data/es9]
free: 9.3gb[4.9%], all indices on this node will be marked read-only
...
Caused by: ElasticsearchException: [es/get] failed:
[no_shard_available_action_exception] No shard available for [get [metadatas][dbVendor]]
ERROR web[][o.s.s.p.Platform] Background initialization failed. Stopping SonarQube
SonarQube の設定ミスではなく、Docker のディスクが埋まっていただけでした。
Elasticsearch は使用率 95% を超えると全インデックスを read-only にするので、
起動時のインデックス作成が失敗します。
docker run --rm alpine df -h /
# overlay 188.7G 170.3G 8.8G 95% / ← これが原因
docker system df
# Images 97.15GB (77.15GB reclaimable)
# Build Cache 27.02GB (27.02GB reclaimable)
私は docker system prune -af で 118.2GB 回収し、95% → 8% になって起動しました。
ただしこれは未使用イメージも全部消えるので、まずは影響の小さい
docker builder prune -af(ビルドキャッシュのみ)から試すのがおすすめです。
SonarQube は最低 15GB 程度の空きを確保してから起動してください。
2. 初期セットアップを API で自動化する
画面で admin / admin でログインするとパスワード変更を求められ、
そのあとトークンを発行してコピーする、という手順になります。
チームに配る手順書としては、ここはスクリプト化したいところです。


つまずき② change_password のパスが違う
まず 404 になりました。
POST /api/authentication/change_password
→ {"errors":[{"msg":"Unknown url : /api/authentication/change_password"}]}
正しくは /api/users/change_password でした。
バージョンで変わるので、実機の Web API 一覧から探すのが確実です。
なお /api/webservices/list は認証必須で、匿名だと空が返ります。
curl -s -u admin:admin 'http://localhost:9000/api/webservices/list' \
| tr '{' '\n' | grep -oE '"path":"[^"]*"|"key":"change_password"' \
| grep -B1 change_password
# "path":"api/users"
# "key":"change_password"
自動化スクリプトの中身
# 1. 初期パスワードの変更(冪等にするため、まず現状を確認する)
# SonarQube は認証失敗でも 200 で {"valid":false} を返す点に注意
curl -s -u "admin:$NEW_PASSWORD" "$SONAR_URL/api/authentication/validate" | grep -q '"valid":true' \
|| curl -s -u "admin:admin" -X POST "$SONAR_URL/api/users/change_password" \
--data-urlencode "login=admin" \
--data-urlencode "previousPassword=admin" \
--data-urlencode "password=$NEW_PASSWORD"
# 2. 解析用トークンの発行
# 同名トークンがあると失敗するので、先に失効させておくと冪等になる
curl -s -u "admin:$NEW_PASSWORD" -X POST "$SONAR_URL/api/user_tokens/revoke" \
--data-urlencode "name=qube-test-analysis"
curl -s -u "admin:$NEW_PASSWORD" -X POST "$SONAR_URL/api/user_tokens/generate" \
--data-urlencode "name=qube-test-analysis" \
--data-urlencode "type=GLOBAL_ANALYSIS_TOKEN"
# 3. プロジェクトの作成
curl -s -u "admin:$NEW_PASSWORD" -X POST "$SONAR_URL/api/projects/create" \
--data-urlencode "project=qube-test-backend" \
--data-urlencode "name=qube-test (backend)"
トークンの type は実機で確認すると 3 種類ありました。
| type | 用途 |
|---|---|
USER_TOKEN |
ユーザーの代理。API 全般に使える |
GLOBAL_ANALYSIS_TOKEN |
解析専用。全プロジェクトを 1 本で解析できる |
PROJECT_ANALYSIS_TOKEN |
解析専用。プロジェクト単位 |
モノレポで 2 プロジェクトを解析するので GLOBAL_ANALYSIS_TOKEN を選びました。
3. 解析設定(sonar-project.properties)
Community Build にはモノレポ機能がありません。1 キーにまとめると言語ごとの
カバレッジや Quality Gate を分けられないので、2 プロジェクトとして登録します。
backend(Django / DRF)
sonar.projectKey=qube-test-backend
sonar.projectName=qube-test (backend / Django REST Framework)
# New Code の基準になる重要な設定。
# 検証用にここでは固定しているが、実務では CI でリリースバージョンを注入する。
# 固定したままだと New Code が永久に空になる(理由は後編で詳説)
sonar.projectVersion=0.1.0
sonar.sources=.
# tests は「テストコード」として登録する(指摘の重み付けが変わる)
sonar.tests=tests
sonar.test.inclusions=tests/**/*.py
# migrations は自動生成なので対象外
sonar.exclusions=tests/**,**/migrations/**,coverage.xml,**/__pycache__/**,db.sqlite3
sonar.python.version=3.13
sonar.python.coverage.reportPaths=coverage.xml
sonar.sourceEncoding=UTF-8
各行の意味
| 設定 | 必須度 | 意味 |
|---|---|---|
projectKey |
必須 | SonarQube 上の一意な ID。これが無いと解析できない |
projectName |
任意 | 画面の表示名。省略すると key がそのまま表示される |
projectVersion |
任意 | New Code の基準になる(後編で詳説) |
sources |
必須 | 解析するディレクトリ |
tests |
任意 | テストコードの場所 |
test.inclusions |
任意 | その中で何をテストとみなすか |
exclusions |
任意 | 解析から外すもの |
python.version |
推奨 | バージョン依存ルールの精度が上がる |
python.coverage.reportPaths |
カバレッジを使うなら必須 | projectBaseDir からの相対パス |
sourceEncoding |
日本語があるなら必須 | 指定しないとコメントが文字化けする |
なお projectKey を指定してスキャンすると、プロジェクトは自動で作られます。
事前に画面や API で作成する必要はありません。
frontend(Vue3 / TypeScript)
sonar.projectKey=qube-test-frontend
sonar.projectName=qube-test (frontend / Vue3)
# .vue も JS/TS アナライザが解析してくれる
sonar.sources=src
# sonar.sources と sonar.tests が同じ src を指すので、
# exclusions と test.inclusions で役割を分ける。
# この分離が無いと「同じファイルが source と test の両方に属する」エラーになる
sonar.tests=src
sonar.test.inclusions=src/**/*.spec.ts
sonar.exclusions=src/**/*.spec.ts,node_modules/**,coverage/**,dist/**
sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.sourceEncoding=UTF-8
frontend はここがトリッキー
Vue や React では、テストがソースと同じディレクトリに同居します
(format.ts と format.spec.ts が隣同士)。Django のように tests/ ディレクトリで
分かれていません。
そのため sources と tests の両方に src を指定し、2 行で役割を切り分けます。
sonar.test.inclusions=src/**/*.spec.ts # 「*.spec.ts はテストです」
sonar.exclusions=src/**/*.spec.ts,... # 「*.spec.ts は本番コードから除外」
この 2 行のどちらかが欠けると、「同じファイルが source と test の両方に属している」
というエラーでスキャンが失敗します。 Vue3 プロジェクトで最初にぶつかる設定ミスです。
node_modules/** の除外も必須です。入れ忘れると数万ファイルを解析しようとして
終わりません。
カバレッジのプロパティ名は言語ごとに違う
| プロパティ | レポート形式 | |
|---|---|---|
| Python | sonar.python.coverage.reportPaths |
Cobertura XML |
| JS / TS | sonar.javascript.lcov.reportPaths |
LCOV |
sonar.typescript.lcov.reportPaths は非推奨です。 TypeScript でも
sonar.javascript.lcov.reportPaths を使います(.vue も同じ)。
スキャンの実行
projectBaseDir を切り替えて 2 回走らせます。
docker compose run --rm scanner sonar-scanner -Dsonar.projectBaseDir=/usr/src/backend
docker compose run --rm scanner sonar-scanner -Dsonar.projectBaseDir=/usr/src/frontend
Q. 「スキャン」は具体的に何をしているのか
入力は 2 種類だけです。
sonar.sources で指定した範囲のファイル(exclusions を引いたもの) ← コード本体
coverage.xml / lcov.info ← テストツールの出力
そして単一の処理ではなく、目的別の「センサー」が順番に走ります。
ログを見ると分かります。
| センサー | 何をしているか |
|---|---|
Python Sensor / JavaScript/TypeScript analysis
|
構文解析してルールに照合。指摘の本体 |
Cobertura Sensor for Python coverage |
coverage.xml を読み込む |
TextAndSecretsSensor |
全ファイルを「ただのテキスト」として走査し、鍵や API トークンのパターンを探す |
IaC Project Sensor |
Dockerfile / docker-compose.yml / Terraform を解析 |
Zero Coverage Sensor |
カバレッジ情報が無かったファイルを「0%」として記録する |
CPD Executor |
トークン列を比較して重複コードを検出 |
TextAndSecretsSensor が独立しているのが重要です。 SECRET_KEY を検出したルールは
secrets:S6687 で、python: ではありません。Python の構文解析ではなく
テキストのパターンマッチなので、.py に限らず .env や .yml も対象になります。
Zero Coverage Sensor は後述の「数字が合わない問題」の原因になります。
送信されるもの
INFO Analysis report generated in 136ms, dir size=348.4 kB
INFO Analysis report compressed in 40ms, zip size=80.0 kB
INFO Analysis report uploaded in 136ms
INFO EXECUTION SUCCESS
EXECUTION SUCCESS は「送信完了」の意味でしかありません。
サーバー側の Compute Engine が非同期で処理してからダッシュボードに反映されます。
INFO Note that you will be able to access the updated dashboard
once the server has processed the submitted analysis report
スキャン直後に画面を見ても数字が変わっていないことがあります。
処理状況は画面の Administration → Background Tasks、または API で確認できます。
curl -s -u "admin:$PASS" "$SONAR_URL/api/ce/activity_status"
# → {"pending":0,"inProgress":0,...} になれば完了
なお、表示用にソースコード自体もサーバーに送られます(Code タブで読めるため)。
社内の SonarQube に解析を投げる=そのサーバーにコードが渡る、という認識は
持っておいた方がいいです。
4. つまずき③ カバレッジが 0% になる罠
前提:カバレッジとは何か
**「テストを実行したとき、ソースコードのうち何割が実際に通ったか」**の割合です。
def calculate_score(todo):
if todo.priority == "high": # ①
return 30 # ②
if todo.priority == "mid": # ③
return 20 # ④
return 10 # ⑤
priority="high" のテストだけ書くと、通るのは ①② だけです。
実行された行 2 / 実行可能な行 5 = カバレッジ 40%
「テストが通っている=正しい」ではなく、「テストが触ってすらいない箇所がどれだけあるか」
を測る指標です。40% なら、残り 60% は壊れていても誰も気づかない状態です。
測り方は 3 種類あり、SonarQube は Line と Branch を合成した独自の値を使います。
| 種類 | 何を見るか |
|---|---|
| Line(行) | その行が実行されたか |
| Branch(分岐) |
if の true 側と false 側の両方を通ったか |
| Function(関数) | その関数が呼ばれたか |
Q. テストを書いていないプロジェクトでは使えないのか
使えます。テストが必要なのはカバレッジだけです。
| 項目 | テストが必要か | 対象範囲 |
|---|---|---|
| 指摘の検出(Bug / Vulnerability / Code Smell) | 不要 |
sonar.sources の全ファイル |
| 重複検出 | 不要 | 同じ |
| 複雑度・行数 | 不要 | 同じ |
| カバレッジ | 必須 | テストが触った範囲+触っていない範囲(0% として) |
検証でも、テストを 1 行も書いていない utils/insecure.ts と utils/legacy.ts から
指摘がちゃんと出ています(S2245、S1135、S4144)。カバレッジだけが 0.0% です。
そして 0% であること自体が有用な情報です。「複雑度 22 なのにテスト 0%」という
組み合わせが見えれば、そこが最も危険な箇所だと判断できます。
カバレッジ連携をしない場合は sonar.*.coverage.reportPaths を書かなければよく、
その際は Quality Gate からカバレッジ条件を外してください。外さないと
「レポートが無い → 0% → 永久に不合格」になります。
導入の順序としては、まず指摘と複雑度だけ見る → テストを書き始めたら
カバレッジを足す、という段階的な入れ方が現実的です。
テストが無いことを理由に導入を諦める必要はありません。
ここからが本題
カバレッジ連携で一番ハマりやすいのがここです。 原因はほぼ、
レポート内のファイルパスと sonar.projectBaseDir が一致していないことです。
コンテナでテストを走らせると、レポートには /usr/src/backend/todos/views.py のような
コンテナ内の絶対パスが書かれます。一方 SonarQube は projectBaseDir からの相対パスで
ファイルを識別するので、一致せず「カバレッジ情報の無いファイル」として扱われます。
エラーにならず静かに 0% になるので気づきにくいです。
対策:構成で防ぐ
リポジトリルート → /usr/src (全サービス共通)
backend のテスト実行 workdir : /usr/src/backend → projectBaseDir と一致
frontend のテスト実行 workdir: /usr/src/frontend → projectBaseDir と一致
そのうえで、Python は relative_files = True が必須です。
# backend/.coveragerc
[run]
relative_files = True
source = .
omit =
manage.py
config/wsgi.py
config/settings.py
*/migrations/*
tests/*
[xml]
output = coverage.xml
JS/TS は vitest を frontend/ 起点で動かし、reporter に lcov を含めます。
// frontend/vite.config.ts
test: {
environment: 'jsdom',
coverage: {
provider: 'v8',
reporter: ['text-summary', 'lcov'], // lcov が SonarQube 用
reportsDirectory: 'coverage',
// テストが 1 つも無いファイルも 0% として集計させる。
// ここを絞りすぎると「カバレッジが高く見えるだけ」になる
include: ['src/**/*.ts', 'src/**/*.vue'],
exclude: ['src/main.ts', 'src/**/*.spec.ts'],
},
}
スキャン前に必ず確認する
$ grep -m3 filename= backend/coverage.xml
filename="config/urls.py"
filename="todos/legacy.py" # ← 相対パスならOK
$ grep -m3 '^SF:' frontend/coverage/lcov.info
SF:src/App.vue
SF:src/api/client.ts # ← 相対パスならOK
ここが絶対パスになっていたら、SonarQube に渡しても 0% になります。
この設計で、実際に backend 47.7% / frontend 52.0% が一発で取り込めました。
Q. 手元の数字と SonarQube の数字が合わない
取り込みは成功しているのに、数字が一致しません。 これは不具合ではなく定義の違いです。
| 手元のツール | SonarQube | |
|---|---|---|
| backend | 96%(coverage.py) | 78.9% ↓ 下がった |
| frontend | 46.18%(vitest) | 52.0% ↑ 上がった |
backend が下がる理由 — .coveragerc の omit です。
omit =
manage.py
config/wsgi.py
config/settings.py
coverage.py は「除外したので集計対象外」として分母から外します。
一方 SonarQube にとってこれらは sonar.sources=. の範囲内にある普通のソースファイルで、
coverage.xml に情報が無いため Zero Coverage Sensor が「0% のファイル」として
記録します。分母に入るので全体が下がります。
これは実務で重要です。
.coveragercのomitだけでは、SonarQube 側の数字は逆に下がります。
SonarQube 側でも無視させたいならsonar-project.propertiesに
sonar.coverage.exclusionsを書く必要があります。
frontend が上がる理由 — 計算式の違いです。
SonarQube の Coverage = (カバーされた行 + カバーされた分岐) / (実行可能行 + 全分岐)
Lines : 121 / 262
Branches : 42 / 53
(121 + 42) / (262 + 53) = 163 / 315 = 51.7% ≒ SonarQube の 52.0%
vitest の Statements 46.18% は行だけの値です。SonarQube は分岐(79.24%)も
混ぜるので上がります。
記事やレポートで数字を出すときは「どのツールの値か」を明示した方が安全です。
前編のまとめ
ここまでで、Vue3 + DRF のプロジェクトが解析され、カバレッジまで取り込まれた状態に
なりました。押さえた要点は 6 つです。
- イメージタグは
sonarqube:communityを明示する(latestは将来ブレる) - Docker に 15GB 以上の空きを確保する(ES の watermark で起動に失敗する)
- 永続運用するなら PostgreSQL を用意する(組み込み H2 はアップグレードでデータを失う)
- Community Build はモノレポ非対応。プロジェクトを 2 つに分け、
projectBaseDirを切り替えて 2 回スキャンする -
カバレッジはレポート内のパスと
projectBaseDirを一致させる。
Python はrelative_files = True、JS/TS はsonar.javascript.lcov.reportPaths - テストが無いプロジェクトでも導入できる(カバレッジ条件を外すだけ)
検証に使った一式(Docker Compose / Makefile / 自動化スクリプト / 仕込み入りアプリ)は
make all だけで起動から解析結果の表示まで通るようにまとめてあります。
cp .env.example .env
make all
後編に続きます
ただし、この時点でダッシュボードを見ると拍子抜けします。
あれだけ指摘が出ているのに、Quality Gate は「Passed」と表示されるのです。
バグではなく、SonarQube の設計思想によるものでした。
後編ではその正体(New Code という概念)から始めて、次の内容を扱います。
- 実際に検出された指摘の一覧と、その読み方
-
仕込んだのに検出されなかった指摘 —
v-htmlの XSS が出ない理由 - Quality Gate を意図的に不合格にし、修正して合格させるまで
- 日本語コメントが誤検知されたときの正しい対処
- GitHub Actions 連携と、Community Build の限界
→ 後編「Vue3 + DRF に SonarQube を入れる(後編)— 指摘の読み方と Quality Gate の落とし穴」

