CI/CD 驗證設計
本文件是 OdfKit GitHub Actions 驗證與交付流程的長期說明。它不是臨時計畫檔, 而是維護 CI/CD 分層、逾時與診斷輸出的規則來源。
主 CI
.github/workflows/ci.yml 是每次程式碼變更的快速回歸入口。
maintainabilityjob(先跑、ubuntu):靜態閘門,失敗則不啟動完整測試矩陣。Test-MergeConflictMarkers.ps1Test-OneLineXmlSummary.ps1 -FailOnIssuesTest-BilingualXmlDocs.ps1 -FailOnNewIssuesTest-LocalizerKeyParity.ps1 -FailOnIssuesGenerate-LocalizerExceptionsFromJson.ps1 -VerifyOnly(JSON ↔ C# 一致)dotnet build OdfKit(全域AnalysisLevel=latest、AnalysisMode=Recommended;RunAnalyzersDuringBuild=true,含 PublicApiAnalyzers;只有明確升為 error 的規則阻擋建置)
full-regressionjob 依賴maintainability,在 Ubuntu 對net8.0與net10.0執行完整測試套件;它是每次 PR 與 main push 的必要回歸證據。TRX 與 blame diagnostics 只在失敗時上傳。Cobertura 報表各自必須通過 line 88% 與 branch 57% 最低門檻, 不得以另一個 TFM 的較高數值抵銷退化。testjob 依賴maintainability,在windows-latest執行。Ubuntu 的雙 TFM 覆蓋由full-regression提供,不再重複建立相同 Smoke 矩陣。net8.0與net10.0都必須先建置OdfKit.Tests。net8.0與net10.0都必須執行Category=Smoke,避免只有較新 TFM 有測試證據。- 建置與測試分成不同步驟;煙霧測試再依 docs、api、package-entries、 package-roundtrip-core、package-roundtrip-embedded、package-roundtrip-preservation、 vertical-slice、core-security 分成不同步驟,避免單一 testhost 長時間承載整批測試時難以定位停滯。
- 測試步驟有較短逾時,避免整個 job 黑箱卡到總逾時。
- 測試輸出 TRX 與 blame 診斷檔,失敗時才以短期產物上傳。
- 雙 TFM 套件相容性(
EnablePackageValidation)由nuget-pack.yml/Test-NuGetPack.ps1在 pack 時把關。
Windows 與 net10.0 曾在單一 Category=Smoke 批次或單一
PackageRoundTripTests 批次中留下 OdfKit.Tests.exe 測試子處理程序,即使個別測試單跑可通過,
也可能讓 VSTest/testhost 無法結束。煙霧測試分片是針對這個 testhost 收尾風險的必要設計,
不得合回單一全量煙霧測試步驟,也不得把所有 package round-trip 測試合回單一步驟。
完整格式矩陣來回讀寫(PackageRoundTripMatrixTests)保留在 Regression / Compliance
分層並標示為 explicit,不放入主 CI Smoke,避免 VSTest filter 與大量矩陣案例組合造成
testhost 收尾不穩。
煙霧測試只放「快速、無外部 Office / LibreOffice 依賴、可在 GitHub-hosted runner 穩定完成」 的回歸案例。需要外部應用程式、真實大型 corpus、視覺比對或效能統計的工作不得塞入主 CI。
專用驗證工作流程
| 工作流程 | 目的 | 是否自動跑 |
|---|---|---|
odf-corpus.yml |
repo 內 ODF corpus 與可選外部 corpus 驗證 | PR / main |
odf-external-baseline.yml |
以固定版本 Jing 與 ODF Validator 驗證完整 RELAX NG/package 對標 | PR / main / 手動 |
odf-policy.yml |
安全與政策規則測試 | PR / main |
typed-dom-coverage.yml |
typed DOM coverage floor 與產物 | PR / main |
trim-smoke.yml |
核心 NativeAOT/trim smoke,以及 net48 WebFont sidecar NativeAOT 實測 | PR / main |
nuget-pack.yml |
二十一個 NuGet 套件的單次封裝、完整 SPDX 3.0.1 SBOM、WebFont 發布演練與四平台 consumer smoke,包含 Imaging native runtime | PR / main |
performance-benchmark.yml |
DOM 與 ODS 串流效能/配置量回歸基準 | 每週 / 手動 |
libreoffice-interop.yml |
目前穩定版 LibreOffice 的真實雙 TFM 互通 | 每週 / 手動 |
apache-openoffice-interop.yml |
釘選 Apache OpenOffice 4.1.16 與官方 SHA-256,以 MSI administrative image 執行 ODT/ODS/ODP/ODG、DataPilot 雙 TFM 互通 | 手動 |
api-docs.yml |
17 語系 GitHub Pages API reference 建置(DocFX)與部署;結構與閘門見 api-docs-site.md | PR(僅建置)/ main / 手動 |
github-release.yml |
tag 驅動的發佈流程 | tag |
codeql.yml |
CodeQL 靜態安全分析與 SARIF 上傳 | PR / main / 每週 |
cns11643-baseline.yml |
CNS 11643 開放資料對照基線驗證 | PR / main(僅對照表相關路徑) |
scorecard.yml |
OpenSSF Scorecard 供應鏈檢查與 SARIF 上傳 | main、每週、branch protection 變更 |
發行 workflow 只負責交付快照,不是 v0.0.1 完滿條件。完滿狀態由每個 main 提交的必要
CI 與專用排程證據持續維持;不得把契約內缺口延後到下一個 tag。
LibreOffice、Microsoft Office COM 與 PDF 像素級比對屬外部環境驗收,必須由專用工作流程 或本機腳本明確啟用,不得混入主 CI 的煙霧測試。LibreOffice 互通由排程工作流程持續驗證; Apache OpenOffice 因自動排程數量上限與較大的安裝成本,保留為釘選版本的手動 workflow; Microsoft Office COM 與像素級比對仍依可用環境手動執行。
performance-benchmark.yml 的每週路徑只執行 Windows/Linux 的必要硬閘門。macOS
informational benchmark 與四種 IIS hosting model 的 WebFont 持續負載必須由
workflow_dispatch 明確 opt-in;不得為了取得額外綠燈自動排程。LibreOffice 雙 TFM
則在同一個 Windows job 安裝一次後依序執行,避免為相同外部程式建立兩台 runner。
自動排程 workflow 上限為四個;新增的每週 OpenSSF Scorecard 是第四個,沿用短期 SARIF
artifact 與 10 分鐘 job timeout,不得再以未評估的排程擴大 runner 用量。
外部 RELAX NG baseline 與快速 corpus job 分離。工作流程從
eng/external-tools.json 讀取 Jing 與 ODF Validator 的固定版本、來源 URL 與 SHA-256;各自的
cache key 同時包含來源、cache revision、版本與完整雜湊,且不設定 restore-keys。無論 cache
是否命中,安裝腳本都會重新計算 archive 與 JAR 的 SHA-256;已存在但不符時立即終止,不用
重新下載掩蓋錯誤。確認 cache 項目異常後,必須明確遞增 cacheRevision 取得新 key。
cache miss 的下載先寫入唯一暫存檔,驗證成功後才移入正式路徑。CI 不快取 corpus 驗證輸出、
暫存 manifest 或測試結果,因此舊結果不會被誤當成本次驗證證據。
同一個外部 baseline 另釘選與排程 LibreOffice 相同版本的 extended manifest schema。工作流程
由目前 CLI 即時產生 AES-256-GCM/Argon2id wholesome ODT、抽出外層 manifest,再交由 Jing
驗證;schema 下載與 cache 命中皆重算 SHA-256,產生的文件與驗證結果不進入 cache。
同一工作流程也以臨時 GnuPG RSA 金鑰執行 OpenPGP session-key 解密與 OdfKit package
round-trip,再將執行期產出的根層 manifest:encrypted-key manifest 交由 Jing 驗證;測試金鑰
與文件只存在於 runner 暫存目錄。
LibreOffice 每週雙 TFM 工作流程則以 Python UNO 開啟 wholesome 密碼文件,並以臨時
GnuPG RSA 金鑰完成 OpenPGP OdfKit → LibreOffice → OdfKit 修改後雙向 round-trip;已明確
啟用實機互通時若缺少 UNO runtime,測試必須失敗而非略過。
該工作流程另須安裝原生 GnuPG。LibreOffice 不自帶 OpenPGP 後端(program\ 只有
gpgmepp.dll 與 gpgme-w32spawn.exe),官方說明要求另行安裝 OpenPGP 軟體;GitHub 的
Windows runner 映像未內含 GnuPG,只有 Git for Windows 的 MSYS gpg,其 POSIX 路徑語意無法
供 gpgme 驅動,因此缺少原生 GnuPG 時 loadComponentFromURL 會回傳 None,錯誤訊息只說
文件無法載入。工作流程因此釘選官方簡易安裝檔(gnupg-w32-<version>_<date>.exe)。該安裝檔
刻意不做 Authenticode 簽章——官方只對 Gpg4win 與 VS-Desktop MSI 簽章——所以驗證改為官方
.sig 對釘選的發布金鑰指紋(gnupg.org/signature_key.html),並以發布公告公佈的 SHA-1 作
第二道獨立錨點;VALIDSIG 的簽署者指紋必須落在釘選清單內,否則失敗。驗證只需公鑰,不啟動
gpg-agent,且一律在工作區相對目錄內執行,避免 MSYS gpg 把 Windows 絕對路徑接在自身 POSIX
工作目錄後面。安裝後將 ODFKIT_GPG_PATH 寫入 GITHUB_ENV 指向原生 gpg.exe,使測試與
LibreOffice 使用同一個 GnuPG,不讓 PATH 上版本不同的 MSYS gpg 與其共用 GNUPGHOME 與
agent socket。
NuGet 驗證由 Ubuntu 僅封裝一次,產生 SHA256SUMS 後以上傳 artifact 將同一份短期快照
分送至 Linux x64、Windows x64、Windows ARM64 與 macOS ARM64 runner。每個 consumer job
都會先驗證 SHA-256,再以安裝發布套件的獨立專案執行 managed 與 Imaging native runtime
smoke。artifact 保留一天且不作為跨次 workflow cache;NuGet restore cache 則刻意只以
runner.os 與明確 revision 分區,不加入架構或 RID,因為套件快取本身可攜帶多個 RID 資產,
加入架構只會複製同一份下載內容。相依套件集合有實質變更時由維護者調升 revision;不得讓
任意專案檔變動自動複製整份 cache。此工作流程只在 PR、main push 與手動觸發時執行,
沒有每週排程。
GitHub Actions 資源與 cache 預算
GitHub Free 的共用 runner 不是互動式除錯環境。可在本機完成的格式化、建置、測試與
失敗重現必須先在本機執行;遠端只保留跨平台、乾淨 consumer、權限或託管環境才能提供的
證據。官方文件說明標準公開儲存庫 Windows runner 為 4 vCPU、16 GB RAM、14 GB SSD,
而 -latest 表示 GitHub 提供的最新穩定映像,不保證等同作業系統供應商最新版:
GitHub-hosted runners。
GitHub Actions cache 的儲存庫預設上限為 10 GB,七天未使用的項目會移除;超過容量時 依最久未存取順序淘汰,若 key 無界增生會造成 cache thrashing: Dependency caching reference。 OdfKit 因此採下列可機器驗證的契約:
eng/ci-resource-policy.json將 10 GB 設為硬性規劃基準,8 GB 為軟目標;這是預設 GitHub 配額的治理值,不是主動建立 8 GB cache 的許可。- workflow 一律透過
.github/actions/cache-odfkit。PR 只使用actions/cache/restore, 不建立 branch-scoped cache;受信任的非 PR workflow 才能儲存。 cache-odfkit必須由 workflow 直接呼叫,不得被其他 composite action 巢狀包裝。actions/cache的 post(儲存)步驟在多層 composite 下會取得「最外層 composite 的 inputs」而非自己的(actions/runner#2030), 因此 workflow →setup-dotnet-odfkit→cache-odfkit→actions/cache的呼叫鏈會讓 post 步驟找不到path,輸出Input required and not supplied: path並使儲存靜默失敗—— 日誌只會看到還原命中舊 key,Cache saved永遠不出現。NuGet cache 因此由每個 job 在setup-dotnet-odfkit之後直接呼叫cache-odfkit(NUGET_PACKAGES與目錄由 setup 步驟建立,順序不可顛倒)。eng/Test-CiResourcePolicy.ps1同時阻擋巢狀包裝與 job 漏接 cache 的情形。- NuGet global-packages 使用 OS 與明確 revision 的穩定 key;nupkg 可同時包含多個 RID,
不按 runner 架構複製。cache 是 immutable;相依集合變更後,在 revision 尚未調升前會
還原既有內容並下載缺少項目,但不會更新既有項目。累積差額或相依變更值得保留時必須調升
nuget-cache-revision,不得用 commit SHA 或廣泛hashFiles複製整份 cache。 - 外部 corpus/工具 key 只納入會改變該 cache bytes 的版本、face、SHA-256 與小型 metadata; 不得把無關 manifest 變動納入 key。命中後仍重新驗證來源 SHA-256。
- artifact 最多保留 14 天;NuGet 分送快照維持 1 天,CI diagnostics 與 WebFont 實證通常 7 天。GitHub 支援個別 artifact 設定留存期限: Store and share data。
- 所有 job 都必須有 timeout。自動排程 workflow 上限為
eng/ci-resource-policy.json的maxScheduledWorkflowCount(目前為四個:codeql.yml、libreoffice-interop.yml、performance-benchmark.yml、scorecard.yml),並啟用cancel-in-progress: true,避免排程重疊;GitHub 也明確說明排程可能在高負載時延遲: Scheduled workflows。
2026-07-19 稽核時,repository cache 實際使用 6,716,245,741 bytes、25 個項目;其中三個
近期 Windows x64 NuGet fingerprint cache 合計約 4.50 GB。改用穩定 revision key 後,遷移期
可能短暫保留一份新 cache,但舊 fingerprint 項目會依七天未使用政策自然淘汰;在未超過
10 GB 且沒有安全事件時,不以破壞性遠端刪除製造冷 cache 與重複下載。
修改 workflow、共用 action 或政策後必須執行:
pwsh eng/Test-CiResourcePolicy.ps1
Jing 20241231 會直接以 repo 內 OASIS ODF 1.0~1.4 schema 驗證該 schema 適用的 flat 文件,
以及 ZIP package 的 content.xml、styles.xml、meta.xml 與 settings.xml。通用 schema
未定義 office:formula,所以 Formula/FormulaTemplate/FlatFormula 由 Jing 報告明列排除,
並保留在內部 package gate。ODF Validator 0.13.0
另對適用的 ZIP package 執行容器/分類對標與正負 canary。Database 因該版本拒絕其合法
mimetype,Formula/FormulaTemplate 因該版本在 ODF 1.4 觸發上游 NPE,僅從 ODF Validator
集合排除。兩條外部 baseline 都是阻擋 gate。
逾時與診斷
CI 必須優先產生可診斷失敗,而不是只延長逾時。
- job 保留整體逾時,防止 runner 無限占用。
- 主要 build / test 步驟也要設定逾時,讓卡住的位置能被定位。
- 一般煙霧測試步驟使用較短停滯逾時;來回讀寫與 ZIP / Flat XML 互轉測試可使用稍長 停滯逾時,但仍須拆成小分片,不得只靠單一長逾時掩蓋 testhost 停滯。
dotnet test必須輸出 TRX。- 煙霧測試啟用 blame hang;日常 CI 使用
--blame-hang-dump-type none避免產物爆量。 - Crash dump 與 hang dump type
mini/full僅用於手動診斷重跑,避免日常 CI 在 Windows testhost 收尾階段被 dump collector 反向拖慢或放大產物。
分層規則
新增或調整測試時,請依下列原則標記:
Smoke:快速、穩定、跨平台、無外部二進位依賴。Regression:功能回歸,可由完整測試或專用工作流程執行。Scenario:較完整的高階情境測試。Interop:外部格式或應用程式互通。Corpus:manifest / fixture corpus 驗證。Policy:安全、profile、sanitization 與治理規則。Stress/Performance:不得進入主 CI 煙霧測試。
如果某個測試只在 Windows 或特定 TFM 會執行特殊路徑,該測試不應只靠單一矩陣格背書; 至少要在 CI 設計上讓另一個 TFM 或 OS 能提供可比較資料。