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 全绿。
121 KiB
远程 CI 完整指南(Gitea Actions · 本机 Windows Runner)
适用范围(先读这一段):本仓库的远程 CI 只跑在本机——runner 名
win-stm32-ci, 装在D:\Docker\act_runner,连接的是本机原生 Gitea(http://127.0.0.1:3001)。 其他机器、其他项目都不需要、也不会用到本文这套配置,勿照搬。 日常改代码的等价自查是本地跑scripts/ci_local.py(与 CI 同一套判定),不依赖远程 CI 是否在线——runner 掉线不影响本地门禁。
适用:为 STM32F4-Base 部署 / 排障 CI(
.gitea/workflows/ci.yml,L0 + L0.5)。 前置阅读:docs/测试说明/测试开发标准流程.md §9(CI 覆盖边界与判定细节)、 docs/环境与部署/项目部署环境要求.md §2.11(runner 环境要求)。 本文只讲怎么配,不重复"为什么这么设计"。 本篇由原两篇 CI 指南CI配置指南.md与Gitea_Runner部署指南.md(原位于「环境与部署」目录, 两篇均已删除)于 2026-10-12 合并而成,去重后为单一来源。
适用环境(2026-10-02 实测确认):
- Gitea 1.25.4,Windows 原生二进制(
D:\Docker\Gitea\gitea.exe,不是 Docker), 以 Windows 服务运行(tasklist显示 Session =Services),监听0.0.0.0:3001- 配置
D:\Docker\Gitea\custom\conf\app.ini,[actions] ENABLED = true已开启 ✅- 数据库 MySQL(
127.0.0.1:3306),仓库根D:\Docker\Gitea\data\gitea-repositories- 对外域名
http://gitea.bitnasdaq.vip/(经反代),本地直连http://127.0.0.1:3001✅- Docker Desktop 已装但 daemon 未运行 → 本文不使用容器,全程原生
⚠️ 重要版本变更(2026-10-02 实测):act_runner 已更名为
gitea-runner, 当前最新版 v4.0.1(发布页https://gitea.com/gitea/runner/releases, 注意仓库路径已从gitea/act_runner变为gitea/runner)。 v4 移除了daemon install/start/stop/status子命令(那些是 v0.2.x 的接口)。 另:v4 的log段只有level和job(无file键),配置用gitea-runner config init / set / get / generate管理。
🔴 常驻方式修正(2026-10-02 实测推翻):
gitea-runner daemon是纯控制台程序, 不支持作为 Windows 服务运行(二进制内无服务管理代码,也无service.go); 用sc.exe/New-Service注册后Start-Service必然失败。 正确方式是 Windows 任务计划程序 —— 见 §3;且 2026-10-11 实测进一步要求 在计划任务之上加看门狗循环重拉 —— 见 §2.4。
0.0 ⚠️ CI 体积门禁:从盲区到同码凭证强制(必读,「体积门禁 CI 强制路径」残余已销项)
曾经的盲区已关闭为强制路径(2026-10-25,「体积门禁 CI 强制路径」残余销项,
task/A5-size-gate→564715c)。mem-numbers钩子的事实源是 Keil 构建产物MDK-ARM/**/*.map(已 gitignore),CI 刻意不跑@build→ 该钩子在远端只会被跳过(现以SKIP名单诚实显示 Skipped,不再伪装 Passed)。 补上的强制路径 = 同码凭证:本地@build0/0 后ram_baseline.py --update顺手盖scripts/size_attestation.json(三道前置:.map可解析 /.map不早于指纹内源码 mtime / 受管区 ≤92%);CI 无需 Keil,L0 作业末尾的「体积门禁」步用scripts/check_size_gate.py --ci复核两件事——工作树指纹 == 凭证指纹(改了代码从没测过 = 拦)+ 按凭证数字复跑 92% 红线。 主干 push 拦红,其余事件--warn-only;本地ci_local.py的 L0c 步无条件 fail-closed。仍守不住的三项(正文唯一口径 =
scripts/check_size_gate.pydocstring):CI 不重算绝对数字 (只复核同码性与红线复算);编译器选项(.uvprojx)不在指纹内;凭证由本地真实构建产生, 链路本身不经 Keil 授权就无法伪造,但"盖戳后换机器环境"的差异只能靠 mtime 前置兜。本地兜底动作不变(每次
@build后):ram_baseline.py --update(含盖戳)+check_mem_numbers.py --scan。若 runner 以后装了 Keil,mem-numbers会在有.map时照常执行。
0. 全景:CI 需要三样东西就位
① Gitea 服务端开关 ② Windows Runner 注册 ③ 仓库里的 workflow 文件
[actions] ENABLED=true labels 含 windows .gitea/workflows/ci.yml
│ │ │
└────────────┬───────────┴──────────────────────────────┘
▼
push / PR 触发 → runner 拉起容器/壳 → 跑 L0 + L0.5 → 结果回显到提交
三者缺一,CI 就是空转的(workflow 存在但永不执行 / 执行但拉不到 runner)。
当前仓库状态:③ 已就绪(本机提交 e904865,尚未 push);① ② 需人工确认。
0.1 本机拓扑:Gitea 和 Runner 在同一条机器上
Gitea 和 Runner 在同一条机器上 —— 这是本部署最关键的事实:
┌─────────────────────────── 本机 (Windows) ────────────────────────────┐
│ │
│ gitea.exe ── Windows 服务 ── 监听 0.0.0.0:3001 │
│ ▲ │
│ │ ① runner 注册 / 心跳 / 领任务:走 http://127.0.0.1:3001 │
│ │ (本地回环,不经公网、不依赖 DNS/反代) │
│ │ │
│ gitea-runner.exe ── 计划任务 GiteaActionsRunner(win-stm32-ci) │
│ │ │
│ └─ ② 领到 job 后,直接在**本机**跑: │
│ git fetch / pre-commit / cppcheck / cl.exe │
│ (标签 windows:host = 用宿主环境,非容器) │
│ │
│ 外部用户 → https://gitea.bitnasdaq.vip → 反代 → 127.0.0.1:3001 │
└───────────────────────────────────────────────────────────────────────┘
两个地址要分清(最容易搞错的地方):
| 用途 | 地址 | 说明 |
|---|---|---|
| Runner 连接 Gitea | http://127.0.0.1:3001 |
本机回环,最稳。用 gitea.bitnasdaq.vip 也行,但会多绕一圈 DNS + 反代,且若反代没配对内网 IP 会失败 |
| 用户在浏览器访问 | https://gitea.bitnasdaq.vip |
走反代;注册时网页上显示的示例命令用的是这个域名,你要手动换成 127.0.0.1:3001 |
⚠️ 注册时用哪个地址,决定了 runner 以后一直连哪里。 注册信息存在
.runner里,之后改起来要重新注册,建议一开始就用http://127.0.0.1:3001。
0.2 部署前置清单(需人工确认)
2026-10-12 自
docs/测试说明/测试开发标准流程.md§9.4 并入——这几项是"CI 能不能跑起来"的前提, 属于机器侧知识,故与本文其余内容放在一起,不再散在测试流程文档里。
| 项 | 要求 |
|---|---|
| Gitea Actions | 实例开启 [actions] ENABLED = true(site admin 配置),见 §1 |
| Windows runner | 已注册且 labels 含 windows;装有 Python 3.x、cppcheck、pre-commit、MSVC(VS2019/2022 C++ 工具链 + Windows SDK) |
| runner 必须有看门狗 | gitea-runner v4 的 daemon 无服务支持、且首次 Declare 失败就直接退出进程 → Gitea 重启一次,CI 就静默停摆(2026-10-11 实测:run 16~21 共 12 个 job 零日志)。用 scripts/ci_runner_bootstrap.ps1 装"等 Gitea 就绪 + 崩溃自拉起"的看门狗 + 计划任务,见 §2.4 |
| 机器级工具变量 | runner 以 SYSTEM 运行,看不见交互用户的 PATH/用户级 site。用 ci_runner_bootstrap.ps1 -SetEnv 写入 MACHINE 级 DTU_PRECOMMIT_PYTHON / DTU_CPPCHECK / DTU_MSVC_CL,见 §4.1.3 |
| 缺 MSVC 时 | L0.5 降级为 WARNING 跳过(不硬失败),但等于该层裸奔,须尽快补装 |
1. 服务端:开启 Gitea Actions
1.1 确认版本与开关
Gitea ≥ 1.21 才内置 Actions(实测本实例 1.25.4,满足)。以 site admin 登录后:
- 管理后台 → 站点管理 → 配置 / Site Administration → Configuration,搜索
actions:
[actions]
ENABLED = true
1.2 编辑配置文件(推荐,永久生效)
配置文件位置(按部署方式二选一):
| 部署方式 | 配置文件路径 |
|---|---|
| 二进制 / 直接跑 | <gitea 目录>/custom/conf/app.ini |
| Docker | 容器内 /data/gitea/conf/app.ini(挂载卷里的 custom/conf/app.ini) |
在 app.ini 追加(若无 [actions] 段):
[actions]
ENABLED = true
改完 重启 Gitea 服务(Docker:docker restart <容器名>;二进制:重启进程)。
1.2.1 DEFAULT_ACTIONS_URL 是什么(容易搞反)
它不是你的 Gitea 地址,而是控制 uses: actions/checkout@v4 这类 action 从哪里下载:
| 取值 | 行为 |
|---|---|
| 不配(默认) | 从 github.com 拉取 action |
self |
从你自己的 Gitea 实例拉取(要求 action 已镜像进你的实例) |
| 具体 URL | ⚠ 本实例的 1.25.4 上这一档没有可用取值:该版本只保留 github(默认)与 self 两个关键字,写具体 URL 不再改变下载源(2026-10 核对时踩到的第一现场就是这里,本表旧版写的是老行为)。别把希望放在这个键上——修法见 §5.3:不依赖 uses: |
⚠️ 不要把你的实例域名写进去当普通 URL——要"从自己实例拉"就用关键字
self; 且写了self还得先把 action 镜像进自己实例,否则actions/checkout依然拉不到。
按你的网络环境选择:
| 场景 | 配置 |
|---|---|
| 服务器能访问 github.com | 什么都不配(默认走 github.com,最省事) |
| 内网,不能访问 github.com | 本仓库现状(2026-10-27):workflow 里 uses: 已归零,DEFAULT_ACTIONS_URL 配不配都不影响(见 §5.3)。若哪天有人把 uses: 加回来,这条路就会重新变成外网依赖——self 需先镜像 action,而自定义 URL 在本版本无效 |
Gitea 官方 FAQ 原文:"From
https://github.comby default, or from your own instance when[actions].DEFAULT_ACTIONS_URLisself." 使用别处 action 也可在 workflow 里直接写全路径:uses: https://gitea.com/owner/repo@ref/uses: self:owner/repo@ref。
1.3 验证
浏览器打开仓库地址,顶部/侧栏应出现 Actions 标签页。 若仍提示"此仓库未启用 Actions",说明开关没生效,回到 1.1 检查。
⚠️ 若实例装在内网且不能访问公网,
actions/checkout@v4这类官方 action 会拉不下来。 处置见本文 §5.3。
2. Runner 端:注册 Windows Runner
CI 的两个 job 都是 runs-on: windows,所以必须有一个 labels 含 windows 的 runner。
推荐用 act_runner(Gitea 官方 runner)。
2.1 获取注册令牌
- 仓库级 runner(推荐,只服务本仓库):
仓库 → 设置 / Settings → Actions → Runners → 创建 Runner,复制页面显示的
Registration Token(形如
xxxYYYzzz...)。 - 全局 runner(服务整个实例):站点管理 → Actions → Runners。
记下 Gitea 地址,例如 https://gitea.bitnasdaq.vip。
本机(原生 Gitea)的具体操作路径:
- 浏览器登录
https://gitea.bitnasdaq.vip - 进仓库
wandering1/STM32F4-Base - Settings(设置)→ Actions → Runners → 创建 Runner / Create new Runner
- 页面会显示一段命令,形如:
./gitea-runner register --no-interactive --instance https://gitea.bitnasdaq.vip --token xxxxxxxxxxxxxxxxxxxx
复制其中的 --token 后面那一串(这是 Registration Token,一次性用途)。
页面上的 --instance 值不要照抄——见 §2.2.3。
💡 Token 存在于
action_runner_token表(MySQL,见本文 §6.1)。 每仓库一个有效 token,注册一次后即被消费;过期(约 1 小时)或已用则需重新创建。
2.2 下载并注册 runner
本机现状(唯一口径):二进制 gitea-runner.exe v4.0.1(实测 20,623,872 B ≈ 20.6 MB),
装在 D:\Docker\act_runner,注册 --instance 用 http://127.0.0.1:3001。
具体步骤见 §2.2.1 起,命令集与字段语义见 §2.2.2 / §2.3。
🗑 历史备查,勿照抄:本文初版写的是
act_runnerv0.2.11 +C:\act_runner+--instance https://gitea.bitnasdaq.vip。三点均已过时,且本机从未这样部署过 (2026-10-12 核实:C:\act_runner至今不存在): ① v0.2.x 与 v4 的二进制名与命令集都不同(v4 已无daemon install/start/stop/status,见 §3.0); ② 目录不同;③--instance填公网域名会多绕一圈 DNS + 反代,反代没配对内网 IP 就直接失败。 照抄那段会装出一个连不上 Gitea 的 runner,故原命令块已删除,只留此警示。
关键点:--labels "windows:host" 中的 windows 必须逐字与 workflow 的
runs-on: windows 对应(Gitea runner 的 label 名是自定义的,默认是 ubuntu-latest,
不注册 windows 这个 label 就永远匹配不到 job)。
:host表示直接跑在宿主 Windows 上(不是容器)。本场景必须用host—— 容器里拿不到宿主已装的 Keil/MSVC/cppcheck,且 Windows 容器支持很差。
2.2.1 选对版本并下载 gitea-runner
| 项 | 值 |
|---|---|
| 架构 | amd64(PROCESSOR_ARCHITECTURE = AMD64,实测) |
| 版本 | v4.0.1(2026-10-02 实测最新稳定版) |
| 下载页 | https://gitea.com/gitea/runner/releases ← 注意仓库名已从 act_runner 改为 runner |
| 文件名 | gitea-runner-4.0.1-windows-amd64.exe ← 二进制名也从 act_runner 改为 gitea-runner |
# 建目录(与 Gitea 同级,便于管理)
New-Item -ItemType Directory -Force -Path "D:\Docker\act_runner" | Out-Null
cd "D:\Docker\act_runner"
# 下载 v4.0.1(版本号请替换为 releases 页当前最新)
Invoke-WebRequest -Uri "https://gitea.com/gitea/runner/releases/download/v4.0.1/gitea-runner-4.0.1-windows-amd64.exe" `
-OutFile "gitea-runner.exe"
# 校验能跑
.\gitea-runner.exe --version
期望输出:gitea-runner version v4.0.1。
💡 本机实测(2026-10-02):下载 20.6 MB,耗时约 5 秒,
gitea.com可直连(HTTP 200)。
2.2.2 v4.0.1 的命令集(与 v0.2.x 的差异)
Available Commands:
bug-report Print information useful when filing a bug report
cache-server Start a cache server for the cache action
config Generate, read and edit config files
daemon Run as a runner daemon
exec Run workflow locally.
help Help about any command
register Register a runner to the server
⚠️ 没有
daemon install/start/stop/status—— 这些是 v0.2.x 的接口,v4 已移除。 常驻必须用 Windows 原生方式(见 §3)。
config子命令很方便,建议优先用它而不是手改 YAML:config init(生成最小配置)/config generate(打印全部选项) /config set <key> <value>/config get <key>。
2.2.3 执行注册(关键:instance 用 127.0.0.1)
cd "D:\Docker\act_runner"
.\gitea-runner.exe register `
--no-interactive `
--instance "http://127.0.0.1:3001" `
--token "粘贴刚才复制的 Registration Token" `
--name "win-stm32-ci" `
--labels "windows:host"
四个参数逐一说明:
| 参数 | 值 | 为什么 |
|---|---|---|
--instance |
http://127.0.0.1:3001 |
本机回环。因为 Gitea 就在这台机器上,回环最稳、最快、不受反代/DNS 影响 |
--token |
网页复制的那串 | 绑定到具体仓库(仓库级 runner) |
--name |
win-stm32-ci |
runner 显示名,随意但建议有辨识度 |
--labels |
windows:host |
🔴 最容易踩的坑:windows 必须与 workflow 的 runs-on: windows 逐字一致;:host 表示用宿主环境直接跑(非容器)——本场景必须 host,容器里拿不到 MSVC/cppcheck |
⚠️
--labels写错的后果:runner 会注册成功、显示绿色,但任务永远排队不执行 (没有任何 runner 匹配runs-on: windows)。这是最隐蔽的失败模式。
期望输出:Runner registered successfully,并在 D:\Docker\act_runner\ 生成 .runner 文件(记录注册凭据)。
2.2.4 验证注册
浏览器刷新 Settings → Actions → Runners,应看到:
| 列 | 期望值 |
|---|---|
| Name | win-stm32-ci |
| Labels | windows(:host 是执行方式,UI 可能只显示 windows) |
| Status | 🟢 Idle |
2.3 配置 config.yaml(可选但建议)
D:\Docker\act_runner\config.yaml 里确认/补充(本机完整落地版见 §2.3.2):
runner:
labels:
- "windows:host" # 与 register 一致
capacity: 1 # 并行 job 数;单机建议 1,避免 cppcheck 抢 CPU
timeout: 30m0s
fetch_timeout: 1m0s # 🔴 不得超过 RPC timeout(1m0s),否则被自动截断并告警
cache:
enabled: true
2.3.1 用 config 子命令管理(v4 推荐)
推荐用 config 子命令生成/修改(v4 新增,比手改 YAML 安全):
cd "D:\Docker\act_runner"
.\gitea-runner.exe config init # 生成最小 config.yaml
.\gitea-runner.exe config set runner.capacity 1 # 逐项设置
.\gitea-runner.exe config set runner.timeout 30m
.\gitea-runner.exe config set cache.enabled true
.\gitea-runner.exe config set log.level info
.\gitea-runner.exe config get runner.capacity # 校验
.\gitea-runner.exe config generate # 打印全部可用选项(含注释)
2.3.2 本机已落地的配置与字段语义
本机已落地的配置(D:\Docker\act_runner\config.yaml):
log:
# trace/debug/info/warn/error/fatal/panic;排障时临时改 debug
level: info
# 每个 job 的任务日志另存一份到本地(防上报 Gitea 失败时日志丢失)
job:
dir: "D:\\Docker\\act_runner\\log\\job"
retention: 168h
max_size: 100MB
runner:
file: .runner # 注册凭据(由 register 生成,勿手改)
capacity: 1 # 并行 job 数;单机建议 1(cppcheck 吃 CPU)
timeout: 30m0s # 单 job 超时;L0+L0.5 约 1~3 分钟
fetch_timeout: 1m0s # 🔴 不得超过 RPC timeout(1m0s):原写 5m0s 会被自动截断并刷告警,2026-10-02 已改
fetch_interval: 2s
# 🔴 标签:windows 必须与 workflow 的 runs-on: windows 逐字一致
labels:
- "windows:host"
cache:
enabled: true
dir: "D:\\Docker\\act_runner\\cache"
container:
network: ""
privileged: false
options: ""
valid_volumes: []
docker_host: ""
force_pull: false
⚠️ v4 的
log段只有level和job两个键 —— 没有file键 (config set log.file ...会报unknown config key)。 想要本地日志用log.job.dir(按 job 落文件),或把服务输出重定向到文件。⚠️ 改了
runner.labels必须重启 runner 才生效 (Gitea 官方 FAQ:"A runner declares its labels every time it starts ... so change them there and restart the runner.")
2.4 常驻方式:必须用看门狗,不能只起 daemon(2026-10-11 实测重写)
🔴
daemon install在 v4 已不存在:本机 runner 是gitea-runner v4.0.1,命令集只有bug-report / cache-server / config / daemon / exec / register,没有任何 Windows 服务 管理代码(daemon是纯控制台程序,无法向 SCM 回报SERVICE_RUNNING)。 旧版本文写的act_runner.exe daemon install在本机不可用。
🔴🔴 更关键的坑:
daemon在首次 Declare 失败时直接退出进程(不重试)。 runner 日志(D:\Docker\act_runner\log\runner-stdout.log)实测:level=error msg="fail to invoke Declare" error="unavailable: dial tcp 127.0.0.1:3001: connectex: ... actively refused it." Error: unavailable: dial tcp 127.0.0.1:3001 ...之后没有任何重试记录——进程已经没了。于是 Gitea 重启一次 / 机器重启一次, CI 就静默停摆:2026-10-08 起 runner 掉线,run 16~21 共 12 个 job 一行日志都没产生; 又因
ci.yml的concurrency.cancel-in-progress: true,队列里的 run 会被下一次 push 直接取消 (16~20 =cancelled、21 = 长期queued)——症状看起来是"任务凭空消失",而不是"任务失败", 极难从 UI 反推根因。
🔴 为什么"已经有计划任务"仍然会死(2026-10-11 实测,别以为注册过就安全): 旧任务跑的是
run_runner.cmd,里面是单次gitea-runner.exe daemon ...(无循环、无重启语义)。 开机时 Gitea 往往还没起来 → daemon 首次 Declare 失败立刻退出 → 任务"成功结束" → 计划任务认为一切正常,永远不会再拉。日志里 10-09 18:50 连续三次Starting runner daemon+ 三次 Declare 失败,就是三次"起了就死、死了没人管"。 所以必须换成看门狗(等就绪 + 循环重拉),单靠计划任务的"失败重启"救不了 ——daemon 是"正常退出",不是"失败"。
正确做法:一条命令装好"等 Gitea 就绪 + 崩溃自拉起 + 开机自启"(管理员权限,仓库根目录):
powershell -ExecutionPolicy Bypass -File scripts\ci_runner_bootstrap.ps1 -SetEnv -StartNow
它做三件事(细节见脚本头注释):
| 动作 | 内容 |
|---|---|
| ① 机器级工具变量 | 按 CI 同一套探测写 MACHINE 级 DTU_PRECOMMIT_PYTHON / DTU_CPPCHECK / DTU_MSVC_CL(已存在则跳过,-Force 才覆盖) |
| ② 看门狗启动器 | 生成 D:\Docker\act_runner\run_runner_watchdog.ps1:先轮询 <GiteaUrl>/api/v1/version 直到 Gitea 应答,再起 daemon;daemon 退出 → 记日志 → 10s 后重拉,永不放弃 |
| ③ 计划任务 | GiteaActionsRunner:开机 +1 分钟触发、以 SYSTEM + 最高权限运行、运行时限=不限、失败每 1 分钟重启(×999)、不允许多实例 |
先干跑,再真装(不需要管理员,把 action/trigger/settings 全构建一遍并打印,什么都不写):
powershell -ExecutionPolicy Bypass -File scripts\ci_runner_bootstrap.ps1 -DryRun
随时体检(只读,不改任何东西):
powershell -ExecutionPolicy Bypass -File scripts\ci_runner_bootstrap.ps1 -Status
输出会告诉你:计划任务是否存在(含上次运行时间与返回码)、daemon 是否在跑、Gitea 是否可达、 三个机器级变量是否已设。
⚠️
-Status要用管理员跑:非管理员会话不一定看得见GiteaActionsRunner(本次排障就被这一点误导过一次,以为任务从未注册)。脚本在非管理员下会额外提示这一点。
🔴 改这个脚本时守住两条(都是踩出来的): ①
StartWhenAvailable是任务设置属性(MSFT_TaskSettings,被MSFT_TaskSettings3继承), 触发器上没有这个属性——写$trigger.StartWhenAvailable会抛SetValueInvocationException中断脚本(实测,且 CIM 属性列表里看不到它,只能直接赋值); ② 必须"先构建全部对象、后注销旧任务"——早期版本先Unregister再构建,一旦构建阶段抛异常, 旧任务已被删、新任务没建成,CI 直接裸奔。
注册成功后,回到 仓库 → Settings → Actions → Runners,应看到 win-stm32-ci
状态为 Idle / 绿色。
⚠️
.runner里的address是 runner 实际连接的地址(本机http://127.0.0.1:3001), 与ROOT_URL(http://gitea.bitnasdaq.vip/)不是一回事;bootstrap 会以.runner为准。
3. 常驻底层:为什么不能用 Windows 服务 + 任务计划程序手动方式
本节是 2026-10-02 的实测记录("不能用 Windows 服务"的根因)与任务对象字段说明, 保留作根因依据与手动排障参考。正式部署一律用 §2.4 的
scripts/ci_runner_bootstrap.ps1: §3.2 的旧脚本deploy_runner_task.ps1生成的是单次 daemon,2026-10-11 实测证明它 首次 Declare 失败就退出、死了没人管;§3.3 已改写为与现行 bootstrap 等价的看门狗任务 对象,只用于理解字段或在别的机器上手搓。
3.0 🔴 关键结论:不能用 Windows 服务
本条为 2026-10-02 实测推翻既有假设后的修正结论,务必先读。
先前文档(及 install_service.ps1)主张用 sc.exe / New-Service 把
gitea-runner daemon 注册成 Windows 服务。实测不可行,Start-Service 必然失败:
Start-Service : 由于以下错误无法启动服务"Gitea Actions Runner (gitea-runner)" ...
根因(对 gitea-runner.exe v4.0.1 二进制逆向确认):
| 证据 | 结论 |
|---|---|
命令集只有 bug-report / cache-server / config / daemon / exec / register(对应源码 internal/app/cmd/{bug_report,cache-server,cmd,config,daemon,exec,register}.go) |
没有 service.go,没有任何服务管理子命令 |
daemon --help 仅 --labels / --once 两个 flag |
daemon 无服务模式开关 |
二进制内虽出现 OpenSCManagerW / CreateServiceW / StartServiceCtrlDispatcherW 等符号 |
它们来自 Docker SDK(容器作业路径),不是 runner 的服务安装代码——切勿据此误判 |
daemon 是纯控制台程序:它从不调用 StartServiceCtrlDispatcher,
因此 SCM 启动它后收不到 SERVICE_RUNNING 回报,判定超时 → 报错 1053。
正确做法:Windows 任务计划程序(Task Scheduler)。 它是控制台程序常驻的标准手段,功能上等价于服务:
| 服务能做的 | 任务计划程序对应能力 |
|---|---|
| 开机自启 | 触发器 -AtStartup(+ 延迟 30s 让 Gitea 先起) |
| 不登录也运行 | 以 SYSTEM 账户运行 |
| 最高权限 | -RunLevel Highest |
| 崩溃自动重启 | -RestartCount 999 -RestartInterval 1min |
| 开机/手动启停 | Enable/Disable/Start/Stop-ScheduledTask |
3.1 前台先试跑(强烈建议)
先别急着常驻——用前台模式确认能连上:
cd "D:\Docker\act_runner"
.\gitea-runner.exe daemon --config .\config.yaml
看到这两行即成功:
level=info msg="Starting runner daemon"
level=info msg="runner: win-stm32-ci, with version: v4.0.1, with labels: [windows], declare successfully"
让这个窗口开着,去 Gitea 页面确认 runner 是 Idle,然后 Ctrl+C 停掉、再做常驻。
3.2 🗑 已废弃:deploy_runner_task.ps1(2026-10-02 方案,被 §2.4 推翻)
不要再用它部署。 它生成的 run_runner.cmd 跑的是单次 gitea-runner.exe daemon
(无循环、无重启语义),而 §2.4 已实测证明 daemon 首次 Declare 失败就正常退出——
计划任务把它记成"成功结束",-RestartCount 永不触发,于是 Gitea 重启一次 CI 就永久停摆。
脚本流程(供辨认用):提权检查 → 前置文件检查 → 清理旧 Windows 服务 → 生成 run_runner.cmd
→ Register-ScheduledTask(开机+登录双触发器、SYSTEM、最高权限、崩溃重启)→ 启动 → 校验存活。
它的"崩溃重启"参数看着齐全,但对"正常退出"完全无效——这正是它失效的那一点。
✅ 已于 2026-10-12 删除
D:\Docker\act_runner\deploy_runner_task.ps1与run_runner.cmd(删除前核实:只有check_env.ps1的一句提示引用它,已同步改指ci_runner_bootstrap.ps1; 计划任务的动作目标是run_runner_watchdog.ps1,与这两个文件无关,删除不影响在跑的 runner)。 现行部署一律用仓库里的scripts/ci_runner_bootstrap.ps1。
3.3 手动等价命令(等价于 §2.4 的 bootstrap,用于理解任务对象)
日常请直接跑 scripts/ci_runner_bootstrap.ps1 -SetEnv -StartNow。下面是它注册的等价对象,
只在需要理解字段或在别的机器上手搓时使用:
$Dir = 'D:\Docker\act_runner'
$launcher = Join-Path $Dir 'run_runner_watchdog.ps1' # 由 bootstrap 生成的看门狗
# ① 动作:跑【看门狗】而不是 daemon 本体(单次 daemon 就是 §3.2 失效的根因)
$action = New-ScheduledTaskAction -Execute 'powershell.exe' `
-Argument "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$launcher`"" `
-WorkingDirectory $Dir
# ② 触发器:只要开机触发
$trigger = New-ScheduledTaskTrigger -AtStartup
$trigger.Delay = 'PT1M' # 给 Gitea 启动留时间(看门狗自己也会等,双保险)
# ③ 设置
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
-RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1) `
-ExecutionTimeLimit ([TimeSpan]::Zero) -MultipleInstances IgnoreNew
$settings.StartWhenAvailable = $true # 🔴 属【任务设置】,写在触发器上会抛异常
Register-ScheduledTask -TaskName 'GiteaActionsRunner' -Action $action -Trigger $trigger `
-Settings $settings -User 'SYSTEM' -RunLevel Highest `
-Description 'Gitea Actions Runner for STM32F4-Base CI (labels: windows:host)'
# ④ 启动
Start-ScheduledTask -TaskName 'GiteaActionsRunner'
⚠️
ExecutionTimeLimit = 0(不限时长)必须显式设置—— 任务计划程序默认 72 小时强制结束任务,会让 runner 每 3 天被杀一次。
⚠️
-WorkingDirectory只对New-ScheduledTaskAction有效(这是它区别于 WinPS 5.1New-Service的地方)。SYSTEM 账户的默认工作目录不可控,所以启动器里 仍要显式Set-Location或用绝对路径。
🗑 旧方案还额外挂了
-AtLogOn触发器并以-LogonType ServiceAccount运行。现行口径 只留开机触发:runner 与登录会话无关,多一个触发器只会让人误以为"必须有人登录才行"。
3.4 任务管理速查
Get-ScheduledTask -TaskName GiteaActionsRunner # 状态
Get-ScheduledTaskInfo -TaskName GiteaActionsRunner # 上次运行结果
Start-ScheduledTask -TaskName GiteaActionsRunner # 启动
Stop-ScheduledTask -TaskName GiteaActionsRunner # 停止
Disable-ScheduledTask -TaskName GiteaActionsRunner # 禁用自启
Enable-ScheduledTask -TaskName GiteaActionsRunner # 恢复自启
Unregister-ScheduledTask -TaskName GiteaActionsRunner -Confirm:$false # 完全卸载
改完 config.yaml 后需 Stop 再 Start 任务才生效(纯注释改动不需要——注释不参与解析)。
🔴 重跑
ci_runner_bootstrap.ps1会先注销再注册任务,而注销不会带走旧实例的进程: 旧看门狗变成孤儿、继续养着旧 daemon,旧 daemon 占住.runner.lock,新看门狗就永远起不来 (症状见 §7"online 却每 10 秒重启"那行)。2026-10-12 起-StartNow会先停掉残留的看门狗与 daemon 再启动并回显单实例确认;-Status也会把重复实例与锁冲突报出来。
💡
LastTaskResult数值解读(别被非 0 吓到):
值 含义 267009(0x41301)✅ SCHED_S_TASK_RUNNING—— 任务正在运行,不是错误(常驻任务常态值)0(0x0)✅ 上次运行正常结束 267011(0x41303)任务尚未运行过 267014(0x41306)任务被用户终止 判断 runner 是否健康,看这三项而不是
LastTaskResult: ①Get-Process gitea-runner有进程(其父进程应是跑run_runner_watchdog.ps1的 powershell); ②log\runner-daemon.log最新一行是declare successfully; ③ Gitea 页面 runner 显示 Idle(或 MySQL 查action_runner.last_online)。⚠️
Get-ScheduledTaskInfo的Next Run Time为空是正常的——触发器是"开机 +1 分钟", 没有下一个日历时间可报;常驻进程靠看门狗保活,不靠计划器反复拉起。别把空值当成"任务坏了"。
3.5 ⚠️ 清理遗留的无效服务
若早前按旧文档建过服务,删掉它(留着只会误导):
Stop-Service gitea-runner -Force -ErrorAction SilentlyContinue
sc.exe delete gitea-runner
gitea-runner这个名字将不再作为服务存在。 之后提到"gitea-runner" 一律指计划任务GiteaActionsRunner或进程gitea-runner.exe。
3.6 ⚠️ 权限要求(本机实测的硬约束)
注册计划任务必须用「提权的管理员」Shell。本机实测:
| 检查项 | 结果 |
|---|---|
| 当前用户 | DESKTOP-KM0OG7Q\Administrator |
是否提权(IsInRole(Administrator)) |
False |
| 非提权下注册任务 | ❌ 拒绝访问(Register-ScheduledTask 需要 elevated token) |
"是 Administrator 用户" ≠ "有管理员权限" —— Windows 的 UAC 会把普通登录会话降权。 必须右键 PowerShell → 以管理员身份运行,标题栏会显示「管理员」字样。
💡 计划任务以
SYSTEM账户运行,拥有完整权限,可正常访问本地127.0.0.1:3001的 Gitea。
4. Runner 软件环境:装什么
runner 是一台干净的 Windows,下面这些必须装,否则 job 会失败或降级跳过。
| 项 | 必需性 | 影响 | 安装 |
|---|---|---|---|
| Python 3.x | 🔴 必需 | l0-gates 环境自检失败 → job 硬失败 |
winget install Python.Python.3.12 |
| cppcheck | 🔴 必需 | 同上(cppcheck --version 探测不过) |
winget install Cppcheck.Cppcheck |
| pre-commit | 🔴 必需 | 同上(python -m pre_commit --version 探测) |
pip install pre-commit 或 uv tool install pre-commit |
| MSVC(VS + Windows SDK) | 🟡 建议 | 缺则 l05-host-unit-tests 告警降级跳过(不硬失败,但 L0.5 裸奔) |
winget install Microsoft.VisualStudio.2022.BuildTools,勾"使用 C++ 的桌面开发" |
| Keil / ST-LINK | ⛔ 不需要 | CI 刻意不跑 @build |
— |
| 开发板 / 串口 | ⛔ 不需要 | L1/L2/L3 不上 CI | — |
Git 另算:git --version 任意版本即可(你已在用)。
本机实测状态(2026-10-02):
Python : C:\ProgramData\anaconda3\python.exe → 3.12.4 ✅
cppcheck : C:\Program Files\Cppcheck\cppcheck.exe → 2.21.0 ✅
pre-commit: 4.6.0 (anaconda3) ✅
MSVC : VS2019 BuildTools 14.29.30133 (Hostx64\x64\cl.exe) ✅
→ 四项全齐,无需额外安装。
4.1 关键:变量必须对 SYSTEM 可见(不是"对你可见")
runner 以 SYSTEM 服务/计划任务运行,它看到的 PATH 与 site-packages 和你登录时完全不同:
| 你以为 | SYSTEM 实际看到 | 后果 |
|---|---|---|
python 在 PATH 里 |
PATH 上是 C:\msys64\ucrt64\bin\python.exe,没有 pre-commit |
L0 环境自检失败 |
pip install --user pre-commit 装好了 |
用户级 site 对 SYSTEM 完全不可见(换任何 python 路径都没用) | 同上 |
仓库里有 .venv |
.venv 已 .gitignore,runner 全新工作区根本没有 |
同上 |
cppcheck 在 PATH 里 |
机器 PATH 无 C:\Program Files\Cppcheck |
L0 环境自检失败 |
机制补充(2026-10-02 实测):计划任务以
SYSTEM账户运行时,继承的是系统级 PATH (注册表HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment的Path), 不含任何用户级 PATH。核对方法:用check_env.ps1(见 §9)的「系统 PATH 视角」检查项, 或以 SYSTEM 身份跑一次where python验证。
CI 侧的探测统一走 scripts/ci_find_python.ps1(两个 job 共用、可离线自测):
DTU_PRECOMMIT_PYTHON(显式;设了就必须有效,不静默回退)
-> PATH python -> 仓库 .venv -> 常见机器级安装(Program Files\Python3xx、
%LOCALAPPDATA%\Programs\Python\Python3xx、%ProgramData%\anaconda3|miniconda3)
cppcheck 同理:DTU_CPPCHECK → PATH → %ProgramFiles%\Cppcheck\cppcheck.exe
(及 %ProgramFiles(x86)%\Cppcheck\cppcheck.exe)。
推荐做法:跑一次 bootstrap,把机器级变量固化下来(探测逻辑与 CI 完全一致):
powershell -ExecutionPolicy Bypass -File scripts\ci_runner_bootstrap.ps1 -SetEnv
手工自检(三条都必须对 SYSTEM 成立,而不只是对你的终端成立):
python --version # 期望 Python 3.x
cppcheck --version # 期望 Cppcheck 2.21.0 或更高
python -m pre_commit --version
⚠️ 改了机器级变量后必须重启 runner(计划任务:
Stop-ScheduledTask+Start-ScheduledTask) ——环境变量只在进程启动时读取,daemon 不重启就吃不到新值。
4.1.1 ⚠️⚠️ 关键坑:pre-commit 必须在「全局 site-packages」,装到用户态等于没装
这是 2026-10-02 排查 L0 job 反复失败(::error::no Python with a SYSTEM-visible pre-commit)的真正根因,
也是本环境最隐蔽的一个坑。
现象:pre-commit 在你自己的终端里好好的(pre-commit --version 有输出),
但 CI 的 l0-gates job 三个候选 Python 全部探测失败:
::error::no Python with a SYSTEM-visible pre-commit. Tried:
C:\msys64\ucrt64\bin\python.exe |
<workdir>\.venv\Scripts\python.exe |
C:\ProgramData\anaconda3\python.exe
📌 上为 2026-10-02 的历史报错原文。workflow 现已改为
DTU_PRECOMMIT_PYTHON(机器级)→ PATH 的python→ 工作区.venv顺序(§4.1.3), 且报错行会指明是变量无效还是自动发现落空。
根因(两层):
-
用户态 site-packages 对 SYSTEM 不可见。
pip install pre-commit(不带提权)默认装到 user-site:C:\Users\Administrator\AppData\Roaming\Python\Python312\site-packages。 而 SYSTEM 账户的%APPDATA%是C:\Windows\system32\config\systemprofile\AppData\Roaming—— 完全是另一个目录, 它根本看不到 Administrator 的 user-site。任何python.exe路径都救不了这个。 -
pip 的「已满足」判定陷阱(更阴)。 就算你提权执行
pip install --no-user pre-commit,pip 在判断 「这个包是不是已经装了」时看的是整个sys.path——其中包含 user-site。 既然 user-site 里已有同版本的包,pip 会直接打印Requirement already satisfied: xxx in ...\roaming\...\site-packages然后完全跳过安装到全局目录。 结果:pip 报"成功",全局目录里却什么都没有。实测:9 个依赖里 8 个因版本与用户态不同而被 pip 升级(侥幸落到全局), 唯独
cfgv用户态已是同版本 3.5.0 → 被静默跳过 →-s视角下ModuleNotFoundError: No module named 'cfgv'。
诊断命令(-s = 禁用 user-site,模拟 SYSTEM 视角):
# 看全局目录到底有没有(而不是 pip 怎么说)
& 'C:\ProgramData\anaconda3\python.exe' -s -m pre_commit --version
# 逐个依赖点名,找出被静默跳过的那个
& 'C:\ProgramData\anaconda3\python.exe' -s -c "import cfgv, identify, nodeenv, virtualenv, distlib, filelock, platformdirs, yaml; print('all ok')"
# 反查 pip 为何跳过(会显示 "Requirement already satisfied ... roaming ...")
& 'C:\ProgramData\anaconda3\python.exe' -m pip install --no-user --dry-run cfgv
修复(必须提权):脚本 install_precommit_global.ps1 +
install_precommit_global2.ps1(逐包安装 + 逐个 import 复验);
对被静默跳过的包补一支 fix_cfgv_global.ps1,核心是强制重装:
& $Py -m pip install --no-user --force-reinstall --no-deps `
--disable-pip-version-check --no-warn-script-location cfgv
--force-reinstall无视"已满足"判定,--no-deps避免牵连其它包。 三个脚本都在D:\Docker\act_runner\。
验证(四条全过才算好):
$Py = 'C:\ProgramData\anaconda3\python.exe'
& $Py -s -m pre_commit --version # pre-commit 4.6.2
Test-Path 'C:\ProgramData\anaconda3\Lib\site-packages\cfgv.py' # True
# 关键:确认文件真在【全局】目录,而不是 roaming
Get-ChildItem 'C:\ProgramData\anaconda3\Lib\site-packages' -Filter 'cfgv*'
⚠️ 别再往
.venv里装就以为 CI 能用了——CI 虽然会检查.venv\Scripts\python.exe, 但actions/checkout@v4拉的是干净克隆,.venv是 gitignore 的,根本不存在。
4.1.2 ⚠️ 关键坑:doc-links 钩子「本地过、CI 挂」= 引用了 gitignore 的运行时文件
现象:文档链接存在性 钩子在本地 pre-commit run --all-files 通过,
一上 CI 就 Failed(exit 1),报的行/路径你在本地明明存在。
根因:CI 的 actions/checkout@v4 拉的是干净克隆,而钩子校验的是
仓库根相对路径是否存在(check_backtick_paths,见 scripts/check_doc_links.py)。
若文档引用了 gitignore 的运行时产物——
如 .dsh/skills/serial-assistant/scripts/state/meta.json(运行时未创建,.gitignore:132 排除整个 state/)——
你本机因为跑过工具所以文件在,克隆里则不存在 → 判为失效引用。
修复:按 docs/文档规范.md §3.3 给该行加状态词(脚本的 STATUS_HINTS:
计划 / 未创建 / 尚未创建 / 已删除 / 删除 / 不存在 / 缺失 / 断链 / 待建 / 待创建)。
| `.dsh/skills/serial-assistant/scripts/state/meta.json`(运行时未创建) | daemon 元数据(pid / 版本 / gen) |
⚠️ 状态词必须与引用在【同一行】且 ≤40 字符(
STATUS_PROXIMITY)。 把说明写在上一行是无效的——脚本逐行调用has_status_hint(line, ...), 跨行的文字不在窗口内。
本地复现 CI 条件的姿势(否则永远测不出来):
# 把运行时目录临时移开,模拟干净克隆,跑完再移回
mv .dsh/skills/serial-assistant/scripts/state .dsh/skills/serial-assistant/scripts/state.bak_ci
& '.venv\Scripts\python.exe' scripts/check_doc_links.py --all # 或全局 python -m pre_commit run doc-links --all-files
mv .dsh/skills/serial-assistant/scripts/state.bak_ci .dsh/skills/serial-assistant/scripts/state
更一般的教训:任何校验"文件是否存在/数字是否一致"的钩子,都要在 "无本地产物"的干净克隆里验一次,别只在开发机上跑。
4.1.3 机器级 DTU_* 环境变量(runner 的正式覆盖通道,2026-10-02 起)
workflow 已改为统一消费 DTU_* 接口(见 本地工具路径环境变量方案.md §3.3):
显式变量优先 → 自动发现回退,与 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 任务看到):
[Environment]::SetEnvironmentVariable('DTU_PRECOMMIT_PYTHON', 'C:\ProgramData\anaconda3\python.exe', 'Machine') # 示例:本机 runner
[Environment]::SetEnvironmentVariable('DTU_CPPCHECK', 'C:\Program Files\Cppcheck\cppcheck.exe', 'Machine')
[Environment]::SetEnvironmentVariable('DTU_MSVC_CL', 'C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\VC\Tools\MSVC\14.29.30133\bin\Hostx64\x64\cl.exe', 'Machine')
# ★ 设完必须重启 runner 任务(环境块是任务启动时快照的)
Stop-ScheduledTask -TaskName GiteaActionsRunner
Start-ScheduledTask -TaskName GiteaActionsRunner
与 §4.1.1 的关系:机器级
DTU_PRECOMMIT_PYTHON是正式通道(指向哪个解释器由你说了算, 值无效时 job 直接报变量名、不猜);"把 pre-commit 装进全局 site-packages"仍是兜底 (变量未设 + 全局 site 可见也能过)。两条路并存不冲突:变量设了就用变量,没设走探测。 显式变量值无效(文件不存在 / 无 pre-commit 模块)→ job 硬失败并指明变量名——不会 静默回退到别的解释器,避免 runner 悄悄换了 Python 导致钩子行为漂移。
4.2 MSVC 的路径要求(探测逻辑)
l05-host-unit-tests 的 MSVC 探测优先读机器级 DTU_MSVC_CL(x64 cl.exe 完整路径;
值无效 → 硬失败、不静默回退);未设置时按标准布局自动探测(根目录由
%ProgramFiles / %ProgramFiles(x86) 推导,不写死盘符;与
test/host/common/unit_test_harness.py 的 _default_vs_roots() 保持一致):
2019 → Program Files (x86);2022 / 2026(实际目录名 18)→ Program Files,
各取 BuildTools/Community/Professional/Enterprise 子目录,找
VC\Tools\MSVC\*\bin\Hostx64\x64\cl.exe。
本机实测命中第 1 条(2026-10-02):
C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\VC\Tools\MSVC\14.29.30133\bin\Hostx64\x64\cl.exe ✅
⚠️ 装在完全非标准位置(如 D 盘自定义目录)会探测不到 → 被当成"没装 MSVC"跳过 L0.5(仅告警)。 处置(二选一):设机器级
DTU_MSVC_CL指向cl.exe(推荐;值无效会硬报错), 或把 VS 装回标准位置(探测按%ProgramFiles*推导,换盘符的标准布局仍命中)。Windows SDK 同理:默认
<ProgramFiles(x86)>\Windows Kits\10\;非标准位置用DTU_WINDOWS_KITS_ROOT指定。
📌 2026-10-02:workflow 与本地 harness 同步改为"变量优先 + 标准布局推导" (本地工具路径环境变量方案.md)——旧写法要求"同步改两处",现已不需要。
4.3 验证运行器能找到工具链(注册前先跑一遍)
& '.venv\Scripts\python.exe' scripts/ci_local.py
这条命令跑的就是 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。本机实装在 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,可整段粘贴):
# ① 机器级 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 盘 x64cl.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-verifyskill 重跑像素回归。 - 残留待核:口径 §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 驱动才炸。
已落地的修法(四件):
simulator/CMakeLists.txt支持机器级DTU_SDL2_DIR覆盖 SDK 位置;不设该变量时行为与 过去完全一致(实测:不设 → 取simulator/CMakeLists.txt里写死的仓内默认路径 →ui-golden Passed)。- 同文件加配置期硬校验:缺 SDK 时一行
FATAL_ERROR讲清三条出路(本机装 / CI 设变量 / 删旧 build 缓存),而不是几百行 C1083 冒充代码错误。实测护栏生效:改前那种工作区里C1083行数从一整屏降到 0,只剩护栏自己那一条。 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一并列出。- 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 文件
5.1 文件位置与提交
workflow 必须位于仓库根的 .gitea/workflows/ 下(不是 .github/):
<仓库根>/
└─ .gitea/
└─ workflows/
└─ ci.yml ← 本仓库已有,见该文件
它必须被 push 到远程才能生效——目前本仓库的 e904865 还在本地
(origin/v2-on-ui 尚未包含),所以现在 push 之前 CI 不会跑:
git push origin v2-on-ui # ⚠️ 需你明确要求才推送(本仓库约定:默认不推送)
5.2 触发条件
on:
push:
paths-ignore: ['docs/**', '**/*.md', '.workbuddy-ai/**']
pull_request:
paths-ignore: ['docs/**', '**/*.md', '.workbuddy-ai/**']
workflow_dispatch: # 页面可手动触发
- 纯文档改动不触发(省 runner);
- 想手动跑一次:仓库 → Actions → 选
CI→ Run workflow。
5.3 内网无公网时的 action 拉取:本仓库已彻底移除 uses:(2026-10-27 落地)
为什么非自己动不可: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 表第三行),服务端没有可拧的键,
唯一归本仓库管的修法就是不再依赖它。
现状:两个 job 的第 1 步都是原生 git 检出,workflow 里 uses: 归零。检出正文内联在 workflow
里,不能抽成 scripts/*.ps1——它执行时脚本自己还没被检出(鸡生蛋),这与 ci_find_python.ps1 /
ci_gitlint_range.ps1 的抽取方向相反;代价是两个 job 各持一份、必须逐字一致(自查见本节末)。
这份检出做了什么(等价 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,不会默默测一棵错的树;届时的 兜底是改 fetchrefs/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 前后都跑,第二条是内联换来的同步义务):
# 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)')"
5.4 产物存档与结果汇总(2026-10-02 起)
目标:CI 失败时不查 MySQL / 不翻 runner 磁盘,在 Gitea 页面即可看到完整输出与摘要。
零依赖方案(内网首选,不用 actions/upload-artifact):
| 手段 | 说明 |
|---|---|
Tee-Object 落盘 |
两个 job 把关键输出 Tee 到 ci-artifacts/l0-pre-commit.log、ci-artifacts/l05-<模块>.log(留在 runner 工作区,已 gitignore) |
$GITHUB_STEP_SUMMARY |
写入 job 的 step-summary(Gitea UI 运行页直接可见,无需下钻日志) |
::notice:: 汇总行 |
控制台醒目行,便于日志检索 |
汇总行格式:
| 层 | 汇总行 |
|---|---|
| L0 | L0: pre-commit hooks -- passed=N failed=M skipped=K (log: ci-artifacts\l0-pre-commit.log) |
| L0.5 | L0.5: <K> runner(s), <A>/<B> assertions passed, fail=<F>(逐模块另有 ::notice::<模块>: N/M PASSED (failed=0)) |
⚠️
ci-artifacts/已加入.gitignore:它是 runner 工作区的运行时产物,不要提交。⚠️ 若后续改为
actions/upload-artifact(需确认实例能拉该 action):把Tee落盘 保留即可,上传步骤叠加在其后;本方案的落盘文件即上传源。
PowerShell 陷阱(已在 workflow 内处理):runner 注入 $ErrorActionPreference='Stop',
而 & native 2>&1 | Tee-Object 会把 native 的 stderr 变成 ErrorRecord → 在 Stop 下整段中止。
故 pipeline 前后需 Save/Restore 该偏好为 Continue(与"环境自检"步骤同法)。
$LASTEXITCODE 在 Tee-Object 之后仍保留 native 退出码(实测),可直接用于判定。
5.5 两个 job 的步骤要点(2026-10-12 自 docs/测试说明/测试开发标准流程.md §9.2 并入)
| job | runs-on |
步骤要点 |
|---|---|---|
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 末)→ 探测 MSVCMSVC**(按 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 单方面把候选收窄成"PATH 上的 python + 仓库.venv"(而 runner 上这两条都是死路: PATH python 没装 pre-commit、.venv被 gitignore 根本不存在)导致的。改一边必须改另一边。
6. 端到端验证清单
按顺序确认,任一步不过就停在那里排障:
| # | 检查 | 期望 |
|---|---|---|
| 1 | 仓库顶部有 Actions 标签页 | 服务端开关已开 |
| 2 | Settings → Actions → Runners 里 runner 为 Idle | runner 注册成功且在线(offline = 后面全都不会跑,先做第 3 步) |
| 3 | scripts\ci_runner_bootstrap.ps1 -Status |
计划任务已注册 / daemon 在跑 / Gitea 可达 / 三个 DTU_* 已设——四项全绿 |
| 4 | runner 上 python --version / cppcheck --version / python -m pre_commit --version |
三条全通(且对 SYSTEM 成立,见 §4.1) |
| 5 | git push 一次(或页面 Run workflow) |
Actions 页出现运行中任务 |
| 6 | l0-gates 日志 |
using python (…) + runner environment check passed;::notice::L0: pre-commit hooks -- passed=.. failed=0 skipped=..;无 .map 时见 ::warning:: 体积门禁未跑 |
| 7 | l0-gates 的 提交信息门禁 步骤 |
::notice::gitlint: N commit message(s) checked, 0 failed(这一行以前根本不存在,见 §7) |
| 8 | l05-host-unit-tests 日志 |
::notice::L0.5: <N> runner(s), A/A assertions passed, fail=0。N = test/host/ 下 run_unit_test.py 的个数(2026-10-12 实测:master 3 个 / 288 断言,v2-on-ui 10 个);断言总数随单测增减而变,判定只看 fail=0 且 A==B,不要拿历史数字当基准 |
| 9 | job 运行页的 Summary 区块 | 出现 "L0 static gates" / "Commit message gate (gitlint)" / "L0.5 host-side unit tests" 小标题与汇总行 |
| 10 | 提交记录旁出现 ✅ | CI 全绿 |
本地先自查(与 CI 同一套判定,避免推上去才发现问题):
& '.venv\Scripts\python.exe' scripts/ci_local.py
⚠️ 第 5 步的前提(2026-10-02 记录)是当时的最大阻塞:本仓库的
.gitea/workflows/ci.yml还在本地 (提交e904865/1e01950/52e6dcb/c305a8d均未 push)。workflow 不上远程,CI 永远不会跑, 哪怕 runner 配得再完美。
6.1 无网页登录时的状态核验(MySQL 直查)
runner 是否在线,可以直接查 Gitea 数据库确认(本机 MySQL 凭据见
D:\Docker\Gitea\custom\conf\app.ini 的 [database] 段):
MYSQL="/c/Program Files/MySQL/MySQL Server 8.4/bin/mysql.exe"
"$MYSQL" -h 127.0.0.1 -P 3306 -u wandering -p<pwd> -D gitea -e \
"SELECT id,name,version,last_online,agent_labels FROM action_runner;"
| 字段 | 判读 |
|---|---|
name / version |
注册名与版本(应为 win-stm32-ci / v4.0.1) |
agent_labels |
应为 ["windows"] ← 不含 windows 就是标签配错了 |
last_online |
Unix 秒;与当前时间相差 < 30 秒即在正常心跳 |
date +%s # 对比 last_online
相关表:action_runner(已注册 runner)、action_runner_token(注册令牌,
字段 repo_id 对应 repository.id)。
7. 常见故障排查
| 现象 | 根因 | 处置 |
|---|---|---|
运行状态是 cancelled 且一行日志都没有 |
runner 掉线时 run 只在队列里等,下一次 push 触发 cancel-in-progress 把它们取消——所以表现为"任务凭空消失"而不是"任务失败" |
先按下一行修 runner;被取消的 run 可在页面 Re-run,或重新 push |
Runners 页 runner 显示 offline |
daemon 进程不在:gitea-runner v4 的 daemon 首次 Declare 失败就直接退出(Gitea 重启/机器重启后无人拉起) |
§2.4 用 ci_runner_bootstrap.ps1 装看门狗 + 计划任务;-Status 复查 |
l0-gates 报 no Python with a SYSTEM-visible pre-commit |
runner 是 SYSTEM:PATH 上的 python 无 pre-commit、用户级 site 不可见、.venv 未入库(§4.1 三重坑) |
ci_runner_bootstrap.ps1 -SetEnv 写机器级 DTU_PRECOMMIT_PYTHON,或把 pre-commit 装进机器级解释器;改完必须重启 runner |
l0-gates 报 runner missing cppcheck |
机器 PATH 无 Cppcheck 且无机器级 DTU_CPPCHECK |
-SetEnv(会自动发现 C:\Program Files\Cppcheck)或装 Cppcheck.Cppcheck |
钩子条数对不上(CI 报告条数 = 定义数 − 1;2026-10-12 实测 master 18 定义 / 17 报告,v2-on-ui 26 定义 / 25 报告——增删钩子的分支必须重算这个差值) |
gitlint 的 stages: [commit-msg] 写在上游 .pre-commit-hooks.yaml(项目 .pre-commit-config.yaml 里 grep 不到,别据此断定"没限制"),pre-commit run --all-files 跑不到它,且表现为全绿——静默空转 |
已由 scripts/ci_gitlint_range.ps1 单独校验;以后增删钩子必须核对"定义数 == 日志条数 + gitlint" |
gitlint 步骤 Failed 但提交信息其实合规(同一条命令在本地全绿),日志里是 exit code: 254 + Missing git configuration: please set user.name |
actions/checkout 把 HOME 临时指向每次任务的临时目录,而 SYSTEM 账号没有全局 git 身份 → gitlint 在读到任何规则之前就因配置错误退出(254 不是规则违规)。旧脚本末尾那句 ::error::...rule: single-line Chinese title, < 100 chars 是写死的猜测文案,把环境问题伪装成"提交信息不合格";且该步 exit 1 会让后面的 --all-files 整个不跑——看着像"门禁拦住了",实际 26 个钩子一个都没执行 |
ci_gitlint_range.ps1 先探测身份,缺失就用 GIT_CONFIG_COUNT/KEY/VALUE 注入一次性身份(不写任何文件,开发者机器不受影响),并把 254 与真实违规分开报(2 = 门禁没跑起来,1 = 信息确实违规)。实测同一命令:空 HOME 下改前 254、改后 0 failed(2026-10-12 task 31 首次暴露) |
| 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) |
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 |
L0.5 报 produced no '=== ... N/M PASSED ===' summary line |
该模块运行器未输出汇总行(改了 harness 或运行器崩溃) | 核对 test/host/common/unit_test_harness.py 的 summary_marker 与该模块 TestBuild 是否一致 |
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 |
任务跑到一半报 python: 不是内部或外部命令 |
SYSTEM 账户 PATH 没有 Python | 确认 Python 在系统 PATH → 重启任务(§4.1) |
改了 .runner / 换机器 |
注册信息失效 | 删 .runner → 重新 register |
daemon install 报 accepts at most 0 arg(s) |
v4 已移除该子命令,且无任何服务子命令 | 改用任务计划程序(§3) |
🔴 Start-Service gitea-runner 报 无法启动服务(错误 1053) |
daemon 不是服务型程序,不向 SCM 回报 SERVICE_RUNNING |
删除该服务,改用任务计划程序(§3.0 / §3.5) |
Register-ScheduledTask 报 拒绝访问 |
Shell 未提权(IsAdmin=False) |
右键 PowerShell → 以管理员身份运行(§3.6) |
任务 LastTaskResult 非 0 |
启动器路径/权限问题,或任务被手工终止 | 看 log\runner-watchdog.log 与 log\runner-daemon.log;确认 run_runner_watchdog.ps1 存在且为纯 ASCII;0x41301 是"正在运行"不是错误 |
🔴 runner 网页显示 online,但 runner-watchdog.log 每 10 秒刷 starting daemon → daemon exited, exit code 1 |
两个看门狗抢 .runner.lock:Unregister-ScheduledTask 不会带走旧实例启动的进程,孤儿看门狗养着的旧 daemon 仍占着锁,新看门狗起不来(daemon 日志写 another gitea-runner process is already using ".runner" / runner file is already locked by another process)。此时 online 是旧 daemon 在硬撑,任务实际谁也没管 |
scripts\ci_runner_bootstrap.ps1 -Status 会报出重复实例;管理员跑 -StartNow(现已会先清残留进程再启动,runner 约离线几秒)。应急可手工:Get-CimInstance Win32_Process -Filter "Name='powershell.exe'" | ? CommandLine -match 'run_runner_watchdog' 找出多个 pid,杀掉旧的那个,再 taskkill /PID <旧 daemon> 让在册看门狗接管。✅ 2026-10-12 实测收敛:-StartNow 输出 stopping stale watchdog pid 26900 / 19716 → stopping stale daemon pid 53848 → daemon up: pid 40360 (single instance, holds .runner.lock);之后看门狗日志 25 秒零增长、runner 回 online。切换瞬间新看门狗可能先失败 1~2 次(旧 daemon 刚被杀、锁尚未释放),10 秒重拉后自愈,属预期 |
l0-gates 报 Cannot find: pwsh in PATH |
shell: pwsh = PS7,本机只有 WinPS 5.1 |
改 shell: powershell |
🔴 doc-links 钩子本地过、CI 挂 |
引用了 gitignore 的运行时文件(如 .dsh/skills/serial-assistant/scripts/state/*.json),本地存在、干净克隆里没有 |
该行加状态词「未创建/不存在」等(§4.1.2) |
| 任务跑满 72 小时被杀 | 计划任务默认 ExecutionTimeLimit |
设为 0(不限时长),见 §3.3 警示 |
config set log.file ... 报 unknown config key |
v4 的 log 段无 file 键 |
用 log.job.dir 或输出重定向(§2.3) |
脚本报一堆 源文本中包含无法识别的标记 / 字符串缺少终止符 / 中文显示成 鐒跺悗鎵ц |
🔴 .ps1 无 BOM,WinPS 5.1 按 GBK 解读 UTF-8 |
给脚本加 UTF-8 BOM(§7.1) |
7.1 🔴 必读:给含中文的 .ps1 加 UTF-8 BOM
这是本机实测踩过的坑(2026-10-02)。Windows PowerShell 5.1 对无 BOM 的脚本文件 按系统 ANSI 代码页(简中 = GBK / CP936)解码:
- UTF-8 的中文字节被当 GBK 解 → 显示成
鐒跺悗鎵ц这类乱码; - 更糟的是,乱码字符会撞坏字符串引号配对(GBK 双字节的尾字节可能是
"或\) → 解析器报源文本中包含无法识别的标记/字符串缺少终止符: "/缺少右"}"—— 报错行号看起来毫无规律,极易误判成逻辑问题。
修法:给文件头加 3 字节 EF BB BF(UTF-8 BOM)。与仓库里带 BOM 的源文件
(Drivers/BSP/UI/*、App/cfg_file.c)是同一个道理。
# 方法 A:PowerShell 重存为 UTF8BOM
$c = Get-Content "D:\Docker\act_runner\check_env.ps1" -Raw -Encoding UTF8
[System.IO.File]::WriteAllText("D:\Docker\act_runner\check_env.ps1", $c, (New-Object System.Text.UTF8Encoding($true)))
# 方法 B:直接前缀 BOM 字节(bash/Git Bash)
cd "/d/Docker/act_runner"
for f in check_env.ps1; do
printf '\xef\xbb\xbf' > "$f.tmp" && cat "$f" >> "$f.tmp" && mv "$f.tmp" "$f"
done
校验:用 PowerShell 解析器(不执行)确认语法干净:
foreach ($f in @("check_env.ps1","run_runner_watchdog.ps1")) {
$errs = $null
$null = [System.Management.Automation.Language.Parser]::ParseFile(
"D:\Docker\act_runner\$f", [ref]$null, [ref]$errs)
if ($errs.Count) { "$f : FAIL -> $($errs[0].Message)" } else { "$f : OK" }
}
💡 替代方案:改用 PowerShell 7+(
pwsh),它默认按 UTF-8 读脚本、无需 BOM。 但本机只有 WinPS 5.1,故加 BOM 最省事。💡 纯 ASCII 的脚本不受影响,也不需要 BOM。现行两个文件的口径:
check_env.ps1含中文注释 → 必须带 BOM;run_runner_watchdog.ps1(由scripts/ci_runner_bootstrap.ps1生成)刻意写成纯 ASCII,因此对编码免疫—— 生成它的模板也要求纯 ASCII,理由见仓库AGENTS.md的编码铁律。 🗑 旧脚本deploy_runner_task.ps1已废弃(§3.2),不再作为示例对象。
7.2 日志位置与查看方式
计划任务模式下看日志(两种):
| 日志 | 位置 |
|---|---|
| runner 自身 stdout/stderr | D:\Docker\act_runner\log\runner-daemon.log(由看门狗落盘,显式 -Encoding utf8;2026-10-12 之前的行夹带 PowerShell 包装噪声,见 §7.3) |
| 看门狗自身动作(等 Gitea / 拉起 / 退出码 / 重拉) | D:\Docker\act_runner\log\runner-watchdog.log(ASCII) |
| 每个 job 的详细日志 | D:\Docker\act_runner\log\job\ |
| 🗑 历史遗留 | log\runner-stdout.log 是旧 run_runner.cmd 时代的文件,已不再写入;它内部混着 ASCII 与 UTF-16LE 两种编码(PS 5.1 *>> 默认 UTF-16LE),按 UTF-8 读会看到"每个字节后跟一个 NUL"的 t i m e = ... 形态,属正常现象不是新故障 |
看日志:v4 的 log 段没有 file 键,两种落盘方式:
# 方式 A:按 job 落文件(推荐,防上报失败丢日志)
log:
level: info
job:
dir: "D:\\Docker\\act_runner\\log\\job"
# 方式 B:前台运行 / 包装脚本里重定向。
# 🔴 不要写成 `.\gitea-runner.exe daemon > log 2>&1`:PS 5.1 的 `>` 就是 Out-File,
# 默认编码 UTF-16LE,且会把原生 stderr 包成 ErrorRecord(见 §7.3)。要这样写:
& '.\gitea-runner.exe' daemon 2>&1 | ForEach-Object { "$_" } |
Out-File -FilePath 'D:\Docker\act_runner\log\runner.log' -Append -Encoding utf8
任务本身的历史记录:任务计划程序 → 任务计划程序库 → GiteaActionsRunner → 历史记录;
或用 Get-ScheduledTaskInfo -TaskName GiteaActionsRunner 看 LastTaskResult。
7.3 🔴 原生命令 stderr 的两个 PS 5.1 陷阱(本仓库实测,均已修)
runner 的日志走 stderr,于是上面两个日志文件都栽在这两条上。两条都在
scripts/ci_runner_bootstrap.ps1 生成的启动器里修掉了,改脚本时勿回退:
① 重定向的默认编码是 UTF-16LE。 *>> / > 在 PS 5.1 里就是 Out-File,默认
Unicode(UTF-16LE)。同一文件里若还有 Add-Content -Encoding ascii 写的行,就会
一个文件两种编码,按 UTF-8 读出来是 t i m e = 2 0 2 6 ...(每字节后跟 NUL)。
→ 永远显式写 -Encoding utf8(别用 ascii:job/提交文本可能含非 ASCII)。
② 2>&1 把原生 stderr 包成 ErrorRecord。 实测(用无效子命令产生真实 stderr):
| 写法 | 2 行真实日志写成 | 噪声 |
|---|---|---|
& $exe 2>&1 | Out-File |
8 行 | gitea-runner.exe : 前缀 + 4 行 CategoryInfo/FullyQualifiedErrorId 块 |
& $exe 2>&1 | ForEach-Object { "$_" } | Out-File |
2 行 | 0 |
& cmd.exe /c "… 2>&1" | Out-File |
2 行 | 0(等价,但要额外操心 cmd 对含空格路径的引号剥离) |
即每 4 行日志有 3 行是 PowerShell 噪声,而且它就出现在文档让大家第一个打开的文件里。
ErrorRecord.ToString() 就是原始 stderr 行,所以逐项字符串化即可去掉装饰,
且 $LASTEXITCODE 不受影响(看门狗靠它打印退出码)。
💡 顺带一条同类教训:别用"日志尾部还有没有错误串"判断健康状态。日志不轮转, 修好之后旧失败仍留在尾部几十行内——
-Status曾据此对健康 runner 报假警。 现在改看进程存活时长(活过 10 秒重拉间隔就不可能在重启循环里)。
8. 日常维护
- 每周(或每次改完 Gitea/机器重启后)体检:
scripts\ci_runner_bootstrap.ps1 -Status—— 计划任务 / daemon 进程 / Gitea 可达 / 三个机器级DTU_*,四项全绿才算 CI 活着; - 升级 runner:
Stop-ScheduledTask GiteaActionsRunner→ 换gitea-runner.exe→Start-ScheduledTask GiteaActionsRunner(注册信息在.runner,配置在config.yaml); 若换了目录/地址,重跑ci_runner_bootstrap.ps1重新生成看门狗与计划任务; - 改了工具链探测逻辑:Python 侧只有一处事实源
scripts/ci_find_python.ps1(两个 job 共用),本地scripts/ci_local.py的候选表须与它同序; MSVC 探测仍是 workflow 内联 +test/host/common/unit_test_harness.py两处一致; - 钩子集变化(
.pre-commit-config.yaml增删钩子):CI 自动跟随,无需改 workflow; 但必须核对条数——pre-commit run --all-files只跑pre-commitstage,commit-msgstage 的钩子(gitlint)不在其中,靠ci_gitlint_range.ps1单独守(见 §7); @build/ L1 / L2:永远留在本地,不要往 CI 加(需 Keil 授权与真机);- runner 长时间不可用时:CI 只是"多一道网",提交前的本地
ci_local.py仍是 L0 的唯一保障, 不能因"已经配了 CI"放宽(见 docs/任务规划/登记簿.md 的条目「CI 远端闭环未验证」)。
9. 一键检查脚本(可选)
存为 D:\Docker\act_runner\check_env.ps1,注册前跑一遍:
Write-Host "=== Gitea 可达性 ===" -ForegroundColor Cyan
try {
$v = (Invoke-RestMethod -Uri "http://127.0.0.1:3001/api/v1/version" -TimeoutSec 8).version
Write-Host " OK Gitea $v (127.0.0.1:3001)" -ForegroundColor Green
} catch { Write-Host " FAIL 连不上 Gitea: $_" -ForegroundColor Red }
Write-Host "=== 工具链 ===" -ForegroundColor Cyan
foreach ($c in @("python", "cppcheck", "git")) {
$p = Get-Command $c -ErrorAction SilentlyContinue
if ($p) { Write-Host " OK $c -> $($p.Source)" -ForegroundColor Green }
else { Write-Host " FAIL 缺 $c" -ForegroundColor Red }
}
$pc = (python -m pre_commit --version 2>&1)
if ($LASTEXITCODE -eq 0) { Write-Host " OK $pc" -ForegroundColor Green }
else { Write-Host " FAIL 缺 pre-commit" -ForegroundColor Red }
Write-Host "=== DTU_* 机器级变量(§4.1.3)===" -ForegroundColor Cyan
foreach ($n in @("DTU_PRECOMMIT_PYTHON", "DTU_CPPCHECK", "DTU_MSVC_CL")) {
$v = [Environment]::GetEnvironmentVariable($n, "Machine")
if ($v) { Write-Host " OK $n = $v" -ForegroundColor Green }
else { Write-Host " -- $n(未设置:workflow 将走自动发现)" -ForegroundColor Yellow }
}
Write-Host "=== MSVC cl.exe(DTU_MSVC_CL 优先,否则标准布局推导)===" -ForegroundColor Cyan
if ($env:DTU_MSVC_CL -and (Test-Path $env:DTU_MSVC_CL)) {
Write-Host " OK $($env:DTU_MSVC_CL)(来自 DTU_MSVC_CL)" -ForegroundColor Green
} else {
$pf = ${env:ProgramFiles}
$pf86 = ${env:ProgramFiles(x86)}
$roots = @(
(Join-Path $pf86 'Microsoft Visual Studio\2019\BuildTools'),
(Join-Path $pf86 'Microsoft Visual Studio\2019\Community'),
(Join-Path $pf86 'Microsoft Visual Studio\2019\Professional'),
(Join-Path $pf 'Microsoft Visual Studio\2022\BuildTools'),
(Join-Path $pf 'Microsoft Visual Studio\2022\Community'),
(Join-Path $pf 'Microsoft Visual Studio\2022\Professional'),
(Join-Path $pf 'Microsoft Visual Studio\2022\Enterprise'),
(Join-Path $pf 'Microsoft Visual Studio\18\BuildTools'),
(Join-Path $pf 'Microsoft Visual Studio\18\Community')
)
$hit = $null
foreach ($r in $roots) {
if (Test-Path $r) {
$hit = Get-ChildItem -Path $r -Recurse -Filter 'cl.exe' -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -like '*Hostx64\x64*' } | Select-Object -First 1
if ($hit) { break }
}
}
if ($hit) { Write-Host " OK $($hit.FullName)" -ForegroundColor Green }
else { Write-Host " FAIL 未找到 MSVC (Hostx64\x64\cl.exe);可设机器级 DTU_MSVC_CL" -ForegroundColor Red }
}
10. 最小操作清单(TL;DR)
# 1) 下载(v4.0.1;注意仓库名是 gitea/runner,二进制名是 gitea-runner)
New-Item -ItemType Directory -Force -Path "D:\Docker\act_runner" | Out-Null
cd "D:\Docker\act_runner"
Invoke-WebRequest -Uri "https://gitea.com/gitea/runner/releases/download/v4.0.1/gitea-runner-4.0.1-windows-amd64.exe" -OutFile "gitea-runner.exe"
# 2) 注册(token 从网页 Settings→Actions→Runners 拿;instance 用回环)
.\gitea-runner.exe register --no-interactive `
--instance "http://127.0.0.1:3001" `
--token "<粘贴 token>" `
--name "win-stm32-ci" `
--labels "windows:host"
# 3) 前台试跑,确认能连上
.\gitea-runner.exe daemon # 看到 "labels: [windows] ... declare successfully" 即成功,Ctrl+C 停
# 4) 常驻 —— ⚠️ daemon 不是服务型程序,且首次 Declare 失败就直接退出进程
# 所以必须【看门狗循环 + 开机计划任务】,一条命令搞定(管理员 PowerShell,在仓库根目录):
powershell -ExecutionPolicy Bypass -File scripts\ci_runner_bootstrap.ps1 -SetEnv -StartNow
# 体检(四项:计划任务 / daemon 进程 / Gitea 可达 / 机器级 DTU_*)—— 须管理员:
powershell -ExecutionPolicy Bypass -File scripts\ci_runner_bootstrap.ps1 -Status
# 干跑(只打印将要写入的启动器与任务对象,不落盘不注册)—— 免管理员:
powershell -ExecutionPolicy Bypass -File scripts\ci_runner_bootstrap.ps1 -DryRun
# 5) 工具链必须对 SYSTEM 可见(runner 以 SYSTEM 跑,看不见交互用户的 PATH 与用户级 site)
# 正式通道 = 机器级 DTU_* 变量,上一步的 -SetEnv 已自动发现并写入;需手工覆盖时:
# (管理员 PowerShell;下面是本机实测现值,换机按实际路径改)
# [Environment]::SetEnvironmentVariable('DTU_PRECOMMIT_PYTHON', 'C:\ProgramData\Anaconda3\python.exe', 'Machine')
# [Environment]::SetEnvironmentVariable('DTU_CPPCHECK', 'C:\Program Files\Cppcheck\cppcheck.exe', 'Machine')
# [Environment]::SetEnvironmentVariable('DTU_MSVC_CL', '<本机 x64 cl.exe 完整路径>', 'Machine')
# 兜底通道(只有变量没设时才会走到):把 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
# 7) 回网页确认 runner 为 Idle → 触发 CI
# push 触发(注意:纯 .md 改动在 workflow 的 paths-ignore 里,不会触发);
# 手动触发:
# curl -u "<user>:<pass>" -X POST -H "Content-Type: application/json" `
# -d '{"ref":"v2-on-ui"}' `
# "https://gitea.bitnasdaq.vip/api/v1/repos/wandering1/STM32F4-Base/actions/workflows/ci.yml/dispatches"
🗑 本清单第 4 步在 2026-10-02 版里是"计划任务跑单次 daemon"(
deploy_runner_task.ps1)。 2026-10-11 实测证明单次 daemon 首次 Declare 失败即退出、计划任务的失败重启救不到它, 故已改写为看门狗方案;旧脚本的失效机理见 §3.2。
CI 验收标准:两个 job 都要
status=1。口径是规则,不是具体数字(2026-10-12):l0-gates报 定义数 − 1 条钩子(gitlint 属commit-msgstage,不进--all-files报告, 见 §7),其中mem-numbers在无.map时为Skipped(本机唯一常见跳过项);另有提交信息 门禁::notice::gitlint: N checked, 0 failed。l05-host-unit-tests报<N> runner(s), A/A assertions passed, fail=0,N=test/host/下run_unit_test.py个数。 实测快照:master18 定义 / 17 报告、3 套件 / 288 断言;v2-on-ui26 定义 / 25 报告、10 套件。 🗑 更早快照(勿当基准):run#8(2026-10-02)当时是"14 个钩子全 Passed + 39/39 单测"。 判定只看fail=0、failed=0与 A==B;增删钩子或单测后按上面的规则重算,别抄历史数字。
配套脚本:
| 脚本 | 位置 | 用途 | 需要管理员 |
|---|---|---|---|
ci_runner_bootstrap.ps1 |
仓库 scripts/ |
现行唯一部署/体检入口(-Status / -DryRun / -SetEnv -StartNow) |
-Status、-SetEnv -StartNow 需要 |
check_env.ps1 |
D:\Docker\act_runner\ |
环境自检(Gitea 可达 / 工具链 / MSVC / 任务状态) | 否 |
run_runner_watchdog.ps1 |
D:\Docker\act_runner\ |
看门狗启动器,由 bootstrap 生成,是计划任务的动作目标。纯 ASCII,不需要 BOM | 由任务以 SYSTEM 跑 |
🗑 deploy_runner_task.ps1、run_runner.cmd |
D:\Docker\act_runner\ |
已废弃(单次 daemon,§3.2)。仍在磁盘上,建议改名加 .deprecated 或删除,免得误执行把看门狗任务覆盖回单次任务 |
— |
更早基于 Windows 服务的
deploy_runner.ps1/install_service.ps1已删除 —— 那条路走不通(§3.0)。
变更记录
| 日期 | 变更 |
| 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 断言) |
| 2026-10-12 | ✅ 双看门狗缺陷实测收敛 + 修掉日志里的 PS 5.1 stderr 包装噪声与 -Status 假警。① 管理员 -StartNow 杀掉 3 个残留(孤儿看门狗 26900/19716 + 占锁 daemon 53848)→ daemon up: pid 40360 (single instance),监管链 svchost → 看门狗 → daemon,看门狗日志 25 秒零增长、runner 回 online。② 新坑:daemon 日志写 stderr,PS 5.1 的 2>&1 把每行包成 ErrorRecord → gitea-runner.exe : 前缀 + 4 行 CategoryInfo 块,实测 2 行真日志写成 8 行(每 4 行有 3 行是噪声,且就在文档让大家第一个打开的文件里)。启动器改为 2>&1 | ForEach-Object { "$_" } | Out-File -Encoding utf8(实测与 cmd 包装等价但免引号剥离坑,$LASTEXITCODE 不受影响);§7.2 方式 B 的示例原本正是 > log 2>&1(UTF-16LE + 包装双坑),已改;新增 §7.3 汇总这两条。③ -Status 两处诚实化:非管理员读不到 SYSTEM 进程命令行(CommandLine 为 null → 过滤器静默匹配 0 个),改为明说"需提权"而不是谎报"没有看门狗";重启循环判据不再靠"日志尾部有没有错误串"(文件不轮转,修好后旧失败仍在尾部 → 对健康 runner 报假警),改看 daemon 进程存活时长(CIM CreationDate 非管理员可读,Process.StartTime 对 SYSTEM 进程不可读) |
| 2026-10-12 | 🔴 实测揪出"双看门狗抢锁"缺陷并修复 + 机器侧清理。① 现象:runner 网页 online、-Status 说 daemon 在跑,但 runner-watchdog.log 每 10 秒刷 starting daemon → daemon exited, exit code 1,daemon 日志 runner file is already locked by another process。② 根因:Unregister-ScheduledTask 不停旧实例启动的进程——13:22 重跑 bootstrap 时旧看门狗(12:53 起)成孤儿并继续养着旧 daemon、占住 .runner.lock,新看门狗永远抢不到锁,任务实际谁也没管,日志以约 20 MB/天在刷。③ 修复:ci_runner_bootstrap.ps1 的 -StartNow 现在先停孤儿看门狗与残留 daemon 再启动并回显单实例确认;-Status 新增重复实例计数与锁冲突检测。④ 机器侧清理:config.yaml 头部那句错误指引("常驻改用 Windows sc.exe / New-Service",正是 §3.0 证伪的路)改为看门狗口径并指向本文;删除已废弃的 deploy_runner_task.ps1 与 run_runner.cmd(§3.2),check_env.ps1 指向它们的提示同步改指 bootstrap。⑤ 核实:config.yaml 改后 YAML 可解析且所有配置值与改前逐项一致(只动注释);check_env.ps1 带 BOM 且语法 0 错;删除前确认计划任务动作目标是 run_runner_watchdog.ps1,删除不影响在跑 runner |
| 2026-10-12 | 🧹 单一口径清理(以本机实测现状为准):合并稿初版对 4 处分歧采取"两说并存 + 衔接注",本次统一。① 注册口径——act_runner v0.2.11 + C:\act_runner + 公网域名 instance 一段删除可复制命令,只留"勿照抄"警示(核实 C:\act_runner 至今不存在,本机是 gitea-runner v4.0.1 + D:\Docker\act_runner + 127.0.0.1:3001);② config.yaml 示例 fetch_timeout 由 5m0s 改为磁盘实际值 1m0s、timeout 统一 30m0s,并补"不得超过 RPC timeout(1m0s)";③ 常驻口径统一为看门狗——§3.2 deploy_runner_task.ps1 标为已废弃并说明失效机理(单次 daemon = 正常退出,失败重启永不触发),§3.3 手动等价命令重写为看门狗版任务对象(旧版会教人手搓出坏配置),§10 TL;DR 第 4 步改指 ci_runner_bootstrap.ps1;④ 数字口径——L0.5 期望值更新为 288/288(run#8 的 39/39 与 278/278 标注为历史快照,判定只看 fail=0 与 A==B),钩子数按现行 18 定义 / 17 报告。另同步:健康三查与排障表改用现行日志名(runner-daemon.log / runner-watchdog.log,runner-stdout.log 标为遗留且说明其混编码)、BOM 示例对象换成 check_env.ps1 并注明看门狗是纯 ASCII 无需 BOM、配套脚本表补 run_runner_watchdog.ps1 并标出磁盘上仍存的废弃脚本建议删除、补 Next Run Time 为空属正常的说明 |
| 2026-10-12 | 文档合并:本次把 CI配置指南.md 与 Gitea_Runner部署指南.md 合并为本篇(docs/CI/远程CI完整指南.md)并迁入 docs/CI/,去重(runner 注册 / config.yaml / 装软件 / 排障表两篇均有)、补入 runner 看门狗与机器级变量实测教训;相对链接按新位置改写,原两篇互引改为本文内部引用 |
| 2026-10-11 | bootstrap 自身修复:StartWhenAvailable 误设到触发器上(该属性属任务设置,触发器没有)→ 脚本在"已注销旧任务、尚未注册新任务"之间抛 SetValueInvocationException,把 runner 任务删没了。现改为①先构建 action/trigger/settings、后注销旧任务,②Register-ScheduledTask 失败时显式告警"旧任务已删、CI 裸奔",③新增 -DryRun(免管理员即可全量校验任务对象),④-Status 在非管理员下额外提示"看不见任务 ≠ 任务不存在"。同时更正根因:旧任务其实存在,但跑的是单次 daemon——daemon 属"正常退出"而非"失败",计划任务的失败重启救不到它,必须用看门狗循环重拉 |
| 2026-10-11 | 修"很多任务不执行"(runner 静默停摆)+ 补 gitlint 门禁:① §2.4 重写——gitea-runner v4 的 daemon 无服务支持、首次 Declare 失败即退出进程(日志实证),Gitea/机器重启一次 CI 就停摆,run 16~21 共 12 个 job 零日志(cancelled/queued,叠加 cancel-in-progress 后表现为"任务凭空消失"而非失败);新增 scripts/ci_runner_bootstrap.ps1(等 Gitea 就绪 + 崩溃自拉起的看门狗 + 开机计划任务 + -SetEnv 写机器级 DTU_* + -Status 只读体检)。② §3.1 重写——SYSTEM 可见性三重坑(PATH python 是 msys64 无 pre-commit / 用户级 site 对 SYSTEM 不可见 / .venv 已 gitignore)是 run 14~15 L0 全灭根因(33a4aa4 把候选收窄成这两个、又删了 cppcheck 目录兜底,而机器级 DTU_* 从未设置);探测收进 scripts/ci_find_python.ps1(两 job 共用、可离线自测、失败列出全部候选)并恢复机器级安装兜底。③ 新增 gitlint 提交信息门禁:钩子定义数比 CI 报告条数多 1(当时 17/16,master 并入 test-cppcheck 后为 18/17),缺的正是 gitlint——其 stages: [commit-msg] 声明在上游 .pre-commit-hooks.yaml(--all-files 跑不到,且表现为全绿 → 长期无人发现)→ scripts/ci_gitlint_range.ps1 逐条校验本次 push/PR 范围(消息文件须写在仓库内 + UTF-8 无 BOM)。④ GITHUB_ENV/PATH/OUTPUT 写入统一显式 -Encoding ascii(PS 5.1 Out-File -Append 默认 UTF-16LE,实测落盘 42 00 42 00 42 00 3D 00 31 00)。§5 清单 8→10 条、§6 排障表 +5 行、§7 维护项重写 |
| 2026-10-02 | S3 + S14:新增 §0.0「CI 不覆盖的体积门禁」醒目声明;新增 §4.4「产物存档与结果汇总」(零依赖 Tee-Object + $GITHUB_STEP_SUMMARY + ::notice:: 汇总行,不用 actions/upload-artifact;记录 $ErrorActionPreference='Stop' 与 2>&1 | Tee-Object 的陷阱及处置)。§5 验证清单(7→8 条)与 §6 排障表同步更新 |
| 2026-10-02 | workflow 接入 DTU_* 机器级变量(本地工具路径环境变量方案.md 实施):l0-gates 的 Python 探测改为 DTU_PRECOMMIT_PYTHON(显式,无效即硬失败)→ PATH → .venv(删除 C:\ProgramData\anaconda3 固定候选);cppcheck 改为 DTU_CPPCHECK 优先 → PATH(移除 C:\Program Files\Cppcheck 固定回退);l05 的 MSVC 探测改为 DTU_MSVC_CL 优先 → %ProgramFiles* 推导的标准根(与 unit_test_harness.py 同步,不再写死盘符);新增 §5.1.3 机器级变量设置方法(Machine scope + 重启任务)、§5.2 与 check_env 样例、排障表 cppcheck 行、TL;DR 5b 步骤。§5.1.1 的报错原文标注为历史记录 |
| 2026-10-02 | ✅ 首次 CI 全绿(run#8:L0 + L0.5 双 job Success)。定位并修复最后两个拦路虎:① §5.1.1 用户态 site-packages 陷阱——pre-commit 只装在 %APPDATA%\Roaming\Python\...(SYSTEM 不可见);且提权 pip install --no-user 会因 pip 扫整个 sys.path、见 user-site 已满足而静默跳过全局安装(9 依赖中唯 cfgv 因版本相同被跳过→-s 下 ModuleNotFoundError)。修法:提权 + --force-reinstall --no-deps 强制落全局,并以 -s 复验。② §5.1.2 doc-links 假阳性——文档引用了 gitignore 的运行时文件 .dsh/skills/serial-assistant/scripts/state/meta.json,本地存在、干净克隆无 → 按《文档规范》§3.3 加同状态词「运行时未创建」。排障表补 6 条(pre-commit 不可见 / GBK 中文 / pwsh / cppcheck / doc-links / MSVC 降级)。新增配套脚本 install_precommit_global.ps1、install_precommit_global2.ps1、fix_cfgv_global.ps1 |
| 2026-10-02 | 🔴 常驻方式重大修正(实测推翻):Start-Service gitea-runner 必然失败。经二进制逆向确认 gitea-runner v4.0.1 无任何服务管理代码(命令集仅 bug-report/cache-server/config/daemon/exec/register,无 service.go;OpenSCManagerW 等符号来自 Docker SDK,非服务安装代码),daemon 是纯控制台程序、不向 SCM 回报 SERVICE_RUNNING。§4 全节重写为「任务计划程序」方案(新增 §4.0 根因分析、§4.2 deploy_runner_task.ps1、§4.3 手动命令 + 72h 超时警示、§4.5 清理遗留服务、§4.6 提权);删除失效的 deploy_runner.ps1/install_service.ps1;排障表补「错误 1053」「LastTaskResult 非 0」「72 小时被杀」3 条;拓扑图/§5.1/§7 日志/§10 TL;DR 同步更新。另修 config.yaml 的 fetch_timeout 5m→1m(超出 RPC 1m 会被截断并告警) |
| 2026-10-02 | 修 .ps1 编码坑(实测):无 BOM 的 UTF-8 脚本被 WinPS 5.1 按 GBK 解读 → 中文乱码(鐒跺悗鎵ц)并撞坏引号配对,报 源文本中包含无法识别的标记/字符串缺少终止符。新增 §7.1「给含中文的 .ps1 加 UTF-8 BOM」(两种修法 + 解析器校验命令);排障表补该条目;配套脚本已统一加 BOM |
| 2026-10-02 | 按实测修正为 v4.0.1:act_runner 已更名 gitea-runner,发布页迁至 gitea.com/gitea/runner。v4 移除 daemon install/start/stop/status;v4 log 段无 file 键(§3 改用 log.job.dir);补 §1.3 命令集对照、§6.1 MySQL 直查核验、排障表(accepts at most 0 arg(s) / unknown config key log.file 等) |
| 2026-10-02 | 初版:针对本机 Gitea 原生部署(D:\Docker\Gitea\gitea.exe + Windows 服务 + 127.0.0.1:3001)定制 Runner 端部署全流程。核心要点:--instance 用回环地址、--labels "windows:host" 必须精确匹配 runs-on、PATH 运行账户坑、MSVC 路径约束、内网 action 拉取的零依赖改法 |
| 2026-10-01 | 修正 DEFAULT_ACTIONS_URL 说明(原 §1.2 示例误导):新增 §1.2.1 明确它控制的是 action 下载源(默认 github.com;self = 从自己实例拉且需先镜像 action),不是填自己的 Gitea 地址;§4.3 同步修正(原文误写"gitea.com 代理"),并补零依赖写法为内网首选。依据:Gitea 官方 FAQ + config-cheat-sheet |
| 2026-10-01 | 初版:Gitea Actions 配置全流程(服务端开关 / Windows runner 注册 / runner 软件环境 / workflow 位置与触发 / 内网 action 拉取 / 端到端验证清单 / 故障排查 / 日常维护) |