# SonarQube Quality Gate — new-code 策略設定紀錄

> 落地日期：2026-07-25
> Server：`http://192.168.50.171:9000`（SonarQube 26.7.0.124771）
> 涵蓋專案：`guidant-ai-backend` / `guidant-ai-fe`
> Notion case：CM-891「SonarQube Quality Gate 設 new-code 策略」

## 1. 背景與動機

Sonar CODE_SMELL 存量 BE 789 / FE 681 條，其中含既有裁決禁區（S3776 認知複雜度 ×241、
S3516 BLOCKER ×8）——靠人力清不完，也不該全清。若 CI 接入時 Quality Gate 沿用
overall（存量）條件，CI 第一天就會被千條存量打紅，團隊會習慣「CI 紅是正常的」，
gate 形同虛設。

正確做法：**gate 只看 new code（新程式碼零新增 issue 才過），存量當 baseline 不擋版**，
日後分批派 runner 清機械性桶（註解碼 / 未用參數等），禁區維持不動。

## 2. Gate 設定：`Guidant-NewCode`

自訂 gate，兩專案皆已指派（`api/qualitygates/select`）。條件**只有兩條、全在 new code**：

| # | Metric | 條件 | 意義 |
|---|--------|------|------|
| 1 | `new_violations` | > 0 即 FAIL | 新程式碼上任何新 issue（BUG / VULNERABILITY / CODE_SMELL，含所有 severity）都擋版——零新增量 |
| 2 | `new_security_hotspots_reviewed` | < 100% 即 FAIL | 新程式碼引入的 Security Hotspot 必須全部完成人工審查（標 SAFE 或 FIXED）才放行 |

### 刻意不設的條件與理由

- **不設任何 overall（存量）條件**：本 case 核心目的。存量 1,470 條 smell 是 baseline，
  不應擋新版發佈；存量歸還走獨立 backlog case（Notion CM 已有 BE/FE 品質債兩條）。
- **不設 coverage 條件**（內建 Sonar way 有 `new_coverage < 80%` FAIL）：
  - BE：pytest 環境目前壞的（有獨立修復 case），無法產生覆蓋率報告 → 設了必紅。
  - FE：無測試覆蓋率上報機制 → 同樣必紅。
  - 補回前提見 §6。
- **不設 duplication 條件**（Sonar way 有 `new_duplicated_lines_density > 3%`）：
  本專案大量 DDD 樣板碼（repo / mapper / DTO）天然重複率高，貿然設 3% 會產生
  假陽性擋版；先以 new_violations 為主，日後有需要再評估加回。

> 實作備註：SonarQube 26.x `api/qualitygates/create` 建立 gate 時會**自動帶入**
> CaYC（Clean as You Code）四條預設條件（new_violations / new_coverage /
> new_duplicated_lines_density / new_security_hotspots_reviewed），
> 因此實際操作是建立後用 `api/qualitygates/delete_condition` 移除 coverage 與
> duplication 兩條，保留另外兩條。移除後 gate 會被標 `caycStatus: non-compliant`，
> 這只是 Sonar 對「未含 coverage 條件」的標註，不影響 gate 運作。

## 3. New code 定義：`PREVIOUS_VERSION`

- 探索結果：兩專案原本即為全域預設繼承（`inherited: true, type: PREVIOUS_VERSION`）。
- 已用 `api/new_code_periods/set` 對兩專案顯式確認為 `PREVIOUS_VERSION`
  （與繼承值相同，Sonar 回報仍為 inherited，行為一致）。
- **選擇理由（PREVIOUS_VERSION 優於 REFERENCE_BRANCH=main）**：
  1. 目前兩 repo 的掃描都直接在 `main`（含本機 working tree）上執行，沒有
     branch/PR 分析（Community 版也不支援 branch analysis）——若設
     `REFERENCE_BRANCH=main`，main 自己跟自己比，new code 永遠為空，gate 形同虛設。
  2. `PREVIOUS_VERSION` 以 `sonar.projectVersion` 切分：版本未變時，new code 累積
     「自上一版以來」的新增碼，正好對齊本專案「一版一 release」的節奏
     （version-bump skill 進版 → 下一次掃描重置 baseline）。
  3. 目前 scanner 未傳 `sonar.projectVersion`（Sonar 記為 `not provided`），
     new code period 起點落在該專案第一次被掃描的分析（BE 2026-07-25T03:27Z /
     FE 2026-07-25T03:31Z）。**建議 CI 接入時在 scanner 參數帶上
     `-Dsonar.projectVersion=$(pyproject 版號)`**（FE 用 package.json version），
     讓 new code 隨產品版本正確重置。

## 4. 驗證結果（2026-07-25）

兩 repo 各重跑一次掃描（BE `scripts/sonar_scan.sh`、FE 根目錄 `sonar-scanner`，
指令見 `docs/security-reports/README.md`）後查 `api/qualitygates/project_status`：

| 專案 | Gate | 狀態 | new_violations 實際值 |
|------|------|------|----------------------|
| guidant-ai-backend | Guidant-NewCode | **OK（PASSED）** | 0 |
| guidant-ai-fe | Guidant-NewCode | **OK（PASSED）** | 0 |

存量 smell（BE 789 / FE 681）完全不影響 gate 判定，符合「存量不擋版」目標。

## 5. CI 端接法（給「資安掃描進 GitLab CI」case）

兩種擇一，推薦方案 A：

**方案 A — scanner 內建等待（推薦，最簡單）**

```bash
sonar-scanner \
  -Dsonar.projectKey=guidant-ai-backend \
  -Dsonar.projectVersion="${PROJECT_VERSION}" \
  -Dsonar.qualitygate.wait=true \
  -Dsonar.qualitygate.timeout=300 \
  ... # 其餘參數同 scripts/sonar_scan.sh
```

`qualitygate.wait=true` 會讓 scanner 阻塞等 server 端分析完成，gate FAIL 時
scanner 以非零 exit code 結束 → CI job 直接紅。

**方案 B — 掃後輪詢 API（scanner 版本不支援 wait 時的 fallback）**

1. 掃描輸出中取得 ce task id（`report-task.txt` 的 `ceTaskId`）。
2. 輪詢 `api/ce/task?id=<ceTaskId>` 直到 `status=SUCCESS`。
3. 查 `api/qualitygates/project_status?projectKey=<key>`，
   `projectStatus.status != "OK"` 則 `exit 1`。

注意事項：
- CI 用的 token 建議另發專用 analysis token，不共用個人 user token。
- 本機腳本 `scripts/sonar_scan.sh` 未加 `qualitygate.wait`，維持「掃完即走」，
  gate 判定留給 CI；本機要看結果可查 dashboard 或 project_status API。

## 6. 日後補 coverage 條件的前提

補 `new_coverage`（建議門檻 80%，對齊 Sonar way）前，須先完成：

1. **BE**：pytest 環境修復 case 完成（目前跑不動），且 CI 內以
   `pytest --cov --cov-report=xml` 產出 coverage.xml，scanner 加
   `-Dsonar.python.coverage.reportPaths=coverage.xml`。
2. **FE**：建立測試覆蓋率上報（vitest/jest `--coverage` 產 lcov），scanner 加
   `-Dsonar.javascript.lcov.reportPaths=coverage/lcov.info`。
3. 上述任一 repo 就緒即可先對該 repo 加條件——但 gate 是兩專案共用的
   `Guidant-NewCode`，若只有一邊就緒，需拆成兩個 gate（如 `Guidant-NewCode-BE` /
   `-FE`）或等兩邊都就緒再統一加，屆時再裁決。

## 7. 操作留痕（API 呼叫摘要）

| 動作 | API | 結果 |
|------|-----|------|
| 建 gate | `POST api/qualitygates/create name=Guidant-NewCode` | 建立成功（自動含 CaYC 四條件） |
| 刪 coverage 條件 | `POST api/qualitygates/delete_condition` | 204 |
| 刪 duplication 條件 | `POST api/qualitygates/delete_condition` | 204 |
| BE 指派 gate | `POST api/qualitygates/select projectKey=guidant-ai-backend` | 204 |
| FE 指派 gate | `POST api/qualitygates/select projectKey=guidant-ai-fe` | 204 |
| new code period | `POST api/new_code_periods/set type=PREVIOUS_VERSION`（兩專案） | 200 |
| 驗證 | `GET api/qualitygates/project_status`（兩專案） | 皆 OK |
