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