7 Commits

Author SHA1 Message Date
wandering1 529c58da39 docs(CI): 修 run 46 暴露的 doc-links 违规,并更正「CI 工作区跨 run 复用」错话
CI / L0 静态门禁 (pre-commit) (push) Successful in 5m8s
CI / L0.5 主机侧单测 (MSVC) (push) Successful in 2m15s
run 46(你推的 dc5d6a0):L0.5 = Success,40 runner、3188/3188 断言、fail=0;
L0 = 47 Passed / 1 Failed / 2 Skipped —— A 方案确实生效:`ui-golden` 在 CI 里第一次
真编模拟器、真比像素,菜单 8 状态 + 内容页 40 页全过转绿(SYSTEM 命中机器级
`DTU_SDL2_DIR`)。你补跑的 -SetEnv 顺带把 `DTU_MS_COVERAGE` 也钉上了,两个变量成对。

剩下那 1 根红是我自己造的:新加的症状行里写了 gitignored 的构建产物路径,本机
存在、干净检出不存在,于是 `doc-links`「本地过、CI 挂」——正是本文 §4.1.2 记的那
一坑,我一边写它一边踩了它。修法按工具给的提示走(不把不入库的运行时路径当
引用写)。验证没有用本地:在一份**从未构建过模拟器**的干净克隆里先复现(1 处
问题、同一行),再验证修完(0 问题、RC=0),那才是 CI 的处境。

撤回两句错话:我先前写「毒缓存会跨 run 存活」「`-ffd` 是因为 runner 工作区跨运行
复用被忽略文件」——对 CI 都不成立。实测 run 44 / 45 / 46 的 L0 工作区目录哈希互不
相同,**每个 job 一个全新目录**,各付 137~161 s 全量拉取(612 s 的 run 里约 5.4 分钟
是两次拉取)。自愈检查仍然必要,但真正的暴露面是**同一个 job 内**(`ui-golden` 与
板端导航自检共用同一份构建目录,前者中断会毒到后者)以及本机日常开发。§4.5、
§5.3 的 `-ffd` 依据、§7 行与 `ci.yml` 注释四处同步改成实测口径;变更记录按规矩不动
历史行,只在上方加更正行。

自查:两个 job 的检出正文仍逐字一致(7147 字符)、run 块非 ASCII 计数 0、`uses:` 0;
本地暂存钩子 HOOKS_RC=0;干净克隆里 `doc-links` 0 问题、`check_ids` 不可判定 1166 =
上限(未增)。预期这一笔推上去 L0 归零失败——那是自 run 23(10-12)以来第一次
两个 job 全绿。
2026-10-27 17:52:53 +08:00
wandering1 dc5d6a0680 fix(ci): ui-golden 需要机器级 SDL2 SDK——补 DTU_SDL2_DIR、配置期护栏与 build 缓存自愈
CI / L0 静态门禁 (pre-commit) (push) Failing after 7m4s
CI / L0.5 主机侧单测 (MSVC) (push) Successful in 3m6s
2026-10-27 17:32:35 +08:00
wandering1 ae80bb8e34 fix(ci): run 块按点源语义加固——原生 stderr 变终止性错误曾致两 job 3 秒亡
CI / L0 静态门禁 (pre-commit) (push) Failing after 6m43s
CI / L0.5 主机侧单测 (MSVC) (push) Successful in 2m39s
run 44(本分支上一次推送)把上一版原生检出打回原形:L0 用了 3 s、L0.5 用了
2 s 双双失败,日志里是 error: No such remote 'origin' 被包成 NativeCommandError
后整步终止。根因在我上一版:runner 执行 run: 块的方式是 powershell -command
点源那个文件,会话继承的 $ErrorActionPreference 是 Stop,于是原生程序往 stderr
写的任意一行都会被 PS 5.1 包成终止性错误。我写的 git remote get-url origin
2>$null 正是这一类,而 2>$null 不算摊平,反而让它先把整步炸掉。

修法两件事一起做:块首显式 $ErrorActionPreference = Continue(不依赖继承);
所有 git 调用走同一个漏斗 Invoke-DtuGit,把 stderr 摊平成字符串再决定是否打印。
探针顺手换成 git config --get remote.origin.url:远端不存在时它只返回非零码、
不往 stderr 写,从源头不再触发这类错误。

更阴的一条教训是我的测法本身。第一版复现器用 -File 包一层点源,结果四条失败
路径全测成 rc=0。用最小矩阵把语义钉死后:exit 1 写在被点源的文件里时,-File
直跑 = 1;-File 包一层点源 = 0 且继续执行后续语句;-Command 点源(runner 的真
实用法)= 1。也就是说只要我一直用 -File 测,检出失败也会一路伪装成绿,这个洞
比上面那个 bug 更坏。现在统一用 runner 的真实语义测:powershell -NoProfile
-Command 点源块体文件,且前置 $ErrorActionPreference = Stop。

换测法后的实测(全部跑在从 ci.yml 抽出的那份字节上,不是另抄的副本):
- 点源 + Stop、全新工作区:ExitCode=0、checkout ok: c1e7e8c / commits=669、
  dirty_files=0、150.9 s(修复前同一模式就是 run 44 的 3 秒亡);
- DTU_REPO_SHA 空 / DTU_REPO_URL 空 / sha 不在远端 / 坏令牌:ExitCode 均 1,
  坏令牌 0.5 s 返回 unable to get password,不挂起、不烧 job 超时;
- 复用工作区:1.6 s,dirty_files=0,被忽略文件保留,未跟踪垃圾清除;
- 结构面:13 步、两 job 第 1 步逐字一致、uses: 计数 0、run 块非 ASCII 计数 0、
  无 BOM 无 CR;check yaml Passed;check_doc_links --all 180 篇、无法判定 0。

文档同笔落地(教训要跟它救的那段代码一起进历史):指南 §5.3 补 run 44 实录与
退出码矩阵、实测表补 G/H 两行、§7 补 job 几秒就亡一行、两份变更记录各加一行;
remote-ci-ops skill §10 补 run: 块的正确测法。另记一个运维事实:推到已存在
的远端分支会正常建 run(run 44 即如此,推送后约 4 分钟入库,我先前把轮询窗口
取得太短误判成没建),以后验 workflow 不再需要手动 dispatch 与服务凭据。
下一轮可能红的两处我先标出来,红了正是我们要的证据:SYSTEM 下的 ui-golden
(历史上败在 32 位 MSBuild 与 VCTargetsPath)与 test-coverage(缺
DTU_MS_COVERAGE,走自动发现)。
2026-10-27 12:52:56 +08:00
wandering1 c1e7e8c42c fix(104): 拆掉四处恒假的上界判断——合法带只改判下界,口径不变
CI / L0 静态门禁 (pre-commit) (push) Failing after 3s
CI / L0.5 主机侧单测 (MSVC) (push) Failing after 2s
L0 的 cppcheck 在 --all-files 下恒红四处,全在同一文件:
App/protocol/iec104/iec104_comm.c 的 80 / 94 / 107 / 121 行,诊断为
compareValueOutOfTypeRangeError —— 拿 unsigned short 与 65535 比较,
Condition is always false。根因是 IEC104_COMM_CA_MAX 与 PORT_MAX 都等于
65535u,而被比较的量是 uint16_t:那个上界判断在类型上就不可能成立。
这四处随上游 iec104_comm 落地进来,本地提交只跑暂存文件所以没人看见,
CI 跑 --all-files 就每轮都红(同族盲区:暂存面与全量面不等价)。

处置走「语义不动」那一支(用户裁定 2026-10-27:A):只删恒假的半边、
改判 if (v < MIN),合法带口径一字未改(仍照 iec104_comm.h 的常量注释:
ca 1..65535、port 1024..65535),只在第一处写清为何不再写上界、其余三处
短引用。没有把 MAX 改成 65534(按 60870-5-104 的 65535 = 未编址),因为
test/host/iec104/iec104_test.c:1521 明确断言 ca 设成 IEC104_COMM_CA_MAX
必须被接受——那会同时打断这条断言并改变现场设备行为(配 65535 的站会
突然回退工厂默认),属协议决定,不在本次范围;要改请单独裁。

验证:单文件 cppcheck 原样复刻钩子参数 → RC=0;cppcheck --all-files 由
红转绿;iec104-unit-test Passed(即 65535 那条合同照过);@build 0 错误
0 警告(Code 231732→231720,减 12 B,RO/RW/ZI 不变);凭证与两份台账
随之重盖(ER_IROM1 257,292→257,280、Flash 合计 258,812→258,800);
本次触发的 13 根钩子全 Passed,含 test-coverage(删掉 4 行已覆盖代码
未被判成回退)。
2026-10-27 10:34:39 +08:00
wandering1 51ecd36192 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。
2026-10-27 08:01:40 +08:00
wandering1 2d18193d38 fix(ci): 去掉 actions/checkout 外网依赖,MSVC 探测改 vswhere 并钉住工具集世代
两个 job 的第 1 步原先是 `uses: actions/checkout@v4`。`uses:` 的解析发生在
Prepare all required actions 阶段——先于任何步骤、无条件联网(默认 github.com),
且每个 job 各自解析进自己的 .cache/act/<hash>、跨 job 不复用。这条链在本机实测
不稳:run 36(10-21)/ 41(10-25)/ 42(10-26)三次都是 ~45 s 后整 job 死亡、
零钩子输出,而同一分钟手工 curl 同一地址返回 200——手工可达 ≠ CI 可用(解析器
跑在 SYSTEM 下)。Gitea 1.25.4 又去掉了自定义 DEFAULT_ACTIONS_URL,服务端没有
可拧的键,唯一归本仓库管的修法就是不再依赖它:两个 job 第 1 步换成原生 git 全量检出。

关键选择(依据与实测表见 docs/CI/远程CI完整指南.md 同文 §5.3,此处只记结论):
- 完整 refspec + 全量,不是 --depth 1:提交信息门禁要解 base..head 的 range;
  且目标 sha 不在历史里就 ::error:: 硬失败,绝不默默构建一棵错的树;
- clean -ffd 而非 -ffdx:-x 会删被忽略文件,而 runner 工作区跨运行复用它们;
- GIT_TERMINAL_PROMPT=0 + 空 credential.helper:否则认证挑战会等人类输入,
  把 30 分钟 job 超时白烧掉,表现得像 CI 挂了;
- 令牌只走 per-invocation -c http.extraheader(Basic + x-access-token,与
  checkout@v4 同拼法),不落 .git/config、不回显;仓库公开时匿名即可;
- 不抓 tag:本仓库 tag 数 0、无脚本用 describe --tags(实测)。

另修 scripts/ci_runner_bootstrap.ps1 的 Find-MsvcCl。旧版只扫 %ProgramFiles%
派生根且取首个命中:本机 C 盘 VS2019 已于今日 01:00 卸载(C 盘 x64 cl.exe 实测
0 个),旧版因此是「找不到 → 只 Warn → 不写」,机器级变量一直指着那条失效路径,
而 harness 对「显式设置但无效」是硬失败、不做自动回退。现在改为 vswhere 取实例
(能看见非默认盘)+ 工具集版本降序 + -MsvcToolset 前缀钉住世代(默认 14.51.,
与覆盖率基线同版本;钉不上连 WARN 再回落最新,绝不静默)+ -MsvcLatest;
-SetEnv 与 -Status 同步纳入 DTU_MS_COVERAGE。

验证(内层一律 powershell.exe 5.1,忠实复现 runner 执行方式):临时工作区六项
A/B/C/D/E/F——E 是执行**从 ci.yml 里抽出的正文**,测的就是文件那份字节。
A rc=0、commits=665、refspec 完整、分离 HEAD;B 复用工作区后 dirty_files=0、
被忽略文件保留、17ece86..HEAD 解出 18 个提交;C rc=1 且 HEAD 不动;D 坏令牌
fatal: unable to get password 秒失败;令牌改造后再跑 ANON/BOGUS/ANON2=0/1/0。
ci_local 全量:L0.5 = 40 runner、3188/3188 断言、fail=0(MSVC 14.51);
test-coverage 与 ui-golden 均 Passed。.ps1 非 ASCII 字节 0、语法错误 0、无 BOM、
LF;ci.yml 无 BOM 无 CR、13 步、0 个 run 块含非 ASCII、uses: 计数 0。
2026-10-27 08:00:14 +08:00
wandering1 5869c0307b fix(104): 消掉 cppcheck 两根常驻红——回显不再传未初始化缓冲、blen 收作用域
L0 静态门禁(pre-commit)历史 36 次启动里 28 红 / 7 绿 / 1 取消,最后一次绿是
10-12 19:17 的 run 23;此后连续全红。恒定的一根就是 cppcheck:本机(与 runner
同机、同一 Machine 级 Cppcheck 2.21.0)以钩子原样参数复现,诊断与 runner 日志
task-54/58/60/62/64、以及 master 上那条「Failing after 6m49s」的归档 run 26
逐字一致——不是版本漂移,也不是抑制文件缺失。

两条诊断与处置:
- s_cmd_con:body 仅在 n>0 时写入,n==0(回显超长退化为空信息体)时把未初始
  化缓冲连同长度 0 一起传给 s_send_asdu。现实现内 len>0 才 memcpy,故无运行时
  缺陷,但契约脆弱、门禁照拦。改为把 body 声明收进真要回显的分支,空信息体
  分支显式传 NULL。
- s_spont_step:blen 只在 pend_yx 分支使用,声明移入该分支。

验证(基线 b24b2b5):cppcheck 原样复刻钩子 args → RC=0 零输出;@build 0 错误
0 警告(Code 231712→231732,+20 B);ram_baseline.py --update 重盖体积同码凭证、
--write 同步受管区台账,check_mem_numbers.py --write 同步 RAM 快照表。本次触发的
14 根钩子全 Passed(含 test-coverage)。

环境与遗留(非本笔引入):这台 runner 原本只有 VS2019/MSVC 14.29,没有
Microsoft.CodeCoverage.Console.exe,test-coverage 恒 [ENV] exit 2;今日已装
VS2026 BuildTools 18.11.12324.101(E 盘,含 mscoverage 18.11 与 MSVC
14.44/14.51/14.52),本笔按 14.51.36231 跑通覆盖率总闸 40/40 套、相对基线零
回退——与量基线那台同版本,故分母可比。CI 侧仍差两步:① 机器级 DTU_MSVC_CL
还指 C 盘 14.29,需改指 E 盘这套并重启 runner 守护进程(SYSTEM 只在进程启动
时读机器变量);② ci_runner_bootstrap.ps1 的 Find-MsvcCl 只扫 %ProgramFiles%
派生的 C 盘根且取首个命中(2019 在前),重跑 -SetEnv 会把对齐抹掉,应改用
vswhere + 版本降序。
2026-10-27 04:27:11 +08:00
16 changed files with 1004 additions and 84 deletions
+1 -1
View File
@@ -46,7 +46,7 @@ RemoDispBus 推流回归单独跑:`.venv\Scripts\python.exe simulator/tests/ch
| 现象 | 根因 |
|---|---|
| 找不到 `SDL2/SDL.h` | LVGL SDL 驱动写死该路径,头文件须有 `include/SDL2/` 副本(仓库已内置) |
| 找不到 `SDL2/SDL.h` | LVGL SDL 驱动写死该路径,须有 `include/SDL2/` 副本。⚠ **这份 SDK 不入库**(`.gitignore` 排除 `simulator/SDL2/`,实测 `git ls-files` 在该路径命中 0 个文件)——"下载才有"不等于"仓库已内置";CI/runner 的干净检出里没有它,2026-10-27 run 45 就在这里红。CI 侧用机器级 `DTU_SDL2_DIR` 指一份 SDK,见 `docs/CI/远程CI完整指南.md` §4.5 |
| 找不到 `lv_conf.h` | `LV_CONF_DIR`/`SDL2_INCLUDE_DIR` 必须在 `add_subdirectory(lvgl)` **之前**定义 |
| 窗口全黑 | mono 主题未生效 / 缺 `lv_refr_now(NULL)` / 重复喂 tick(SDL 驱动内部已 `lv_tick_set_cb`) |
| 画面正常但按键全无效 | 主循环**不得**调 `SDL_PollEvent`——LVGL SDL 驱动内部 5ms 定时器统一分发,先轮询会抢光事件 |
+246
View File
@@ -0,0 +1,246 @@
---
name: remote-ci-ops
description: 远程 CI(Gitea Actions + 本机 Windows 自建 runner)运维闭环:runner 保活与抢锁清理、"没有 run / 任务消失"的四因定位、红因三分法(job 没跑起来 / runner 缺环境 / 真门禁失败)、无令牌时用 Gitea 的 MySQL 直查 runner 在线与历次 run/job 时长与红绿史(含 status 枚举定死办法)、用 gitea-runner.exe generate-config 枚举 runner 真实配置键而不猜键名、本地对照日志落 ci-artifacts/、门禁数字口径、gitlint 在 CI 的两种空转(含 runner 无 git 身份致 exit 254 假失败)、PS 5.1 原生 stderr 与编码陷阱、改 workflow / 门禁脚本的红线与改后验收清单。背景与全部实测细节在 docs/CI/远程CI完整指南.md,本 skill 只给操作顺序与判据。
whenToUse: CI 没跑、run 被取消或零日志、页面报的失败时长/红绿史跟本地或日志对不上、"这 job 到底跑没跑起来"、门禁假绿/假红、runner 显示 online 却每 10 秒重启、自建 runner 有哪些可用配置项(cache 之类,不确定键名)、要改 .gitea/workflows/ci.yml 或 scripts/ci_*.ps1|py、或没有 API 令牌却要判断 CI 状态时。
---
# remote-ci-ops — 远程 CI 运维闭环
**权威细节**:`docs/CI/远程CI完整指南.md`(部署、全部实测坑、章节导航),唯一入口 `docs/CI/README.md`。
本 skill 只回答两件事:**出问题时按什么顺序查**、**动手前后各验什么**。同一事实不在两处详述。
> **TODO(待同步回指南)**:`docs/CI/远程CI完整指南.md` §6.1 目前只写 `action_runner.last_online` 的在线核验与 `mysql.exe` CLI;
> 本轮把同一通道扩到 `action_run` / `action_run_job` / `action_task` 的历史与时长,且实际驱动是
> **pymysql**(不是 mysql.exe)。下面 §4.3 / §7 是本轮实测口径,落定后应并回`docs/CI/远程CI完整指南.md` §6.1 与 §2.3。
**本机事实**(变了就改指南,别改这里):runner `win-stm32-ci`(`gitea-runner` v4.0.1,labels `[windows]`,与 `runs-on: windows` 匹配);目录 `D:\Docker\act_runner\`;Gitea 1.25.4 监听 `127.0.0.1:3001`,数据库 MySQL `127.0.0.1:3306`/gitea(凭据在 `D:\Docker\Gitea\custom\conf\app.ini` 的 `[database]`,**口令不进脚本、不进报告**);本仓库 `repository.id = 10`(`wandering1/stm32f4-base`,另一仓 stm32f103 = 13);**CI 只跑这一台机器**,与其他机器无关。
## 0. 先分层,别一上来读 workflow
**分层之前先做 §1**:一次红很可能压根不是"门禁内容"问题,直接读 workflow 会白绕一圈。
| 层 | 就位判据 | 不在位的症状 |
|---|---|---|
| ① 服务端 Actions 开关 | `docs/CI/远程CI完整指南.md` §1.1 | push 后 Actions 页**完全空白** |
| ② runner 在线且工具链对 SYSTEM 可见 | `docs/CI/远程CI完整指南.md` §4.1 + 下面 §4 | run 建了但一直排队;或 L0 死在"环境自检"、一个钩子都没跑 |
| ③ workflow 与门禁 | `scripts/ci_local.py` 本地全绿 | 具体 job 红/绿与本地不一致 |
**本地永远先跑这个**(runner 掉线也不影响它,是 L0 的唯一保障;日志落法见 §5):
```powershell
& '.venv\Scripts\python.exe' scripts/ci_local.py --keep-going # L0 + L0b + L0.5
```
## 1. 第一步:这个 job 到底跑没跑起来(红因三分法)
**只看一条判据**:runner 侧那份 job 日志(`D:\Docker\act_runner\log\job\*-task-NN.log`)里
**有没有出现任何一条钩子名(`cppcheck.......Passed`)或 `pre-commit hooks -- passed=A failed=B skipped=C` 汇总行**。
| 类 | 判据(日志里的证据行) | 结论与动作 |
|---|---|---|
| **A. job 没跑起来** | checkout 步骤报拉不到 `actions/checkout` / 连 `github.com` 超时,**通篇零钩子输出** | 门禁内容无罪。内网无公网时的三种处置在 `docs/CI/远程CI完整指南.md` §5.3(推荐零依赖纯 git checkout)。**先修它再谈别的**,否则后面所有"红"都不可读 |
| **B. runner 机器缺环境** | `::warning::runner 未安装 MSVC`、`no Python with a SYSTEM-visible pre-commit`、`runner missing cppcheck`、`test-coverage` 因缺 **mscoverage** 组件而 FAIL(按用户裁定缺件即 FAIL,不 skip)、`ui-golden` 因缺 **SDL2 SDK** 而 FAIL(该 SDK 不入库,见指南 §4.5) | 也是"非代码问题",但**别改成 skip 蒙过去**:按`docs/CI/远程CI完整指南.md` §4.1/§4.2 补机器级工具链或 `DTU_*`,改完重启 runner |
| **C. 真门禁失败** | 钩子清单完整且 `failed>0` 或某条 `Failed`,**且 §5 的本地同命令复现同样的红** | 这才轮到读 workflow / 门禁脚本,按失败钩子逐条定位 |
**顺序不能反**:A/B 两类在本地都是"全绿",用"本地绿 + CI 红"直接推"环境问题"也推不出是 A 还是 B——只有钩子输出能把它们分开。本轮实测:页面报的红看着像门禁失败,实际是 A(checkout 联网超时),而 A 修好之后 L0 还会再红两次——一条 C、两条 B。
## 2. 症状 → 一条判据 → 动作
| 症状 | 判据(跑这一条就够) | 结论 / 动作 |
|---|---|---|
| Actions 页空白、无 run | 看改了啥 + 怎么推的 | 四因:①服务端开关未开 ②workflow 没推到远程 ③**纯文档**(`paths-ignore: docs/**`、`**/*.md`)④**新建分支的首次 push**——`before` 是全零 SHA,变更文件集算不出来被 `paths-ignore` 滤空。④ 的实测:建分支那次 push 无 run,紧接着一次普通 push 立刻出 task;绕法 `workflow_dispatch` 或再推一个非文档提交 |
| **页面报的时长/红数与本地或日志对不上** | §4.3 查 `action_run`/`action_run_job` 的 created/started/stopped | **页面时长不可照抄**。本轮实测页面写 `Failing after 6m49s`,库里 41 次 run **最长 ≈4m33s**,run 41 总时长 95 s(L0 45 s / L0.5 43 s)——**没有任何一条记录对得上 6m49s**(多半掺了排队/重试/页面自身口径)。以库里的 `total=stopped-created`、`run=stopped-started`、`queue=started-created` 三个数为准 |
| run 全 **cancelled**、零日志 | `& '.\scripts\ci_runner_bootstrap.ps1' -Status` | runner 掉线;且 `ci.yml` 的 `concurrency.cancel-in-progress: true` 会把排队 run 被下次 push 取消 → **症状是"任务消失"而不是"任务失败"**(历史上 16–21 连续 12 个 job 零日志即此) |
| `ui-golden` 红、日志是 `C1083: SDL2/SDL.h` 或 `MSB1009: 项目文件不存在`,本地全绿 | 前者 = 检出里根本没有 SDL2 SDK(`.gitignore` 排除它,`git ls-files` 在该 SDK 目录下命中 0 个文件);后者 = 上一轮配置在硬校验处中断,**CMake 缓存文件已写下、工程文件却没生成**,于是下一轮跳过配置 | 设机器级 `DTU_SDL2_DIR`(指南 §4.5);模拟器构建脚本已能自愈,也可手删那份构建目录。**别把构建产物路径当文档引用写**——它不入库,CI 的干净检出里不存在,`doc-links` 会「本地过、CI 挂」(指南 §4.1.2 同族) |
| runner 显示 **online** 却每 10 秒重启、日志暴涨(实测 ~20 MB/天) | daemon 日志出现 `another gitea-runner process is already using ".runner"` | **双看门狗抢锁**:`Unregister-ScheduledTask` **不会**停掉旧实例起的进程 → 孤儿看门狗养着旧 daemon → 它持 `.runner.lock` → 新任务里的 daemon 起不来,而**服务端仍显示 online**(旧 daemon 在服务)。修:管理员 `-StartNow`(它**先杀看门狗、再杀 daemon**,顺序反了会被 10 秒内复活) |
| L0 失败但**本地同一条命令全绿**,日志 `exit code: 254` + `Missing git configuration: please set user.name` | 看是不是 254 | **不是提交信息问题**:`actions/checkout` 把 HOME 指向每次任务的临时目录,SYSTEM 无全局 git 身份 → gitlint 在读规则**之前**就配置错误退出。已由 `scripts/ci_gitlint_range.ps1` 用 `GIT_CONFIG_*` 注入一次性身份修掉;**254 = 门禁没跑起来(exit 2),1 = 信息真违规(exit 1)**,别把两者混成一句 |
| 钩子条数对不上 | `grep -c 'id:' .pre-commit-config.yaml` | **报告数 = 定义数 − 1**(gitlint 属 `commit-msg` stage,`--all-files` 跑不到且表现为全绿 = 静默空转)。增删钩子后重算这个差值 |
| `test-coverage` 恒 `[ENV] exit 2`(缺 `Microsoft.CodeCoverage.Console.exe`),或换 VS 世代后大面积假红 | `& '.venv\Scripts\python.exe' scripts/check_test_coverage.py --list` 看它探到几个根;再看机器级 `DTU_MSVC_CL` 指的是哪套 cl | 该 exe **只随 VS 分发**(NuGet 实测拿不到);装在非默认盘还要显式指 `DTU_MSVC_CL`。安装入口 + 组件 ID + 三条管理员命令在 `docs/CI/远程CI完整指南.md` §4.4 |
| `mem-numbers` 报"文档与 .map 不一致",但**没改过代码也没改过那篇文档** | `.map` 的 mtime 是否早于最近一次切分支 | **跨分支假象**:`.map` 是 gitignored 的 Keil 产物,切分支后它仍属于另一分支的代码。重新 `@build` 即一致,不要去改文档数字 |
| 明明设了 `DTU_KEIL_UV4` 却报 `UV4.exe not found` | `[Environment]::GetEnvironmentVariable('DTU_KEIL_UV4','User')` | 会话没继承用户级变量。**回填时别按"进程里已有的变量名"枚举**——Machine 轮会把只在 User 定义的变量置空(PS 赋空值 = 删除),User 轮就再也看不到它。正确:名字取 Machine/User **并集**,且只在取到值时写入 |
| 日志里出现 `gitea-runner.exe : …` + 4 行 `CategoryInfo`/`FullyQualifiedErrorId` | 实测 2 行真日志写成 8 行 | PS 5.1 的 `2>&1` 把每条原生 stderr 包成 ErrorRecord。修:`2>&1 \| ForEach-Object { "$_" }`(等价于原始行,且 `$LASTEXITCODE` 不受影响)。详见`docs/CI/远程CI完整指南.md` §7.3 |
| 日志文件半 UTF-8 半乱码 | 文件头是否 `FF FE` | `>` / `*>>` 在 PS 5.1 = `Out-File` = **默认 UTF-16LE**;与 `Add-Content -Encoding ascii` 混用会一个文件两种编码 |
## 3. runner 保活与注册(唯一入口脚本)
```powershell
# 免管理员预演,先看它会做什么
& '.\scripts\ci_runner_bootstrap.ps1' -DryRun
# 正式:写机器级 DTU_* + 装看门狗任务 + 立即起(需管理员)
powershell -ExecutionPolicy Bypass -File scripts\ci_runner_bootstrap.ps1 -SetEnv -StartNow
# 体检(非管理员会明说哪些项读不到,而不是谎报)
& '.\scripts\ci_runner_bootstrap.ps1' -Status
```
要点(细节与理由见 `docs/CI/远程CI完整指南.md` §2.4):
- **不能只起 daemon**:v4 无服务支持,且**首次 Declare 失败就退出** → 一次 Gitea 重启就永久静默掉线。
- **不能用 Windows 服务**(服务化方案已被实测推翻,见 `docs/CI/远程CI完整指南.md` §3.0);任务计划程序的"失败后重启"**永不触发**,因为 daemon 的退出是**正常退出**。
- 任务对象:`-AtStartup` + `Delay=PT1M`、`ExecutionTimeLimit` 为零、`RestartCount 999`、`MultipleInstances IgnoreNew`、`$settings.StartWhenAvailable=$true`——⚠ `StartWhenAvailable` 是**任务设置**属性,挂到 trigger 上会抛异常(曾因此注册崩溃)。
- `0x41301` = `SCHED_S_TASK_RUNNING`(不是失败);`-AtStartup` 的任务"下次运行时间"为空是正常的。
- 改完 bootstrap 脚本里的启动器模板,**磁盘上的看门狗要下次 `-StartNow` 或开机才刷新**——不必为此专门重启 runner。
## 4. 没有 API 令牌时怎么判断状态
匿名调 `/api/v1/repos/.../actions/runs` 是 **401**。三条不依赖令牌的路,按"在线 → 落没落地 → 历史与时长"排。
### 4.1 在线核验(查 `action_runner`)
`action_runner.last_online` 与当前 Unix 秒相差 **< 30 秒**即在正常心跳;`agent_labels` 应为 `["windows"]`(不含 windows 就是标签配错)。口径见 `docs/CI/远程CI完整指南.md` §6.1。
### 4.2 看 runner 侧(最直接,能证明"job 真落到本机了")
```powershell
$d = 'D:\Docker\act_runner\log'
# 日志被 daemon 占着,必须共享读打开(ReadAllBytes 会 IOException)
$fs = [System.IO.File]::Open("$d\runner-daemon.log",'Open','Read','ReadWrite')
# 关键行:declare successfully / writing the log of task N to … / Running job with …
Get-ChildItem "$d\job" -File | Sort-Object LastWriteTime -Descending | Select-Object -First 5
```
判读:`declare successfully` 后长时间零输出 = **在线且空闲**;`log\job\*.log` 出现新文件 = 有 task 落地;每个 job 结束时 `[runner] task N finished: success|failure`。**这份 `finished:` 行也是 §4.3 里校验 status 枚举的裁判**(§1 的三分法同样从这些文件起手)。
⚠ 非管理员**读不到 SYSTEM 进程的 `CommandLine`**(返回 `$null` → 过滤器静默匹配 0 个)→ 绝不能据此说"没有看门狗"。`Process.StartTime` 对 SYSTEM 也不可读,但 CIM 的 `CreationDate` 可读 → 用**进程存活时长**对比 10 秒重启间隔来判断重启循环,**不要 grep 不轮转的日志尾部**(旧失败一直在尾部,会对健康 runner 报假警)。
⚠ 查询进程自己的命令行含关键字会**自匹配**:把模式拆开写,如 `'run_runner_watch' + 'dog\.ps1'`。
### 4.3 查 run / job 历史与时长(推翻页面上的说法)
**驱动就认 pymysql**:本机 `C:/ProgramData/Anaconda3/python.exe` 实测 `pymysql=True`、`MySQLdb=False`、`mysql.connector` **缺失**(import 直接 `ModuleNotFoundError`)→ `docs/CI/远程CI完整指南.md` §6.1 的 `mysql.exe` CLI 路线本机走不通,改 pymysql 连 `127.0.0.1:3306`/gitea。
**口令只从 app.ini 运行时读**:`[database]` 段里 `HOST` 是 `host:port` 一体要拆开,`NAME`/`USER`/`PASSWD` 直接用;**绝不要把口令写进脚本字面量、报告、截图或提交**(本轮就是这么漏的一次)。同段还有 `PATH = ...gitea.db`,但 `DB_TYPE = mysql` → **别去读那个 sqlite 文件**。`token`/`PASSWD` 一类行在打印任何配置文件时统一 redact。
```python
# 写成文件跑(ci-artifacts/_q.py),别内联,见 §6
import re, pymysql
ini = open(r'D:/Docker/Gitea/custom/conf/app.ini', encoding='utf-8').read()
sec = ini.split('[database]')[1].split('\n[', 1)[0]
kv = dict(re.findall(r'(?m)^(\w+)\s*=\s*(.*)$', sec))
host, port = kv['HOST'].split(':')
db = pymysql.connect(host=host, port=int(port), user=kv['USER'], password=kv['PASSWD'],
database=kv['NAME'], charset='utf8mb4',
cursorclass=pymysql.cursors.DictCursor)
cur = db.cursor()
cur.execute("SHOW TABLES LIKE 'action%'") # → action_run / action_run_job / action_task /
# action_run_index / action_runner / action_task_step …
cur.execute("SHOW COLUMNS FROM action_run_job") # 🔴 列名以这里为准,别凭记忆 SELECT
```
**取数(本仓库务必带 `repo_id=10`,否则混进 stm32f103 的 run)**:
- `action_run`:`id, repo_id, status, title, ref, sha, event, created, started, stopped`,`ORDER BY id DESC`;
- `action_run_job`:按 run 关联到 `id, status, attempt, task_id, created, started, stopped`(本轮 run 41 → job 85/86)。
**`task_id` 就是 job 日志文件名的 `task-NN`** → `action_run_job.task_id = 66` ↔ `log/job/20261025-133227-task-66.log`,这是 DB 与 runner 日志互校的唯一把手;
- 时长三个数:`total = stopped-created`、`run = stopped-started`、`queue = started-created`(本轮实测 queue 普遍 1 s,所以页面 6m49s 不可能是排队)。时间戳是 **Unix 秒**,`datetime.datetime.fromtimestamp(x)` 再打印;
- **连续红次数按 job 数、不按 run**:把同一 job 历次 `status` 从新到旧排,数连续非"成功"的长度(本轮据此得出 L0 自上线 **13 次全红、从未绿过**,run 28~41)。
**status 枚举:先定死再下结论**(本轮最大的误判源)。
| 码 | 含义 | 依据 |
|---|---|---|
| 0 | Unknown / 未开始 | 本轮映射试错后才定 |
| 1 | Success | 同上 |
| 2 | **Failure** | run 41 = 2 → Failure,与页面红、与 runner 日志 `task finished: failure` 三方一致 |
| ≥3 | **本轮未核实** | 试错时猜过 `3=Failure`/`4=Cancelled`/`5=Skipped`,**都没有交叉验证** → 用到之前先拿一条已知结局的 run 反推(TODO:从 Gitea 源码定死,别再靠猜) |
| 5 / 6 | **非终态**(排队中 / 运行中) | 观测于 run 45:推送后先看到 job=5(未开始)与 6(进行中),数分钟后转 1 / 2;**别把它当成库里坏了或卡死** |
- **写查询时同时打印原始数字和映射名**;`action_run` 与 `action_run_job` **默认按同一套码处理是危险假设**,跨到别的表要重验。
- 判据:同一个 run 换一套映射后红绿翻转(本轮 step 53 判 `Success`、step 56 判 `Failure`)→ 说明你还没定死枚举,**这时任何"红绿史"都不许出口**。
- 中文标题在 bash 终端回显成乱码(本轮 `'docs(log): ???'`)只是**显示**问题,库里数据没坏 → **认 run 用 `id`/`sha`/`ref`,不要用标题文本匹配**;要看中文就把结果写文件再按 UTF-8 读。
## 5. 本地对照:日志一律落 `ci-artifacts/`
判"本地绿 CI 红"必须有同一份可引用的本地日志。落点在仓库内 gitignored 的 `ci-artifacts/`(`.gitignore`:159),**不要写仓库外绝对路径**:
```bash
mkdir -p ci-artifacts && .venv/Scripts/python.exe scripts/ci_local.py --keep-going \
> ci-artifacts/local_ci_$(date +%Y%m%d).log 2>&1; echo EXIT=$? >> ci-artifacts/local_ci_$(date +%Y%m%d).log
```
**本轮实测的替换**:先写到 `D:/Code/_sync_backup_20261026/ci_local_*.log`(仓库外)在后台 bash job 里直接 `No such file or directory` → 改落 `ci-artifacts/` 才跑通。跑门禁这类分钟级任务用后台 job 起,再 `job_output` 收,别占着前台。Keil 构建对照同理落 `ci-artifacts/local_build_*.log`。
## 6. 脚本层:写查询与批量探测的规矩(本轮踩点集中在这里)
- **一段 Python 查询写成 JS 模板字符串会炸**(本轮两次:`Expected unicode escape`、`Expected ';', '}' or <eof>`)。正确做法:按行存字符串数组 → `join('\n')` → 落盘 `ci-artifacts/_qN.py` → 用 `C:/ProgramData/Anaconda3/python.exe` 跑 → **收尾统一 `fs.rmSync` 删掉**(临时查询脚本不入库,也别留在仓库里)。
- **`run_code` 里不能用 `import x from '…'`**(`'import', and 'export' cannot be used outside of module code`)→ `const fs = await import('node:fs')`。
- **批量探测拆小批**:一次调用塞 4 条 Windows 探测(worktree / 目录 / 服务枚举 / app.ini)撞过 120 s deadline,只拿到部分输出。一次 1~2 条,慢源(`Get-ChildItem -Recurse`、`Get-CimInstance Win32_Service` 全量、`generate-config`)单独跑并显式 `timeoutMs`。
- **别对全部 runner job 日志一把梭 grep**:日志混合编码(UTF-16LE / CP936),逐文件返回一堆 `[ERR …]`。要么单文件 `head`/`sed -n 'A,Bp'`/`grep`(本轮有效路径),要么解析时逐文件 try/catch 容错。
- **`pwsh`/`read`/`subagent` 这类工具只能在 `run_code` 程序里 `await tools.xxx()` 调**,直接当工具名调会 `unknown tool`;每次调用**都要带 `description`**(漏了会 `missing required property "description"`)。
## 7. runner 有哪些配置项:先 `generate-config`,别猜键名
**判断"这个键存不存在"的唯一凭据是 runner 自己 dump 出来的默认配置文本**,全程只读——不写 `config.yaml`、不删 `.runner`(那是注册凭据,删了要重新 register)、不重启/注销 runner(只有改 `runner.labels` 才必须重启,`docs/CI/远程CI完整指南.md` §2.3.2)。
```powershell
& 'D:\Docker\act_runner\gitea-runner.exe' generate-config 2>&1 | Out-String -Width 160
```
- 从输出里 **grep 目标键名**(本轮实测清单含 `cache` / `capacity` / `container` / `default_image` / `action_shallow_clone` / `access_key_file` / `addr` / `allocate_pty` / `bind_workdir` / `bucket` / `docker_host` / `docker_timeout` / `enabled` / `endpoint` / `env_file` / `envs` / `external_secret` / `external_secret_file` …,以及 `#set_act_env: true`)。
- **dump 里绝大多数键是带 `#` 的注释行** → "在清单里" ≠ "本机已启用";是否被显式覆盖要跟 `config.yaml` 逐行比:**注释=默认值生效,非注释=本机写死了**。
- **别拿段名当 grep 锚点**:本轮找 `[actions]` 段和 `DEFAULT_ACTIONS_URL` 时段名锚点落空、上下文打出来的是 `log_report_*` 那几行 → 直接按**键名** grep。另外服务端的 `DEFAULT_ACTIONS_URL` 是 app.ini 的键(`docs/CI/远程CI完整指南.md` §1.2.1),不要当成 runner 的键去 `config set`。
- 老教训仍然成立:v4 的 `log` 段只有 `level` 和 `job`,**没有 `file`**,`config set log.file …` 会 `unknown config key`(`docs/CI/远程CI完整指南.md` §2.3.2)→ 改键前以 `generate-config` 输出为准;写完用 `gitea-runner.exe config get <键>` 自证。
## 8. 门禁口径:讲规则,不讲数字
数字随分支和提交漂移(实测同一句"18 个钩子"在另一分支是 26,master 后来涨到 33;单测从 3 套件/288 断言涨到 10 套件/647)。**只有规则稳定**:
- L0:`pre-commit hooks -- passed=A failed=B skipped=C`,其中 **A+B+C = 定义数 − 1**;`skipped` 本机唯一常见项是 `mem-numbers`(无 `.map` 即跳过,并以 `::warning::` 明示"CI 不守体积门禁",见`docs/CI/远程CI完整指南.md` §0.0)。
- L0b:`gitlint: N commit message(s) checked, 0 failed`。
- L0.5:`<N> runner(s), A/A assertions passed, fail=0`,**N = `test/host/` 下 `run_unit_test.py` 的个数**。
- **判定只看 `failed=0`、`fail=0`、A==A**;抄历史数字当基准必然误判。
## 9. 改 workflow / 门禁脚本的红线
`.gitea/workflows/ci.yml`:
- `shell: powershell`,**不是 `pwsh`**(Windows 自带只有 5.1,`pwsh` 会 `Cannot find: pwsh in PATH`)。
- **`run:` 块内必须纯 ASCII**:runner 把每块写成无 BOM 临时 `.ps1` 交给 5.1 → 按 CP936 解码 → 中文导致"字符串缺少终止符"(报错行号正好在含中文那行,极易误判为逻辑错)。中文只写在 YAML 的 `name:`/注释里。
- 写 `GITHUB_ENV` / `GITHUB_PATH` / `GITHUB_OUTPUT` 必须显式 `-Encoding ascii`(`Out-File -Append` 默认 UTF-16LE)。
- 零依赖存档:`Tee-Object` 到 `ci-artifacts/*.log` + `$GITHUB_STEP_SUMMARY` + `::notice::`;**不用 `actions/upload-artifact`**(内网可能拉不到外层 action,`docs/CI/远程CI完整指南.md` §5.3)。
- 工具链探测收在 `scripts/ci_find_python.ps1`(可离线自测 + 机器级安装兜底);`DTU_*` 最高优先级,且**显式设置就必须有效、不静默回退**。
`scripts/ci_*.ps1`:纯 ASCII(含中文的 `.ps1` 才需要 BOM,见`docs/CI/远程CI完整指南.md` §7.1);`-Encoding utf8NoBOM` 是 **PS 7 才有的**,5.1 要写文件用 `[System.IO.File]::WriteAllText($p, $s, (New-Object System.Text.UTF8Encoding($false)))`。
gitlint 专属:消息文件必须写在**仓库内**(跨盘符被 pre-commit 拒 `path is on mount 'C:', start on mount 'D:'`)且 **UTF-8 无 BOM**(BOM 占标题 1 字符,扭曲 100 上限)。
## 10. 改完必须验(顺序别省)
```powershell
# .ps1:纯 ASCII + 语法
$b=[System.IO.File]::ReadAllBytes($p); @($b|?{$_ -gt 127}).Count # 必须 0
$e=$null; [System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path $p),[ref]$null,[ref]$e)|Out-Null; @($e).Count # 必须 0
# run: 块:必须按 runner 的语义测——点源(10-27 用 run 44 换来的两条教训)
# runner 执行方式: powershell -command . '<块体>' ← 只有这个是真语义
$tmp = '.gitea/_blk_probe.ps1' # 用 python 从 ci.yml 抽出某步 run 正文写到这里,别另抄一份
& powershell.exe -NoProfile -Command "`$ErrorActionPreference='Stop'; . (Resolve-Path $tmp)"
# 教训一:块被点源进会话,$ErrorActionPreference 继承为 Stop 时,原生程序往 stderr
# 写一行就变成**终止性** NativeCommandError,`2>` 重定向挡不住(`2>` 加 `$null` 也不行)。
# 所以块首显式 `$ErrorActionPreference='Continue'`,原生调用一律摊平成字符串。
# 教训二:`exit 1` 的退出码语义随调用方式变——`-File` 直跑=1;`-File` 包一层点源=**0 且继续执行**;
# `-Command` 点源=1。用 `-File` 测会把失败全测成 rc=0(我第一版就栽在这,四条失败路径全绿)。
# .yml:可解析 + 全部 run: 块纯 ASCII + 无 BOM + LF
& '.venv\Scripts\python.exe' -c "import yaml;d=yaml.safe_load(open(r'.gitea/workflows/ci.yml',encoding='utf-8'));print(list(d))"
# 文档:链接与章节引用
& '.venv\Scripts\python.exe' scripts/check_doc_links.py --all
# 全量门禁(等价 CI 的两个 job)
& '.venv\Scripts\python.exe' scripts/ci_local.py --keep-going
```
提交信息:标题 ≤100、**正文每行 ≤80**、UTF-8 无 BOM、`git commit -F <文件>`;**先程序化数一遍长度再提交**(中文标题最容易超,`-m` 还会被终端代码页转码)。
推送后到 runner 侧确认(§4.2):两个 job 各自的 `SUMMARY` 行 + `finished: success`。**"推送成功"不等于"CI 跑过"**。
**汇报口径**:说"红了几次 / 跑了多久"必须写明**来源**(库里的 `action_run_job` 时长,还是 runner 日志,还是页面)与**所用的 status 映射**;三者不一致时以库 + 日志为准并说清页面为什么不可信。只读排查就全程只读——不改文件、不 commit/push、不动 runner 注册状态。
## 11. 分支与发布(走通的做法)
- 预演冲突面,不动工作区:`git merge-tree --write-tree --name-only <a> <b>`(exit 1 = 有冲突)。
- 数字口径类冲突的正确解法:**改成规则 + 分分支快照**,而不是选一边留数字——否则同一篇文档在另一分支上必错,下次合并再撞一次。
- 变更记录是**历史,永不改写**,只在最上方按日期加行。
- 想验证 CI 又不想动主干:复制一条分支推上去(`git branch <copy> <src>` → `git push -u origin <copy>`),跑完再删。注意**新建分支的首次 push 不触发 run**(§2 症状表第 1 行)。
- 主干落后很多时先同步再排障:备份脏文件 → 只 stash 被跟踪的改动 → `checkout` → `merge --ff-only` → `stash pop`,同步完再跑 §1(本轮本地落后 248 个提交,先同步才看得出真实红因)。
- 删分支前先算独有提交:`git rev-list --count origin/master..<branch>`,**> 0 就别删**(删了只能靠 SHA 找回);本地一律 `git branch -d`(未合并它会自己拒绝)。
- `git commit` / `git push` 都是"先问"动作(AGENTS.md 边界节),不是任何任务的隐含步骤。
+244 -7
View File
@@ -125,11 +125,128 @@ jobs:
name: L0 静态门禁 (pre-commit)
runs-on: windows
steps:
- uses: actions/checkout@v4
with:
# 钩子要对比基线与历史(doc-links 等读工作区即可,但 gitlint/cubemx 需完整历史)
fetch-depth: 0
- name: 检出源码(原生 git;不依赖 github.com 上的 action)
shell: powershell
env:
DTU_REPO_URL: ${{ github.server_url }}/${{ github.repository }}.git
DTU_REPO_REF: ${{ github.ref }}
DTU_REPO_SHA: ${{ github.sha }}
DTU_REPO_TOKEN: ${{ github.token }}
run: |
# Pure ASCII only (see file header).
#
# WHY NOT a "uses:" action: the runner resolves every uses: by contacting the
# actions URL during "Prepare all required actions" -- BEFORE the first step
# runs. On this intranet runner that call intermittently dies with a connect
# timeout: runs 36 (10-21) / 41 (10-25) / 42 (10-26) each burned ~45 s and
# produced ZERO hook output, while a manual curl to the same URL minutes later
# returned 200. Manual reachability is not CI health: the resolver runs as
# SYSTEM and the runner caches nothing across jobs. Gitea 1.25.4 also removed
# the custom DEFAULT_ACTIONS_URL knob, so the only fix this repo owns is to
# stop depending on the public resolver at all.
#
# KEEP IN SYNC with the same step in the other job. This logic deliberately
# does NOT live in scripts/*.ps1 (unlike the Python / MSVC probes): the file is
# not checked out yet at the moment it would be needed -- chicken and egg.
#
# $ErrorActionPreference is set to Continue EXPLICITLY. The runner executes a
# run: block by dot-sourcing it into a session, so an inherited Stop turns any
# raw native stderr line into a TERMINATING NativeCommandError. Measured
# 2026-10-27, run 44 / task-72: a benign "error: No such remote 'origin'" from
# a probe killed the whole job 3 seconds in, before any gate could run.
$ErrorActionPreference = 'Continue'
function Invoke-DtuGit {
# Single funnel for git. Two reasons it must be a funnel, not bare calls:
# 1) native stderr has to be flattened into plain strings (see above), and
# 2) $LASTEXITCODE has to stay readable by the caller afterwards.
# Returns Code + lines and lets the caller decide what to print.
param([string[]]$G)
$lines = & git @G 2>&1 | ForEach-Object { [string]$_ }
return [pscustomobject]@{ Code = $LASTEXITCODE; Lines = @($lines) }
}
if (-not $env:DTU_REPO_SHA) { Write-Host '::error::DTU_REPO_SHA is empty'; exit 1 }
if (-not $env:DTU_REPO_URL) { Write-Host '::error::DTU_REPO_URL is empty'; exit 1 }
# Never let a fetch wait for a human: the runner has no console, and a
# credential prompt would burn the whole job timeout while looking like a hung
# CI. Anonymous fetch works here (the repository is public; measured HTTP 200
# on /info/refs?service=git-upload-pack), so CI must not depend on a credential
# store it cannot answer.
$env:GIT_TERMINAL_PROMPT = '0'
$ws = (Get-Location).Path
Write-Host "workspace: $ws"
if (-not (Test-Path -LiteralPath (Join-Path $ws '.git'))) {
$r = Invoke-DtuGit -G @('init', '-q', '.')
if ($r.Code -ne 0) { Write-Host '::error::git init failed'; $r.Lines | ForEach-Object { Write-Host " $_" }; exit 1 }
}
# "git remote get-url" writes to stderr when the remote is absent -- that is
# the exact line that killed run 44. Ask the config instead: an absent key
# prints nothing and only sets a non-zero code, so nothing escapes as an error.
$probe = Invoke-DtuGit -G @('config', '--get', 'remote.origin.url')
$url = ($probe.Lines -join '').Trim()
if ($probe.Code -eq 0 -and $url) {
$r = Invoke-DtuGit -G @('remote', 'set-url', 'origin', $env:DTU_REPO_URL)
} else {
$r = Invoke-DtuGit -G @('remote', 'add', 'origin', $env:DTU_REPO_URL)
}
if ($r.Code -ne 0) { Write-Host '::error::configuring remote origin failed'; $r.Lines | ForEach-Object { Write-Host " $_" }; exit 1 }
# Full refspec, NOT --depth 1: the commit-message gate resolves base..head
# ranges and several gates diff against history, so a shallow clone turns them
# into "unknown revision" failures. Tags are skipped on purpose: this repository
# has zero tags and no script uses "git describe --tags" (measured 2026-10-27).
$r = Invoke-DtuGit -G @('config', 'remote.origin.fetch', '+refs/heads/*:refs/remotes/origin/*')
if ($r.Code -ne 0) { Write-Host '::error::setting remote.origin.fetch failed'; exit 1 }
# The token, when the instance provides one, travels only as a per-invocation
# -c override: it never lands in .git/config and is never echoed in logs.
$auth = @('-c', 'credential.helper=', '-c', 'credential.interactive=false')
if ($env:DTU_REPO_TOKEN) {
# Same scheme actions/checkout uses against Gitea git-http: Basic with a
# placeholder user and the token as the password. (Bearer is the GitHub API
# style, not the git-http style.) Honest caveat: this branch CANNOT be
# verified here -- the repository is public, so an anonymous fetch succeeds
# and any token we fabricate locally only proves that a bad one is
# rejected. If the repository ever goes private this is the first line to
# check; the failure mode is a loud rc=1, never a silent fallback.
$pair = 'x-access-token:' + $env:DTU_REPO_TOKEN
$b64 = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($pair))
$auth += @('-c', ('http.extraheader=AUTHORIZATION: Basic ' + $b64))
}
$f = Invoke-DtuGit -G ($auth + @('fetch', '--prune', '--no-tags', 'origin'))
if ($f.Code -ne 0) {
$f.Lines | ForEach-Object { Write-Host " $_" }
Write-Host ('::error::git fetch origin failed (exit ' + $f.Code + ')')
exit 1
}
# Some event refs (PR merge commits, refs outside refs/heads) are not covered
# by the branch refspec -- fall back to an explicit ref fetch, then INSIST the
# wanted SHA is really present rather than silently building the wrong tree.
$want = $env:DTU_REPO_SHA + '^{commit}'
$chk = Invoke-DtuGit -G @('cat-file', '-e', $want)
if ($chk.Code -ne 0) {
Write-Host 'sha not covered by the branch refspec -- fetching the event ref explicitly'
$f2 = Invoke-DtuGit -G ($auth + @('fetch', '--prune', '--no-tags', 'origin', $env:DTU_REPO_REF))
if ($f2.Code -ne 0) { $f2.Lines | ForEach-Object { Write-Host " $_" } }
$chk = Invoke-DtuGit -G @('cat-file', '-e', $want)
}
if ($chk.Code -ne 0) {
Write-Host ('::error::' + $env:DTU_REPO_SHA + ' is not present in the fetched history')
Write-Host ' (refusing to run the gates against a tree we did not verify)'
exit 1
}
$c = Invoke-DtuGit -G @('checkout', '-q', '--detach', $env:DTU_REPO_SHA)
if ($c.Code -ne 0) { $c.Lines | ForEach-Object { Write-Host " $_" }; Write-Host '::error::detached checkout failed'; exit 1 }
$rr = Invoke-DtuGit -G @('reset', '--hard', '-q', $env:DTU_REPO_SHA)
if ($rr.Code -ne 0) { Write-Host '::error::reset --hard failed'; exit 1 }
# -ffd, deliberately not -ffdx: -x would delete IGNORED files. Within ONE job several
# hooks share this directory (pip / the simulator build tree), and local reruns reuse
# it. NOTE measured 2026-10-27: CI gives every job a FRESH workspace (the L0 dirs of
# run 44/45/46 all differ, each paying a full 137-161 s fetch), so -ffd is a safe
# default, not a speed trick.
$cl = Invoke-DtuGit -G @('clean', '-ffd', '-q')
if ($cl.Code -ne 0) { $cl.Lines | ForEach-Object { Write-Host " $_" }; Write-Host '::error::git clean failed'; exit 1 }
$head = (Invoke-DtuGit -G @('rev-parse', '--short', 'HEAD')).Lines -join ''
$ncommits = (Invoke-DtuGit -G @('rev-list', '--count', 'HEAD')).Lines -join ''
$dirty = @(Invoke-DtuGit -G @('status', '--porcelain')).Lines.Count
Write-Host ('checkout ok: ' + $head + ' | commits=' + $ncommits + ' | dirty_files=' + $dirty)
- name: 环境自检(Python / cppcheck / pre-commit)
shell: powershell
run: |
@@ -399,8 +516,128 @@ jobs:
name: L0.5 主机侧单测 (MSVC)
runs-on: windows
steps:
- uses: actions/checkout@v4
- name: 检出源码(原生 git;与 l0-gates 同一实现口径)
shell: powershell
env:
DTU_REPO_URL: ${{ github.server_url }}/${{ github.repository }}.git
DTU_REPO_REF: ${{ github.ref }}
DTU_REPO_SHA: ${{ github.sha }}
DTU_REPO_TOKEN: ${{ github.token }}
run: |
# Pure ASCII only (see file header).
#
# WHY NOT a "uses:" action: the runner resolves every uses: by contacting the
# actions URL during "Prepare all required actions" -- BEFORE the first step
# runs. On this intranet runner that call intermittently dies with a connect
# timeout: runs 36 (10-21) / 41 (10-25) / 42 (10-26) each burned ~45 s and
# produced ZERO hook output, while a manual curl to the same URL minutes later
# returned 200. Manual reachability is not CI health: the resolver runs as
# SYSTEM and the runner caches nothing across jobs. Gitea 1.25.4 also removed
# the custom DEFAULT_ACTIONS_URL knob, so the only fix this repo owns is to
# stop depending on the public resolver at all.
#
# KEEP IN SYNC with the same step in the other job. This logic deliberately
# does NOT live in scripts/*.ps1 (unlike the Python / MSVC probes): the file is
# not checked out yet at the moment it would be needed -- chicken and egg.
#
# $ErrorActionPreference is set to Continue EXPLICITLY. The runner executes a
# run: block by dot-sourcing it into a session, so an inherited Stop turns any
# raw native stderr line into a TERMINATING NativeCommandError. Measured
# 2026-10-27, run 44 / task-72: a benign "error: No such remote 'origin'" from
# a probe killed the whole job 3 seconds in, before any gate could run.
$ErrorActionPreference = 'Continue'
function Invoke-DtuGit {
# Single funnel for git. Two reasons it must be a funnel, not bare calls:
# 1) native stderr has to be flattened into plain strings (see above), and
# 2) $LASTEXITCODE has to stay readable by the caller afterwards.
# Returns Code + lines and lets the caller decide what to print.
param([string[]]$G)
$lines = & git @G 2>&1 | ForEach-Object { [string]$_ }
return [pscustomobject]@{ Code = $LASTEXITCODE; Lines = @($lines) }
}
if (-not $env:DTU_REPO_SHA) { Write-Host '::error::DTU_REPO_SHA is empty'; exit 1 }
if (-not $env:DTU_REPO_URL) { Write-Host '::error::DTU_REPO_URL is empty'; exit 1 }
# Never let a fetch wait for a human: the runner has no console, and a
# credential prompt would burn the whole job timeout while looking like a hung
# CI. Anonymous fetch works here (the repository is public; measured HTTP 200
# on /info/refs?service=git-upload-pack), so CI must not depend on a credential
# store it cannot answer.
$env:GIT_TERMINAL_PROMPT = '0'
$ws = (Get-Location).Path
Write-Host "workspace: $ws"
if (-not (Test-Path -LiteralPath (Join-Path $ws '.git'))) {
$r = Invoke-DtuGit -G @('init', '-q', '.')
if ($r.Code -ne 0) { Write-Host '::error::git init failed'; $r.Lines | ForEach-Object { Write-Host " $_" }; exit 1 }
}
# "git remote get-url" writes to stderr when the remote is absent -- that is
# the exact line that killed run 44. Ask the config instead: an absent key
# prints nothing and only sets a non-zero code, so nothing escapes as an error.
$probe = Invoke-DtuGit -G @('config', '--get', 'remote.origin.url')
$url = ($probe.Lines -join '').Trim()
if ($probe.Code -eq 0 -and $url) {
$r = Invoke-DtuGit -G @('remote', 'set-url', 'origin', $env:DTU_REPO_URL)
} else {
$r = Invoke-DtuGit -G @('remote', 'add', 'origin', $env:DTU_REPO_URL)
}
if ($r.Code -ne 0) { Write-Host '::error::configuring remote origin failed'; $r.Lines | ForEach-Object { Write-Host " $_" }; exit 1 }
# Full refspec, NOT --depth 1: the commit-message gate resolves base..head
# ranges and several gates diff against history, so a shallow clone turns them
# into "unknown revision" failures. Tags are skipped on purpose: this repository
# has zero tags and no script uses "git describe --tags" (measured 2026-10-27).
$r = Invoke-DtuGit -G @('config', 'remote.origin.fetch', '+refs/heads/*:refs/remotes/origin/*')
if ($r.Code -ne 0) { Write-Host '::error::setting remote.origin.fetch failed'; exit 1 }
# The token, when the instance provides one, travels only as a per-invocation
# -c override: it never lands in .git/config and is never echoed in logs.
$auth = @('-c', 'credential.helper=', '-c', 'credential.interactive=false')
if ($env:DTU_REPO_TOKEN) {
# Same scheme actions/checkout uses against Gitea git-http: Basic with a
# placeholder user and the token as the password. (Bearer is the GitHub API
# style, not the git-http style.) Honest caveat: this branch CANNOT be
# verified here -- the repository is public, so an anonymous fetch succeeds
# and any token we fabricate locally only proves that a bad one is
# rejected. If the repository ever goes private this is the first line to
# check; the failure mode is a loud rc=1, never a silent fallback.
$pair = 'x-access-token:' + $env:DTU_REPO_TOKEN
$b64 = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($pair))
$auth += @('-c', ('http.extraheader=AUTHORIZATION: Basic ' + $b64))
}
$f = Invoke-DtuGit -G ($auth + @('fetch', '--prune', '--no-tags', 'origin'))
if ($f.Code -ne 0) {
$f.Lines | ForEach-Object { Write-Host " $_" }
Write-Host ('::error::git fetch origin failed (exit ' + $f.Code + ')')
exit 1
}
# Some event refs (PR merge commits, refs outside refs/heads) are not covered
# by the branch refspec -- fall back to an explicit ref fetch, then INSIST the
# wanted SHA is really present rather than silently building the wrong tree.
$want = $env:DTU_REPO_SHA + '^{commit}'
$chk = Invoke-DtuGit -G @('cat-file', '-e', $want)
if ($chk.Code -ne 0) {
Write-Host 'sha not covered by the branch refspec -- fetching the event ref explicitly'
$f2 = Invoke-DtuGit -G ($auth + @('fetch', '--prune', '--no-tags', 'origin', $env:DTU_REPO_REF))
if ($f2.Code -ne 0) { $f2.Lines | ForEach-Object { Write-Host " $_" } }
$chk = Invoke-DtuGit -G @('cat-file', '-e', $want)
}
if ($chk.Code -ne 0) {
Write-Host ('::error::' + $env:DTU_REPO_SHA + ' is not present in the fetched history')
Write-Host ' (refusing to run the gates against a tree we did not verify)'
exit 1
}
$c = Invoke-DtuGit -G @('checkout', '-q', '--detach', $env:DTU_REPO_SHA)
if ($c.Code -ne 0) { $c.Lines | ForEach-Object { Write-Host " $_" }; Write-Host '::error::detached checkout failed'; exit 1 }
$rr = Invoke-DtuGit -G @('reset', '--hard', '-q', $env:DTU_REPO_SHA)
if ($rr.Code -ne 0) { Write-Host '::error::reset --hard failed'; exit 1 }
# -ffd, deliberately not -ffdx: -x would delete IGNORED files. Within ONE job several
# hooks share this directory (pip / the simulator build tree), and local reruns reuse
# it. NOTE measured 2026-10-27: CI gives every job a FRESH workspace (the L0 dirs of
# run 44/45/46 all differ, each paying a full 137-161 s fetch), so -ffd is a safe
# default, not a speed trick.
$cl = Invoke-DtuGit -G @('clean', '-ffd', '-q')
if ($cl.Code -ne 0) { $cl.Lines | ForEach-Object { Write-Host " $_" }; Write-Host '::error::git clean failed'; exit 1 }
$head = (Invoke-DtuGit -G @('rev-parse', '--short', 'HEAD')).Lines -join ''
$ncommits = (Invoke-DtuGit -G @('rev-list', '--count', 'HEAD')).Lines -join ''
$dirty = @(Invoke-DtuGit -G @('status', '--porcelain')).Lines.Count
Write-Host ('checkout ok: ' + $head + ' | commits=' + $ncommits + ' | dirty_files=' + $dirty)
- name: 探测 MSVC(VS BuildTools / Community 等)
id: msvc
shell: powershell
+1 -1
View File
@@ -185,4 +185,4 @@
| 待办与审计发现登记簿 | `docs/任务规划/README.md` |
| 多任务并行开发(开空间 / 合并回收 / 台架与构建排队) | **`task-worktree` skill**(`@task`,用到自动加载)、`docs/环境与部署/并行任务工作树方案.md` |
| 换机 / 工具路径环境变量 | `docs/环境与部署/` |
| 远程 CI(Gitea Actions,**只跑本机一台 runner**) | [`docs/CI/README.md`](docs/CI/README.md) |
| 远程 CI(Gitea Actions,**只跑本机一台 runner**) | **`remote-ci-ops` skill**(排障顺序与判据,用到自动加载)、[`docs/CI/README.md`](docs/CI/README.md) |
+8 -4
View File
@@ -77,7 +77,11 @@ int iec104_comm_init(void) {
uint16_t iec104_comm_ca(void) {
uint16_t v = s_comm.ca;
if ((v < IEC104_COMM_CA_MIN) || (v > IEC104_COMM_CA_MAX)) {
/* 只判下界。上界 IEC104_COMM_CA_MAX 就是 uint16_t 的最大值,写成
* 「v > 65535」对 uint16_t 恒假(cppcheck compareValueOutOfTypeRangeError),
* 留着只会让下一个读代码的人以为上界真被守过。合法带口径不变,
* 仍照 iec104_comm.h 的常量注释(ca 1..65535 / port 1024..65535)。 */
if (v < IEC104_COMM_CA_MIN) {
return IEC104_COMM_DEF_CA;
}
return v;
@@ -91,7 +95,7 @@ uint16_t iec104_comm_ca(void) {
uint16_t iec104_comm_port(void) {
uint16_t v = s_comm.port;
if ((v < IEC104_COMM_PORT_MIN) || (v > IEC104_COMM_PORT_MAX)) {
if (v < IEC104_COMM_PORT_MIN) { /* 上界恒假,同 iec104_comm_ca 处注释 */
return IEC104_COMM_DEF_PORT;
}
return v;
@@ -104,7 +108,7 @@ uint16_t iec104_comm_port(void) {
* 限定条件:UI 保存交接路(task-9);落盘走 SYS_EVT_CFG_SAVE 整档外科 save
*/
int iec104_comm_ca_set(uint16_t ca) {
if ((ca < IEC104_COMM_CA_MIN) || (ca > IEC104_COMM_CA_MAX)) {
if (ca < IEC104_COMM_CA_MIN) { /* 上界恒假,同 iec104_comm_ca 处注释 */
return -1;
}
s_comm.ca = ca;
@@ -118,7 +122,7 @@ int iec104_comm_ca_set(uint16_t ca) {
* 限定条件:同 iec104_comm_ca_set
*/
int iec104_comm_port_set(uint16_t port) {
if ((port < IEC104_COMM_PORT_MIN) || (port > IEC104_COMM_PORT_MAX)) {
if (port < IEC104_COMM_PORT_MIN) { /* 上界恒假,同 iec104_comm_ca 处注释 */
return -1;
}
s_comm.port = port;
+12 -6
View File
@@ -161,18 +161,23 @@ static int s_send_asdu(iec104_link_t *lk, uint8_t type, uint8_t vsq,
*/
static void s_cmd_con(iec104_link_t *lk, uint8_t type, const uint8_t *obj,
uint16_t obj_len, uint8_t cot, uint8_t pn) {
uint8_t body[BODY_MAX];
uint16_t n = obj_len;
if (n > BODY_MAX) {
n = 0u;
n = 0u; /* 超长回显退化为空信息体 */
}
if (n > 0u) {
uint8_t body[BODY_MAX]; /* 作用域收进真要回显的分支,不把未初始化缓冲传下去 */
(void)memcpy(body, obj, n);
(void)s_send_asdu(lk, type, 1u, cot,
(pn != 0u) ? IEC104_COT_BIT_PN : 0u,
0u, 0u, body, n, 0u);
} else {
(void)s_send_asdu(lk, type, 1u, cot,
(pn != 0u) ? IEC104_COT_BIT_PN : 0u,
0u, 0u, NULL, 0u, 0u);
}
(void)s_send_asdu(lk, type, 1u, cot,
(pn != 0u) ? IEC104_COT_BIT_PN : 0u,
0u, 0u, body, n, 0u);
}
/* ---------------- 数据上送 ---------------- */
@@ -280,12 +285,13 @@ static void s_spont_step(iec104_link_t *lk, uint8_t *budget) {
uint8_t body[8]; /* 最大 = SIQ + CP56(7)(M_SP_TB_1) */
uint8_t i;
uint8_t d;
uint8_t blen;
uint8_t type;
int rc;
while ((*budget > 0u) && ((lk->pend_yx != 0u) || (lk->pend_yc != 0u))) {
if (lk->pend_yx != 0u) {
uint8_t blen;
i = 0u;
while (i < 33u) {
if ((lk->pend_yx & (1uLL << i)) != 0u) {
+10
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 |
@@ -80,7 +85,12 @@
## 变更记录
| 日期 | 变更 |
| 2026-10-27 | run 46:`ui-golden` 转绿(`DTU_SDL2_DIR` 生效)。新的 1 根红是我自己写的 `doc-links` 违规(把 gitignored 构建产物当引用),§4.1.2 同族,已修;并更正「CI 工作区跨 run 复用」的错话——实测每 job 全新目录、每次 137~161 s 全量拉取,毒缓存只在同一 job 内与本机流传(指南 §4.5/§5.3/§7 与 `ci.yml` 注释同步) |
|---|---|
| 2026-10-27 | run 44(推送到已存在分支会正常建 run)暴露的教训写进指南 §5.3:run: 块是被**点源**执行的,`$ErrorActionPreference` 继承 Stop 时原生 stderr 会终止整步;且测 run: 块只能用 `-Command ". 文件"`(`-File` 包点源会把失败测成 rc=0)。§7 补「几秒就亡」一行,skill §10 补测法 |
| 2026-10-27 | run 45 首次真跑门禁(L0 46 根绿、L0.5 3188/3188);唯一红 `ui-golden` 定性为 **runner 缺环境**:SDL2 SDK 不入库。新增指南 §4.5:机器级 `DTU_SDL2_DIR` + CMake 配置期硬校验 + `Find-Sdl2Sdk` + `build_sim.ps1` 毒缓存自愈;§4.1.3 变量表补到 5 个、§5.5 与 §7 各补一处;顺带更正 lvgl-simulator skill 的「SDL2 已内置」说法 |
| 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 |
+242 -33
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 里直接写全路径:
@@ -832,13 +832,15 @@ mv .dsh/skills/serial-assistant/scripts/state.bak_ci .dsh/skills/serial-assistan
#### 4.1.3 机器级 `DTU_*` 环境变量(runner 的正式覆盖通道,2026-10-02 起)
workflow 已改为统一消费 `DTU_*` 接口(见 [本地工具路径环境变量方案.md §3.3](../环境与部署/本地工具路径环境变量方案.md)):
**显式变量优先 → 自动发现回退**,三个 job 步骤相关的变量:
**显式变量优先 → 自动发现回退**,与 job 步骤相关的变量(2026-10-27 实测 `-Status` 列 5 个):
| 变量 | 作用 | 未设置时的回退 |
|---|---|---|
| `DTU_PRECOMMIT_PYTHON` | 指向装有 pre-commit 的 python.exe(L0/L0.5 用) | PATH 的 `python` → 工作区 `.venv`(干净克隆里通常没有) |
| `DTU_CPPCHECK` | 指向 cppcheck.exe(L0 钩子用) | SYSTEM 的 PATH(用户级 PATH 不可见) |
| `DTU_MSVC_CL` | 指向 x64 `cl.exe`(L0.5 用) | `%ProgramFiles%`/`%ProgramFiles(x86)%` 推导的 VS 标准根自动探测 |
| `DTU_MS_COVERAGE` | 指向 `Microsoft.CodeCoverage.Console.exe`(`test-coverage` 钩子用) | 顺 `DTU_MSVC_CL` 所在的那套 VS 根自动发现(§4.4) |
| `DTU_SDL2_DIR` | 指向一份 SDL2 SDK 根(须含 `include\SDL2\SDL.h`;`ui-golden` 要编模拟器) | `simulator\SDL2\SDL2-*`——**干净检出里根本没有这份 SDK,CI 必红**(§4.5) |
设置(**必须管理员**;scope 用 `Machine` 才能被 SYSTEM 任务看到):
@@ -887,6 +889,139 @@ 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` 再跑全量。
### 4.5 UI 像素回归的 SDL2 SDK:CI 工作区**天然没有**(2026-10-27 run 45 实测)
`ui-golden` 要先把 LVGL 模拟器编出来,而模拟器依赖 **SDL2 SDK**。这份 SDK 是**下载来的第三方件**:
`.gitignore` 里明写 `simulator/SDL2/`,`git ls-files` 在该路径下命中 **0 个文件**。于是任何一份干净
检出里都没有它,而开发机上有——所以本地一直绿,**这个缺失在本地永远照不出来**。
run 45 第一次在 CI 跑到 `ui-golden` 时报的是:
```
error C1083: 无法打开包括文件: "SDL2/SDL.h"
(Lib/lvgl/src/drivers/sdl/lv_sdl_private.h:20)
```
⚠ **别把它读成「UI 像素回归红了」**:这是红因三分法里的**第二类(runner 缺环境)**,与像素、与
被检的提交内容都无关。配置阶段那句 `-- SDL2: <路径>` 也**不能**当证据——`simulator/CMakeLists.txt`
过去只是 `set()` 拼路径、不校验存在,缺件时照样打印,直到编译 LVGL 的 SDL 驱动才炸。
**已落地的修法(四件)**:
1. `simulator/CMakeLists.txt` 支持机器级 **`DTU_SDL2_DIR`** 覆盖 SDK 位置;**不设该变量时行为与
过去完全一致**(实测:不设 → 取 `simulator/CMakeLists.txt` 里写死的仓内默认路径 → `ui-golden Passed`)。
2. 同文件加**配置期硬校验**:缺 SDK 时一行 `FATAL_ERROR` 讲清三条出路(本机装 / CI 设变量 /
删旧 build 缓存),而不是几百行 C1083 冒充代码错误。实测护栏生效:改前那种工作区里
`C1083` 行数从一整屏降到 **0**,只剩护栏自己那一条。
3. `scripts/ci_runner_bootstrap.ps1` 新增 `Find-Sdl2Sdk`,顺序:已有 `DTU_SDL2_DIR`(Machine →
User → Process,逐个校验含 `include\SDL2\SDL.h`)→ 机器级 `D:\DTU_SDK\SDL2-*` → 本仓
`simulator/SDL2/SDL2-*`;**命中第 3 档会 WARN**「runner 工作区里不会有这个路径」。`-SetEnv`
写入该变量,`-Status` 一并列出。
4. **SDK 放机器级而不是仓内**:本机已把那份 26 MB 的 SDK 整目录拷到 `D:\DTU_SDK\` 下(子目录名
就是 SDK 自带的版本号目录名)。`Find-Sdl2Sdk` 按 `SDL2-*` 通配发现、**不写死版本**,所以文档
里不钉那个目录名(它形如编号,会撞进 `id-check` 校验 8 的噪声桶,而那条上限只许减不许增)。
同理也别把变量指回某人的开发检出——CI 的工作区是 runner 自己的目录,不是你这份仓。
⚠ **第二个坑(毒缓存):设好变量仍然红,红因已经不是 SDK**。上一次配置在 `FATAL_ERROR` 处中断
时,CMake **已经把缓存文件写下**、工程文件却没生成;`build_sim.ps1` 的判断是「有缓存就不再配置」,
于是下一轮**跳过配置**直接进 MSBuild,报 `MSB1009: 项目文件不存在`——离根因十万八千里。
⚠ **更正(我自己写错的句子)**:这里原本写「这份毒缓存会**跨 run 存活**」,**对 CI 是错的**。实测 CI
每个 job 都用一个**全新**工作区——run 44 / 45 / 46 的 L0 目录哈希分别是 `b4a9…`、`e3ad…`、`74ce…`,
同一个 L0 job 三次都不同,且每次都付 **137~161 s 全量拉取**。所以跨 run 不会残留。真正的暴露面在
**同一个 job 内**:`ui-golden` 与板端导航自检都要编模拟器、共用同一份构建目录,前者中断就会毒到
后者;本机日常开发同理(我就是先在本地那份等价工作区里撞到的)。自愈检查因此仍然必要:
`build_sim.ps1` 第三节原本就有「生成器不符 → 删构建目录重来」的先例,照同一形状补一条——有缓存
但没有工程文件 → 删重。
实测闭环(在一份**没有**仓内 SDK、且带着 run 45 毒缓存的检出里):
```
ws cache present=True ALL_BUILD present=False <- 毒化状态,正是 CI 现在的处境
[RECOVERY] rc=0 UI 像素回归(菜单 8 状态 + 内容页 40 页 黄金参考)....Passed
```
即:只需管理员跑一次 `-SetEnv`(与 §4.4 的三条命令同批),CI 的 `ui-golden` 就会转绿,
且**自愈** run 45 已经污染的那份工作区。
---
## 5. 仓库端:workflow 文件
@@ -923,38 +1058,100 @@ 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` 会删**被忽略**的文件:同一个 job 内多个钩子共用这份工作区(pip、模拟器构建目录等),本机重复跑也靠它。⚠ **更正(2026-10-27 实测):CI 并不跨 run 复用工作区**——run 44/45/46 的 L0 目录哈希 `b4a9…`/`e3ad…`/`74ce…` 各不相同,每个 job 都付一次 137~161 s 全量拉取;所以选 `-ffd` 是**保守**(将来若真复用,被忽略的缓存不会被误删),不是靠它省时间。实测:放一个 gitignored 文件 + 一个未跟踪垃圾 + 改一个跟踪文件 → 重跑后 `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 不重复付全量代价**,只有工作区全新时才慢 |
| G | **点源 + `$ErrorActionPreference` 为 Stop(= runner 语义)**,全新工作区 | 修复后 rc=0、`checkout ok: c1e7e8c / commits=669`、首拉 168 s;**修复前同一模式 3 s 亡**(就是 run 44)——这一行才算真正的门 |
| H | 点源模式下的四条失败路径 | `DTU_REPO_SHA` 空 / `DTU_REPO_URL` 空 / sha 不在远端 / 坏令牌 → **ExitCode 全 = 1**(成功与复用 = 0)。A–F 那批是用 `-File` 测的,G/H 起才换成语义正确的点源测法 |
> ⚠ **一条未证的边界**:`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。
> 🔴 **run 44 真机首验栽了一次,两条教训都属于「本地照不出来」那一类**(10-27 10:38 推上
> `ci/remote-ci` 之后两个 job 分别 **3 s / 2 s 亡**,日志原文如下):
>
> ```
> git : error: No such remote 'origin'
> + CategoryInfo : NotSpecified: (...) [], RemoteException
> + FullyQualifiedErrorId : NativeCommandError
> ##[error]Process completed with exit code 1.
> ```
>
> ① **`2>$null` 不算摊平**。runner 把 `run:` 块**点源**进会话(`shell: powershell -command
> . 那个文件`),会话的 `$ErrorActionPreference` 是 Stop 时,原生程序**任何写到 stderr 的一行**
> 都会被 PS 5.1 包成**终止性** `NativeCommandError`——`2>$null` 不但没吞掉它,反而让它先把整
> 个步骤炸掉(`git remote get-url` 在远端不存在时正是往 stderr 写)。修法两条一起做:块首
> **显式** `$ErrorActionPreference = Continue`;所有原生 git 调用统一走一个把 stderr 摊平成
> 字符串的漏斗(现在这份就是这么写的)。
> ② **测法必须复现「点源」,否则连失败都测不出来**。把 `exit 1` 写在被点源的文件里时,
> 三种调用方式的实测语义如下:
>
> | 调用方式 | `exit 1` 之后 | 进程退出码 |
> |---|---|---|
> | `powershell -File a.ps1` | 进程结束 | 1 |
> | `powershell -File b.ps1`,而 b 里点源 a | **继续执行 b 的后续语句** | **0** |
> | `powershell -Command ". a.ps1"` ← **runner 的用法** | 进程结束 | **1** |
>
> 我第一版复现器恰好用了中间那种,于是把四条失败路径全测成 `rc=0`(**假警报**,同时也
> 照不出教训 ①)。**要验 workflow 里的 `run:` 块,只能用 `-Command` 点源**,并且最好显式
> 把 `$ErrorActionPreference` 前置成 Stop——那才是 runner 的真实语义。
**自查(改 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 +1185,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` 汇总行 → 归档日志说明;**`ui-golden` 需要 SDL2 SDK,而该 SDK 不入库** → 依赖机器级 `DTU_SDL2_DIR`(缺件即 FAIL,护栏与落地见 §4.5) |
| `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 +1261,9 @@ 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:'` 应为空 |
| job **3~5 秒就亡**、日志里是 `CategoryInfo` + `FullyQualifiedErrorId : NativeCommandError`,而同一条命令在本地全绿 | `run:` 块被**点源**进会话且 `$ErrorActionPreference` 继承为 Stop → 原生程序的 stderr 行变成**终止性**错误(`2>$null` 挡不住) | 块首显式设 `Continue`,原生调用一律摊平(`2>&1` 配 `ForEach-Object` 那类漏斗);复现方法见 §5.3 的 run 44 实录 |
| `ui-golden` 红、日志里是 `C1083: SDL2/SDL.h` 或 `MSB1009: 项目文件不存在`,而本地全绿 | 前者 = 检出里根本没有 SDL2 SDK(`.gitignore` 排除、不入库);后者 = **同一个 job 内**前一个钩子配置中断留下的半截构建缓存(CI 不跨 run 残留,见 §4.5 的更正) | 设机器级 `DTU_SDL2_DIR`(§4.5);构建脚本已自愈,也可手删那份构建目录 |
| `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 +1271,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 +1510,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
@@ -1351,7 +1555,12 @@ powershell -ExecutionPolicy Bypass -File scripts\ci_runner_bootstrap.ps1 -DryRun
## 变更记录
| 日期 | 变更 |
| 2026-10-27 | ✅ **run 46:`ui-golden` 转绿(A 方案生效,`DTU_SDL2_DIR` 由 SYSTEM 命中机器级 `D:\DTU_SDK`),L0 = 47 Passed / 1 Failed / 2 Skipped**。那 1 根红是**我自己**:新加的症状行里把 gitignored 的构建产物路径当文档引用写了,本机存在、干净检出不存在 → `doc-links`「本地过、CI 挂」,**正是本文 §4.1.2 记的那一坑**,我一边写它一边踩了它;已在干净克隆(无构建目录)里先复现再修(复现命令与判据同 §4.1.2)。顺带更正我先前两句关于工作区复用的错话:实测 CI **每个 job 一个全新工作区**(run 44/45/46 的 L0 目录哈希 `b4a9…`/`e3ad…`/`74ce…` 互不相同,每次付 137~161 s 全量拉取),所以「毒缓存跨 run 存活」只对**本机与同一 job 内**成立(`ui-golden` 与板端导航自检共用构建目录,前者中断会毒到后者),对跨 run 不成立——§4.5、§5.3 的 `-ffd` 依据、§7 行与 `ci.yml` 注释四处同步改为实测口径(历史行不动,只在上方加本行) |
|---|---|
| 2026-10-27 | 🟡 **run 45 真把门禁跑起来了**:L0 46 根钩子 Passed / L0.5 = 40 runner 3188/3188 fail=0(`checkout ok: ae80bb8 / commits=670`,SYSTEM 读到机器级 `DTU_MSVC_CL` = E 盘 14.51)。唯一红 = `ui-golden`,定性为**第二类 runner 缺环境**而非像素回归:SDL2 SDK 被 `.gitignore` 排除、不入库,干净检出天然没有(本地有 → 一直绿)。新增 §4.5:`CMakeLists.txt` 支持机器级 `DTU_SDL2_DIR`(不设=行为不变,实测 Passed)+ 配置期硬校验(护栏把一整屏 C1083 降到 0 行)+ `Find-Sdl2Sdk` 三级发现(机器级 `D:\DTU_SDK` 优先,命中仓内路径会 WARN)+ **毒缓存坑**:配置中断留下 `CMakeCache.txt` 却没有工程文件,而 `build_sim.ps1` 是「有缓存就不再配置」→ 下一轮跳过配置、报离根因极远的 `MSB1009`,且 `simulator/build/` 被 `git clean -ffd` 刻意保留 → **跨 run 存活**;已按该文件既有先例补「有缓存无工程 → 删重」。实测闭环:在一份没有仓内 SDK 且带毒缓存的等价工作区里,仅设变量即 `Passed`(8+40 全过、自愈)。§4.1.3 变量表由 3 个补到 5 个;§5.5 的 `l0-gates` 步骤要点补上 `ui-golden` 的 SDK 依赖;skill 侧补症状行、三分法 B 类例、`run.status` 5/6 为非终态;顺带更正 lvgl-simulator skill 中「SDL2 仓库已内置」的说法 |
| 2026-10-27 | 🔴 **run 44 真机首验失败一次 → 两条「本地照不出来」的教训写进 §5.3**:把原生检出差上去推到 `ci/remote-ci`(10:38)后两个 job 分别 3 s / 2 s 亡,日志是 `error: No such remote 'origin'` 被包成 `NativeCommandError` 终止整步。教训①:runner 用 `powershell -command . '<文件>'` **点源**执行 run: 块,会话继承 `$ErrorActionPreference=Stop` 时原生程序的 stderr 一行就是**终止性错误**,`2>` 重定向挡不住——修法:块首显式 `Continue` + 所有 git 调用走摊平漏斗(另把 `git remote get-url` 换成 `git config --get`,缺 key 时不写 stderr)。教训②:**测法必须复现点源**——实测 `exit 1` 语义矩阵:`-File` 直跑=1 / `-File` 包点源=**0 且继续执行** / `-Command` 点源=1;我第一版复现器用了中间那种,把四条失败路径全测成 rc=0(假警报,也照不出教训①)。换点源测法后:全新工作区 150 s `checkout ok: c1e7e8c / commits=669`、四条失败路径 ExitCode 全 1、复用工作区 1.6 s 且 gitignored 文件保留。§5.3 实测表补 G/H 两行、§7 补「几秒就亡」一行、`remote-ci-ops` skill §10 补测法。另记:本仓 push 到**已存在**的远端分支会正常建 run(run 44),而新建分支首推不建(已三次复现) |
| 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 断言) |
@@ -47,10 +47,10 @@ dhara 失败类 +84;RO 另 +208,全是该失败类名表)→ `ER_IROM1`
<!-- MEM_SNAPSHOT:BEGIN generated by scripts/check_mem_numbers.py -->
| 区域 | 用量 | 容量 | 占比 |
|---|---|---|---|
| ER_IROM1 | 257,272 B (251.24 KB) | 1,048,576 B (1024 KB) | 24.5% |
| ER_IROM1 | 257,280 B (251.25 KB) | 1,048,576 B (1024 KB) | 24.5% |
| RW_IRAM1 | 80,312 B (78.43 KB) | 131,072 B (128 KB) | 61.3% |
| RW_CCM | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% |
| **Flash (ROM 合计)** | **258,792 B (252.7 KB)** | 1,048,576 B (1024 KB) | **24.7%** |
| **Flash (ROM 合计)** | **258,800 B (252.7 KB)** | 1,048,576 B (1024 KB) | **24.7%** |
<!-- MEM_SNAPSHOT:END -->
各区的容量与常量来源:
+1 -1
View File
@@ -15,7 +15,7 @@
<!-- MEM_BUDGET:BEGIN generated by scripts/ram_baseline.py -->
| 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) |
|---|---|---|---|---|
| ER_IROM1 (Flash) | 257,272 B | 1,048,576 B | 24.5% | 参考 |
| ER_IROM1 (Flash) | 257,280 B | 1,048,576 B | 24.5% | 参考 |
| RW_CCM | 57,376 B | 65,280 B | 87.9% | PASS |
| UNINIT_CCM | 48 B | 256 B | 18.8% | 参考 |
| RW_IRAM1 | 80,312 B | 131,072 B | 61.3% | PASS |
@@ -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` 都编译),所以对外只报**并集**,单套只报"触及几个文件 / 多少行"。
+191 -21
View File
@@ -49,7 +49,11 @@
.PARAMETER SetEnv
Discover and write MACHINE-level DTU_PRECOMMIT_PYTHON / DTU_CPPCHECK /
DTU_MSVC_CL. Existing values are preserved unless -Force.
DTU_MSVC_CL / DTU_MS_COVERAGE / DTU_SDL2_DIR. Existing values are preserved unless
-Force. MSVC discovery goes through vswhere (so VS on a non-default
drive is found) and pins the toolset with -MsvcToolset, because the
coverage baseline was recorded with one specific MSVC version -- see
the remote-CI guide (docs/CI/) section 4.4.
.PARAMETER StartNow
Start the task after registering it.
@@ -65,6 +69,16 @@
.PARAMETER Force
With -SetEnv, overwrite existing machine-level values.
.PARAMETER MsvcToolset
Toolset version PREFIX to prefer when writing DTU_MSVC_CL (default
'14.51.'). 'newest available' is deliberately NOT the rule: the host
unit-test coverage baseline was measured with MSVC 14.51 and the
denominator changes with the compiler generation. When no toolset
matches the prefix the script warns and falls back to the newest.
.PARAMETER MsvcLatest
Ignore -MsvcToolset and take the newest MSVC toolset found.
.OUTPUTS
Progress lines. Exit codes: 0 = ok, 1 = precondition failed, 2 = not elevated.
@@ -80,7 +94,9 @@ param(
[switch]$StartNow,
[switch]$Status,
[switch]$DryRun,
[switch]$Force
[switch]$Force,
[string]$MsvcToolset = '14.51.',
[switch]$MsvcLatest
)
$ErrorActionPreference = 'Stop'
@@ -138,24 +154,149 @@ function Find-Cppcheck {
return $null
}
function Find-MsvcCl {
$pf = $env:ProgramFiles
$pf86 = ${env:ProgramFiles(x86)}
if ([string]::IsNullOrWhiteSpace($pf)) { $pf = 'C:\Program Files' }
if ([string]::IsNullOrWhiteSpace($pf86)) { $pf86 = 'C:\Program Files (x86)' }
$roots = @()
foreach ($year in @('2019', '2022', '2026', '18')) {
foreach ($edition in @('BuildTools', 'Community', 'Professional', 'Enterprise')) {
$roots += (Join-Path $pf86 "Microsoft Visual Studio\$year\$edition")
$roots += (Join-Path $pf "Microsoft Visual Studio\$year\$edition")
function Find-VsInstances {
# VS install roots, newest first, via vswhere. This matters because
# vswhere knows about NON-DEFAULT INSTALL DRIVES: this machine keeps
# VS 18 BuildTools under E:, which a %ProgramFiles%-derived probe can
# never see. Returns an empty array when vswhere is unusable so the
# caller can fall back to the classic (incomplete) root scan.
$vswhere = $env:DTU_VSWHERE
if ([string]::IsNullOrWhiteSpace($vswhere)) {
$pf86 = [Environment]::GetFolderPath('ProgramFilesX86')
if ([string]::IsNullOrWhiteSpace($pf86)) { $pf86 = 'C:\Program Files (x86)' }
$vswhere = Join-Path $pf86 'Microsoft Visual Studio\Installer\vswhere.exe'
}
if (-not (Test-Path -LiteralPath $vswhere)) { return @() }
$raw = & $vswhere -all -products * -format json 2>$null
if ($LASTEXITCODE -ne 0 -or -not $raw) { return @() }
$list = @()
foreach ($i in @($raw | ConvertFrom-Json)) {
if (-not $i -or -not $i.installationPath) { continue }
if (-not (Test-Path -LiteralPath $i.installationPath)) { continue }
$v = [version]'0.0'
if ($i.installationVersion) { [void][version]::TryParse([string]$i.installationVersion, [ref]$v) }
$list += [pscustomobject]@{ Root = [string]$i.installationPath; Version = $v }
}
return @($list | Sort-Object -Property Version -Descending)
}
function Find-MsvcClCandidates {
# Every x64-host cl.exe on this machine, tagged with its toolset version.
# Comparison operators in PowerShell are case-insensitive by default, so
# -match / -notmatch cover HostX64 / Hostx64 spelling differences.
$cands = @()
$roots = @(Find-VsInstances | ForEach-Object { $_.Root })
if ($roots.Count -eq 0) {
Warn 'vswhere unusable -- falling back to %ProgramFiles% roots; VS installs on other drives are INVISIBLE to that probe'
$pf = [Environment]::GetFolderPath('ProgramFiles')
$pf86 = [Environment]::GetFolderPath('ProgramFilesX86')
if ([string]::IsNullOrWhiteSpace($pf)) { $pf = 'C:\Program Files' }
if ([string]::IsNullOrWhiteSpace($pf86)) { $pf86 = 'C:\Program Files (x86)' }
foreach ($year in @('18', '2026', '2022', '2019')) {
foreach ($edition in @('BuildTools', 'Community', 'Professional', 'Enterprise')) {
foreach ($base in @($pf, $pf86)) {
$r = Join-Path $base ('Microsoft Visual Studio\' + $year + '\' + $edition)
if (Test-Path -LiteralPath $r) { $roots += $r }
}
}
}
}
foreach ($r in $roots) {
if (-not (Test-Path -LiteralPath $r)) { continue }
$cl = Get-ChildItem -LiteralPath $r -Recurse -Filter 'cl.exe' -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -like '*Hostx64\x64*' } |
Select-Object -First 1
if ($cl) { return $cl.FullName }
$msvc = Join-Path $r 'VC\Tools\MSVC'
if (-not (Test-Path -LiteralPath $msvc)) { continue }
foreach ($f in (Get-ChildItem -LiteralPath $msvc -Recurse -Filter 'cl.exe' -ErrorAction SilentlyContinue)) {
$full = $f.FullName
if ($full -notmatch '\\bin\\Hostx64\\x64\\cl\.exe$') { continue }
$toolset = ''
if ($full -match '\\VC\\Tools\\MSVC\\([^\\]+)\\bin\\') { $toolset = $Matches[1] }
$ver = [version]'0.0'
[void][version]::TryParse($toolset, [ref]$ver)
$cands += [pscustomobject]@{ Cl = $full; Toolset = $toolset; Version = $ver; VsRoot = $r }
}
}
return @($cands)
}
function Find-MsvcCl {
# Rewritten 2026-10-27. The previous version walked a hard-coded root list
# derived from %ProgramFiles% and returned the FIRST hit: with VS2019
# (MSVC 14.29) and a newer VS both present it handed back 14.29, and it
# could not see the E: install at all. "First hit" is never right for a
# compiler: the coverage baseline (scripts/test_coverage_baseline.json)
# was recorded with MSVC 14.51 and the instrumented denominator moves when
# the compiler generation moves (measured: App/util/sha256.c is 105 lines
# under gcc vs 114 under MSVC -- see the host coverage policy doc, 2.1).
# Hence the default is a PINNED toolset prefix, not "newest".
param([string]$ToolsetPrefix = '', [switch]$Latest)
$cands = @(Find-MsvcClCandidates)
if ($cands.Count -eq 0) { return $null }
$sorted = @($cands | Sort-Object -Property Version -Descending)
$seen = (@($sorted | ForEach-Object { $_.Toolset } | Select-Object -Unique)) -join ', '
if (-not $Latest -and $ToolsetPrefix) {
$hit = @($sorted | Where-Object { $_.Toolset -like ($ToolsetPrefix + '*') })
if ($hit.Count -gt 0) {
Info ('MSVC pinned to toolset prefix ' + $ToolsetPrefix + '* -> ' + $hit[0].Toolset + ' (' + $hit.Count + ' of ' + $cands.Count + ' candidate(s); toolsets found: ' + $seen + ')')
return $hit[0].Cl
}
Warn ('no MSVC toolset matching prefix ' + $ToolsetPrefix + ' on this machine (found: ' + $seen + ')')
Warn 'falling back to the NEWEST toolset -- that changes the coverage denominator; read the remote-CI guide (docs/CI/) section 4.4 before accepting it'
}
Info ('using newest MSVC toolset ' + $sorted[0].Toolset + ' of ' + $cands.Count + ' candidate(s); toolsets found: ' + $seen)
return $sorted[0].Cl
}
# ---------------------------------------------------------------------------
# Locate an SDL2 SDK root usable by the UI simulator (ui-golden gate).
# The SDK is DOWNLOADED, not tracked: .gitignore excludes simulator/SDL2/, so a
# CI workspace never contains it. Measured 2026-10-27 run 45: ui-golden died with
# C1083 "SDL2/SDL.h" on the runner while every local run was green -- a missing
# third-party SDK, not a pixel regression. Discovery order:
# 1) an already-valid DTU_SDL2_DIR (Machine, then Process)
# 2) a machine-wide copy under -MachineSdkRoot (default D:\DTU_SDK)
# 3) this repository checkout's simulator/SDL2/SDL2-* -- works for local runs,
# but a runner workspace will not contain it, so on the CI box prefer 1 or 2
# Returns the root that contains include\SDL2\SDL.h, else $null.
# ---------------------------------------------------------------------------
function Find-Sdl2Sdk {
param([string]$MachineSdkRoot = 'D:\DTU_SDK')
$rel = 'include\SDL2\SDL.h'
$cand = [System.Collections.Generic.List[string]]::new()
foreach ($sc in @('Machine', 'User', 'Process')) {
$v = [Environment]::GetEnvironmentVariable('DTU_SDL2_DIR', $sc)
if ($v) { $cand.Add($v) }
}
if ($MachineSdkRoot -and (Test-Path -LiteralPath $MachineSdkRoot)) {
Get-ChildItem -LiteralPath $MachineSdkRoot -Directory -Filter 'SDL2-*' -ErrorAction SilentlyContinue |
Sort-Object Name -Descending | ForEach-Object { $cand.Add($_.FullName) }
}
$repoSdk = Join-Path (Split-Path -Parent $PSScriptRoot) 'simulator\SDL2'
if (Test-Path -LiteralPath $repoSdk) {
Get-ChildItem -LiteralPath $repoSdk -Directory -Filter 'SDL2-*' -ErrorAction SilentlyContinue |
Sort-Object Name -Descending | ForEach-Object { $cand.Add($_.FullName) }
}
foreach ($c in $cand) {
if ($c -and (Test-Path -LiteralPath (Join-Path $c $rel))) { return $c }
}
return $null
}
function Find-CoverageConsole {
# Microsoft.CodeCoverage.Console.exe ships ONLY inside Visual Studio
# (measured 2026-10-27: NuGet candidates contain no such .exe, or only the
# legacy Team Tools CodeCoverage.exe whose CLI differs). Its location
# inside a VS install is a fixed relative path:
param([string]$ClPath = '')
$rel = 'Common7\IDE\Extensions\Microsoft\CodeCoverage.Console\Microsoft.CodeCoverage.Console.exe'
$roots = @()
if ($ClPath) {
$vsRoot = ''
# cl.exe = <root>\VC\Tools\MSVC\<ver>\bin\Hostx64\x64\cl.exe
if ($ClPath -match '^(.*)\\VC\\Tools\\MSVC\\') { $vsRoot = $Matches[1] }
if ($vsRoot) { $roots += $vsRoot }
}
foreach ($i in @(Find-VsInstances)) { $roots += $i.Root }
foreach ($r in ($roots | Where-Object { $_ } | Select-Object -Unique)) {
$cand = Join-Path $r $rel
if (Test-Path -LiteralPath $cand) { return $cand }
}
return $null
}
@@ -273,10 +414,10 @@ if ($Status) {
if (Test-Gitea $GiteaUrl) { Ok "Gitea reachable at $GiteaUrl" }
else { Fail "Gitea NOT reachable at $GiteaUrl" }
foreach ($n in @('DTU_PRECOMMIT_PYTHON', 'DTU_CPPCHECK', 'DTU_MSVC_CL')) {
foreach ($n in @('DTU_PRECOMMIT_PYTHON', 'DTU_CPPCHECK', 'DTU_MSVC_CL', 'DTU_MS_COVERAGE', 'DTU_SDL2_DIR')) {
$v = Get-MachineEnv $n
if ($v) { Ok ("MACHINE {0} = {1}" -f $n, $v) }
else { Warn "MACHINE $n not set (CI auto-discovery still applies; see scripts/ci_find_python.ps1)" }
else { Warn "MACHINE $n not set (CI auto-discovery still applies; -SetEnv writes it, docs/CI/ guide sections 4.4 / 4.5 explain the pairing)" }
}
exit 0
}
@@ -359,8 +500,37 @@ if ($SetEnv) {
if ($cpp) { Set-MachineEnv 'DTU_CPPCHECK' $cpp 'DTU_CPPCHECK' }
else { Warn 'cppcheck not found (install Cppcheck.Cppcheck, or set DTU_CPPCHECK manually)' }
$cl = Find-MsvcCl
if ($cl) { Set-MachineEnv 'DTU_MSVC_CL' $cl 'DTU_MSVC_CL' }
# ui-golden needs the SDL2 SDK and a checked-out tree never has it (see
# Find-Sdl2Sdk). Pin it machine-wide so the runner resolves it under SYSTEM too.
$sdl = Find-Sdl2Sdk
if ($sdl) {
Set-MachineEnv 'DTU_SDL2_DIR' $sdl 'DTU_SDL2_DIR'
$repoRoot = (Split-Path -Parent $PSScriptRoot) + [IO.Path]::DirectorySeparatorChar
if ($sdl.StartsWith($repoRoot, [StringComparison]::OrdinalIgnoreCase)) {
Warn 'DTU_SDL2_DIR points INSIDE this repo checkout -- a runner workspace will not contain that path.'
Warn 'copy an SDK to D:\DTU_SDK (or point DTU_SDL2_DIR at a machine-wide path), then re-run -SetEnv'
}
}
else {
Warn 'no SDL2 SDK found: the ui-golden gate fails with C1083 SDL2/SDL.h (measured run 45).'
Warn 'download it per the simulator deployment guide (docs/CI/ guide, section 4.5) or copy one to D:\DTU_SDK, then re-run -SetEnv'
}
$cl = Find-MsvcCl -ToolsetPrefix $MsvcToolset -Latest:$MsvcLatest
if ($cl) {
Set-MachineEnv 'DTU_MSVC_CL' $cl 'DTU_MSVC_CL'
# Pin the coverage collector too, and pin it INSIDE the same VS as the
# chosen cl.exe: the gate derives it from the compiler ancestor anyway,
# but an explicit machine value also covers the case where somebody
# later points DTU_MSVC_CL at another install. Both vars must be
# changed together (docs/CI/ guide, section 4.4).
$cov = Find-CoverageConsole -ClPath $cl
if ($cov) { Set-MachineEnv 'DTU_MS_COVERAGE' $cov 'DTU_MS_COVERAGE' }
else {
Warn 'Microsoft.CodeCoverage.Console.exe NOT found -- the test-coverage gate FAILS with [ENV] exit 2 (by the 2026-10-24 ruling it does not skip).'
Warn 'install VS BuildTools 17.9+/18.x including Microsoft.VisualStudio.PackageGroup.TestTools.CodeCoverage, then re-run -SetEnv'
}
}
else { Warn 'MSVC cl.exe (Hostx64\x64) not found -- L0.5 will degrade to "skipped" without it' }
Info 'machine env vars are read when a PROCESS STARTS: the daemon must be restarted below'
+1
View File
@@ -9,3 +9,4 @@ date code ro rw zi er_irom1 er_pct iram1 iram1_pct ccm ccm_pct top_rwzi
2026-10-24 213780 18860 1204 131852 232640 22.2 75632 57.7 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4128;sys_log_mcu.o=3944;default_task.o=2736
2026-10-25 224860 24640 1332 135100 249500 23.8 79008 60.3 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4128;sys_log_mcu.o=3944;default_task.o=2736
2026-10-26 231712 25560 1520 136216 257272 24.5 80312 61.3 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4003;default_task.o=2736
2026-10-27 231720 25560 1520 136216 257280 24.5 80312 61.3 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4003;default_task.o=2736
1 date code ro rw zi er_irom1 er_pct iram1 iram1_pct ccm ccm_pct top_rwzi
9 2026-10-24 213780 18860 1204 131852 232640 22.2 75632 57.7 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4128;sys_log_mcu.o=3944;default_task.o=2736
10 2026-10-25 224860 24640 1332 135100 249500 23.8 79008 60.3 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4128;sys_log_mcu.o=3944;default_task.o=2736
11 2026-10-26 231712 25560 1520 136216 257272 24.5 80312 61.3 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4003;default_task.o=2736
12 2026-10-27 231720 25560 1520 136216 257280 24.5 80312 61.3 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4003;default_task.o=2736
+6 -6
View File
@@ -1,15 +1,15 @@
{
"built_at": "2026-10-26T23:32:22+08:00",
"fingerprint": "06808c35e47b1113c385277d666806349a09249384347b3ecfd6f27ee79948b0",
"built_at": "2026-10-27T10:17:30+08:00",
"fingerprint": "1204cb60a4a9a44c3a3435166e07d7d0a54f32c98768951e79c85d6de422a447",
"fingerprint_files": 1430,
"fingerprint_source": "git-index",
"gated_regions": [
"RW_IRAM1",
"RW_CCM"
],
"git_head": "b623a406ad58c2d281d8e54f118999a0016da328",
"git_head": "51ecd36192982fec4bfd1f50fbfba3555c571c4c",
"map": "MDK-ARM/STM32F407-Demo/STM32F407-Demo.map",
"map_mtime": 1793026332,
"map_mtime": 1793067428,
"pct": {
"ER_IROM1": 24.54,
"RW_CCM": 87.89,
@@ -19,7 +19,7 @@
"regions": {
"ER_IROM1": {
"max": 1048576,
"size": 257272
"size": 257280
},
"RW_CCM": {
"max": 65280,
@@ -37,7 +37,7 @@
"schema": 2,
"scope_note": "\u540c\u7801\u51ed\u8bc1\uff1a\u6307\u7eb9\u8986\u76d6\u56fa\u4ef6\u4f53\u79ef\u76f8\u5173\u6e90\u7801\uff0c\u53e3\u5f84\u89c1\u672c\u811a\u672c docstring",
"sizes": {
"code": 231712,
"code": 231720,
"ro_data": 25560,
"rw_data": 1520,
"zi_data": 136216
+24 -2
View File
@@ -9,7 +9,15 @@ set(LVGL_DIR "${PROJECT_ROOT}/Lib/lvgl")
set(UI_DIR "${PROJECT_ROOT}/Drivers/BSP/UI")
set(NET_DIR "${PROJECT_ROOT}/Drivers/BSP/NET")
set(NETCFG_DIR "${PROJECT_ROOT}/Drivers/BSP/NETCFG")
set(SDL2_DIR "${CMAKE_CURRENT_SOURCE_DIR}/SDL2/SDL2-2.32.10")
# SDL2 是**下载来的第三方 SDK**,simulator/SDL2/ 被 .gitignore 排除、**不入库**(§3.1 部署文档)。
# 于是 CI/runner 的工作区里天然没有它:2026-10-27 远程 CI(run 45)第一次跑到 ui-golden 就
# 报 C1083 找不到 SDL2/SDL.h——本地有 SDK 所以本地一直是绿的,照不出来。允许用机器级
# DTU_SDL2_DIR 指向任意一份 SDK;**不设该变量时行为与今天完全一致**。
if(DEFINED ENV{DTU_SDL2_DIR} AND NOT "$ENV{DTU_SDL2_DIR}" STREQUAL "")
set(SDL2_DIR "$ENV{DTU_SDL2_DIR}")
else()
set(SDL2_DIR "${CMAKE_CURRENT_SOURCE_DIR}/SDL2/SDL2-2.32.10")
endif()
#=============================================================================
# 编译标准
@@ -33,7 +41,21 @@ endif()
# LVGL SDL 驱动内部 #include "SDL2/SDL.h",include 目录下已有 SDL2/ 子目录
set(SDL2_INCLUDE_DIR "${SDL2_DIR}/include" CACHE PATH "SDL2 include directory for LVGL")
#=============================================================================
# 缺 SDK 就在**配置期**一行说清。过去这里只打印拼出来的路径、不校验存在,于是配置阶段
# 照样输出 "SDL2: <路径>",直到编译 LVGL 的 SDL 驱动才炸出几百行 C1083,看着像代码错了
# 缺 SDK 就在**配置期**一行说清。过去这里只打印拼出来的路径、不校验存在,于是配置阶段
# 照样输出 "SDL2: <路径>",直到编译 LVGL 的 SDL 驱动才炸出几百行 C1083,看着像代码错了
# (CI run 45 的第一反应就是"像素回归红了吗")。查的是 SDL2_INCLUDE_DIR 而非 SDL2_DIR:
# 它是 CACHE 变量,复用旧 build 目录时缓存值可能比 SDL2_DIR 更旧,两处都得被这一条盖住。
if(NOT EXISTS "${SDL2_INCLUDE_DIR}/SDL2/SDL.h")
message(FATAL_ERROR
"SDL2 SDK 不可用:找不到 ${SDL2_INCLUDE_DIR}/SDL2/SDL.h"
" - 本机首次部署:照 docs/UI设计/LVGL模拟器部署与使用说明.md §3.1 下载 SDK 到 simulator/SDL2/"
" - CI / runner:工作区里没有、也不该有这份 SDK;请设机器级 DTU_SDL2_DIR 指向一份 SDK"
" (scripts/ci_runner_bootstrap.ps1 -SetEnv 会自动发现并写入该变量)"
" - 若刚换过 SDK 位置:build 目录里的旧缓存可能仍指向旧路径,删掉 simulator/build 再配置"
)
endif()
# LVGL 子目录(引用项目自带的 LVGL v9.3.0 源码)
#=============================================================================
# LVGL 内部会自动检测 lv_conf.h(通过 LV_CONF_INCLUDE_SIMPLE 宏)
+10
View File
@@ -117,6 +117,16 @@ if (Test-Path $cache) {
}
}
# 半截缓存检查:上一次配置如果在 CMakeLists 里 FATAL_ERROR 中断(例如缺 SDL2 SDK),
# CMakeCache.txt 已经写下、工程文件却根本没生成,于是下面「有缓存就不配置」会跳过配置,
# 失败点跑到离根因很远的 MSBuild(MSB1009: 项目文件不存在)——2026-10-27 在 CI 等价工作区
# 实测:设好 DTU_SDL2_DIR 之后仍然红,红因就是这个残留缓存。与上面的生成器不符同理,
# 删掉 build/ 重来,让报错回到配置期那一行。
if ((Test-Path $cache) -and -not (Test-Path (Join-Path $buildDir 'ALL_BUILD.vcxproj'))) {
Write-Host '[build_sim] build/ 有缓存但没有工程文件(上次配置中断)-> 删除重建' -ForegroundColor Yellow
if (-not (Remove-BuildDir $buildDir)) { exit 3 }
}
# --- 4. 配置 + 构建 ---------------------------------------------------------
if (-not (Test-Path (Join-Path $buildDir 'CMakeCache.txt'))) {
Write-Host '[build_sim] CMake 配置 ...'