docs(CI): §5.3 重写并记录覆盖率工具链落地,更正 §1.2.1 与本会话先前的错判

补上今天两件 CI 工作的落地记录,并把错误结论改对(历史行不动,只在上方加行)。

新增 §4.4「覆盖率工具链(mscoverage)与 VS2026 BuildTools」。为什么只能装:
那个 exe 没有免安装的独立发行版——实测 NuGet 两个候选包,
Microsoft.Testing.Extensions.CodeCoverage 18.12.0 的 109 个条目里 .exe 数为 0,
Microsoft.CodeCoverage 18.11.0 只有老版 CodeCoverage.exe(CLI 语法还不同),
所以它只随 Visual Studio 本体分发。安装入口是 aka.ms/vs/18/stable/
vs_buildtools.exe(release 那条短链不存在,会掉到"未找到"页);组件 ID
Microsoft.VisualStudio.PackageGroup.TestTools.CodeCoverage 由 18.11 的
VisualStudio.vsman 解析得到(清单里唯一引用 DynamicCodeCoverage 与
CodeCoverage.Console.Targeted 的组)。含可直接粘贴的三条管理员命令、-Force
陷阱、Restart-Service Schedule 判据,以及「装在非默认盘 = 门禁与 CI 都看不见」
的成因:Find-MsvcCl 与 _default_vs_roots 都只由 %ProgramFiles% 派生根。

§5.3 整节重写:本仓库已移除 uses:,记录六项实测(A/B/C/D/E/F,其中 E 是执行
从 ci.yml 抽出的正文、测的就是文件里那份字节)与两条自查命令,并明标两条
**未证边界**:PR 的 refs/pull/<n>/merge 兜底在本实例无法证实(0 个 PR、
ls-remote 该命名空间为空);Basic 令牌那一支本地验证不了(公开仓库匿名即过,
伪造令牌只能证明坏的会被拒)。旧版那段"可直接抄"的示例标为禁抄:shell: pwsh
在 runner 上 PATH 无 pwsh 会直接失败,--depth 1 让 base..head 报 unknown
revision,两条都会让下一个照抄的人多红一轮。

更正两处过时/错误说法:① §1.2.1 表第三行——自定义 DEFAULT_ACTIONS_URL 在
1.25.4 已无可用取值,旧写法会把排障的人引向一个不存在的开关;② 我自己先前
写的「旧探测会把机器级变量覆回 C 盘 14.29」——本机实情是 C 盘 VS2019 已于
10-27 01:00 卸载(C 盘 x64 cl.exe 实测 0 个),旧探测的行为是「找不到 → 只
Warn → 不写」,于是变量一直指着失效路径。§7 的 checkout 行相应改为「看见此
症状=有人把 uses: 加回来了」。

另:口径文档 §7 第 8 条加实测补注(「CI runner 与本机同一台」对量基线那台不
成立,本机当日 vswhere 只有 VS2019);新增 remote-ci-ops skill 并接上 AGENTS
与 docs/CI/README 两处指针;指南与 README 各加变更记录。纯文档,按 workflow
的 paths-ignore 不触发 CI。
This commit is contained in:
2026-10-27 08:01:40 +08:00
parent 2d18193d38
commit 51ecd36192
5 changed files with 396 additions and 33 deletions
+7
View File
@@ -20,6 +20,10 @@
目录下的两篇指南 `CI配置指南.md` 与 `Gitea_Runner部署指南.md` 合并而来,**两篇均已删除**,
去重后为单一来源)。
> **agent 排障时的操作顺序**在 **`remote-ci-ops` skill**(`.dsh/skills/remote-ci-ops/SKILL.md`,
> 用到自动加载):症状 → 一条判据 → 动作,加上改 workflow / 门禁脚本的红线与改后验收清单。
> 分工是——**本目录讲"为什么"和"怎么装",skill 讲"出问题时按什么顺序查"**,同一事实不在两处详述。
| 想干什么 | 去看 |
|---|---|
| 先搞清 CI 需要哪三样东西就位、本机拓扑、部署前置清单 | §0 / §0.1 / §0.2 |
@@ -29,6 +33,7 @@
| `config.yaml` 各字段语义(capacity / timeout / labels / 日志目录…) | §2.3 |
| **常驻:为什么必须看门狗、不能只起 daemon、也不能做成 Windows 服务** | §2.4 + §3.0 |
| runner 装什么软件、为什么变量必须对 **SYSTEM** 可见、机器级 `DTU_*` | §4(§4.1.1~§4.1.3 是三个真坑) |
| **覆盖率工具链(mscoverage)怎么装、机器级 MSVC 怎么指**(含三条可直接粘贴的管理员命令) | §4.4 |
| workflow 位置 / 触发 / 内网拉不到 `actions/checkout` 的三种处置 | §5(§5.3 零依赖写法) |
| 两个 job 各自跑哪些步骤、两条 Python 探测链要同序同步 | §5.5 |
| 端到端验证清单、无网页登录时直查 MySQL 核验 | §6 / §6.1 |
@@ -81,6 +86,8 @@
| 日期 | 变更 |
|---|---|
| 2026-10-27 | 指南 §5.3 **整节重写并落地**:workflow 两个 job 的 `uses:` 归零(原生 git 全量检出,六项实测 A/B/C/D/E/F + 两条自查命令见 §5.3);§1.2.1 第三行按 1.25.4 实际行为改正(自定义 `DEFAULT_ACTIONS_URL` 已无效,旧写法会把人引向一个不存在的开关);§7 的 checkout 行改为“看见这症状=有人把 `uses:` 加回来了”。另:`scripts/ci_runner_bootstrap.ps1` 的 MSVC 探测改为 `vswhere` + 工具集钉世代(默认 14.51,与覆盖率基线同版本),`-SetEnv` 开始成对写 `DTU_MS_COVERAGE` |
| 2026-10-27 | 新增指南 §4.4「覆盖率工具链(mscoverage)与 VS2026 BuildTools」并加进上面的目录表。当天另修掉 `cppcheck` 两根真红(`ci/remote-ci` 的 `5869c03`)——此前 L0 的 28 次红里混着**三类**:runner 缺覆盖率组件、外网拉 `actions/checkout` 抖动、真代码问题。**别再一律当成环境噪声**,按 §7 逐条定性 |
| 2026-10-12 | 🐛 **首次真跑出 run**:`L0.5` **成功**(`10 runner(s), 647/647, fail=0`,与本地逐字一致);`L0` **假失败**——gitlint 因 runner 无 git 身份而 `exit 254`(`Missing git configuration: please set user.name`),旧文案把它说成"提交信息违规",且该步 `exit 1` 连带让 26 个钩子全部没跑。已在 `ci_gitlint_range.ps1` 用 `GIT_CONFIG_*` 注入一次性身份(不写文件)并区分 254/1 退出码。另记:**新建分支的首次 push 不触发 run**,需 `workflow_dispatch` 或再推一个非文档提交。详见指南 §7 |
| 2026-10-12 | 🔀 本轮 CI 修复随 `master` **合入 `v2-on-ui`**(该分支另有 21 个功能提交;冲突仅 `AGENTS.md` 与 `docs/README.md` 两处,解法是"保留其新版结构 + 把 CI 指针搬进对应位置")。因该分支门禁更多(**26 钩子 / 10 单测套件** vs master 的 18 / 3),指南里写死的数字已改为**规则 + 分分支快照**,见指南 §6 / §7 / §10 |
| 2026-10-12 | ✅ 双看门狗缺陷**实测收敛**(管理员 `-StartNow` 清掉 3 个残留 → 单实例、链路恢复、runner online)。另修两处:daemon 日志走 stderr 被 PS 5.1 `2>&1` 包成 ErrorRecord(2 行真日志写成 8 行,每 4 行 3 行噪声)→ 启动器改 `ForEach-Object { "$_" }`;`-Status` 诚实化(非管理员读不到 SYSTEM 进程命令行 → 明说需提权而非谎报"无看门狗";重启循环判据改看进程存活时长,不再靠不轮转日志的尾部字符串报假警)。详见指南 §7.3 |
+149 -32
View File
@@ -156,7 +156,7 @@ ENABLED = true
|---|---|
| **不配(默认)** | 从 **[github.com](https://github.com)** 拉取 action |
| `self` | 从**你自己的 Gitea 实例**拉取(要求 action 已镜像进你的实例) |
| 具体 URL(如 `https://gitea.com`) | 从该地址拉取 |
| 具体 URL | ⚠ **本实例的 1.25.4 上这一档没有可用取值**:该版本只保留 `github`(默认)与 `self` 两个关键字,写具体 URL **不再改变下载源**(2026-10 核对时踩到的第一现场就是这里,本表旧版写的是老行为)。别把希望放在这个键上——**修法见 §5.3:不依赖 `uses:`** |
> ⚠️ **不要把你的实例域名写进去当普通 URL**——要"从自己实例拉"就用关键字 **`self`**;
> 且**写了 `self` 还得先把 action 镜像进自己实例**,否则 `actions/checkout` 依然拉不到。
@@ -166,7 +166,7 @@ ENABLED = true
| 场景 | 配置 |
|---|---|
| 服务器**能**访问 github.com | **什么都不配**(默认走 github.com,最省事) |
| 内网,**不能**访问 github.com | 建议**改用零依赖写法**(见本文 §5.3 方案 3),`DEFAULT_ACTIONS_URL` 配不配都行;<br>若坚持用 `actions/checkout@v4`,则需 `DEFAULT_ACTIONS_URL = self` **且**把该 action 镜像进实例 |
| 内网,**不能**访问 github.com | **本仓库现状(2026-10-27)**:workflow 里 `uses:` 已归零,`DEFAULT_ACTIONS_URL` 配不配都不影响(见 §5.3)。若哪天有人把 `uses:` 加回来,这条路就会重新变成外网依赖——`self` 需先镜像 action,而自定义 URL 在本版本无效 |
> Gitea 官方 FAQ 原文:*"From `https://github.com` by default, or from your own instance
> when `[actions].DEFAULT_ACTIONS_URL` is `self`."* 使用别处 action 也可在 workflow 里直接写全路径:
@@ -887,6 +887,85 @@ Start-ScheduledTask -TaskName GiteaActionsRunner
这条命令跑的就是 CI 的同一套判定。**本地能过,runner 上基本就能过**(前提是 PATH 一致)。
### 4.4 覆盖率工具链(mscoverage)与 VS2026 BuildTools——2026-10-27 实测落地
`test-coverage` 总闸依赖 `Microsoft.CodeCoverage.Console.exe`;缺它按用户裁定 **FAIL、不 skip**
(2026-10-24;口径见 `docs/测试说明/主机单测覆盖率口径.md` §7 第 8 条)。这台 runner 在 2026-10-27 之前
**恒** `[ENV] exit 2`:机器上只有 VS2019 生成工具(MSVC 14.29),没有该 exe。
**为什么只能走安装**:该 exe **没有免安装的独立发行版**——2026-10-27 实测 NuGet 两个候选包:
`Microsoft.Testing.Extensions.CodeCoverage` 18.12.0(109 个条目、**.exe 数为 0**,全是 managed dll,那是
给 MSTest / Testing-Platform 用的扩展库)与 `Microsoft.CodeCoverage` 18.11.0(只有老版
`build/netstandard2.0/CodeCoverage/CodeCoverage.exe`,**零个 Console**)→ `Microsoft.CodeCoverage.Console.exe`
只随 Visual Studio 本体分发(17.9+ / 18.x 的 `Common7\IDE\Extensions\Microsoft\CodeCoverage.Console\`)。
**安装入口与组件 ID**(`aka.ms/vs/18/release/…` 实测**不存在**(落到 Bing 的短链未找到页),`stable` 与
`insiders` 才有;组件 ID 由 `aka.ms/vs/18/stable/channel` → `VisualStudio.vsman`(18.11.12324.101,17 MB)
解析得到:`Microsoft.VisualStudio.PackageGroup.TestTools.CodeCoverage` 是 18.11 清单里**唯一**引用
`Microsoft.VisualStudio.TestTools.DynamicCodeCoverage` 与 `Microsoft.CodeCoverage.Console.Targeted`
(X86/X64/ARM64 三份 vsix)的组):
```powershell
# 管理员 PowerShell。本机实装在 E 盘——换机若装回 C 盘默认路径,下面的「非默认盘」警告即不适用
curl.exe -L -o "$env:TEMP\vs_buildtools18.exe" https://aka.ms/vs/18/stable/vs_buildtools.exe
& "$env:TEMP\vs_buildtools18.exe" --quiet --wait --norestart --nocache `
--installPath "E:\Program Files (x86)\Microsoft Visual Studio\18\BuildTools" `
--add Microsoft.VisualStudio.Workload.VCTools `
--add Microsoft.VisualStudio.PackageGroup.TestTools.CodeCoverage `
--includeRecommended
```
装完实测内容:`CodeCoverage.Console\Microsoft.CodeCoverage.Console.exe`(mscoverage 18.11.0-preview.26421.2,
**能脱离 vcvars 独立跑**,`collect -f cobertura` 就是门禁所用的那条命令)+ `Team Tools\Dynamic Code Coverage
Tools\CodeCoverage.exe`(老通道,脚本不用它)+ MSVC **14.44.35207 / 14.51.36231 / 14.52.36815**;Windows SDK
复用 C 盘现成的 `Windows Kits\10\10.0.26100`。**选 14.51 是为了与量 `scripts/test_coverage_baseline.json` 那台
同版本**(口径 §2 记 VS 2026 / MSVC 14.51)——覆盖率分母随编译器变(口径 §2.1 实测 `sha256.c`:gcc 105 行 vs
MSVC 114 行),**换编译器世代 = 换分母**;指错版本只会把「ENV 红」变成「假红」。
⚠ **装在非默认盘 = 门禁与 CI 都看不见,必须显式指机器级变量**:`scripts/ci_runner_bootstrap.ps1` 的
`Find-MsvcCl`(§2.4)与 `test/host/common/unit_test_harness.py` 的 `_default_vs_roots()` 都只由
`%ProgramFiles%` / `%ProgramFiles(x86)%` 派生根(本机 = C 盘),且 `Find-MsvcCl` **取首个命中**(候选表里
2019 排在最前)。好消息是 **`DTU_MS_COVERAGE` 不用设**:`scripts/check_test_coverage.py` 的 `_vs_roots()`
第一个候选是「`cl.exe` 上溯 8 级的 VS 根」——机器级 `DTU_MSVC_CL` 一指过去,同根下的标准相对路径就自动命中
该 exe(本机实测:40/40 套跑通、相对基线零回退)。。`-SetEnv` 现在也会把
`DTU_MS_COVERAGE` **显式写上**(探测顺序:先按选中的 `cl.exe` 反推同一套 VS 根,再遍历 vswhere 实例)——
**这两个变量要成对改**:它们指向同一套 VS 才是自洽状态;写死它的代价是将来卸掉 VS 时门禁会
硬失败并打印 `[ENV]`,而那正是我们想要的(宁可响亮地红,不要静默地绿)。
**本机三条命令(管理员 PowerShell,可整段粘贴)**:
```powershell
# ① 机器级 MSVC 指到 E 盘 VS2026 的 14.51(覆盖率分母与基线同版本)
[Environment]::SetEnvironmentVariable('DTU_MSVC_CL','E:\Program Files (x86)\Microsoft Visual Studio\18\BuildTools\VC\Tools\MSVC\14.51.36231\bin\Hostx64\x64\cl.exe','Machine')
# ② 重启 runner 守护进程(非 -Status 路径会重写看门狗、并先 Unregister 再 Register 计划任务,离线十几秒)
& 'D:\Code\DTU Code\STM32F4-Base\scripts\ci_runner_bootstrap.ps1' -StartNow
# ③ 只读体检;关键行 = MACHINE DTU_MSVC_CL = E:\...\14.51.36231\bin\Hostx64\x64\cl.exe
& 'D:\Code\DTU Code\STM32F4-Base\scripts\ci_runner_bootstrap.ps1' -Status
```
- ✅ **`-SetEnv` 的 MSVC 探测已重写**(同日)。旧版 `Find-MsvcCl` 只扫 `%ProgramFiles%` 派生的 C 盘根、
且**取首个命中**(候选表 2019 在最前)。本机 C 盘的 VS2019 已于 **2026-10-27 01:00** 被卸(`…\2019`
只剩空目录、C 盘 x64 `cl.exe` 实测 **0 个**),所以旧版在本机的真实行为**不是**“覆回 14.29”,而是
**找不到 → 只 Warn → 什么都不写**,机器级变量于是长期指着那条已失效的 C 盘路径(后果:CI 的 L0.5
走 `DTU_MSVC_CL` 显式通道 → 路径不存在 = 硬失败,不做自动回退,见 `unit_test_harness.py`)。
现在改成 `vswhere -all -products *` 取实例(**能看见非默认盘**)+ 工具集版本降序,并按 `-MsvcToolset`
**前缀钉住世代**(默认 `14.51.` = 与覆盖率基线同版本;钉不上会连 WARN 再回落最新,绝不静默),
`-MsvcLatest` 显式要最新。实测本机:候选 3 个(14.44 / 14.51 / 14.52,全在 E 盘)→ 默认返回
**14.51.36231**;`-Latest` 返回 14.52;`-MsvcToolset 99.` 走告警回落路径。
- ⚠ 但 `Set-MachineEnv` **没有 `-Force` 就不覆盖已有值**(保留 + 告警)——所以探测修好后,**首次仍要带
`-Force`** 才能把机器级变量从旧值换成新探到的路径;带 `-Force` 前**先看清它探到了哪套**(`-SetEnv`
会打印候选与命中),别盲刷。
- ⚠ 若 ③ 显示变量已对、而 CI 日志仍打印旧 `cl.exe`:计划任务服务的环境块没刷新,`Restart-Service Schedule`
后重跑 ②。**唯一算数的证据是 CI 日志**——L0.5 步骤会打印它实际使用的 `cl.exe` 来源。
- 装完 18 世代后 `vswhere` 会挑到最高版 → 模拟器与 `ui-golden` 的 CMake 生成器变成 `Visual Studio 18 2026`
(`simulator/build_sim.ps1` 已支持 16/17/18 三档),UI 相关改动按 `ui-verify` skill 重跑像素回归。
- 残留待核:口径 §7 第 8 条记的「Community 18.10 与 BuildTools 18.9 两份安装」**不在本机**——2026-10-27
`vswhere -all -products *` 实测本机 18 世代实例只有刚装的这套 E 盘 BuildTools(另一账户下是否有 per-user
安装,非提权读不到该账户 HKCU,**未证**)。→ 「CI runner 与本机同一台」这句要按**量基线那台可能是另一台
机器**来理解;换机或换账户一律先 `check_test_coverage.py --list` 再跑全量。
---
## 5. 仓库端:workflow 文件
@@ -923,38 +1002,69 @@ on:
- 纯文档改动**不触发**(省 runner);
- 想手动跑一次:仓库 → Actions → 选 `CI` → **Run workflow**。
### 5.3 内网无公网时的 action 拉取(重要)
### 5.3 内网无公网时的 action 拉取:本仓库已彻底移除 `uses:`(2026-10-27 落地)
workflow 用了 `actions/checkout@v4`。**默认从 `github.com` 拉取**(见 §1.2.1;
不是从你自己的实例,也不是从 gitea.com —— 这点很反直觉);实例不通
github.com 会在 checkout 步骤卡住/失败。三种处置:
**为什么非自己动不可**:`uses:` 的解析发生在 **`Prepare all required actions`** 阶段——**先于任何步骤**、
无条件联网(默认 github.com,见 §1.2.1),而且**每个 job 各自解析进自己的 `.cache/act/<hash>`,跨 job 不
复用**。这条链在本机实测不稳:run 36(10-21)/ 41(10-25)/ 42(10-26,分支 `IEC60870`)三次都是 **~45 s
后整 job 死亡、零钩子输出**,而同一分钟手工 `curl` 同一地址返回 200——**手工可达 ≠ CI 可用**(解析器跑在
SYSTEM 下)。Gitea 1.25.4 又去掉了自定义 `DEFAULT_ACTIONS_URL`(§1.2.1 表第三行),服务端没有可拧的键,
**唯一归本仓库管的修法就是不再依赖它**。
1. **镜像到自有实例**:`DEFAULT_ACTIONS_URL = self`,**并先把 `actions/checkout` 导入你自己的 Gitea**;
2. **指向可达镜像**:`DEFAULT_ACTIONS_URL` 指向某个能访问的 action 站(或在 workflow 里
直接写全路径 `uses: https://<host>/owner/repo@ref`);
3. **零依赖**(内网最稳,推荐):把 checkout 换成纯 git 命令,不依赖任何 action:
**现状**:两个 job 的第 1 步都是原生 git 检出,workflow 里 `uses:` **归零**。检出正文**内联**在 workflow
里,**不能**抽成 `scripts/*.ps1`——它执行时脚本自己还没被检出(鸡生蛋),这与 `ci_find_python.ps1` /
`ci_gitlint_range.ps1` 的抽取方向**相反**;代价是两个 job 各持一份、**必须逐字一致**(自查见本节末)。
```yaml
- name: Checkout
shell: pwsh
run: |
git init .
git remote add origin $env:GITHUB_SERVER_URL/$env:GITHUB_REPOSITORY.git
git fetch --depth 1 origin $env:GITHUB_SHA
git checkout $env:GITHUB_SHA
**这份检出做了什么**(等价 `checkout@v4` + `fetch-depth: 0`):`init` 或复用现有 `.git` → `remote
set-url origin <server_url>/<repository>.git` → 完整 refspec `+refs/heads/*:refs/remotes/origin/*` 全量
`fetch --prune --no-tags` → 若事件 sha 不在分支 refspec 覆盖内(PR 合并提交等)再显式 fetch 事件 ref →
**验证 sha 真在历史里**(不在就 `::error::` 硬失败,**绝不默默构建一棵错的树**)→ `checkout --detach` →
`reset --hard` → `clean -ffd`。
四个刻意的选择(每条都有实测依据):
| 选择 | 为什么 |
|---|---|
| `clean -ffd`,**不是** `-ffdx` | `-x` 会删**被忽略**的文件,而 runner 工作区跨运行复用它们(pip / pre-commit 缓存)。实测:放一个 gitignored 的 `ci-artifacts/kept.log` + 一个未跟踪垃圾 + 改一个跟踪文件 → 重跑后 `dirty_files=0`、垃圾被清、**被忽略文件原样保留** |
| `GIT_TERMINAL_PROMPT=0` + `-c credential.helper=` | 否则一次认证挑战会让 git **等人类输入**,而 runner 没有控制台 → 白占满 30 分钟 job 超时、表现得像“CI 挂了”。实测灌一个坏令牌:`fatal: unable to get password from user` → **rc=1 立即失败**,不挂起 |
| 令牌只走 per-invocation `-c http.extraheader` | 不写进 `.git/config`、不回显。匿名也能拉(仓库公开:`/info/refs?service=git-upload-pack` 实测 200)→ CI 不因此新增“令牌必须可用”的依赖 |
| 不抓 tag | 本仓库 tag 数 = 0,且无任何脚本用 `git describe --tags`(2026-10-27 实测);要用时再加 |
> 🔴 **旧版本节给过一段“可以直接抄”的示例,里面两条都是坑,别再照抄**:① `shell: pwsh` —— runner 的
> PATH 里没有 `pwsh`,会直接 `Cannot find: pwsh in PATH`,job 在跑任何业务逻辑之前就失败(本文件头部
> 已写死只能 `shell: powershell`);② `git fetch --depth 1` —— 浅克隆让 `scripts/ci_gitlint_range.ps1`
> 的 `base..head` 和任何需要历史的门禁报 unknown revision。至于方案 1/2(`self` 需先把 action 镜像进
> 实例、自定义 URL 在 1.25.4 无效),都不如**直接不依赖**。
**实测六项**(临时工作区、内层一律 `powershell.exe` **5.1** 忠实复现 runner“写成无 BOM .ps1 再执行”的方式;
关键一步是 **E:把正文从 `ci.yml` 里抽出来跑**,测的就是文件里那份字节):
| # | 场景 | 结果 |
|---|---|---|
| A | 全新工作区、匿名无令牌 | rc=0:`commits=665`、refspec 完整、分离 HEAD、远端分支 4 / tag 0、`dirty_files=0` |
| B | 复用工作区(改跟踪文件 + 未跟踪垃圾 + gitignored 文件) | rc=0:`dirty_files=0`、垃圾清掉、**ignored 保留**、`17ece86..HEAD` 解出 **18 个提交**(恰等于上游那 18 笔 → gitlint 的 range 可用) |
| C | 目标 sha 不在远端(未推的本地提交) | **rc=1** `::error::<sha> is not present in the fetched history`,且 HEAD 纹丝不动 |
| D | 灌一个坏令牌 | rc=1,`fatal: unable to get password from user`(**快速失败**,没挂住) |
| E | 执行**从 `ci.yml` 抽出的**正文 | rc=0、与 A 同数;**零 ErrorRecord 噪声**;首次全量拉取 172 s(18.65 MiB / loopback) |
| F | 同一工作区再跑一次 | rc=0、秒级——**稳态 CI 不重复付全量代价**,只有工作区全新时才慢 |
> ⚠ **一条未证的边界**:`pull_request` 也是本 workflow 的触发器,而 PR 事件的 `github.ref` 是
> `refs/pull/<n>/merge`,**不在**我用的分支 refspec 内 → 会走「再显式 fetch 事件 ref」那条兜底。
> 这台实例**至今 0 个 PR**(库里 `pull_request` 计数 = 0),`git ls-remote 'refs/pull/*'` 也返回空,
> 所以「Gitea 1.25.4 到底提不提供 merge ref」**在这里无法证实**。真撞上的话**症状是响亮的**:
> `::error::<sha> is not present in the fetched history` + rc=1,**不会**默默测一棵错的树;届时的
> 兜底是改 fetch `refs/pull/<n>/head`(gitlint 的 base 本来就是 `github.event.pull_request.base.sha`,
> 不受影响)。同一条备注也适用于 `${{ github.server_url }}`:原生检出与 `checkout@v4` 用的是**同一个**
> server_url 拼法,**可达性假设没有变化**——变的只是不再联网 github.com。
**自查(改 workflow 前后都跑,第二条是内联换来的同步义务)**:
```powershell
# 1) uses: 必须为 0(注释里的不算)
Select-String -Path '.gitea/workflows/ci.yml' -Pattern '^\s*-?\s*uses:'
# 2) 两个 job 的第 1 步正文必须逐字一致
& '.venv\Scripts\python.exe' -c "import yaml;d=yaml.safe_load(open('.gitea/workflows/ci.yml',encoding='utf-8'));a=[j['steps'][0]['run'] for j in d['jobs'].values()];print('JOBS=%d CHARS=%d' % (len(a),len(a[0])));print('OK identical' if len(set(a))==1 else 'DIFFERS -- keep in sync (see docs/CI guide 5.3)')"
```
> 方案 3 无需任何外部 action 与任何 `DEFAULT_ACTIONS_URL` 配置;代价是失去 checkout 的缓存优化
> (对本仓库影响很小)。
> 参考:Gitea FAQ「Where will the runner download scripts when using actions such as `actions/checkout@v4`?」
**本机环境判断(2026-10-02)**:Gitea 装了 `OFFLINE_MODE = true`,说明这台机器的外网访问
**可能受限**。如果 runner 拉不到 github.com,checkout 步骤会卡住。
> ⚠️ 若走方案 1:**`self` 是关键字,不是写你的域名**(见 §1.2.1)。
**建议**:**先按原样试一次**。若 checkout 步骤报网络错误,再换成上面的零依赖写法(方案 3)。
### 5.4 产物存档与结果汇总(2026-10-02 起)
**目标**:CI 失败时**不查 MySQL / 不翻 runner 磁盘**,在 Gitea 页面即可看到完整输出与摘要。
@@ -988,8 +1098,8 @@ github.com 会在 checkout 步骤卡住/失败。三种处置:
| job | `runs-on` | 步骤要点 |
|---|---|---|
| `l0-gates` | `windows` | checkout(`fetch-depth: 0`,gitlint/cubemx 需完整历史)→ 环境自检(**Python 探测走 `scripts/ci_find_python.ps1`**、cppcheck 按 `DTU_CPPCHECK` → PATH → 标准安装目录,缺则 `::error::` + exit 1)→ `pre_commit install --install-hooks` → **提交信息门禁 `scripts/ci_gitlint_range.ps1`**(见 §7 排障表"钩子条数对不上"行)→ **决定跳过哪些钩子**(探测 `.map`,写入 `GITHUB_OUTPUT`;无 `.map` 时 `::warning::` 声明体积门禁未跑)→ `pre-commit run --all-files --show-diff-on-failure`(`env: SKIP`,输出 `Tee` 到 `ci-artifacts/l0-pre-commit.log`)→ 打印 `::notice::L0: passed/failed/skipped` 汇总行 → 归档日志说明 |
| `l05-host-unit-tests` | `windows` | checkout → **探测 MSVC**(按 VS_ROOTS 找 `Hostx64\x64\cl.exe`)→ 遍历 `test/host/**/run_unit_test.py` 逐个跑(输出 `Tee` 到 `ci-artifacts/l05-<模块>.log`;解析各模块 `N/M PASSED` 汇总行 → 聚合 `::notice::L0.5: K runner(s), A/B assertions passed`;失败 `::error::` 并置退出码);**未探测到 MSVC 时打印醒目 WARNING 并降级跳过**(不硬失败,避免"没装 VS 的 runner 全红") |
| `l0-gates` | `windows` | **原生 git 检出**(全量 refspec,不再用 `actions/checkout`;见 §5.3)→ 环境自检检(**Python 探测走 `scripts/ci_find_python.ps1`**、cppcheck 按 `DTU_CPPCHECK` → PATH → 标准安装目录,缺则 `::error::` + exit 1)→ `pre_commit install --install-hooks` → **提交信息门禁 `scripts/ci_gitlint_range.ps1`**(见 §7 排障表"钩子条数对不上"行)→ **决定跳过哪些钩子**(探测 `.map`,写入 `GITHUB_OUTPUT`;无 `.map` 时 `::warning::` 声明体积门禁未跑)→ `pre-commit run --all-files --show-diff-on-failure`(`env: SKIP`,输出 `Tee` 到 `ci-artifacts/l0-pre-commit.log`)→ 打印 `::notice::L0: passed/failed/skipped` 汇总行 → 归档日志说明 |
| `l05-host-unit-tests` | `windows` | **原生 git 检出**(与 `l0-gates` 第 1 步逐字一致,内联带来的同步义务,自查见 §5.3 末)→ **探测 MSVC**MSVC**(按 VS_ROOTS 找 `Hostx64\x64\cl.exe`)→ 遍历 `test/host/**/run_unit_test.py` 逐个跑(输出 `Tee` 到 `ci-artifacts/l05-<模块>.log`;解析各模块 `N/M PASSED` 汇总行 → 聚合 `::notice::L0.5: K runner(s), A/B assertions passed`;失败 `::error::` 并置退出码);**未探测到 MSVC 时打印醒目 WARNING 并降级跳过**(不硬失败,避免"没装 VS 的 runner 全红") |
> 🔴 **两条 Python 探测链必须同序同步**:本地 `scripts/ci_local.py` 与 CI 侧
> `scripts/ci_find_python.ps1` 的候选顺序要一致。2026-10-11 那次 L0 全灭,就是 workflow
@@ -1064,7 +1174,7 @@ date +%s # 对比 last_online
| push 后 Actions 页**空白/无任务** | ① 服务端开关未开 ② workflow 没 push 到远程 ③ 只改了 `docs/**`(被 `paths-ignore` 滤掉) ④ **新建分支的首次 push**(实测 2026-10-12:创建 `v2-on-ui-ci` 那次 push 没产生 run——runner 在线空闲、job 日志零新增;紧接着一次普通 push 立刻出 task。原因:`before` 是全零 SHA,变更文件集算不出来,`paths-ignore` 求值时被滤空) | ①②③ 逐条核对 §1 / §5.1 / §5.2;④ 用 `workflow_dispatch` 手动触发,或再推一个非文档路径的提交 |
| 任务一直 **Waiting / 排队** | 没有 label 匹配 `windows` 的 runner | 检查 `--labels "windows:host"`;Settings→Runners 是否 Idle |
| `l0-gates` 报 `::error::runner 缺少:Python, cppcheck` | runner 没装或不在 PATH | 装完后**重启 runner 服务**(新进程才吃到新 PATH) |
| `checkout` 步骤卡住/超时 | 拉不到 `actions/checkout@v4`(内网) | 见 §5.3 |
| `Prepare all required actions` 或 `checkout` 卡住/超时(~45 s 后整 job 亡、零钩子输出) | 有人把 `uses:` 加回了 workflow —— 该阶段**先于任何步骤**无条件联网,手工 curl 可达不代表 runner 可达 | 本仓库 2026-10-27 起已无 `uses:`(§5.3 有六项实测与自查命令);自查:`Select-String .gitea/workflows/ci.yml -Pattern '^\s*-?\s*uses:'` 应为空 |
| `l05` 日志出现 `::warning::runner 未安装 MSVC` | VS 装在非默认路径或未装 | 见 §4.2(默认路径 / 同步改两处根目录) |
| `mem-numbers` 显示 `Skipped` | 正常(CI 无 Keil,无 `.map`) | 无需处理;但注意 **CI 未守体积门禁**(见 §0.0),改代码后本地 `@build` + `check_mem_numbers.py --scan` 兜底 |
| L0.5 显示 `2 runner(s), X/Y ...` 但 X<Y | 有单测用例未通过 | 看该模块的 `::error::unit test failed` 行与其 `ci-artifacts/l05-<模块>.log` |
@@ -1072,6 +1182,7 @@ date +%s # 对比 last_online
| `Tee-Object` 步骤报 `TerminatorExpectedAtEndOfString` / 字符串缺少终止符 | `run:` 块混入了非 ASCII(GBK 解码错位) | 见文件头 🔴🔴;`run:` 内必须纯 ASCII |
| `l05` 报 `fatal error C1083: 无法打开包括文件 "stdio.h"` | Windows SDK 缺失 | 装 VS 时勾"使用 C++ 的桌面开发"+ SDK |
| 单测退出码 2 | 环境错误(多为无 MSVC) | 同上 |
| `test-coverage` 恒 Failed、日志写 `[ENV] 找不到 Microsoft.CodeCoverage.Console.exe`;或换了 VS 世代后覆盖率大面积「假红」 | 该机没有 VS 17.9+/18.x(**该 exe 只随 VS 分发**,NuGet 拿不到);或 VS 装在非默认盘而机器级 `DTU_MSVC_CL` 仍指旧世代 | §4.4(安装入口 + 组件 ID + 三条可直接粘贴的命令 + `-Force` 陷阱) |
| `pre-commit` 首次运行久 | 首次要下载各钩子的环境 | 正常,runner 缓存后会快 |
| `daemon` 启动报 **connection refused / dial tcp** | `--instance` 填了不可达地址 | 删 `.runner` → 重新 `register`,`--instance http://127.0.0.1:3001` |
| 启动报 **token** 相关错误 | Registration Token 过期(约 1 小时)或已用过 | 回网页重新创建 runner 拿新 token,重新 `register` |
@@ -1310,6 +1421,10 @@ powershell -ExecutionPolicy Bypass -File scripts\ci_runner_bootstrap.ps1 -DryRun
# 兜底通道(只有变量没设时才会走到):把 pre-commit 装进【全局】site-packages,见 §4.1.1
# ⚠️ 改过任何环境变量都必须 Stop 再 Start 任务——环境块是任务启动时的快照
# 5b) 覆盖率工具链(mscoverage)/ 把机器级 MSVC 指到 VS2026 —— 见 §4.4
# 安装入口、组件 ID、三条可直接粘贴的管理员命令都在 §4.4;本机 VS 装在 E 盘,
# 非默认盘必须显式设 DTU_MSVC_CL(否则门禁与 CI 都看不见它)。别加 -Force
# 6) 复验(-s = 模拟 SYSTEM 视角,必须过)
& 'C:\ProgramData\Anaconda3\python.exe' -s -m pre_commit --version # pre-commit 4.6.2
@@ -1352,6 +1467,8 @@ powershell -ExecutionPolicy Bypass -File scripts\ci_runner_bootstrap.ps1 -DryRun
| 日期 | 变更 |
|---|---|
| 2026-10-27 | ✂️ **CI 侧两处根治 + 更正我上一条的一处推断**:① **workflow 去掉 `uses:`**——两个 job 的第 1 步换成原生 git 全量检出(完整 refspec + 分离 HEAD + `reset --hard` + `clean -ffd`、令牌只走 per-invocation `-c http.extraheader`、`GIT_TERMINAL_PROMPT=0` 防挂死、目标 sha 不在历史里就 `::error::` 硬失败而不是默默构建错树)。动因是 run 36/41/42 三次 **`Prepare all required actions` 阶段 ~45 s 外网超时把整个 job 打死、零钩子输出**,而同一分钟手工 `curl` 返回 200。§5.3 整节重写,含六项实测(A/B/C/D/E/F——关键是 E:**把正文从 `ci.yml` 里抽出来跑**,测的就是文件里那份字节)与两条自查命令;§7 的 checkout 行同步改为“看见此症状=有人把 `uses:` 加回来了”。② **`scripts/ci_runner_bootstrap.ps1` 的 `Find-MsvcCl` 重写**:`vswhere -all -products *` 取实例(**能看见非默认盘**)+ 工具集版本降序 + `-MsvcToolset` 前缀**钉住世代**(默认 `14.51.`,与覆盖率基线同版本;钉不上连 WARN 再回落最新,绝不静默)+ 新增 `-MsvcLatest`;`-SetEnv` 增写 `DTU_MS_COVERAGE`(须与 `DTU_MSVC_CL` 成对改)。③ **更正**:先前写“旧探测会把机器级变量覆回 C 盘 14.29”——本机实情是 C 盘 VS2019 已于 **01:00 卸载**(`…\2019` 只剩空目录、C 盘 x64 `cl.exe` 实测 0 个),旧 `-SetEnv` 在本机是**找不到 → 只 Warn → 不写**,于是变量一直指着失效路径(CI 的 L0.5 走显式通道即硬失败,不做静默回退)。实测新逻辑:候选 3 个(14.44/14.51/14.52,全在 E 盘)→ 默认命中 **14.51.36231**,`-Latest` 给 14.52,`-MsvcToolset 99.` 走告警回落。④ §1.2.1 表第三行按 1.25.4 实际行为改正(自定义 `DEFAULT_ACTIONS_URL` 取值已无效)——旧写法会把下一个排障的人引向一个不存在的开关 |
| 2026-10-27 | 🧯 **拆穿「L0 恒红 = 环境噪声」这个想当然 + 覆盖率工具链补齐**:① `cppcheck` 恒红**不是**版本漂移——本机与 runner 同机、同 exe(2.21.0)、同参数复现即红,两根 `uninitializedVariable`(`s_cmd_con` 在空信息体分支把未初始化 `body` 传下去、`s_spont_step` 的 `blen` 作用域过宽)是真代码问题,已在 `ci/remote-ci` 的 `5869c03` 修掉(`@build` 0/0,`Code 231712→231732`)。② 新增 §4.4:`Microsoft.CodeCoverage.Console.exe` **只随 VS 分发**(NuGet 两候选包实测 .exe 数为 0 / 只有老版 `CodeCoverage.exe`),本机补装 VS2026 BuildTools 18.11 于 **E 盘**后仍因非默认盘不被看见,须显式机器级 `DTU_MSVC_CL`(指 14.51 = 与 `scripts/test_coverage_baseline.json` 同版本);三条管理员命令、`-Force` 陷阱、`Restart-Service Schedule` 判据一并写入。③ 排障教训:**页面时长与本地对不上时以 `commit_status` + runner 日志为准**——页面那条「L0 Failing after 6m49s」在库里是 run 26 的 409 s(cppcheck 真红),同一 sha 另有一次 5 s 的 checkout 超时,**两者不是一回事**,别拿一个解释另一个 |
| 2026-10-25 | 🔒 **A-5 残余销项联动(§0.0 重写)**:体积门禁由"CI 不守"改为**同码凭证强制**——本地 `@build` 0/0 后 `ram_baseline.py --update` 盖 `scripts/size_attestation.json`,L0 作业末尾 `check_size_gate.py --ci` 复核同码性 + 红线复算(runner 无需 Keil);主干 push 拦红、其余事件 warn-only;`mem-budget` 补进 SKIP 名单诚实显示。指纹取 git 索引 blob(跨 checkout 稳定,修 7c4628b);三项仍守不住的口径唯一正文 = 脚本 docstring |
| 2026-10-12 | 🐛 **第一次真跑出 run,当场暴露 gitlint 门禁空转**:把本轮修复推到新分支 `v2-on-ui-ci` 后拿到 task 31/32——**L0.5 成功**(`10 runner(s), 647/647 assertions passed, fail=0`,与本地逐字一致),**L0 失败**却与提交信息毫无关系:`exit code: 254` + `Missing git configuration: please set user.name`。根因是 `actions/checkout` 把 HOME 指向每次任务的临时目录、SYSTEM 账号没有 git 身份,gitlint 在读到规则之前就配置错误退出;旧脚本还把失败原因**写死**成"标题超 100 字符",加上该步 `exit 1` 让后面 26 个钩子**一个都没跑**。修法:脚本先探测身份,缺失就用 `GIT_CONFIG_COUNT/KEY/VALUE` 注入一次性身份(不写文件、不碰开发者机器),并区分 254(门禁没跑起来 → `exit 2`)与真实违规(→ `exit 1`)。另记一条新坑:**新建分支的首次 push 不产生 run**(§7 排障表 ④) |
| 2026-10-12 | 🔀 **随 `master` 合入 `v2-on-ui`:数字口径改为分支中立**。本指南原先写死 master 的数字(18 钩子 / 17 报告、3 runner / 288 断言),而 `v2-on-ui` 是 **26 钩子 / 10 套件**——同一篇文档在两个分支上必然有一个是错的。§6 验收第 8 行、§7 排障表、§10 验收标准三处改为讲规则(**报告数 = 定义数 − 1**;**runner 数 = `test/host/` 下 `run_unit_test.py` 个数**)+ 分分支实测快照。本次合并只带文档与脚本,**零 C 代码改动**;合并后本分支 `@build` 0 错 0 警、`ci_local.py` 全绿(L0 25 钩子 + L0.5 10 runner / 647 断言) |
@@ -163,6 +163,11 @@
16:17,都是各自安装当日,不是后来补勾)→ VS 18 的 BuildTools 默认就带它。真实风险面只剩:没有
VS 只有裸 MSVC 的机器、`DTU_MS_COVERAGE` 指错、以及靠 PATH 兜底的环境。CI runner 与本机同一台
(`docs/CI/README.md`);**换机仍先跑 `--list` 再跑全量**。
⚠ **2026-10-27 实测补注(换机场景真发生了)**:这台 runner 机当日 `vswhere -all -products *` 只有
VS2019 生成工具(MSVC 14.29)→ 上面「CI runner 与本机同一台」这句对**量基线那台**不成立。补装 VS2026
BuildTools 18.11 后仍因装在 E 盘(非默认盘)被门禁与 CI 同时看不见,最终靠显式机器级 `DTU_MSVC_CL` 指
14.51(与量基线同版本)跑通 40/40 套、零基线回退。安装入口、组件 ID、三条命令与 `-Force` 陷阱统一记在
`docs/CI/远程CI完整指南.md` §4.4——**本处不重复,免得两处漂移**。
9. **并集口径下"单套百分比"没有意义。** 同一文件被多套触及很常见(`sha256.c` 被 `sha256` 与
`ftp_auth` 都编译),所以对外只报**并集**,单套只报"触及几个文件 / 多少行"。