Files
STM32F4-Base/docs/CI/远程CI完整指南.md
T
wandering1 842d711ed6 refactor(编号): 测试验证层级去号——CI job / 门禁输出 / 命令行旗标换语义名 + ASCII 段名
用户裁定「我不想看到编号」。测试验证层级这一族的整族清退分三道并行:本笔是**代码与 CI 道**
(CI 步骤与门禁输出面),代码注释面另走、文档面由文档道走。

口径(旧 → 语义名 → ASCII 段名,段名只进日志/旗标/job id/文件名):
  L0 静态门禁 static | L0a 钩子解释器预检 preflight | L0b 提交信息门禁 commitmsg
  L0c 体积门禁 size | L0.5 主机侧纯逻辑单测 host-unit | L0.5c 覆盖率 coverage

- scripts/ci_local.py 43 站点:段标题与 [SUMMARY] 标签改 ASCII 段名(static: /
  host-unit:),函数 run_l0/run_l05 → run_static_gates/run_host_unit_tests,
  收尾「已跑:」列表用段名。旗标改 --static-only / --host-unit-only,**旧旗标
  --l0-only / --l05-only 保留为 argparse 别名**(肌肉记忆与既有脚本片段不断档,
  实测 --help 两名列出)。
- .gitea/workflows/ci.yml 42 站点:job id l0-gates → static-gates、
  l05-host-unit-tests → host-unit-tests;步骤名与 step-summary 标题改语义名;
  日志名 l0-pre-commit.log → static-pre-commit.log、l05-<模块>.log →
  host-unit-<模块>.log;::error::/::notice:: 文案同步。yaml.safe_load 核过
  两个 job id 与步骤名。
  host-unit-tests;步骤名与 step-summary 标题改语义名;日志名 l0-pre-commit.log →
  static-pre-commit.log、l05-<模块>.log → host-unit-<模块>.log;::error::/::notice::
  文案同步。yaml.safe_load 核过两个 job id 与步骤名。
- 门禁脚本 18 站点:check_registration(登记簿理由串 7)、check_test_coverage 5、
  README 8、check_generated_fresh 2、ram_baseline 2、check_size_gate 1、
  ci_find_python.ps1 2、ci_runner_bootstrap.ps1 2、.pre-commit-config.yaml 2、
  requirements.txt 1(保持纯 ASCII)。
- skills 7 站点:task-worktree(含 [SUMMARY] 样例两行)、mem-budget、ui-verify、
  task_worktree.py 的实跑标签。
- AGENTS.md 3 站点:CI 段「只跑静态门禁 + 主机侧纯逻辑单测」、自查命令段、
  「2026-10-11 静态门禁全灭」事件名。
- 归档:编号治理历史.md 新增 §10 换算表 + **撞族警示**(同形 L0/L1/L2/L3 还属 UI 设计
  令牌层 / UI 菜单层级 / 采集链 / 链路时延段 / CubeMX 三层 / 硬件位号与 FTP 应答字面量
  等六个族,其中 6 处在字符串字面量里,改了就是改行为——故全程只按行内置换、先判族)。
- docs/产品需求 §7.4 测试分层表:去号 + 顺带修掉上一批**错族替换疤**(该表 L1/L2 行曾被
  采集链层名「语义取数与有效性 / 全周傅氏相量」「保护判据」顶替,现按内容改写为板端测试
  套件 / 主机侧回归两行)。同文档 §1.5 G5 判据行同步。

门禁:ci_local --help 两面旗标并列;py_compile 7 个 .py 全过;yaml 解析通过;
check_ids_new --worktree rc=0;git diff --check 干净;17 个改动文件 FFFD=0、无 CR、
BOM 与原文件一致(含 requirements.txt 纯 ASCII)。本族无机器识别面(单字母+数字是
新增行门禁的自述盲区),口径来自自建逐行扫描器,复扫本族形态归零。
- 交接说明:文档道 teammate 在改 CI 指南时中断,剩余 9 处双写疤与 29 处 CI 标识
  (job id / 日志名 / 汇总行字面量)由 Lead 接手改完,逐处仍走断言式行内置换。
2026-10-28 17:46:52 +08:00

1373 lines
94 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 远程 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`](../../scripts/ci_local.py)
> (与 CI 同一套判定),**不依赖远程 CI 是否在线**——runner 掉线不影响本地门禁。
> **适用**:为 STM32F4-Base 部署 / 排障 CI(`.gitea/workflows/ci.yml`,静态门禁 + 主机侧纯逻辑单测)。
> **前置阅读**:[docs/测试说明/测试开发标准流程.md §9](../测试说明/测试开发标准流程.md)(CI 覆盖边界与判定细节)、
> [docs/环境与部署/项目部署环境要求.md §2.11](../环境与部署/项目部署环境要求.md)(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)。
> 补上的强制路径 = **同码凭证**:本地 `@build` 0/0 后 `ram_baseline.py --update` 顺手盖
> `scripts/size_attestation.json`(三道前置:`.map` 可解析 / `.map` 不早于指纹内源码 mtime /
> 受管区 ≤92%);CI 无需 Keil,静态门禁作业末尾的「体积门禁」步用 `scripts/check_size_gate.py --ci`
> 复核两件事——**工作树指纹 == 凭证指纹**(改了代码从没测过 = 拦)+ **按凭证数字复跑 92% 红线**。
> 主干 push 拦红,其余事件 `--warn-only`;本地 `ci_local.py` 的 静态门禁c 步无条件 fail-closed。
>
> 仍守不住的三项(正文唯一口径 = `scripts/check_size_gate.py` docstring):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 拉起容器/壳 → 跑 静态门禁 + 主机侧纯逻辑单测 → 结果回显到提交
```
**三者缺一,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 时 | 主机侧纯逻辑单测降级为 WARNING 跳过(不硬失败),但**等于该层裸奔**,须尽快补装 |
---
## 1. 服务端:开启 Gitea Actions
### 1.1 确认版本与开关
Gitea **≥ 1.21** 才内置 Actions(实测本实例 **1.25.4**,满足)。以 **site admin** 登录后:
- 管理后台 → **站点管理 → 配置 / Site Administration → Configuration**,搜索 `actions`:
```ini
[actions]
ENABLED = true
```
### 1.2 编辑配置文件(推荐,永久生效)
配置文件位置(按部署方式二选一):
| 部署方式 | 配置文件路径 |
|---|---|
| 二进制 / 直接跑 | `<gitea 目录>/custom/conf/app.ini` |
| Docker | 容器内 `/data/gitea/conf/app.ini`(挂载卷里的 `custom/conf/app.ini`) |
在 `app.ini` 追加(若无 `[actions]` 段):
```ini
[actions]
ENABLED = true
```
改完 **重启 Gitea 服务**(Docker:`docker restart <容器名>`;二进制:重启进程)。
#### 1.2.1 `DEFAULT_ACTIONS_URL` 是什么(**容易搞反**)
它**不是你的 Gitea 地址**,而是控制 **`uses: actions/checkout@v4` 这类 action 从哪里下载**:
| 取值 | 行为 |
|---|---|
| **不配(默认)** | 从 **[github.com](https://github.com)** 拉取 action |
| `self` | 从**你自己的 Gitea 实例**拉取(要求 action 已镜像进你的实例) |
| 具体 URL(如 `https://gitea.com`) | 从该地址拉取 |
> ⚠️ **不要把你的实例域名写进去当普通 URL**——要"从自己实例拉"就用关键字 **`self`**;
> 且**写了 `self` 还得先把 action 镜像进自己实例**,否则 `actions/checkout` 依然拉不到。
**按你的网络环境选择**:
| 场景 | 配置 |
|---|---|
| 服务器**能**访问 github.com | **什么都不配**(默认走 github.com,最省事) |
| 内网,**不能**访问 github.com | 建议**改用零依赖写法**(见本文 §5.3 方案 3),`DEFAULT_ACTIONS_URL` 配不配都行;<br>若坚持用 `actions/checkout@v4`,则需 `DEFAULT_ACTIONS_URL = self` **且**把该 action 镜像进实例 |
> Gitea 官方 FAQ 原文:*"From `https://github.com` by default, or from your own instance
> when `[actions].DEFAULT_ACTIONS_URL` is `self`."* 使用别处 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)的具体操作路径:
1. 浏览器登录 `https://gitea.bitnasdaq.vip`
2. 进仓库 **`wandering1/STM32F4-Base`**
3. **Settings(设置)→ Actions → Runners → 创建 Runner / Create new Runner**
4. 页面会显示一段命令,形如:
```
./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_runner` **v0.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`** |
```powershell
# 建目录(与 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)
```powershell
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):
```yaml
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 安全):
```powershell
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`):
```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 超时;静态门禁+主机侧纯逻辑单测约 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
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
powershell -ExecutionPolicy Bypass -File scripts\ci_runner_bootstrap.ps1 -DryRun
```
随时体检(只读,不改任何东西):
```powershell
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 前台先试跑(强烈建议)
**先别急着常驻**——用前台模式确认能连上:
```powershell
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`。下面是它注册的**等价**对象,
只在需要理解字段或在别的机器上手搓时使用:
```powershell
$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.1 `New-Service` 的地方)。SYSTEM 账户的默认工作目录不可控,所以启动器里
> 仍要显式 `Set-Location` 或用绝对路径。
> 🗑 旧方案还额外挂了 `-AtLogOn` 触发器并以 `-LogonType ServiceAccount` 运行。现行口径
> **只留开机触发**:runner 与登录会话无关,多一个触发器只会让人误以为"必须有人登录才行"。
### 3.4 任务管理速查
```powershell
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 ⚠️ 清理遗留的无效服务
若早前按旧文档建过服务,删掉它(留着只会误导):
```powershell
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** | 🔴 必需 | `static-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)** | 🟡 建议 | 缺则 `host-unit-tests` **告警降级跳过**(不硬失败,但 主机侧纯逻辑单测裸奔) | `winget install Microsoft.VisualStudio.2022.BuildTools`,勾"使用 C++ 的桌面开发" |
| Keil / ST-LINK | ⛔ 不需要 | CI 刻意不跑 `@build` | — |
| 开发板 / 串口 | ⛔ 不需要 | 板端测试套件/主机侧回归/人工目检不上 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** | 静态门禁环境自检失败 |
| `pip install --user pre-commit` 装好了 | **用户级 site 对 SYSTEM 完全不可见**(换任何 python 路径都没用) | 同上 |
| 仓库里有 `.venv` | `.venv` 已 `.gitignore`,runner 全新工作区**根本没有** | 同上 |
| `cppcheck` 在 PATH 里 | 机器 PATH 无 `C:\Program Files\Cppcheck` | 静态门禁环境自检失败 |
> 机制补充(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
powershell -ExecutionPolicy Bypass -File scripts\ci_runner_bootstrap.ps1 -SetEnv
```
手工自检(三条都必须对 SYSTEM 成立,而不只是对你的终端成立):
```powershell
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 排查 **静态门禁 job 反复失败**(`::error::no Python with a SYSTEM-visible pre-commit`)的真正根因,
也是本环境最隐蔽的一个坑。
**现象**:`pre-commit` 在你自己的终端里好好的(`pre-commit --version` 有输出),
但 CI 的 `static-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),
> 且报错行会指明是变量无效还是自动发现落空。
**根因(两层)**:
1. **用户态 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` 路径都救不了这个**。
2. **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 视角):
```powershell
# 看全局目录到底有没有(而不是 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`,核心是强制重装:
```powershell
& $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\`。
**验证(四条全过才算好)**:
```powershell
$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](../文档规范.md) 给该行加**状态词**(脚本的 `STATUS_HINTS`:
`计划 / 未创建 / 尚未创建 / 已删除 / 删除 / 不存在 / 缺失 / 断链 / 待建 / 待创建`)。
```markdown
| `.dsh/skills/serial-assistant/scripts/state/meta.json`(运行时未创建) | daemon 元数据(pid / 版本 / gen) |
```
> ⚠️ **状态词必须与引用在【同一行】且 ≤40 字符**(`STATUS_PROXIMITY`)。
> 把说明写在**上一行**是无效的——脚本逐行调用 `has_status_hint(line, ...)`,
> 跨行的文字不在窗口内。
**本地复现 CI 条件的姿势**(否则永远测不出来):
```bash
# 把运行时目录临时移开,模拟干净克隆,跑完再移回
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](../环境与部署/本地工具路径环境变量方案.md)):
**显式变量优先 → 自动发现回退**,三个 job 步骤相关的变量:
| 变量 | 作用 | 未设置时的回退 |
|---|---|---|
| `DTU_PRECOMMIT_PYTHON` | 指向装有 pre-commit 的 python.exe(静态门禁/主机侧纯逻辑单测用) | PATH 的 `python` → 工作区 `.venv`(干净克隆里通常没有) |
| `DTU_CPPCHECK` | 指向 cppcheck.exe(静态门禁钩子用) | SYSTEM 的 PATH(用户级 PATH 不可见) |
| `DTU_MSVC_CL` | 指向 x64 `cl.exe`(主机侧纯逻辑单测用) | `%ProgramFiles%`/`%ProgramFiles(x86)%` 推导的 VS 标准根自动探测 |
设置(**必须管理员**;scope 用 `Machine` 才能被 SYSTEM 任务看到):
```powershell
[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 的路径要求(探测逻辑)
`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"跳过 主机侧纯逻辑单测(仅告警)。
> 处置(二选一):设机器级 `DTU_MSVC_CL` 指向 `cl.exe`(推荐;值无效会硬报错),
> 或把 VS 装回标准位置(探测按 `%ProgramFiles*` 推导,换盘符的标准布局仍命中)。
>
> Windows SDK 同理:默认 `<ProgramFiles(x86)>\Windows Kits\10\`;非标准位置用 `DTU_WINDOWS_KITS_ROOT` 指定。
> 📌 2026-10-02:workflow 与本地 harness 同步改为"变量优先 + 标准布局推导"
> ([本地工具路径环境变量方案.md](../环境与部署/本地工具路径环境变量方案.md))——旧写法要求"同步改两处",现已不需要。
### 4.3 验证运行器能找到工具链(注册前先跑一遍)
```powershell
& '.venv\Scripts\python.exe' scripts/ci_local.py
```
这条命令跑的就是 CI 的同一套判定。**本地能过,runner 上基本就能过**(前提是 PATH 一致)。
---
## 5. 仓库端:workflow 文件
### 5.1 文件位置与提交
workflow 必须位于**仓库根**的 `.gitea/workflows/` 下(不是 `.github/`):
```
<仓库根>/
└─ .gitea/
└─ workflows/
└─ ci.yml ← 本仓库已有,见该文件
```
**它必须被 push 到远程才能生效**——目前本仓库的 `e904865` 还在本地
(`origin/v2-on-ui` 尚未包含),所以**现在 push 之前 CI 不会跑**:
```powershell
git push origin v2-on-ui # ⚠️ 需你明确要求才推送(本仓库约定:默认不推送)
```
### 5.2 触发条件
```yaml
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 拉取(重要)
workflow 用了 `actions/checkout@v4`。**默认从 `github.com` 拉取**(见 §1.2.1;
不是从你自己的实例,也不是从 gitea.com —— 这点很反直觉);实例不通
github.com 会在 checkout 步骤卡住/失败。三种处置:
1. **镜像到自有实例**:`DEFAULT_ACTIONS_URL = self`,**并先把 `actions/checkout` 导入你自己的 Gitea**;
2. **指向可达镜像**:`DEFAULT_ACTIONS_URL` 指向某个能访问的 action 站(或在 workflow 里
直接写全路径 `uses: https://<host>/owner/repo@ref`);
3. **零依赖**(内网最稳,推荐):把 checkout 换成纯 git 命令,不依赖任何 action:
```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
```
> 方案 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 页面即可看到完整输出与摘要。
**零依赖方案**(内网首选,**不用 `actions/upload-artifact`**):
| 手段 | 说明 |
|---|---|
| `Tee-Object` 落盘 | 两个 job 把关键输出 `Tee` 到 `ci-artifacts/static-pre-commit.log`、`ci-artifacts/host-unit-<模块>.log`(**留在 runner 工作区**,已 gitignore) |
| `$GITHUB_STEP_SUMMARY` | 写入 job 的 **step-summary**(Gitea UI 运行页直接可见,无需下钻日志) |
| `::notice::` 汇总行 | 控制台醒目行,便于日志检索 |
**汇总行格式**:
| 层 | 汇总行 |
|---|---|
| 静态门禁 | `static: pre-commit hooks -- passed=N failed=M skipped=K (log: ci-artifacts\static-pre-commit.log)` |
| 主机侧纯逻辑单测 | `host-unit: <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` | 步骤要点 |
|---|---|---|
| `static-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/static-pre-commit.log`)→ 打印 `::notice::static: passed/failed/skipped` 汇总行 → 归档日志说明 |
| `host-unit-tests` | `windows` | checkout → **探测 MSVC**(按 VS_ROOTS 找 `Hostx64\x64\cl.exe`)→ 遍历 `test/host/**/run_unit_test.py` 逐个跑(输出 `Tee` 到 `ci-artifacts/host-unit-<模块>.log`;解析各模块 `N/M PASSED` 汇总行 → 聚合 `::notice::host-unit: 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 那次 静态门禁全灭,就是 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 | `static-gates` 日志 | `using python (…)` + `runner environment check passed`;`::notice::static: pre-commit hooks -- passed=.. failed=0 skipped=..`;无 `.map` 时见 `::warning::` 体积门禁未跑 |
| 7 | `static-gates` 的 **提交信息门禁** 步骤 | `::notice::gitlint: N commit message(s) checked, 0 failed`(**这一行以前根本不存在**,见 §7) |
| 8 | `host-unit-tests` 日志 | `::notice::host-unit: <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** 区块 | 出现 "静态门禁 static gates" / "Commit message gate (gitlint)" / "主机侧纯逻辑单测 host-side unit tests" 小标题与汇总行 |
| 10 | 提交记录旁出现 ✅ | CI 全绿 |
**本地先自查**(与 CI 同一套判定,避免推上去才发现问题):
```powershell
& '.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]` 段):
```bash
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 秒即在正常心跳** |
```bash
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` 复查 |
| `static-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** |
| `static-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 |
| `static-gates` 报 `::error::runner 缺少:Python, cppcheck` | runner 没装或不在 PATH | 装完后**重启 runner 服务**(新进程才吃到新 PATH) |
| `checkout` 步骤卡住/超时 | 拉不到 `actions/checkout@v4`(内网) | 见 §5.3 |
| `l05` 日志出现 `::warning::runner 未安装 MSVC` | VS 装在非默认路径或未装 | 见 §4.2(默认路径 / 同步改两处根目录) |
| `mem-numbers` 显示 `Skipped` | 正常(CI 无 Keil,无 `.map`) | 无需处理;但注意 **CI 未守体积门禁**(见 §0.0),改代码后本地 `@build` + `check_mem_numbers.py --scan` 兜底 |
| 主机侧纯逻辑单测显示 `2 runner(s), X/Y ...` 但 X<Y | 有单测用例未通过 | 看该模块的 `::error::unit test failed` 行与其 `ci-artifacts/host-unit-<模块>.log` |
| 主机侧纯逻辑单测报 `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) | 同上 |
| `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 秒重拉后自愈,属预期 |
| `static-gates` 报 `Cannot find: pwsh in PATH` | `shell: pwsh` = PowerShell 7(本机只有 Windows PowerShell 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`)是同一个道理。
```powershell
# 方法 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)))
```
```bash
# 方法 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 解析器(不执行)确认语法干净:
```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` 键**,两种落盘方式:
```yaml
# 方式 A:按 job 落文件(推荐,防上报失败丢日志)
log:
level: info
job:
dir: "D:\\Docker\\act_runner\\log\\job"
```
```powershell
# 方式 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-commit` stage,
`commit-msg` stage 的钩子(gitlint)不在其中,靠 `ci_gitlint_range.ps1` 单独守(见 §7);
- **`@build` / 板端测试套件 / 主机侧回归**:永远留在本地,不要往 CI 加(需 Keil 授权与真机);
- **runner 长时间不可用**时:CI 只是"多一道网",**提交前的本地 `ci_local.py` 仍是 静态门禁的唯一保障**,
不能因"已经配了 CI"放宽(见 [docs/任务规划/登记簿.md](../任务规划/登记簿.md) 的条目「CI 远端闭环未验证」)。
---
## 9. 一键检查脚本(可选)
存为 `D:\Docker\act_runner\check_env.ps1`,注册前跑一遍:
```powershell
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)
```powershell
# 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 任务——环境块是任务启动时的快照
# 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):
> `static-gates` 报 **定义数 − 1 条钩子**(gitlint 属 `commit-msg` stage,不进 `--all-files` 报告,
> 见 §7),其中 `mem-numbers` 在无 `.map` 时为 `Skipped`(本机唯一常见跳过项);另有提交信息
> 门禁 `::notice::gitlint: N checked, 0 failed`。`host-unit-tests` 报
> **`<N> runner(s), A/A assertions passed, fail=0`**,`N` = `test/host/` 下 `run_unit_test.py` 个数。
> 实测快照:`master` 18 定义 / 17 报告、3 套件 / 288 断言;`v2-on-ui` 26 定义 / 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-25 | 🔒 **A-5 残余销项联动(§0.0 重写)**:体积门禁由"CI 不守"改为**同码凭证强制**——本地 `@build` 0/0 后 `ram_baseline.py --update` 盖 `scripts/size_attestation.json`,静态门禁作业末尾 `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——**主机侧纯逻辑单测成功**(`10 runner(s), 647/647 assertions passed, fail=0`,与本地逐字一致),**静态门禁失败**却与提交信息毫无关系:`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` 全绿(静态门禁 25 钩子 + 主机侧纯逻辑单测 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`;④ 数字口径——主机侧纯逻辑单测期望值更新为 **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 静态门禁全灭根因(`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` 实施):`static-gates` 的 Python 探测改为 `DTU_PRECOMMIT_PYTHON`(显式,无效即硬失败)→ PATH → `.venv`(删除 `C:\ProgramData\anaconda3` 固定候选);cppcheck 改为 `DTU_CPPCHECK` 优先 → PATH(**移除 `C:\Program Files\Cppcheck` 固定回退**);`host-unit` 的 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:静态门禁 + 主机侧纯逻辑单测双 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 拉取 / 端到端验证清单 / 故障排查 / 日常维护) |