Skip to main content

elf-cicd-backend

Elf Express .NET 10 後端 CI 規範(restore / build / test + coverlet.msbuild 合併行覆蓋率 55% 門檻 + Testcontainers PostgreSQL 18)。 當任務涉及新增或修改 .github/workflows/ci.yml 的 server job、global.json 釘 SDK、 dotnet test 覆蓋率門檻、coverlet 設定、測試專案 csproj、CI 整合測試(Testcontainers)需要 PostgreSQL, 或 .NET 測試在 CI 失敗時觸發。

Source facts

Repository
elf-express/skillshare-source
Last source activity
September 27, 2026 at 01:26
Detected SKILL.md language
Chinese
Stars
0
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
4 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
elf-cicd-backend
description
Elf Express .NET 10 後端 CI 規範(restore / build / test + coverlet.msbuild 合併行覆蓋率 55% 門檻 + Testcontainers PostgreSQL 18)。 當任務涉及新增或修改 .github/workflows/ci.yml 的 server job、global.json 釘 SDK、 dotnet test 覆蓋率門檻、coverlet 設定、測試專案 csproj、CI 整合測試(Testcontainers)需要 PostgreSQL, 或 .NET 測試在 CI 失敗時觸發。
metadata
{"version":"1.0.0","owner":"Elf Express"}
# Elf Express 後端 CI(elf-cicd-backend) > 本 skill 只管 `ci.yml` 裡的 `server` job 與 .NET 端的釘版 / 覆蓋率設定。 > 檔頭(`name` / `on`)與 `web` / `e2e` job 見 **elf-cicd-frontend**; > PR AI 審查見 **elf-cicd-review**;版號(含 csproj `<Version>`)見 **elf-cicd-versioning**; > 測試寫法見 **elf-unit** / **elf-integration**;SqlSugar 用法見 **elf-sqlsugar**;NuGet 版本見 **elf-stack**。 ## 1. 何時使用 - 建立或修改 `ci.yml` 的 `server` job - 新增 .NET 測試專案、調整 coverlet / 覆蓋率門檻 - 新增 `global.json` 或升級 .NET SDK - 整合測試需要真的 PostgreSQL(Testcontainers,fixture 由 elf-integration 負責) - `dotnet test` 本機綠、CI 紅 ## 2. 固定規則 **SDK 釘版** 1. MUST 在 repo 根目錄放 `global.json`,`sdk.version` 為 10.0 的 SDK、`rollForward: latestFeature`(見 `templates/global.json`)。 為什麼:沒有 global.json,每台機器用它裝過最新的 SDK,編譯結果與分析器警告會不同。 2. MUST 在 CI 用 `actions/setup-dotnet@v5` + `global-json-file: global.json`。MUST NOT 用 `dotnet-version:`(不論 `10.0.x`、`latest` 或其他)。 為什麼:SDK 版本只能有一個來源;改 `global.json`,CI 就跟著改。 3. MUST NOT 在 `global.json` 加 `"test": { "runner": "Microsoft.Testing.Platform" }`。 為什麼:本規範的覆蓋率門檻靠 coverlet.msbuild 掛在 VSTest 流程上,切換 runner 後 coverlet 不再插樁,門檻會「靜默消失」。 4. MUST 所有 `actions/*`(checkout / setup-dotnet / upload-artifact …)統一用 `@v5`。 **方案與目錄** 5. MUST 使用單一方案檔 `server/<AppName>.slnx`,CI 的 restore / build / test 全部指向它。 為什麼:指向單一專案會漏掉其他測試專案;新增的測試專案沒加進 `.slnx` 就等於沒跑。 6. MUST 主專案放 `server/src/<Project>/`(API 為 `server/src/<AppName>.Api/`);單元測試放 `server/tests/<Project>.Tests/`,一個主專案對應一個 `<Project>.Tests`(見 elf-unit)。 整合測試(需要 Docker,含 contract / `WebApplicationFactory` 測試)MUST 放在獨立的 `server/tests/<AppName>.Api.IntegrationTests/`(見 elf-integration)。 7. MUST 新增測試專案後立刻加入 `.slnx`(`dotnet sln server/<AppName>.slnx add server/tests/<Project>.Tests/<Project>.Tests.csproj`)。 8. 測試 `.csproj` MUST 遵守 Central Package Management:`PackageReference` MUST NOT 帶 `Version=`(版本統一在 `server/Directory.Packages.props`),MUST NOT 自訂 `TargetFramework`(繼承 `server/Directory.Build.props`)。見 `templates/Project.Tests.csproj`。 **步驟順序(固定)** 9. MUST 依序:`dotnet restore` → `dotnet build --no-restore -c Release` → `dotnet test --no-build -c Release` → 合併覆蓋率門檻檢查。 為什麼:分開幾步才看得出是哪一段壞;`--no-restore` / `--no-build` 保證測的就是剛剛 build 出來的那份 Release 產物。 **覆蓋率(本規範選定:coverlet.msbuild + MSBuild 屬性,門檻套在「合併後」的整體)** 10. MUST 在每個測試專案參考 `coverlet.msbuild`,並移除 `dotnet new xunit` 預設帶入的 `coverlet.collector`。 11. MUST 以 solution 層級跑一次 `dotnet test`,帶 `-p:CollectCoverage=true`,並用 `MergeWith` + `-m:1` 把所有測試專案的覆蓋率合併成一份(完整指令見 `templates/ci-server-job.yml` 與 3.2)。 12. MUST 以 `Threshold=55`、`ThresholdType=line`、`ThresholdStat=total` 的語意檢查**合併後**的 `coverage/coverage.cobertura.xml`:整體行覆蓋率 < 55% → CI 紅。 MUST NOT 要求每個測試專案各自達到 55%。為什麼:單元測試與整合測試量的是同一份程式碼的不同面向,逐專案檢查會讓「加起來夠、各自不夠」的專案永遠紅。 只有一個測試專案時,可直接在 `dotnet test` 帶 `-p:Threshold=55 -p:ThresholdType=line -p:ThresholdStat=total`(效果相同);有兩個以上時一律用合併門檻。 13. MUST NOT 把 `branch` 列入門檻。branch 50% 為**建議值**,只由 advisory step 印 `::warning::`。 14. MUST NOT 使用 `coverlet.collector` / `--collect:"XPlat Code Coverage"`。為什麼:它不讀 coverlet.msbuild 的屬性,與 `-p:CollectCoverage=true` 並用還會重複插樁,數字不可信、偶發失敗。 15. MUST NOT 為了過門檻加 `[ExcludeFromCodeCoverage]`、`-p:Exclude=...` 或寫沒有 assert 的測試。排除清單變動必須在 PR 說明理由。 **資料庫(PostgreSQL 18 + SqlSugar 5.x)** 16. 需要真的資料庫的測試 MUST 用 Testcontainers(`postgres:18`,由 elf-integration 的 fixture 啟動)。MUST NOT 在 `ci.yml` 加 `services: postgres`。 為什麼:同一份 fixture 本機與 CI 行為一致;GitHub `ubuntu-latest` runner 內建 Docker,不需要額外設定。 17. MUST 釘 `postgres:18`,MUST NOT 用 `postgres:latest` / `postgres`。為什麼:新 major 會在沒有 PR 的情況下改變測試行為。 18. 測試用拋棄帳密全團隊統一:user `elf`、password `elf_test_pw`、db `elf_test`;連線字串 MUST 含 `Timezone=UTC`。MUST NOT 放真實密碼或 production 連線字串。 要改連既有資料庫(例如除錯)時,由 elf-integration 的 `ELF_TEST_PG_CONNECTION` 環境變數切換;CI 範本不設定它。 19. MUST NOT 在 CI 讓測試連外部共用資料庫。每次 job 都是全新空庫,測試自己建表 / 種資料。 **Job 本身** 20. MUST 維持 job id `server`、`name: server (build · tests)`。為什麼:branch protection 的 required check 以 `name` 比對,改名會讓 PR 永遠等不到狀態。 21. MUST NOT 使用 `paths:` 過濾或 `continue-on-error: true`(理由同 elf-cicd-frontend 規則 24、26)。 ## 3. 標準範本 | 檔案 | 範本 | 放到 | |------|------|------| | server job | [`templates/ci-server-job.yml`](templates/ci-server-job.yml) | 貼到 `.github/workflows/ci.yml` 的 `jobs:` 底下 | | SDK 釘版 | [`templates/global.json`](templates/global.json) | repo 根目錄 `global.json` | | 測試專案 | [`templates/Project.Tests.csproj`](templates/Project.Tests.csproj) | `server/tests/<Project>.Tests/<Project>.Tests.csproj` | `coverlet.msbuild`、`Microsoft.NET.Test.Sdk`、xUnit、`Microsoft.AspNetCore.Mvc.Testing`、`Testcontainers.PostgreSql` 的版本統一寫在 `server/Directory.Packages.props`(見 elf-stack)。 ### 3.1 目錄結構 ``` global.json server/ ├── <AppName>.slnx ├── Directory.Build.props ← TargetFramework 等共用屬性 ├── Directory.Packages.props ← 所有 NuGet 版本(CPM) ├── src/ │ └── <AppName>.Api/ │ └── <AppName>.Api.csproj ← 含 <Version>,由 scripts/version.mjs 改寫 └── tests/ ├── <AppName>.Api.Tests/ ← 單元測試 │ ├── <AppName>.Api.Tests.csproj │ └── Services/OrderServiceTests.cs ← 資料夾結構比照主專案 └── <AppName>.Api.IntegrationTests/ ← 整合 / contract 測試(需要 Docker) └── <AppName>.Api.IntegrationTests.csproj ``` ### 3.2 本機重現 CI(與 CI 完全相同的指令) 需要 Docker 執行中(整合測試用 Testcontainers 自己起 `postgres:18`)。 ```bash dotnet restore server/<AppName>.slnx dotnet build server/<AppName>.slnx --no-restore --configuration Release rm -rf coverage dotnet test server/<AppName>.slnx --no-build --configuration Release -m:1 \ -p:CollectCoverage=true \ -p:CoverletOutput="$PWD/coverage/" \ -p:MergeWith="$PWD/coverage/coverage.json" \ -p:CoverletOutputFormat=\"json,cobertura\" # 合併後的整體行覆蓋率(門檻 0.55) grep -o -m1 'line-rate="[0-9.]*"' coverage/coverage.cobertura.xml ``` ### 3.3 覆蓋率報告位置 `-p:CoverletOutput` 指到 repo 根目錄的 `coverage/`;每個測試專案跑完都把結果併入 `coverage/coverage.json`, 最後寫出的 `coverage/coverage.cobertura.xml` 就是**所有測試專案合併後**的結果。門檻檢查、advisory branch warning 與 artifact 都只看這一份。 `-m:1` 讓測試專案依序執行,避免多個專案同時寫同一個合併檔。 ## 4. 檢查清單 - [ ] repo 根目錄有 `global.json`,`rollForward: latestFeature`,沒有 `test.runner` - [ ] CI 用 `actions/setup-dotnet@v5` + `global-json-file: global.json`,沒有 `dotnet-version:` - [ ] 所有 `actions/*` 都是 `@v5` - [ ] 所有 restore / build / test 都指向 `server/<AppName>.slnx` - [ ] 單元測試在 `server/tests/<Project>.Tests/`、整合測試在 `server/tests/<AppName>.Api.IntegrationTests/`,且都已加入 `.slnx` - [ ] 測試 csproj 參考 `coverlet.msbuild`,**沒有** `coverlet.collector`、沒有 `Version=`、沒有自己的 `TargetFramework` - [ ] `dotnet test` 帶 `CollectCoverage` + `MergeWith` + `-m:1`;門檻檢查看合併後的 `coverage/coverage.cobertura.xml`(line ≥ 55%) - [ ] 門檻裡沒有 `branch`;branch 只有 advisory warning - [ ] `ci.yml` 沒有 `services: postgres`;DB 測試走 Testcontainers `postgres:18` - [ ] 測試帳密為 `elf` / `elf_test_pw` / `elf_test`,連線字串含 `Timezone=UTC` - [ ] job `name: server (build · tests)` 未變更 - [ ] 本機跑 3.2 的指令全綠,且合併後 line-rate ≥ 0.55 - [ ] 刻意改壞一段被測邏輯,確認測試會紅,再改回來 ## 5. 常見錯誤 | 症狀 | 原因 | 修正 | |------|------|------| | 覆蓋率明明很低 CI 卻綠 | 用的是 `coverlet.collector`(`--collect`),coverlet.msbuild 的屬性完全沒作用 | 換成 `coverlet.msbuild`(規則 10、14) | | 覆蓋率門檻突然不生效 | `global.json` 被加了 `test.runner: Microsoft.Testing.Platform` | 移除,維持 VSTest(規則 3) | | 整體夠、但某個測試專案單獨報 threshold 失敗 | 在多專案的 solution 上直接帶 `-p:Threshold=55`,每個專案各自被檢查 | 改用 `MergeWith` 合併後再檢查(規則 11、12) | | 合併報告數字忽高忽低 / 檔案損毀 | 沒帶 `-m:1`,多個測試專案同時寫 `coverage.json` | 加 `-m:1` | | 新測試專案的測試 CI 沒跑 | 沒加進 `server/<AppName>.slnx` | `dotnet sln ... add ...` | | 整合測試報 Docker 無法連線 | 在沒有 Docker 的 runner(非 Linux / 自架)上執行 | 用 `ubuntu-latest`(內建 Docker);本機先啟動 Docker | | `NU1008`(CPM 專案不應定義版本) | 測試 csproj 的 `PackageReference` 帶了 `Version=` | 刪掉 `Version=`,版本放 `Directory.Packages.props` | | 本機綠、CI 資料庫行為不同 | 本機 Postgres 版本與 CI / production 不同,或用 `latest` | 全部釘 `postgres:18` | | 時間欄位差 8 小時 | 連線字串沒有 `Timezone=UTC` | 所有連線字串加 `Timezone=UTC` | | SqlSugar 讀資料時 `Activator.CreateInstance` 丟例外 | Entity 用了 C# `required` 成員(參考專案踩過) | 改用屬性預設值(見 elf-sqlsugar) | | 測試在 CI 綠、`dotnet run` 卻找不到檔案 | 程式用相對於「目前目錄」的路徑;測試剛好從輸出目錄跑(參考專案踩過) | 改用 `AppContext.BaseDirectory` | | `dotnet test --no-build` 找不到組件 | build 用 Release、test 沒帶 `--configuration Release` | 每一步都帶同一個 configuration | ## 6. 待確認 - `global.json` 的確切 SDK 版本(範本暫填 `10.0.100`):團隊要釘哪個 feature band / patch 待確認。 - 是否改用 xUnit v3 待確認(版本號一律在 `Directory.Packages.props`,由 elf-stack 維護)。 - 是否把單元測試與整合測試分成兩段執行(例如 `--filter "Category!=Integration"`),待確認;目前範本一次跑全部並合併覆蓋率。 - 覆蓋率摘要是否要貼到 PR(例如 ReportGenerator 產生 `$GITHUB_STEP_SUMMARY`),待確認;目前只上傳 artifact + warning。 - `coverlet.msbuild` 搭配 `dotnet test --no-build` + `MergeWith` 在 .NET 10 SDK 的行為未在實機驗證;首次導入時請確認 CI log 出現 coverlet 的 `Calculating coverage result...`,且 `coverage/coverage.cobertura.xml` 的 line-rate 合理。
View on GitHub