远程 CI 文档独立成 docs/CI/:两篇重叠指南合并为一篇,AGENTS.md 压成一行指针

动因:远程 CI 只跑在本机一台 runner(win-stm32-ci)上,其他机器与项目都不涉及,
但内容散在 4 处约 2000 行——两篇高度重叠的指南躺在「环境与部署」里与换机核对清单
并列,AGENTS.md 提交门禁节还塞了 4 条 runner 运维经验,导致每次会话都被这些机器
专属细节干扰。

结构:新建 docs/CI/,README.md 作唯一入口(适用范围 + 章节速查 + 常用命令),
远程CI完整指南.md 为合并稿。

- 合并:CI配置指南 477 行 + Gitea_Runner部署指南 917 行 → 一篇 1300 行;骨架内容
  逐行比对核实无丢失(仅章节重编号与相对链接按新位置改写),重复段按"留新版"去重,
  独有内容全部搬入(下载/config.yaml 字段语义/不能用 Windows 服务的根因/内网 action
  三种处置/check_env 自检/TL;DR/排障表 13 条独有行/MySQL 直查核验)
- 再分配:测试流程 §9.2 两个 job 的步骤细节并入指南 §5.5、§9.4 部署前置表并入 §0.2;
  测试流程 §9 只留测试分层视角(覆盖哪些层、触发与 paths-ignore、本地等价、
  "CI 不替代本地门禁")
- AGENTS.md:4 条 CI 经验(看门狗 / gitlint stage / UTF-16LE / CI 概览)压成 1 行指针
- 引用同步 6 个文件:docs/README 目录树与索引、项目部署环境要求 3 处、本地工具路径
  环境变量方案、待处理内容、scripts/README(补登 3 个 CI 脚本并标注为本机专属)

验证:check_doc_links.py --all 125 篇 0 问题;pre-commit run --all-files 17 项全 Passed。
两篇旧文档以 git rm 删除(历史仍在 git 中);各文档变更记录的历史行按仓库惯例不改写,
只新增 2026-10-12 行。
This commit is contained in:
2026-10-12 14:03:44 +08:00
parent e6bcad86cb
commit ba40cc0d20
10 changed files with 673 additions and 675 deletions
+1 -4
View File
@@ -35,10 +35,7 @@ STM32F407ZGTx @168MHz(HSE 8MHz → PLL M=4/N=168/P=2);**仅 Keil MDK-ARM v
- 提交信息:**单行中文标题 ≤100 字符**(gitlint `title-max-length=100`)+ **正文每行 ≤80 字符**(gitlint 默认 `body-max-line-length`,本项目未关)→ 长说明一律用**短标题 + `git commit -F -` 多行正文**(中文长标题极易超限);UTF-8 无 BOM + `git commit -F <文件>`(防终端转码破坏)
- 代码规范 V1.0:小写下划线命名(`g_`/`s_`/`p_`/`a_` 前缀)、头文件保护宏 `XXX_H`、常量在右、文件头模块注释块 → `docs/代码规范/`
- 全库扫描 `pre-commit run --all-files`;详见 `docs/代码规范/pre-commit使用说明.md`
- **CI(Gitea Actions,2026-10-01 T-6)**:`.gitea/workflows/ci.yml` 在 push/PR 跑 **L0 静态门禁 + gitlint 提交信息门禁 + L0.5 主机侧单测**(Windows runner);**不含 L1/L2/L3**(需真机+ST-LINK)、**刻意不跑 `@build`**(需 Keil 授权)、无 `.map` 时自适应跳过 `mem-numbers`。本地等价自查:`& '.venv\Scripts\python.exe' scripts/ci_local.py`(**runner 未就绪时这是 L0 的唯一保障**,不能因"已配 CI"放宽本地门禁)。详见 `docs/测试说明/测试开发标准流程.md` §9
- **`pre-commit run --all-files` 跑不到 `gitlint`**(2026-10-11 实测):`stages: [commit-msg]` 写在 **gitlint 上游的 `.pre-commit-hooks.yaml`**(本机证据 `~\.cache\pre-commit\repo*\.pre-commit-hooks.yaml`,**项目 `.pre-commit-config.yaml` 里根本没有这一行**——查不到不等于没限制,别在仓库配置里 grep 半天下结论),而 `--all-files` 只跑 `pre-commit` stage → **18 个钩子定义、CI 只报 17 个(`passed=16 skipped=1`)、且表现为全绿**(提交信息门禁曾在 CI 完全空转)。现由 `scripts/ci_gitlint_range.ps1`(CI)与 `ci_local.py` 的 **L0b** 段(本地)逐条校验;**增删钩子后必须核对"定义数 == 日志条数 + gitlint"**
- **CI runner 必须有看门狗**(2026-10-11 实测):`gitea-runner` v4 的 `daemon` **无服务支持、首次 Declare 失败就直接退出进程**(Gitea 重启一次即停摆,run 16~21 共 12 个 job 零日志;叠加 `cancel-in-progress` 后症状是"任务凭空消失"而非失败)。⚠️ **计划任务的"失败重启"救不了它**——daemon 是**正常退出**不是失败,旧任务跑单次 `daemon` 会"成功结束"后再也不拉,必须**看门狗循环重拉**。体检 `& '.\scripts\ci_runner_bootstrap.ps1' -Status`(**须管理员**:非管理员会话可能看不见该任务)、干跑 `-DryRun`(免管理员校验任务对象)、部署 `-SetEnv -StartNow`(看门狗 + 开机计划任务 + MACHINE 级 `DTU_PRECOMMIT_PYTHON`/`DTU_CPPCHECK`/`DTU_MSVC_CL`——runner 以 SYSTEM 跑,看不见用户级 PATH/site)。改该脚本时守住两条:`StartWhenAvailable` 属**任务设置**(触发器上没这属性,误设抛 `SetValueInvocationException`)、且必须**先构建对象后注销旧任务**(否则旧任务被删、新任务没建成)
- **写 `GITHUB_ENV`/`GITHUB_PATH`/`GITHUB_OUTPUT` 必须显式 `-Encoding ascii`**(2026-10-11 实测):PS 5.1 的 `Out-File -Append` **默认 UTF-16LE**,会把 `42 00 42 00 3D 00` 这种带 NUL 的行追加进 runner 以 UTF-8 建的同一文件里
- **远程 CI(Gitea Actions)的全部资料集中在 [`docs/CI/README.md`](docs/CI/README.md)**——服务端开关、runner 部署与保活、workflow、排障,以及 gitlint stage / UTF-16LE / 看门狗等实测坑都在那里,**不放本节**(它**只跑在本机一台 runner `win-stm32-ci` 上**,其他机器与项目都不涉及,故与日常开发规范分开放,避免每次会话被这些机器专属细节干扰)。日常改代码的等价自查是 `& '.venv\Scripts\python.exe' scripts/ci_local.py`(L0 + L0b + L0.5)——**runner 不在线也不影响本地门禁,不能因"已配 CI"放宽本地要求**
## 平台约束
+71
View File
@@ -0,0 +1,71 @@
# 远程 CI(Gitea Actions)—— 本目录是唯一入口
> **适用范围(先读这句,可以省下很多时间)**
> 本仓库的远程 CI **只跑在本机这一台 runner 上**:runner 名 `win-stm32-ci`,装在
> `D:\Docker\act_runner`,连的是本机原生 Gitea `http://127.0.0.1:3001`。
> **其他机器、其他项目都不涉及,也不需要配这套东西。**
>
> 日常改代码**不需要读本目录**:提交前的等价自查是本地门禁,runner 在不在线都照样跑——
>
> ```powershell
> & '.venv\Scripts\python.exe' scripts/ci_local.py # L0 静态 + L0b 提交信息 + L0.5 主机单测
> ```
>
> 只有在这三种情况下才需要进来:**① 换机/重装后要重新部署 runner;② CI 不出任务或莫名
> 取消;③ 改 `.gitea/workflows/ci.yml` 本身。**
## 索引
全部内容是**一篇**文档:[`远程CI完整指南.md`](远程CI完整指南.md)(2026-10-12 由原「环境与部署」
目录下的两篇指南 `CI配置指南.md` 与 `Gitea_Runner部署指南.md` 合并而来,**两篇均已删除**,
去重后为单一来源)。
| 想干什么 | 去看 |
|---|---|
| 先搞清 CI 需要哪三样东西就位、本机拓扑、部署前置清单 | §0 / §0.1 / §0.2 |
| **CI 不守哪些门禁**(体积门禁盲区 S14、gitlint 空转) | §0.0 + §7 排障表"钩子条数对不上"行 |
| 服务端开启 Actions、`DEFAULT_ACTIONS_URL` 到底管什么 | §1(含 §1.2.1,**这条最容易搞反**) |
| 下载 / 注册 runner(`--instance` 用回环、labels 必须精确匹配 `runs-on`) | §2.2 |
| `config.yaml` 各字段语义(capacity / timeout / labels / 日志目录…) | §2.3 |
| **常驻:为什么必须看门狗、不能只起 daemon、也不能做成 Windows 服务** | §2.4 + §3.0 |
| runner 装什么软件、为什么变量必须对 **SYSTEM** 可见、机器级 `DTU_*` | §4(§4.1.1~§4.1.3 是三个真坑) |
| workflow 位置 / 触发 / 内网拉不到 `actions/checkout` 的三种处置 | §5(§5.3 零依赖写法) |
| 两个 job 各自跑哪些步骤、两条 Python 探测链要同序同步 | §5.5 |
| 端到端验证清单、无网页登录时直查 MySQL 核验 | §6 / §6.1 |
| 出问题了按症状查表(含 `0x41301`、1053、72h 被杀、`.ps1` 无 BOM 乱码…) | §7 |
| 日常维护(改钩子集 / 改 workflow 后要不要动 runner) | §8 |
| 一键自检脚本、最小操作清单 TL;DR | §9 / §10 |
## 常用命令速查
```powershell
# 本地等价门禁(不依赖 runner,日常用这个)
& '.venv\Scripts\python.exe' scripts/ci_local.py
# runner 体检(四项:计划任务 / 进程 / Gitea 可达 / 机器级变量)—— 须管理员
& '.\scripts\ci_runner_bootstrap.ps1' -Status
# 干跑:只打印将要写入的启动器与任务对象,不落盘不注册(免管理员)
& '.\scripts\ci_runner_bootstrap.ps1' -DryRun
# 部署 / 修复:写机器级 DTU_* + 生成看门狗启动器 + 注册开机计划任务 + 立即启动(须管理员)
& '.\scripts\ci_runner_bootstrap.ps1' -SetEnv -StartNow
```
> ⚠️ 上面三条 `ci_runner_bootstrap.ps1` 是**本机运维**动作,会改系统计划任务与机器级环境
> 变量;返回码 `0x41301` 是"任务正在运行"、`next run` 为空是开机触发器的正常表现,
> 都不是故障(详见指南 §3.4)。
## 相关文档
- 测试分层与门禁决策(L0~L3 各层由谁把关):[`../测试说明/测试开发标准流程.md`](../测试说明/测试开发标准流程.md)
- workflow 本体:[`../../.gitea/workflows/ci.yml`](../../.gitea/workflows/ci.yml)
- 本地等价脚本:[`../../scripts/ci_local.py`](../../scripts/ci_local.py)
- runner 机器的工具链环境要求:[`../环境与部署/项目部署环境要求.md`](../环境与部署/项目部署环境要求.md)
- `DTU_*` 工具路径变量机制:[`../环境与部署/本地工具路径环境变量方案.md`](../环境与部署/本地工具路径环境变量方案.md)
## 变更记录
| 日期 | 变更 |
|---|---|
| 2026-10-12 | **新建本目录**:远程 CI 内容原先散在 `docs/环境与部署/`(两篇互相重叠的指南)、`docs/测试说明/测试开发标准流程.md` §9、`AGENTS.md` 提交门禁节共 4 处,约 2000 行;现合并为单一指南并把 `AGENTS.md` 压成一行指针。动因:远程 CI 只涉及本机一台 runner,与日常开发规范混在一起每次会话都被读,造成困惑 |
@@ -1,4 +1,17 @@
# Gitea Actions Runner 部署指南(本机 Windows 原生版)
# 远程 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`,L0 + L0.5)。
> **前置阅读**:[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**),
@@ -6,10 +19,7 @@
> - 配置 `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 未运行** → **本指南不使用容器,全程原生**
>
> **本文只讲 Runner 端**(下载 → 注册 → 配置 → 常驻 → 验证 → 排障)。
> Gitea 服务端开关已就绪,无需再动。CI 整体设计见 `docs/测试说明/测试开发标准流程.md` §9。
> - Docker Desktop **已装但 daemon 未运行** → **本文不使用容器,全程原生**
> ⚠️ **重要版本变更(2026-10-02 实测)**:act_runner 已更名为 **`gitea-runner`**,
> 当前最新版 **v4.0.1**(发布页 `https://gitea.com/gitea/runner/releases`,
@@ -21,11 +31,45 @@
> 🔴 **常驻方式修正(2026-10-02 实测推翻)**:`gitea-runner daemon` 是**纯控制台程序**,
> **不支持作为 Windows 服务运行**(二进制内无服务管理代码,也无 `service.go`);
> 用 `sc.exe` / `New-Service` 注册后 `Start-Service` 必然失败。
> **正确方式是 Windows 任务计划程序** —— 见 **§4**。
> **正确方式是 Windows 任务计划程序** —— 见 **§3**;且 2026-10-11 实测进一步要求
> 在计划任务之上加**看门狗循环重拉** —— 见 **§2.4**。
---
## 0. 先理解:这台机器上的拓扑
## ⚠️ 0.0 CI 不覆盖的体积门禁(必读,S14)
> **CI 不守 Flash/RAM 体积门禁。** `mem-numbers` 钩子的事实源是 Keil 构建产物
> `MDK-ARM/**/*.map`(已 gitignore),而 CI 刻意不跑 `@build` → **无 `.map` → 该钩子被跳过**。
>
> 后果:**新增任务 / 栈 / 缓冲 / 代码导致的体积增长(或文档内存数字失真)不会在 CI 被拦下。**
>
> 兜底(每次改代码后必做):
> ```powershell
> & '.venv\Scripts\python.exe' .dsh\skills\keil-build\scripts\keil_build.py # 0/0 且生成 .map
> & '.venv\Scripts\python.exe' scripts/check_mem_numbers.py --scan
> ```
> 若 CI 允许在带 Keil 的 runner 上跑,则该钩子会在有 `.map` 时**照常执行**(workflow 自适应)。
>
> CI 运行时该跳过以 **`::warning::`** 明示(不是弱化的 notice),并写入 job 的
> **step-summary** 醒目行,避免被忽略。
---
## 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 在同一条机器上** —— 这是本部署最关键的事实:
@@ -57,11 +101,146 @@
> ⚠️ **注册时用哪个地址,决定了 runner 以后一直连哪里**。
> 注册信息存在 `.runner` 里,之后改起来要重新注册,**建议一开始就用 `http://127.0.0.1:3001`**。
### 0.2 部署前置清单(需人工确认)
> 2026-10-12 自《测试开发标准流程》§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-runner
## 1. 服务端:开启 Gitea Actions
### 1.1 选对版本
### 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 下载并注册 act_runner
> ⚠️ 下面这段是初版(act_runner v0.2.x 时代)的通用写法,按原样保留备查;
> 本机实际部署请按 §2.2.1 起(**gitea-runner v4.0.1**,二进制名与目录都不同)。
```powershell
# 1) 下载(示例版本,请取 releases 页最新 Windows amd64)
# https://gitea.com/gitea/act_runner/releases
Invoke-WebRequest -Uri "https://gitea.com/gitea/act_runner/releases/download/v0.2.11/act_runner-0.2.11-windows-amd64.exe" `
-OutFile "C:\act_runner\act_runner.exe"
# 2) 注册:URL 用 Gitea 根地址,token 用 2.1 复制的
cd C:\act_runner
.\act_runner.exe register `
--no-interactive `
--instance "https://gitea.bitnasdaq.vip" `
--token "粘贴 Registration Token" `
--name "win-stm32-ci" `
--labels "windows:host"
```
**关键点**:`--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
| 项 | 值 |
|---|---|
@@ -70,8 +249,6 @@
| 下载页 | https://gitea.com/gitea/**runner**/releases ← 注意仓库名已从 `act_runner` 改为 `runner` |
| 文件名 | `gitea-runner-4.0.1-windows-amd64.exe` ← 二进制名也从 `act_runner` 改为 **`gitea-runner`** |
### 1.2 下载
```powershell
# 建目录(与 Gitea 同级,便于管理)
New-Item -ItemType Directory -Force -Path "D:\Docker\act_runner" | Out-Null
@@ -89,7 +266,7 @@ Invoke-WebRequest -Uri "https://gitea.com/gitea/runner/releases/download/v4.0.1/
> 💡 **本机实测**(2026-10-02):下载 20.6 MB,耗时约 5 秒,`gitea.com` 可直连(`HTTP 200`)。
### 1.3 v4.0.1 的命令集(与 v0.2.x 的差异)
#### 2.2.2 v4.0.1 的命令集(与 v0.2.x 的差异)
```
Available Commands:
@@ -103,34 +280,13 @@ Available Commands:
```
> ⚠️ **没有 `daemon install/start/stop/status`** —— 这些是 v0.2.x 的接口,v4 已移除。
> 常驻必须用 Windows 原生方式(见 §4)。
> 常驻必须用 Windows 原生方式(见 §3)。
>
> `config` 子命令很方便,建议优先用它而不是手改 YAML:
> `config init`(生成最小配置)/ `config generate`(打印全部选项)
> / `config set <key> <value>` / `config get <key>`。
---
## 2. 注册 Runner
### 2.1 先拿 Registration Token
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` 值**不要照抄**——见下。
> 💡 **Token 存在于 `action_runner_token` 表**(MySQL,见 `Gitea_Runner部署指南` §附录)。
> 每仓库一个有效 token,**注册一次后即被消费**;过期(约 1 小时)或已用则需重新创建。
### 2.2 执行注册(关键:instance 用 127.0.0.1)
#### 2.2.3 执行注册(关键:instance 用 127.0.0.1)
```powershell
cd "D:\Docker\act_runner"
@@ -157,7 +313,7 @@ cd "D:\Docker\act_runner"
**期望输出**:`Runner registered successfully`,并在 `D:\Docker\act_runner\` 生成 `.runner` 文件(记录注册凭据)。
### 2.3 验证注册
#### 2.2.4 验证注册
浏览器刷新 **Settings → Actions → Runners**,应看到:
@@ -167,9 +323,21 @@ cd "D:\Docker\act_runner"
| Labels | `windows`(`:host` 是执行方式,UI 可能只显示 `windows`) |
| Status | 🟢 **Idle** |
---
### 2.3 配置 `config.yaml`(可选但建议)
## 3. 配置 config.yaml
`C:\act_runner\config.yaml` 里确认/补充:
```yaml
runner:
labels:
- "windows:host" # 与 register 一致
capacity: 1 # 并行 job 数;单机建议 1,避免 cppcheck 抢 CPU
timeout: 30m
cache:
enabled: true
```
#### 2.3.1 用 `config` 子命令管理(v4 推荐)
**推荐用 `config` 子命令生成/修改**(v4 新增,比手改 YAML 安全):
@@ -184,6 +352,8 @@ cd "D:\Docker\act_runner"
.\gitea-runner.exe config generate # 打印全部可用选项(含注释)
```
#### 2.3.2 本机已落地的配置与字段语义
**本机已落地的配置**(`D:\Docker\act_runner\config.yaml`):
```yaml
@@ -226,15 +396,93 @@ container:
> ⚠️ **改了 `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` 为准。
---
## 4. 让 Runner 常驻(任务计划程序)
## 3. 常驻底层:为什么不能用 Windows 服务 + 任务计划程序手动方式
### 4.0 🔴 关键结论:**不能用 Windows 服务**
> 本节是 2026-10-02 的实测记录("不能用服务"的根因 + 任务计划程序手动方式),
> 保留作根因依据与手动排障参考。**正式部署一律用 §2.4 的 `scripts/ci_runner_bootstrap.ps1`**——
> 本节 §3.2 / §3.3 生成的 `run_runner.cmd` 跑的是**单次** daemon,2026-10-11 实测证明
> 它首次 Declare 失败就退出、死了没人管(见 §2.4)。
### 3.0 🔴 关键结论:**不能用 Windows 服务**
> **本条为 2026-10-02 实测推翻既有假设后的修正结论,务必先读。**
先前本文档(及 `install_service.ps1`)主张用 `sc.exe` / `New-Service` 把
先前文档(及 `install_service.ps1`)主张用 `sc.exe` / `New-Service` 把
`gitea-runner daemon` 注册成 Windows 服务。**实测不可行**,`Start-Service` 必然失败:
```
@@ -263,7 +511,7 @@ Start-Service : 由于以下错误无法启动服务"Gitea Actions Runner (gitea
| 崩溃自动重启 | `-RestartCount 999 -RestartInterval 1min` |
| 开机/手动启停 | `Enable/Disable/Start/Stop-ScheduledTask` |
### 4.1 前台先试跑(强烈建议)
### 3.1 前台先试跑(强烈建议)
**先别急着常驻**——用前台模式确认能连上:
@@ -281,7 +529,7 @@ level=info msg="runner: win-stm32-ci, with version: v4.0.1, with labels: [window
**让这个窗口开着**,去 Gitea 页面确认 runner 是 Idle,然后 Ctrl+C 停掉、再做常驻。
### 4.2 一键部署脚本(推荐)
### 3.2 一键部署脚本 `deploy_runner_task.ps1`(2026-10-02 方案,已被 §2.4 取代)
管理员 PowerShell 执行:
@@ -294,7 +542,7 @@ powershell -ExecutionPolicy Bypass -File "D:\Docker\act_runner\deploy_runner_tas
→ `Register-ScheduledTask`(开机+登录双触发器、SYSTEM 账户、最高权限、崩溃重启)
→ 启动任务 → 校验进程存活。
### 4.3 手动等价命令
### 3.3 手动等价命令
不想用脚本时,管理员 PowerShell 逐条执行:
@@ -333,7 +581,7 @@ Start-ScheduledTask -TaskName "GiteaActionsRunner"
> WinPS 5.1 `New-Service` 的地方)。不过仍建议用 `.cmd` 包装再 `cd /d`,
> 因为 SYSTEM 账户的默认工作目录不可控。
### 4.4 任务管理速查
### 3.4 任务管理速查
```powershell
Get-ScheduledTask -TaskName GiteaActionsRunner # 状态
@@ -361,7 +609,7 @@ Unregister-ScheduledTask -TaskName GiteaActionsRunner -Confirm:$false # 完全
> ② `log\runner-stdout.log` 最新一行是 `declare successfully`;
> ③ Gitea 页面 runner 显示 **Idle**(或 MySQL 查 `action_runner.last_online`)。
### 4.5 ⚠️ 清理遗留的无效服务
### 3.5 ⚠️ 清理遗留的无效服务
若早前按旧文档建过服务,删掉它(留着只会误导):
@@ -373,7 +621,7 @@ sc.exe delete gitea-runner
> **`gitea-runner` 这个名字将不再作为服务存在。** 之后提到"gitea-runner"
> 一律指**计划任务 `GiteaActionsRunner`** 或**进程 `gitea-runner.exe`**。
### 4.6 ⚠️ 权限要求(本机实测的硬约束)
### 3.6 ⚠️ 权限要求(本机实测的硬约束)
**注册计划任务必须用「提权的管理员」Shell**。本机实测:
@@ -391,18 +639,20 @@ sc.exe delete gitea-runner
---
## 5. Runner 必须装齐的软件
## 4. Runner 软件环境:装什么
Runner 会**直接在本机跑** CI 步骤,所以本机必须有一套工具链。
(**你本机是开发机,大概率已经全有** —— 逐条核对下表。)
runner 是一台干净的 Windows,**下面这些必须装**,否则 job 会失败或降级跳过。
| 项 | 检查命令 | 期望 | 缺了怎么办 |
| 项 | 必需性 | 影响 | 安装 |
|---|---|---|---|
| **Python 3.x** | `python --version` | `Python 3.x` | `winget install Python.Python.3.12` |
| **cppcheck** | `cppcheck --version` | `Cppcheck 2.21.0` | `winget install Cppcheck.Cppcheck` |
| **pre-commit** | `python -m pre_commit --version` | `pre-commit 4.x` | `pip install pre-commit` |
| **MSVC** | 见下 | `cl.exe` 存在 | `winget install Microsoft.VisualStudio.2022.BuildTools`(勾"使用 C++ 的桌面开发") |
| Git | `git --version` | 任意 | 你已在用 |
| **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)**:
@@ -415,24 +665,51 @@ MSVC : VS2019 BuildTools 14.29.30133 (Hostx64\x64\cl.exe) ✅
**→ 四项全齐,无需额外安装。**
### 5.1 ⚠️ 关键坑:PATH 与运行账户
### 4.1 关键:变量必须对 **SYSTEM** 可见(不是"对你可见")
**问题的本质**:计划任务以 **`SYSTEM`** 账户运行时,继承的是**系统级 PATH**
(注册表 `HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment` 的 `Path`),
**不含任何用户级 PATH**。
runner 以 **SYSTEM 服务/计划任务**运行,它看到的 PATH 与 site-packages **和你登录时完全不同**:
- 你在自己的终端里能用 `python`,**不代表 SYSTEM 账户里也能用**;
- **装完任何新软件 / 改过系统 PATH 后,必须重启 runner 任务**:
| 你以为 | 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 环境自检失败 |
```powershell
Stop-ScheduledTask -TaskName GiteaActionsRunner
Start-ScheduledTask -TaskName GiteaActionsRunner
```
> **核对方法**:用 `check_env.ps1` 的「系统 PATH 视角」检查项,
> 机制补充(2026-10-02 实测):计划任务以 **`SYSTEM`** 账户运行时,继承的是**系统级 PATH**
> (注册表 `HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment` 的 `Path`),
> **不含任何用户级 PATH**。核对方法:用 `check_env.ps1`(见 §9)的「系统 PATH 视角」检查项,
> 或以 SYSTEM 身份跑一次 `where python` 验证。
### 5.1.1 ⚠️⚠️ 关键坑:`pre-commit` 必须在「全局 site-packages」,装到用户态等于没装
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 排查 **L0 job 反复失败**(`::error::no Python with a SYSTEM-visible pre-commit`)的真正根因,
也是本环境最隐蔽的一个坑。
@@ -448,7 +725,7 @@ Start-ScheduledTask -TaskName GiteaActionsRunner
```
> 📌 上为 2026-10-02 的历史报错原文。workflow 现已改为
> **`DTU_PRECOMMIT_PYTHON`(机器级)→ PATH 的 `python` → 工作区 `.venv`** 顺序(§5.1.3),
> **`DTU_PRECOMMIT_PYTHON`(机器级)→ PATH 的 `python` → 工作区 `.venv`** 顺序(§4.1.3),
> 且报错行会指明是变量无效还是自动发现落空。
**根因(两层)**:
@@ -510,7 +787,7 @@ Get-ChildItem 'C:\ProgramData\anaconda3\Lib\site-packages' -Filter 'cfgv*'
> ⚠️ 别再往 `.venv` 里装就以为 CI 能用了——CI 虽然会检查 `.venv\Scripts\python.exe`,
> 但 `actions/checkout@v4` 拉的是**干净克隆**,`.venv` 是 gitignore 的,**根本不存在**。
### 5.1.2 ⚠️ 关键坑:`doc-links` 钩子「本地过、CI 挂」= 引用了 gitignore 的运行时文件
#### 4.1.2 ⚠️ 关键坑:`doc-links` 钩子「本地过、CI 挂」= 引用了 gitignore 的运行时文件
**现象**:`文档链接存在性 (D-3/D-7)` 钩子在**本地** `pre-commit run --all-files` 通过,
一上 CI 就 `Failed`(exit 1),报的行/路径你在本地明明存在。
@@ -521,7 +798,7 @@ Get-ChildItem 'C:\ProgramData\anaconda3\Lib\site-packages' -Filter 'cfgv*'
如 `.dsh/skills/serial-assistant/scripts/state/meta.json`(运行时未创建,`.gitignore:132` 排除整个 `state/`)——
你本机因为跑过工具所以文件在,**克隆里则不存在** → 判为失效引用。
**修复**:按 `docs/文档规范.md` §3.3 给该行加**状态词**(脚本的 `STATUS_HINTS`:
**修复**:按 [docs/文档规范.md §3.3](../文档规范.md) 给该行加**状态词**(脚本的 `STATUS_HINTS`:
`计划 / 未创建 / 尚未创建 / 已删除 / 删除 / 不存在 / 缺失 / 断链 / 待建 / 待创建`)。
```markdown
@@ -544,9 +821,9 @@ mv .dsh/skills/serial-assistant/scripts/state.bak_ci .dsh/skills/serial-assistan
> 更一般的教训:**任何校验"文件是否存在/数字是否一致"的钩子,都要在
> "无本地产物"的干净克隆里验一次**,别只在开发机上跑。
### 5.1.3 机器级 `DTU_*` 环境变量(runner 的正式覆盖通道,2026-10-02 起)
#### 4.1.3 机器级 `DTU_*` 环境变量(runner 的正式覆盖通道,2026-10-02 起)
workflow 已改为统一消费 `DTU_*` 接口(见 `本地工具路径环境变量方案.md` §3.3):
workflow 已改为统一消费 `DTU_*` 接口(见 [本地工具路径环境变量方案.md §3.3](../环境与部署/本地工具路径环境变量方案.md)):
**显式变量优先 → 自动发现回退**,三个 job 步骤相关的变量:
| 变量 | 作用 | 未设置时的回退 |
@@ -566,27 +843,35 @@ Stop-ScheduledTask -TaskName GiteaActionsRunner
Start-ScheduledTask -TaskName GiteaActionsRunner
```
> 与 §5.1.1 的关系:机器级 `DTU_PRECOMMIT_PYTHON` 是**正式通道**(指向哪个解释器由你说了算,
> 与 §4.1.1 的关系:机器级 `DTU_PRECOMMIT_PYTHON` 是**正式通道**(指向哪个解释器由你说了算,
> 值无效时 job 直接报变量名、不猜);"把 pre-commit 装进全局 site-packages"仍是**兜底**
> (变量未设 + 全局 site 可见也能过)。两条路并存不冲突:变量设了就用变量,没设走探测。
> 显式变量值无效(文件不存在 / 无 pre-commit 模块)→ job **硬失败**并指明变量名——**不会**
> 静默回退到别的解释器,避免 runner 悄悄换了 Python 导致钩子行为漂移。
### 5.2 ⚠️ 关键坑:MSVC 的路径必须"标准"(或显式给变量)
CI 的 MSVC 探测(`l05` job)优先读机器级 `DTU_MSVC_CL`(§5.1.3);
未设置时按**标准布局自动探测**(根目录由 `%ProgramFiles` / `%ProgramFiles(x86)` 推导,
不再写死盘符):2019 → `Program Files (x86)`;2022 / 2026(实际目录名 `18`)→ `Program Files`,
各取 BuildTools/Community/Professional/Enterprise 子目录,找
`VC\Tools\MSVC\*\bin\Hostx64\x64\cl.exe`。本地 `unit_test_harness.py` 用同一套推导(已同步)。
### 4.2 MSVC 的路径要求(探测逻辑)
**你本机实测命中第 1 条**:
`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` ✅
> ⚠️ 若将来 MSVC 装到 **D 盘或完全非标准路径** → 自动探测不到 → 两种处置(二选一):
> - 设机器级 `DTU_MSVC_CL` 指向 `cl.exe`(**推荐**,不动任何代码;值无效会硬报错、不静默跳过)
> - 或把 VS 装回标准根(探测逻辑按 `%ProgramFiles*` 推导,换盘符的标准布局仍命中)
> ⚠️ **装在完全非标准位置(如 D 盘自定义目录)会探测不到** → 被当成"没装 MSVC"跳过 L0.5(仅告警)。
> 处置(二选一):设机器级 `DTU_MSVC_CL` 指向 `cl.exe`(推荐;值无效会硬报错),
> 或把 VS 装回标准位置(探测按 `%ProgramFiles*` 推导,换盘符的标准布局仍命中)。
>
> Windows SDK 同理:默认 `<ProgramFiles(x86)>\Windows Kits\10\`;非标准位置用 `DTU_WINDOWS_KITS_ROOT` 指定。
### 5.3 验证运行器能找到工具链(注册前先跑一遍)
> 📌 2026-10-02:workflow 与本地 harness 同步改为"变量优先 + 标准布局推导"
> ([本地工具路径环境变量方案.md](../环境与部署/本地工具路径环境变量方案.md))——旧写法要求"同步改两处",现已不需要。
### 4.3 验证运行器能找到工具链(注册前先跑一遍)
```powershell
& '.venv\Scripts\python.exe' scripts/ci_local.py
@@ -596,21 +881,139 @@ CI 的 MSVC 探测(`l05` job)优先读机器级 `DTU_MSVC_CL`(§5.1.3)
---
## 6. 端到端验证
## 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 产物存档与结果汇总(S3,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 自《测试开发标准流程》§9.2 并入)
| job | `runs-on` | 步骤要点 |
|---|---|---|
| 1 | `.\gitea-runner.exe daemon` 前台启动 | 打印 `win-stm32-ci` + `labels: [windows]` + `declare successfully` |
| 2 | 浏览器 Settings → Actions → Runners | 🟢 Idle |
| 3 | 确认 workflow 已推到远程(**见下方注意**) | 远程分支有 `.gitea/workflows/ci.yml` |
| 4 | Push 一次(或页面 **Run workflow**) | Actions 页出现运行中任务 |
| 5 | 看 `l0-gates` 日志 | `pre-commit ... Passed`;`mem-numbers` 显示 `Skipped`(无 Keil `.map`,正常) |
| 6 | 看 `l05-host-unit-tests` 日志 | `=== Task Watchdog Tests: 39/39 PASSED (failed=0) ===` |
| 7 | 提交记录旁 | ✅ 绿灯 |
| `l0-gates` | `windows` | checkout(`fetch-depth: 0`,gitlint/cubemx 需完整历史)→ 环境自检(**Python 探测走 `scripts/ci_find_python.ps1`**、cppcheck 按 `DTU_CPPCHECK` → PATH → 标准安装目录,缺则 `::error::` + exit 1)→ `pre_commit install --install-hooks` → **提交信息门禁 `scripts/ci_gitlint_range.ps1`**(见 §7 排障表"钩子条数对不上"行)→ **决定跳过哪些钩子**(探测 `.map`,写入 `GITHUB_OUTPUT`;无 `.map` 时 `::warning::` 声明体积门禁未跑)→ `pre-commit run --all-files --show-diff-on-failure`(`env: SKIP`,输出 `Tee` 到 `ci-artifacts/l0-pre-commit.log`)→ 打印 `::notice::L0: passed/failed/skipped` 汇总行 → 归档日志说明 |
| `l05-host-unit-tests` | `windows` | checkout → **探测 MSVC**(按 VS_ROOTS 找 `Hostx64\x64\cl.exe`)→ 遍历 `test/host/**/run_unit_test.py` 逐个跑(输出 `Tee` 到 `ci-artifacts/l05-<模块>.log`;解析各模块 `N/M PASSED` 汇总行 → 聚合 `::notice::L0.5: K runner(s), A/B assertions passed`;失败 `::error::` 并置退出码);**未探测到 MSVC 时打印醒目 WARNING 并降级跳过**(不硬失败,避免"没装 VS 的 runner 全红") |
> ⚠️ **第 3 步是当前的最大阻塞**:本仓库的 `.gitea/workflows/ci.yml` 还在本地
> 🔴 **两条 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: 3 runner(s), 278/278 assertions passed, fail=0` |
| 9 | job 运行页的 **Summary** 区块 | 出现 "L0 static gates" / "Commit message gate (gitlint)" / "L0.5 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 配得再完美。
@@ -640,32 +1043,39 @@ date +%s # 对比 last_online
---
## 7. 排障对照表
## 7. 常见故障排查
| 现象 | 根因 | 处置 |
|---|---|---|
| runner 一直 **Idle**,push 后任务**排队不跑** | `--labels` 不是 `windows`(最常见) | 改 `config.yaml` 的 `runner.labels: ["windows:host"]` → 重启任务(§4.4) |
| 运行状态是 **`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;当前 18 定义 / 17 报告) | `gitlint` 的 `stages: [commit-msg]` 写在**上游** `.pre-commit-hooks.yaml`(项目 `.pre-commit-config.yaml` 里 grep 不到,别据此断定"没限制"),`pre-commit run --all-files` **跑不到它**,且表现为全绿——静默空转 | 已由 `scripts/ci_gitlint_range.ps1` 单独校验;**以后增删钩子必须核对"定义数 == 日志条数 + gitlint"** |
| push 后 Actions 页**空白/无任务** | ① 服务端开关未开 ② workflow 没 push 到远程 ③ 只改了 `docs/**`(被 `paths-ignore` 滤掉) | 逐条核对 §1 / §5.1 / §5.2 |
| 任务一直 **Waiting / 排队** | 没有 label 匹配 `windows` 的 runner | 检查 `--labels "windows:host"`;Settings→Runners 是否 Idle |
| `l0-gates` 报 `::error::runner 缺少:Python, cppcheck` | runner 没装或不在 PATH | 装完后**重启 runner 服务**(新进程才吃到新 PATH) |
| `checkout` 步骤卡住/超时 | 拉不到 `actions/checkout@v4`(内网) | 见 §5.3 |
| `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) | 同上 |
| `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 → 重启任务(§5.1) |
| `l0-gates` 报 `::error::runner 缺少:Python, cppcheck` | 同上,PATH 未生效 | 装齐 + 重启任务(§5.1) |
| `checkout` 步骤卡住 / 拉不到 action | Gitea 默认从 **github.com** 拉 action,本机不通外网 | 见 §8 |
| `l05` 日志 `::warning::runner 未安装 MSVC` | MSVC 不在标准路径 | 见 §5.2 |
| `l05` 报 `fatal error C1083: 无法打开 "stdio.h"` | 缺 Windows SDK | 装 VS 时勾"使用 C++ 的桌面开发" + SDK |
| 日志里 `pre-commit` 第一次特别慢 | 首次下载各钩子环境 | 正常,`cache.enabled: true` 后第二次快 |
| 任务跑到一半报 `python: 不是内部或外部命令` | SYSTEM 账户 PATH 没有 Python | 确认 Python 在**系统** PATH → 重启任务(§4.1) |
| **改了 `.runner` / 换机器** | 注册信息失效 | 删 `.runner` → 重新 `register` |
| `daemon install` 报 `accepts at most 0 arg(s)` | **v4 已移除该子命令,且无任何服务子命令** | **改用任务计划程序**(§4) |
| 🔴 `Start-Service gitea-runner` 报 **无法启动服务**(错误 1053) | **`daemon` 不是服务型程序**,不向 SCM 回报 `SERVICE_RUNNING` | **删除该服务,改用任务计划程序**(§4.0 / §4.5) |
| `Register-ScheduledTask` 报 **拒绝访问** | Shell 未提权(`IsAdmin=False`) | 右键 PowerShell → **以管理员身份运行**(§4.6) |
| `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-stdout.log`;确认 `.cmd` 存在且为 ASCII 编码 |
| 🔴 `l0-gates` 报 `no Python with a SYSTEM-visible pre-commit` | `pre-commit` 只装在**用户态** site-packages(SYSTEM 不可见);或 pip 因 user-site 已满足而**静默跳过**全局安装 | 提权装到**全局**(§5.1.1),务必用 `-s` 复验 |
| `l0-gates` 报 `TerminatorExpectedAtEndOfString` / 缺少右引号 | `run:` 块含**中文**,被 WinPS 5.1 按 GBK 解码撞坏引号 | `run:` 块改**纯 ASCII**(中文只留 `name:`/注释) |
| `l0-gates` 报 `Cannot find: pwsh in PATH` | `shell: pwsh` = PS7,本机只有 WinPS 5.1 | 改 `shell: powershell` |
| `l0-gates` 报 `runner missing: cppcheck` | cppcheck 仅在用户级 PATH(SYSTEM 不可见) | 设机器级 `DTU_CPPCHECK`(§5.1.3,**workflow 不再回落固定目录**),或把 cppcheck 目录加入系统 PATH 后重启任务 |
| 🔴 `doc-links` 钩子本地过、CI 挂 | 引用了 **gitignore 的运行时文件**(如 `.dsh/skills/serial-assistant/scripts/state/*.json`),本地存在、干净克隆里没有 | 该行加状态词「未创建/不存在」等(§5.1.2) |
| `l05` 日志 `::warning::runner has no MSVC` 但仍 **PASS** | 缺 MSVC 时 L0.5 **降级跳过**(不硬失败) | 装 VS BuildTools(§5.2),否则该层无覆盖 |
| 任务跑满 72 小时被杀 | 计划任务默认 `ExecutionTimeLimit` | 设为 `0`(不限时长),见 §4.3 警示 |
| `config set log.file ...` 报 `unknown config key` | **v4 的 log 段无 `file` 键** | 用 `log.job.dir` 或输出重定向(§3) |
| 🔴 `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
@@ -711,9 +1121,13 @@ foreach ($f in @("deploy_runner_task.ps1","check_env.ps1")) {
>
> 💡 **纯 ASCII 的脚本不受影响**。本目录的 `.ps1` 均含中文注释,现已统一加 BOM。
### 7.2 日志位置与查看方式
**计划任务模式下看日志(两种)**:
| 日志 | 位置 |
|---|---|
| runner **自身** stdout/stderr | `D:\Docker\act_runner\log\runner-stdout.log` |
| runner **自身** stdout/stderr | `D:\Docker\act_runner\log\runner-stdout.log`(由 `run_runner.cmd` 重定向) |
| **每个 job** 的详细日志 | `D:\Docker\act_runner\log\job\` |
**看日志**:v4 的 `log` 段**没有 `file` 键**,两种落盘方式:
@@ -731,48 +1145,27 @@ log:
.\gitea-runner.exe daemon > "D:\Docker\act_runner\log\runner.log" 2>&1
```
**计划任务模式下看日志(两种)**:
| 日志 | 位置 |
|---|---|
| runner **自身** stdout/stderr | `D:\Docker\act_runner\log\runner-stdout.log`(由 `run_runner.cmd` 重定向) |
| **每个 job** 的详细日志 | `D:\Docker\act_runner\log\job\` |
任务本身的历史记录:**任务计划程序 → 任务计划程序库 → `GiteaActionsRunner` → 历史记录**;
或用 `Get-ScheduledTaskInfo -TaskName GiteaActionsRunner` 看 `LastTaskResult`。
---
## 8. ⚠️ 内网 action 拉取问题(你的环境很可能命中)
## 8. 日常维护
workflow 里用了 `actions/checkout@v4`。Gitea **默认从 `https://github.com` 拉取这个 action**
(不是从你自己的实例,也不是从 gitea.com —— 这点很反直觉,见
`CI配置指南.md` §1.2.1)。
**你的环境判断**:Gitea 装了 `OFFLINE_MODE = true`,说明这台机器的外网访问**可能受限**。
如果 runner 拉不到 github.com,checkout 步骤会卡住。
### 处置:改用零依赖写法(内网首选)
把 `.gitea/workflows/ci.yml` 里的 `actions/checkout@v4` 两步替换为纯 git 命令:
```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
```
**优点**:不依赖任何外部 action、不需要配 `DEFAULT_ACTIONS_URL`、不需要镜像 action。
**代价**:失去 checkout 的缓存优化(对本仓库影响很小)。
> 另两种方案(`DEFAULT_ACTIONS_URL = self` + 镜像 action 进实例 / 指向可达镜像站)见
> `docs/环境与部署/CI配置指南.md` §4.3。**注意 `self` 是关键字,不是写你的域名。**
**建议**:**先按原样试一次**。若 checkout 步骤报网络错误,再换成上面的零依赖写法。
- **每周(或每次改完 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` / L1 / L2**:永远留在本地,不要往 CI 加(需 Keil 授权与真机);
- **runner 长时间不可用**时:CI 只是"多一道网",**提交前的本地 `ci_local.py` 仍是 L0 的唯一保障**,
不能因"已经配了 CI"放宽(见 [docs/待处理内容.md](../待处理内容.md) 的部署前置警示块)。
---
@@ -797,7 +1190,7 @@ $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_* 机器级变量(§5.1.3)===" -ForegroundColor Cyan
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 }
@@ -864,14 +1257,14 @@ powershell -ExecutionPolicy Bypass -File "D:\Docker\act_runner\deploy_runner_tas
# -Principal (New-ScheduledTaskPrincipal -UserId SYSTEM -LogonType ServiceAccount -RunLevel Highest)
# Start-ScheduledTask -TaskName GiteaActionsRunner
# 5) ⚠️⚠️ 把 pre-commit 装到【全局】site-packages(否则 L0 必挂,见 §5.1.1)
# 5) ⚠️⚠️ 把 pre-commit 装到【全局】site-packages(否则 L0 必挂,见 §4.1.1)
# —— 这是本环境最容易漏、也最难自己看出来的一步(用户态对 SYSTEM 不可见)
powershell -ExecutionPolicy Bypass -File "D:\Docker\act_runner\install_precommit_global.ps1"
powershell -ExecutionPolicy Bypass -File "D:\Docker\act_runner\install_precommit_global2.ps1"
# 若有包报"安装成功"但 import 失败(pip 静默跳过):
powershell -ExecutionPolicy Bypass -File "D:\Docker\act_runner\fix_cfgv_global.ps1"
# 5b) 2026-10-02 起推荐:改设机器级 DTU_* 变量(§5.1.3,显式覆盖、报错直指变量名);
# 5b) 2026-10-02 起推荐:改设机器级 DTU_* 变量(§4.1.3,显式覆盖、报错直指变量名);
# 全局 site-packages 安装保留为未设变量时的兜底。设完同样要重启 runner 任务。
# (管理员 PowerShell;值按本机实际路径改)
# [Environment]::SetEnvironmentVariable('DTU_PRECOMMIT_PYTHON', 'C:\ProgramData\anaconda3\python.exe', 'Machine')
@@ -889,6 +1282,10 @@ powershell -ExecutionPolicy Bypass -File "D:\Docker\act_runner\fix_cfgv_global.p
# "https://gitea.bitnasdaq.vip/api/v1/repos/wandering1/STM32F4-Base/actions/workflows/ci.yml/dispatches"
```
> ⚠️ 上面第 4 步是 2026-10-02 的方案(计划任务跑**单次** daemon)。2026-10-11 实测证明
> 单次 daemon 首次 Declare 失败即退出、计划任务的失败重启救不到它——
> **正式部署一律用 §2.4 的 `scripts/ci_runner_bootstrap.ps1`**(看门狗循环重拉 + 开机计划任务)。
> **CI 验收标准**(2026-10-02 已达成,run#8):`action_run_job` 里两个 job 均 `status=1`——
> `l0-gates`(14 个 pre-commit 钩子全 Passed,`内存/Flash` 在无 `.map` 时 Skipped)
> + `l05-host-unit-tests`(MSVC 探测 + 39/39 单测)。
@@ -901,7 +1298,7 @@ powershell -ExecutionPolicy Bypass -File "D:\Docker\act_runner\fix_cfgv_global.p
| `deploy_runner_task.ps1` | **一键部署**(自检 + 清旧服务 + 建计划任务 + 启动 + 验证) | **是** |
> 早前基于 Windows 服务的 `deploy_runner.ps1` / `install_service.ps1` 已删除
> —— 那条路走不通(§4.0)。
> —— 那条路走不通(§3.0)。
---
@@ -909,9 +1306,15 @@ powershell -ExecutionPolicy Bypass -File "D:\Docker\act_runner\fix_cfgv_global.p
| 日期 | 变更 |
|---|---|
| 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 拉取 / 端到端验证清单 / 故障排查 / 日常维护) |
+3
View File
@@ -23,6 +23,7 @@ docs/
├── 内存管理/ RAM 预算、分配策略与优化调研
├── 问题记录/ 模块陷阱台账、专项审计与项目评审报告
├── 环境与部署/ 环境搭建与部署核对清单
├── CI/ 远程 CI(Gitea Actions)唯一入口 —— **只涉及本机一台 runner,日常开发不必读**
├── UI设计/ UI 架构、页面设计、字体与模拟器
├── 硬件平台/ 网表分析报告与语义图
├── 芯片手册/ 外部芯片数据手册转录(只读资料)
@@ -214,6 +215,7 @@ docs/
| 文档 | 内容 |
|---|---|
| [`项目部署环境要求.md`](环境与部署/项目部署环境要求.md) | 开发机软件环境、目标板资源预算、现场部署与验收步骤 |
| [`CI/README.md`](CI/README.md) | **远程 CI(Gitea Actions)唯一入口**:服务端开关 / runner 部署与保活 / workflow / 排障。**只涉及本机一台 runner `win-stm32-ci`,其他机器与项目都不涉及**;日常改代码用本地门禁 `scripts/ci_local.py`,不必读这里 |
### 9.3 台账与外部资料
@@ -262,6 +264,7 @@ docs/
| 日期 | 变更 | 说明 |
|---|---|---|
| 2026-10-12 | 远程 CI 文档独立成目录 | 新建 `docs/CI/`:原散在 4 处的远程 CI 内容(`环境与部署/CI配置指南.md` 477 行 + `环境与部署/Gitea_Runner部署指南.md` 917 行 + `测试开发标准流程.md` §9 + `AGENTS.md` 提交门禁节 4 条,共约 2000 行)**合并为 `CI/远程CI完整指南.md` 一篇**,`CI/README.md` 作唯一入口;`AGENTS.md` 压成一行指针、测试流程 §9 只留覆盖边界与本地等价。动因:远程 CI **只跑在本机一台 runner**,与日常开发规范混在一起每次会话都被读,造成困惑 |
| 2026-10-10 | 新增子系统设计 | 新建 `docs/子系统设计/ADC采样节拍设计说明.md`(ADC 采集链路 1 kHz 硬件定时器触发的 CubeMX 配置规格与代码改造清单,B1 遗留项规划);本索引 §5.1 补登记 |
| 2026-10-10 | 新增算法专题 | 新建 `docs/算法/继电保护功能复现规划.md`(保护链路复现总规划);本索引 §1 目录树补 `算法/`、§5 拆为 5.1 功能子系统 / 5.2 算法专题并登记 `算法/` 两篇文档 |
| 2026-10-05 | 新增产品需求文档 | 新建 `docs/产品需求/产品需求设计文档.md`(面向产品的需求基线);本索引新增 §3 产品需求、§2 推荐阅读顺序置顶、§1 目录树补 `产品需求/`;后续章节号顺延(架构设计 §4 … 变更记录 §12) |
+1 -1
View File
@@ -228,7 +228,7 @@
#### 3.3.4 A-5 残余:CI 体积门禁盲区 ⬜
**现状**:`mem-numbers` 钩子**在 CI 上被跳过**(CI 无 `.map`)→ **CI 不守体积门禁**。
S14 已在 workflow 显式提示"体积门禁未执行"(`::notice::`)并在 `CI配置指南.md` 声明,
S14 已在 workflow 显式提示"体积门禁未执行"(`::notice::`)并在 `docs/CI/远程CI完整指南.md` §0.0 声明,
但**盲区本身未消除**——看到 CI 全绿仍可能误以为体积也守住了。
**可选处置**:
+26 -32
View File
@@ -14,8 +14,7 @@
| `docs/UI设计/UI_自动化测试流程.md` | UI 模拟器回归(ui-verify)细节 |
| `docs/代码规范/pre-commit使用说明.md` | pre-commit 门禁配置与范围 |
| `docs/环境与部署/项目部署环境要求.md` | 构建 / 测试工具链环境 |
| `docs/环境与部署/CI配置指南.md` | **CI(Gitea Actions)服务端 + workflow 配置全流程与排障** |
| `docs/环境与部署/Gitea_Runner部署指南.md` | **Runner 端部署全流程**(下载/注册/常驻/排障) |
| `docs/CI/README.md` | **远程 CI(Gitea Actions)唯一入口**:服务端 / runner 部署与保活 / workflow / 排障(仅本机一台 runner,日常开发不必读) |
| `docs/测试说明/<模块>_Test_Guide.md` | 各模块测试规范(CH395F / GD5F / SD2506 / FTP) |
---
@@ -529,34 +528,31 @@ flowchart LR
> 新增任务/栈/缓冲/代码导致的体积增长**不会在 CI 被拦**。故该跳过以 `::warning::` 明示
> (不是弱化的 notice)并写入 step-summary。**兜底必须靠本地**:改代码后 `@build`(0/0)
> → `python scripts/check_mem_numbers.py --scan`。详见
> [`../环境与部署/CI配置指南.md`](../环境与部署/CI配置指南.md) §0.0。
> [`../CI/远程CI完整指南.md`](../CI/远程CI完整指南.md) §0.0。
> 🔴 **`gitlint` 属 `commit-msg` stage,`--all-files` 永远跑不到它(2026-10-11 审计)**:
> `.pre-commit-config.yaml` 定义 **18** 个钩子,而 CI 日志只报 **17** 个(`passed=16 skipped=1`)——
> 缺的正是 gitlint。⚠️ **`stages: [commit-msg]` 不在项目配置里**,它写在 **gitlint 上游的
> `.pre-commit-hooks.yaml`**(本机可查证 `~\.cache\pre-commit\repo*\.pre-commit-hooks.yaml`),
> 所以在仓库 `.pre-commit-config.yaml` 里 grep `stages` 会一无所获、进而误判"没有限制";
> 而 `pre-commit run --all-files` 只跑 `pre-commit` stage。即"单行中文标题 ≤100 字符"这条门禁
> **本地有、CI 无**,且表现为**全绿**而非失败,所以长期无人发现。现由 **`scripts/ci_gitlint_range.ps1`** 逐条校验本次 push/PR
> 引入的提交(PR 用 `base..head`;push 用 `event.before..sha`,`before` 全零/不可达时回退最近
> N 条;跳过 merge 提交)。本地等价:`ci_local.py` 的 **L0b** 段。
> 缺的正是 gitlint。即"单行中文标题 ≤100 字符"这条门禁**本地有、CI 无**,且表现为**全绿**
> 而非失败,所以长期无人发现。现由 CI 侧 **`scripts/ci_gitlint_range.ps1`**(PR 用 `base..head`、
> push 用 `event.before..sha`、跳过 merge 提交)与本地 `ci_local.py` 的 **L0b** 段分别逐条校验。
> ⚠️ 机制根因(`stages: [commit-msg]` 写在 **gitlint 上游** 的 `.pre-commit-hooks.yaml` 里,
> 项目配置中 grep 不到)与完整排障口径见
> [`../CI/远程CI完整指南.md`](../CI/远程CI完整指南.md) §7"钩子条数对不上"行。
### 9.2 两个 job
| job | `runs-on` | 步骤要点 |
| job | 覆盖层 | 一句话职责 |
|---|---|---|
| `l0-gates` | `windows` | checkout(`fetch-depth: 0`,gitlint/cubemx 需完整历史)→ 环境自检(**Python 探测走 `scripts/ci_find_python.ps1`**、cppcheck 按 `DTU_CPPCHECK` → PATH → 标准安装目录,缺则 `::error::` + exit 1)→ `pre_commit install --install-hooks` → **提交信息门禁 `scripts/ci_gitlint_range.ps1`**(见上方 🔴)→ **决定跳过哪些钩子**(探测 `.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` 汇总行 → 归档日志说明 |
| `l0-gates` | **L0** | `pre-commit run --all-files`(cppcheck / 编码规范 / 文档一致性 / 主机侧单测钩子)+ **提交信息门禁**(gitlint,见上方 🔴);无 `.map` 时自适应跳过 `mem-numbers` |
| `l05-host-unit-tests` | **L0.5** | 探测 MSVC 后遍历 `test/host/**/run_unit_test.py` 逐个跑并聚合断言数;**未装 MSVC 时降级为 WARNING 跳过**(不硬失败,但等于该层裸奔) |
| `l05-host-unit-tests` | `windows` | checkout → **探测 MSVC**(按 VS_ROOTS 找 `Hostx64\x64\cl.exe`)→ 遍历 `test/host/**/run_unit_test.py` 逐个跑(输出 `Tee` 到 `ci-artifacts/l05-<模块>.log`;解析各模块 `N/M PASSED` 汇总行 → 聚合 `::notice::L0.5: K runner(s), A/B assertions passed`;失败 `::error::` 并置退出码);**未探测到 MSVC 时打印醒目 WARNING 并降级跳过**(不硬失败,避免"没装 VS 的 runner 全红") |
触发:`push`(`paths-ignore: docs/** / **/*.md / .workbuddy-ai/**` → **纯文档改动不会触发 CI**)
与 `pull_request`,另留 `workflow_dispatch` 手动触发;同分支 `cancel-in-progress: true`。
两个 job 都写 step-summary + `::notice::` 汇总行,日志落 `ci-artifacts/*.log`。
**结果可见性(S3,2026-10-02)**:两个 job 均写 `$GITHUB_STEP_SUMMARY`(job 运行页的
**Summary** 区块直接可见)+ `::notice::` 汇总行;日志落 `ci-artifacts/*.log`(runner 工作区,
已 gitignore)。**刻意不用 `actions/upload-artifact`**(内网可能拉不到外层 action,见
`CI配置指南.md` §4.3/§4.4)——零依赖方案下,"不查 MySQL / 不翻磁盘即可排障"的目标由
step-summary 达成。
触发:`push`(`paths-ignore: docs/** / **/*.md / .workbuddy-ai/**`)与 `pull_request`,另留 `workflow_dispatch` 手动触发。
`concurrency: group: ci-${{ github.ref }}` + `cancel-in-progress: true`(同分支旧运行自动取消,省 runner)。
> 📖 **步骤级细节不在本文**(探测链顺序、`GITHUB_OUTPUT` 跳过决策、`Tee` 落盘、产物存档、
> 内网拉不到 action 的处置):见 [`../CI/远程CI完整指南.md`](../CI/远程CI完整指南.md)
> §5.5(两个 job 的步骤要点)/ §5.2(触发)/ §5.3(内网 action)/ §5.4(产物与汇总)。
### 9.3 本地等价:`scripts/ci_local.py`
@@ -582,18 +578,15 @@ CI 只在 push / PR 时跑,且需 runner 就绪。**提交前自查、或 runn
### 9.4 部署前置(需人工确认)
| 项 | 要求 |
|---|---|
| Gitea Actions | 实例开启 `[actions] ENABLED = true`(site admin 配置) |
| 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 就绪 + 崩溃自拉起"的看门狗 + 计划任务 |
| 机器级工具变量 | runner 以 **SYSTEM** 运行,看不见交互用户的 PATH/用户级 site。用 `ci_runner_bootstrap.ps1 -SetEnv` 写入 **MACHINE** 级 `DTU_PRECOMMIT_PYTHON` / `DTU_CPPCHECK` / `DTU_MSVC_CL` |
| 缺 MSVC 时 | L0.5 降级为 WARNING 跳过(不硬失败),但**等于该层裸奔**,须尽快补装 |
CI 能不能跑起来,取决于 runner 机器上四件事是否就位:**服务端开关 / runner 注册与 labels /
看门狗常驻 / 机器级 `DTU_*` 工具变量**。清单与逐项部署步骤都在
[`../CI/远程CI完整指南.md`](../CI/远程CI完整指南.md):§0.2(前置清单)、§2(注册,
`--instance` 用回环、labels 必须精确匹配 `runs-on`)、§2.4(**看门狗**——缺了它 Gitea 一重启
CI 就静默停摆,2026-10-11 实测 run 16~21 共 12 个 job 零日志)、§4.1.3(runner 以 SYSTEM 跑,
必须写 MACHINE 级变量)。
> 📖 **配置步骤 / 排障**(服务端开关、runner 注册命令、PATH 与 MSVC 路径要求、
> 内网拉不到 action 的处置、故障排查表)见
> [`../环境与部署/CI配置指南.md`](../环境与部署/CI配置指南.md)
> 与 [`../环境与部署/Gitea_Runner部署指南.md`](../环境与部署/Gitea_Runner部署指南.md)。
> ⚠️ 对测试流程只需记住一条:**runner 不在线时,`ci_local.py` 就是 L0 / L0.5 的唯一保障**。
> "CI 显示绿"与"本地门禁通过"是两回事,见 §9.3 与 §7.1。
---
@@ -632,6 +625,7 @@ CI 只在 push / PR 时跑,且需 runner 就绪。**提交前自查、或 runn
| 日期 | 变更 |
|---|---|
| 2026-10-12 | **§9 瘦身:机器侧内容迁出**(远程 CI 文档独立成 `docs/CI/`)。§9.2 两个 job 的**步骤级细节**并入 `docs/CI/远程CI完整指南.md` §5.5、§9.4 部署前置表并入其 §0.2;本文 §9 只保留**测试分层视角**的内容(CI 覆盖哪些层、两个 job 各守哪层、触发与 paths-ignore 语义、本地等价 `ci_local.py`、"CI 不替代本地门禁")。§9.1 两个 🔴 块压缩为结论 + 指针(机制根因与排障口径归 `docs/CI/`)。相关文档表两行 CI 条目合并为一行指向 `docs/CI/README.md` |
| 2026-10-10 | §4.2.1 台账:`Drivers/BSP/NETCFG/net_cfg_legacy.c` 由 `⬜ 候选` 改为 **`⛔ 已删除`**——旧档 NCFG 迁移整体移除(`net_cfg_legacy.{c,h}` 删除、`cfg_store_legacy_path()` 撤销;旧档 `/cfg/netcfg.bin` 不读、不删、不迁移,配置回落工厂默认),该测试候选随之撤销 |
| 2026-10-02 | **S6**:`test/run_all_suites.py` 新增 **`--check-build`**(只编译不烧录,纯离机,不需要 MCU/串口/ST-LINK);新增 **§3.6.1** 说明其解决的问题(测试构建无独立门禁,2026-08~09 CH395F 套件实例)、判定语义与用法。**实测 4 套件全部 `BUILD_OK: 0 errors, 0 warnings`**(即 T-4 提到的"CH395F 不可编译"已修复) |
| 2026-10-02 | **S1-b + S2**:新增 **§4.2.1「L0.5 适格模块台账」**(判据 + 模块状态表 + "两条实证":sys_clock / cfg_file 首测即各抓一处生产缺陷);§4.1 登记 `test/host/cfg_file/` 新单测。同步新增 [`配置文件单元测试说明.md`](配置文件单元测试说明.md)(含抓出的 `s_encode` 返回值误判 P0 缺陷记录) |
-477
View File
@@ -1,477 +0,0 @@
# CI 配置指南(Gitea Actions)
> **适用**:为 STM32F4-Base 部署 / 排障 CI(`.gitea/workflows/ci.yml`,L0 + L0.5)。
> **前置阅读**:`docs/测试说明/测试开发标准流程.md` §9(CI 覆盖边界与判定细节)、
> `docs/环境与部署/项目部署环境要求.md` §2.11(runner 环境要求)。
> 本文只讲**怎么配**,不重复"为什么这么设计"。
---
## ⚠️ 0.0 CI 不覆盖的体积门禁(必读,S14)
> **CI 不守 Flash/RAM 体积门禁。** `mem-numbers` 钩子的事实源是 Keil 构建产物
> `MDK-ARM/**/*.map`(已 gitignore),而 CI 刻意不跑 `@build` → **无 `.map` → 该钩子被跳过**。
>
> 后果:**新增任务 / 栈 / 缓冲 / 代码导致的体积增长(或文档内存数字失真)不会在 CI 被拦下。**
>
> 兜底(每次改代码后必做):
> ```powershell
> & '.venv\Scripts\python.exe' .dsh\skills\keil-build\scripts\keil_build.py # 0/0 且生成 .map
> & '.venv\Scripts\python.exe' scripts/check_mem_numbers.py --scan
> ```
> 若 CI 允许在带 Keil 的 runner 上跑,则该钩子会在有 `.map` 时**照常执行**(workflow 自适应)。
>
> CI 运行时该跳过以 **`::warning::`** 明示(不是弱化的 notice),并写入 job 的
> **step-summary** 醒目行,避免被忽略。
---
## 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);① ② 需人工确认。**
---
## 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 | 建议**改用零依赖写法**(见 §4.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 会拉不下来。
> 处置见 §4.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`。
### 2.2 下载并注册 act_runner
```powershell
# 1) 下载(示例版本,请取 releases 页最新 Windows amd64)
# https://gitea.com/gitea/act_runner/releases
Invoke-WebRequest -Uri "https://gitea.com/gitea/act_runner/releases/download/v0.2.11/act_runner-0.2.11-windows-amd64.exe" `
-OutFile "C:\act_runner\act_runner.exe"
# 2) 注册:URL 用 Gitea 根地址,token 用 2.1 复制的
cd C:\act_runner
.\act_runner.exe register `
--no-interactive `
--instance "https://gitea.bitnasdaq.vip" `
--token "粘贴 Registration Token" `
--name "win-stm32-ci" `
--labels "windows:host"
```
**关键点**:`--labels "windows:host"` 中的 `windows` 必须**逐字**与 workflow 的
`runs-on: windows` 对应(Gitea runner 的 label 名是自定义的,默认是 `ubuntu-latest`,
**不注册 `windows` 这个 label 就永远匹配不到 job**)。
> `:host` 表示直接跑在宿主 Windows 上(不是容器)。本场景**必须用 `host`**——
> 容器里拿不到宿主已装的 Keil/MSVC/cppcheck,且 Windows 容器支持很差。
### 2.3 配置 `config.yaml`(可选但建议)
`C:\act_runner\config.yaml` 里确认/补充:
```yaml
runner:
labels:
- "windows:host" # 与 register 一致
capacity: 1 # 并行 job 数;单机建议 1,避免 cppcheck 抢 CPU
timeout: 30m
cache:
enabled: true
```
### 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. 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 | — |
### 3.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 环境自检失败 |
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 不重启就吃不到新值。
### 3.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`。
> ⚠️ **装在完全非标准位置(如 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. 仓库端:workflow 文件
### 4.1 文件位置与提交
workflow 必须位于**仓库根**的 `.gitea/workflows/` 下(不是 `.github/`):
```
<仓库根>/
└─ .gitea/
└─ workflows/
└─ ci.yml ← 本仓库已有,见该文件
```
**它必须被 push 到远程才能生效**——目前本仓库的 `e904865` 还在本地
(`origin/v2-on-ui` 尚未包含),所以**现在 push 之前 CI 不会跑**:
```powershell
git push origin v2-on-ui # ⚠️ 需你明确要求才推送(本仓库约定:默认不推送)
```
### 4.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**。
### 4.3 内网无公网时的 action 拉取(重要)
workflow 用了 `actions/checkout@v4`。**默认从 `github.com` 拉取**(见 §1.2.1);实例不通
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`?」
### 4.4 产物存档与结果汇总(S3,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. 端到端验证清单
按顺序确认,任一步不过就停在那里排障:
| # | 检查 | 期望 |
|---|---|---|
| 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 成立,见 §3.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`(**这一行以前根本不存在**,见 §6) |
| 8 | `l05-host-unit-tests` 日志 | `::notice::L0.5: 3 runner(s), 278/278 assertions passed, fail=0` |
| 9 | job 运行页的 **Summary** 区块 | 出现 "L0 static gates" / "Commit message gate (gitlint)" / "L0.5 host-side unit tests" 小标题与汇总行 |
| 10 | 提交记录旁出现 ✅ | CI 全绿 |
**本地先自查**(与 CI 同一套判定,避免推上去才发现问题):
```powershell
& '.venv\Scripts\python.exe' scripts/ci_local.py
```
---
## 6. 常见故障排查
| 现象 | 根因 | 处置 |
|---|---|---|
| 运行状态是 **`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` 未入库(§3.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;当前 18 定义 / 17 报告) | `gitlint` 的 `stages: [commit-msg]` 写在**上游** `.pre-commit-hooks.yaml`(项目 `.pre-commit-config.yaml` 里 grep 不到,别据此断定"没限制"),`pre-commit run --all-files` **跑不到它**,且表现为全绿——静默空转 | 已由 `scripts/ci_gitlint_range.ps1` 单独校验;**以后增删钩子必须核对"定义数 == 日志条数 + gitlint"** |
| push 后 Actions 页**空白/无任务** | ① 服务端开关未开 ② workflow 没 push 到远程 ③ 只改了 `docs/**`(被 `paths-ignore` 滤掉) | 逐条核对 §1 / §4.1 / §4.2 |
| 任务一直 **Waiting / 排队** | 没有 label 匹配 `windows` 的 runner | 检查 `--labels "windows:host"`;Settings→Runners 是否 Idle |
| `l0-gates` 报 `::error::runner 缺少:Python, cppcheck` | runner 没装或不在 PATH | 装完后**重启 runner 服务**(新进程才吃到新 PATH) |
| `checkout` 步骤卡住/超时 | 拉不到 `actions/checkout@v4`(内网) | 见 §4.3 |
| `l05` 日志出现 `::warning::runner 未安装 MSVC` | VS 装在非默认路径或未装 | 见 §3.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) | 同上 |
| `pre-commit` 首次运行久 | 首次要下载各钩子的环境 | 正常,runner 缓存后会快 |
---
## 7. 日常维护
- **每周(或每次改完 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` 单独守(见 §6);
- **`@build` / L1 / L2**:永远留在本地,不要往 CI 加(需 Keil 授权与真机);
- **runner 长时间不可用**时:CI 只是"多一道网",**提交前的本地 `ci_local.py` 仍是 L0 的唯一保障**,
不能因"已经配了 CI"放宽(见 `docs/待处理内容.md` 的部署前置警示块)。
---
## 变更记录
| 日期 | 变更 |
|---|---|
| 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-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 拉取 / 端到端验证清单 / 故障排查 / 日常维护) |
@@ -81,7 +81,7 @@ $env:DTU_STLINK_CLI = 'D:\Tools\ST-LINK Utility\ST-LINK_CLI.exe'
- 新电脑只配置本机 `DTU_*` 环境变量(§3),clone 仓库后可直接运行各开发命令,**不修改任何脚本/工作流中的本机路径**。
- 显式变量指向无效路径时,命令以非零退出码失败且错误信息指明变量名,不静默使用其他工具。
- 仓库内不出现提交的本机绝对工具路径(文档中的示例路径除外)。
- 跨机验证操作清单见 `项目部署环境要求.md` §2.12;runner 侧机器级变量设置见 `Gitea_Runner部署指南.md` §5.1.3。
- 跨机验证操作清单见 `项目部署环境要求.md` §2.12;runner 侧机器级变量设置见 `docs/CI/远程CI完整指南.md` §4.1.3。
---
@@ -84,7 +84,7 @@ Graphviz / Cppcheck 的安装位置差异一律用 `DTU_*` 环境变量表达(
> 统一解析语义:变量**显式设置但无效 → 立即报错**(指出变量名与收到的路径),不静默回退到别的安装;
> 未设置 → 按工具走 PATH / 标准布局自动发现(可选工具允许降级,如 Graphviz 缺失只跳渲染)。
> CI runner 以 **SYSTEM 服务**运行,用户级变量对其不可见——runner 用**机器级**变量注入(见
> `Gitea_Runner部署指南.md` 环境变量节)。
> `docs/CI/远程CI完整指南.md` §4.1.3)。
### 2.2 MCU 构建工具链(必需)
@@ -415,7 +415,7 @@ workflow 见 `.gitea/workflows/ci.yml`;覆盖边界与判定细节见 [`../测
| Gitea 版本 | ≥ 1.21(实测远程 **1.25.4**) | workflow 置于 `.gitea/workflows/`,兼容 GitHub Actions 语法 |
| Actions 开关 | 实例 `[actions] ENABLED = true` | site admin 配置;未开启则 push 不触发任何 job |
| runner OS | **Windows**(`runs-on: windows`) | labels 须含 `windows`;与开发机同平台,避免脚本语法分叉 |
| runner 软件 | Python 3.x、cppcheck、pre-commit、**MSVC**(VS C++ 工具链 + Windows SDK) | 前三个缺 → `l0-gates` **硬失败**(`::error::`);MSVC 缺 → `l05-host-unit-tests` **降级跳过并告警**(不硬失败,但等于 L0.5 裸奔)。runner 以 SYSTEM 服务运行,**用户级 PATH/变量不可见**:自动发现失败时用**机器级** `DTU_PRECOMMIT_PYTHON` / `DTU_CPPCHECK` / `DTU_MSVC_CL` 注入(见 `Gitea_Runner部署指南.md` 环境变量节,§2.1.1) |
| runner 软件 | Python 3.x、cppcheck、pre-commit、**MSVC**(VS C++ 工具链 + Windows SDK) | 前三个缺 → `l0-gates` **硬失败**(`::error::`);MSVC 缺 → `l05-host-unit-tests` **降级跳过并告警**(不硬失败,但等于 L0.5 裸奔)。runner 以 SYSTEM 服务运行,**用户级 PATH/变量不可见**:自动发现失败时用**机器级** `DTU_PRECOMMIT_PYTHON` / `DTU_CPPCHECK` / `DTU_MSVC_CL` 注入(见 `docs/CI/远程CI完整指南.md` §4.1.3) |
| Keil | **不需要** | 刻意不跑 `@build`(需授权);`.map` 缺失时 `mem-numbers` 钩子自动跳过 |
| `mem-numbers` | 有 `.map` 就跑,无则 `SKIP` | Keil 构建产物 `.map` 已 gitignore;自建带 Keil 的 runner 会照常跑该钩子 |
@@ -635,7 +635,7 @@ $env:DTU_KEIL_UV4 = 'D:\NotExist\UV4.exe' # 仅当前会话,不动用户级
| LVGL 模拟器部署 | `docs/UI设计/LVGL模拟器部署与使用说明.md`、`simulator/README.md` |
| 串口调试 / 自动化测试 | `docs/测试说明/串口助手使用指南.md`、`docs/测试说明/串口助手设计说明.md`、`docs/测试说明/自动化测试流程.md` |
| **测试分层 / L0.5 主机侧单测** | `docs/测试说明/测试开发标准流程.md`(分层与门禁)· `docs/测试说明/看门狗单元测试说明.md`(L0.5 范式:MSVC 离线编译 + 平台桩) |
| **CI(Gitea Actions)** | `docs/环境与部署/Gitea_Runner部署指南.md`(**Runner 端部署全流程:下载 / 注册 / 常驻 / 排障**,针对本机原生 Gitea 定制)· `docs/环境与部署/CI配置指南.md`(服务端 + workflow 全流程)· `docs/测试说明/测试开发标准流程.md` §9(覆盖边界) |
| **CI(Gitea Actions)** | `docs/CI/README.md`(**远程 CI 唯一入口**:服务端 / Runner 部署与保活 / workflow / 排障,**仅本机一台 runner,日常开发不必读**)· `docs/测试说明/测试开发标准流程.md` §9(覆盖边界) |
| pre-commit / cppcheck | `docs/代码规范/pre-commit使用说明.md`、`docs/代码规范/cppcheck使用说明.md` |
| 引用关系图(文件引用) | `docs/架构设计/引用关系图说明.md`(生成工具 `tools/refgraph`;渲染依赖 Graphviz,§2.8) |
| 各模块测试指南 | `docs/测试说明/CH395F_Test_Guide.md`、`GD5F2GQ5UE_Test_Guide.md`、`SD2506_Test_Guide.md`、`FTP_Test_Guide.md` |
@@ -647,6 +647,7 @@ $env:DTU_KEIL_UV4 = 'D:\NotExist\UV4.exe' # 仅当前会话,不动用户级
| 日期 | 变更 |
|---|---|
| 2026-10-12 | **远程 CI 文档迁出本目录**:`CI配置指南.md` 与 `Gitea_Runner部署指南.md` 合并为 **`docs/CI/远程CI完整指南.md`**(入口 `docs/CI/README.md`),本文三处引用改指新路径与新章节号(§4.1.3 机器级变量、§7 相关文档登记)。原因:远程 CI 只涉及本机一台 runner,不应与"任何机器都要过"的部署核对清单混在一起 |
| 2026-10-10 | **删除旧档 NCFG 迁移**(升级/迁移口径变更):旧固件遗留的 `/cfg/netcfg.bin` 不再"首启自动迁移"——旧档若存在则被忽略(不读、不删、不迁移),配置直接回落工厂默认(`/config.ini` `[network]` 段模板)。`net_cfg_legacy.c/h` 与 `cfg_store_legacy_path()` 已移除;携带旧档的设备升级后需经 UI 或 FTP 重新配置网口。§五 现场部署 IP 行同步 |
| 2026-10-02 | **模拟器构建修复**(配置引擎重构遗留):`simulator/CMakeLists.txt` 的配置文件引擎引用同步到迁移后位置——包含目录 `App/`→`App/cfg/`(`cfg_file.h`/`cfg_file_store.h`)、源文件 `App/cfg_file.c`→`App/cfg/cfg_file.c`、包含目录 `test/`→`test/board/`(`test_config.h`/`net_latency_test.h`);全量 include 离线核查无其他断链,模拟器全量构建恢复(`--cfgtest` 四场景 PASS,rc=0)。§2.12 验证链 ⑤ 行同步更新 |
| 2026-10-02 | 更正 Device Pack 必装范围:`Keil::CMSIS` pack 不再列为必装项——本工程不依赖它(核心头文件由仓库 `Drivers/CMSIS/` 提供,`.uvprojx` 只引用 DFP pack;2026-10-02 本机实测未装 CMSIS pack 构建 0/0 通过),且其 keil.com 直链(5.7.0~6.1.0)均已下架 404;必装的只有 `Keil::STM32F4xx_DFP`(3.1.1,与 `.uvprojx` PackID 一致)。§2.2 / 第 2 步 / 搭建清单 / §2.10 同步更正 |
+7 -1
View File
@@ -27,7 +27,13 @@
| 脚本 / 文件 | 作用 |
|---|---|
| `ci_local.py` | **本地跑与 CI 等价的门禁**(L0 pre-commit + L0.5 主机侧单测):自动选用装了 pre-commit 的解释器;无 `.map` 时自适应跳过 `mem-numbers`;退出码 0/1/2 与 CI 对齐。CI 定义见 `../.gitea/workflows/ci.yml`,说明见 `docs/测试说明/测试开发标准流程.md` §9 |
| `ci_local.py` | **本地跑与 CI 等价的门禁**(L0 pre-commit + L0b 提交信息 + L0.5 主机侧单测):自动选用装了 pre-commit 的解释器;无 `.map` 时自适应跳过 `mem-numbers`;退出码 0/1/2 与 CI 对齐。CI 定义见 `../.gitea/workflows/ci.yml`,覆盖边界见 `docs/测试说明/测试开发标准流程.md` §9 |
| `ci_gitlint_range.ps1` | CI 侧**提交信息门禁**:逐条校验本次 push/PR 引入的提交(`--all-files` 跑不到 `commit-msg` stage,故必须单独守) |
| `ci_find_python.ps1` | CI 侧 Python 探测(候选顺序须与 `ci_local.py` 同序同步,否则 CI 与本地判定会分叉) |
| `ci_runner_bootstrap.ps1` | **本机 runner 运维脚本**(体检 `-Status` / 干跑 `-DryRun` / 部署 `-SetEnv -StartNow`):装"等 Gitea 就绪 + 崩溃自拉起"的看门狗 + 开机计划任务 + 机器级 `DTU_*` |
> ⚠️ 后三个脚本是**本机 runner 专属运维**(远程 CI 只跑在本机一台机器上),用法、原理与
> 踩坑全部集中在 `docs/CI/README.md`,本表只登记不展开。
## 使用方式