merge: master 的远程 CI 修复带入 v2-on-ui(门禁数字改为分支中立)
把 master 上本轮 10 个 CI 相关提交合入 v2-on-ui,使该分支同样具备可用的 runner 保活与本地门禁自查能力。合并只涉及文档与脚本,零 C 代码改动。 冲突两处,均按"保留 v2-on-ui 新版结构 + 搬入语义"解决: - AGENTS.md:丢弃 master 侧已被瘦身版取代的旧门禁 bullet,保留其分层边界 行;把"CI 资料集中在 docs/CI/README.md、只跑本机一台 runner"这条指针 并入新版 CI bullet 与文末索引表。 - docs/README.md:两侧都是变更记录历史行,按日期先后并列保留。 数字口径改为分支中立:本分支 26 个钩子 / 10 个主机单测套件,master 是 18 / 3,写死任一数字都会让同一篇文档在另一分支上失真。指南 §6 验收第 8 行、§7 排障表、§10 验收标准改为讲规则(报告数 = 定义数 − 1;runner 数 = test/host 下 run_unit_test.py 个数)+ 分分支实测快照。 验证:@build 0 错误 0 警告;ci_local.py 全绿(L0 25 钩子 + L0.5 10 runner / 647 断言,fail=0)。首轮 mem-numbers 失败系 master 构建遗留的 .map 所 致,本分支重新构建后即一致。
This commit is contained in:
+128
-106
@@ -21,7 +21,7 @@
|
||||
#
|
||||
# 产物存档(S3,2026-10-02):两个 job 把关键输出 `Tee-Object` 到 `ci-artifacts/*.log`,
|
||||
# 并写 $GITHUB_STEP_SUMMARY(Gitea UI 直接可见)+ `::notice::` 汇总行。
|
||||
# **刻意不用 `actions/upload-artifact`**——内网可能拉不到外层 action(见 CI配置指南 §4.3);
|
||||
# **刻意不用 `actions/upload-artifact`**——内网可能拉不到外层 action(见 docs/CI/远程CI完整指南.md §5.3);
|
||||
# 零依赖方案下日志留在 runner 工作区,UI 通过 step-summary 即可排障。
|
||||
# ⚠️ `ci-artifacts/` 已 gitignore(运行时产物)。
|
||||
#
|
||||
@@ -43,6 +43,35 @@
|
||||
# 那些由 Gitea 按 UTF-8 读,不喂给 PowerShell,安全)。
|
||||
# ⚠️ 与 AGENTS.md「字符串字面量必须纯 ASCII」同源;改脚本时务必守住。
|
||||
#
|
||||
# 🔴🔴 工具链探测必须【可离线自测 + 保留机器级安装兜底】(2026-10-11 第三次实测修正):
|
||||
# `33a4aa4`(DTU_* 变量改造)把 Python 探测内联进本文件,并把候选**收窄成两个**
|
||||
# (PATH python + 仓库 .venv)——而 runner 上这两个都是死路:
|
||||
# · SYSTEM 账号的 PATH python = `C:\msys64\ucrt64\bin\python.exe`,**没有** pre-commit;
|
||||
# · 仓库 `.venv` 已 gitignore,runner 的全新工作区里**根本不存在**。
|
||||
# 唯一可用的机器级解释器(`C:\ProgramData\anaconda3\python.exe`)不再被探测,而文档
|
||||
# 要求的机器级 `DTU_PRECOMMIT_PYTHON` 从未设置(Machine 作用域一个 DTU_* 都没有)
|
||||
# → run 14/15 的 L0 **全部死在"环境自检",一个钩子都没跑**。cppcheck 同理:
|
||||
# 旧写法"不在 PATH 就把 C:\Program Files\Cppcheck 加进 PATH"的兜底也被删了。
|
||||
# **故探测逻辑搬进 `scripts/ci_find_python.ps1`**(两个 job 共用、可离线跑、失败时
|
||||
# 打印全部候选),并恢复常见机器级安装位置兜底;`DTU_*` 仍是最高优先级、
|
||||
# 且"显式设置就必须有效、不静默回退"的语义不变。
|
||||
#
|
||||
# 🔴 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**
|
||||
# 里(项目配置里 grep 不到这一行),而 `pre-commit run --all-files` 只跑 pre-commit stage。
|
||||
# 即 AGENTS.md 的"单行中文标题 ≤100 字符"门禁**本地有、CI 无**,
|
||||
# 且表现为"全绿"而非失败,所以一直没人发现。现由 `scripts/ci_gitlint_range.ps1`
|
||||
# 逐条校验本次 push/PR 引入的提交(消息文件必须写在**仓库内**且 **UTF-8 无 BOM**:
|
||||
# 跨盘符会被 pre-commit 拒绝 `path is on mount 'C:', start on mount 'D:'`,
|
||||
# BOM 会占掉标题 1 个字符、扭曲 100 上限)。
|
||||
#
|
||||
# 🔴 写 `GITHUB_ENV` / `GITHUB_PATH` / `GITHUB_OUTPUT` 必须显式 `-Encoding ascii`
|
||||
# (2026-10-11 实测):PS 5.1 的 `Out-File -Append` **默认 UTF-16LE**——往 runner
|
||||
# 用 UTF-8 建的同一个文件里追加,落盘就是 `B\x00B\x00B\x00=\x001\x00` 这种带 NUL 的行
|
||||
# (实测字节:`42 00 42 00 42 00 3D 00 31 00`)。本文件写入的这些键值全是 ASCII,
|
||||
# 故统一显式 ascii;`GITHUB_STEP_SUMMARY` 是 Markdown,保持 utf8。
|
||||
#
|
||||
# ⚠️ 前置(部署到 Gitea 时需确认):
|
||||
# - 该 Gitea 实例已启用 Actions:`[actions] ENABLED = true`(site admin 配置)
|
||||
# - 已注册一个 **Windows** runner(labels 含 `windows`),且装有:
|
||||
@@ -97,98 +126,73 @@ jobs:
|
||||
# NOTE: keep every run: block pure ASCII. The runner writes this block
|
||||
# to a BOM-less .ps1 executed by Windows PowerShell 5.1, which decodes
|
||||
# it as GBK -- non-ASCII bytes there break string quoting.
|
||||
$repoRoot = (Get-Location).Path
|
||||
|
||||
# --- pick a Python that actually has pre-commit -------------------
|
||||
# The runner runs as SYSTEM, whose PATH is NOT the dev user's PATH.
|
||||
# Probe order per the DTU_* env var plan (kept in sync with
|
||||
# scripts/ci_local.py):
|
||||
# DTU_PRECOMMIT_PYTHON (explicit; invalid value = hard error)
|
||||
# -> PATH python -> repo .venv
|
||||
# Discovery lives in scripts/ci_find_python.ps1 so that
|
||||
# * both jobs share ONE implementation,
|
||||
# * it can be run (and its failure list inspected) offline,
|
||||
# * the well-known machine-wide install locations stay probed.
|
||||
# The previous inline 2-candidate probe is what killed every L0 run
|
||||
# since 33a4aa4 -- see the file header.
|
||||
#
|
||||
# CAUTION 1: a failing native command writes to stderr; with
|
||||
# $ErrorActionPreference='Stop' that would abort the whole script
|
||||
# (NativeCommandError) before the next candidate is tried. So relax
|
||||
# the preference around each probe, and restore it afterwards.
|
||||
#
|
||||
# CAUTION 2: pre-commit may be installed in the USER site of some
|
||||
# account (e.g. %APPDATA%\Roaming\Python\...), which SYSTEM cannot
|
||||
# see at all -- no python.exe path can help then. Probe with `-s`
|
||||
# (no user-site) so we only accept an interpreter that works for
|
||||
# SYSTEM, and say so clearly when none is found.
|
||||
$py = $null
|
||||
$saved = $ErrorActionPreference
|
||||
$envPy = $env:DTU_PRECOMMIT_PYTHON
|
||||
if ($envPy) {
|
||||
if (-not (Test-Path $envPy)) {
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Write-Host "::error::DTU_PRECOMMIT_PYTHON is set but does not exist: $envPy"
|
||||
Write-Host "::error::fix the variable (full path to a python.exe with pre-commit), or unset it to use auto-discovery"
|
||||
exit 1
|
||||
}
|
||||
$ErrorActionPreference = 'Continue'
|
||||
& $envPy -s -m pre_commit --version 2>&1 | Out-Null
|
||||
$rc = $LASTEXITCODE
|
||||
$ErrorActionPreference = $saved
|
||||
if ($rc -ne 0) {
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Write-Host "::error::DTU_PRECOMMIT_PYTHON does not have a SYSTEM-visible pre-commit: $envPy"
|
||||
exit 1
|
||||
}
|
||||
$py = $envPy
|
||||
} else {
|
||||
$candidates = @(
|
||||
(Get-Command python -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source),
|
||||
(Join-Path $repoRoot '.venv\Scripts\python.exe')
|
||||
)
|
||||
foreach ($c in $candidates) {
|
||||
if (-not $c) { continue }
|
||||
if (-not (Test-Path $c)) { continue }
|
||||
$ErrorActionPreference = 'Continue'
|
||||
& $c -s -m pre_commit --version 2>&1 | Out-Null
|
||||
$rc = $LASTEXITCODE
|
||||
$ErrorActionPreference = $saved
|
||||
if ($rc -eq 0) { $py = $c; break }
|
||||
}
|
||||
}
|
||||
$ErrorActionPreference = 'Stop'
|
||||
# The script prints diagnostics with Write-Host and emits ONLY the
|
||||
# chosen path on the success stream, so capturing it is exact.
|
||||
# It also appends PYTHON=<path> to GITHUB_ENV (as ASCII) for later
|
||||
# steps, which is why $env:PYTHON works in the steps below.
|
||||
$py = & './scripts/ci_find_python.ps1' -NeedPreCommit -WriteEnv
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
if (-not $py) {
|
||||
Write-Host "::error::no Python with a SYSTEM-visible pre-commit (DTU_PRECOMMIT_PYTHON not set; tried PATH python and repo .venv)"
|
||||
Write-Host "::error::pre-commit is probably installed only in a USER site (invisible to SYSTEM)."
|
||||
Write-Host "::error::fix: set a MACHINE-level DTU_PRECOMMIT_PYTHON for the runner service and restart it, or install pre-commit globally for the SYSTEM account (see the Gitea runner deployment guide in docs/)"
|
||||
exit 1
|
||||
Write-Host "::error::ci_find_python.ps1 succeeded but returned no path"
|
||||
exit 2
|
||||
}
|
||||
Write-Host "using python: $py"
|
||||
& $py --version
|
||||
& $py -s -m pre_commit --version
|
||||
"PYTHON=$py" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||
$py = "$py".Trim()
|
||||
|
||||
# --- cppcheck -----------------------------------------------------
|
||||
# Order per the DTU_* plan: DTU_CPPCHECK (explicit; invalid = hard
|
||||
# error, no fallback) -> PATH (Get-Command cppcheck). No fixed
|
||||
# install-dir guess: a USER-scope PATH entry is invisible to SYSTEM,
|
||||
# so on the runner set a MACHINE-level DTU_CPPCHECK if cppcheck is
|
||||
# not on SYSTEM's PATH.
|
||||
# Order: DTU_CPPCHECK (explicit; invalid = hard error, no fallback)
|
||||
# -> PATH -> the standard install directory.
|
||||
#
|
||||
# The directory fallback is NOT cosmetic: the runner service runs as
|
||||
# SYSTEM, whose PATH has no Cppcheck entry (a USER-scope PATH entry is
|
||||
# invisible to it), and no machine-level DTU_CPPCHECK is set on a
|
||||
# stock runner. Dropping this fallback in 33a4aa4 is what made L0 die
|
||||
# on cppcheck as well, right after the Python probe had been fixed.
|
||||
$found = $null
|
||||
$envCpp = $env:DTU_CPPCHECK
|
||||
if ($envCpp) {
|
||||
if (-not (Test-Path $envCpp)) {
|
||||
Write-Host "::error::DTU_CPPCHECK is set but does not exist: $envCpp"
|
||||
Write-Host "::error::fix the variable (full path to cppcheck.exe), or unset it to use auto-discovery"
|
||||
exit 1
|
||||
}
|
||||
$dir = Split-Path -Parent $envCpp
|
||||
$env:Path = "$env:Path;$dir"
|
||||
$dir | Out-File -FilePath $env:GITHUB_PATH -Append
|
||||
Write-Host "using cppcheck from DTU_CPPCHECK: $envCpp"
|
||||
& $envCpp --version
|
||||
$found = $envCpp
|
||||
Write-Host "using cppcheck from DTU_CPPCHECK: $found"
|
||||
} else {
|
||||
$cpp = Get-Command cppcheck -ErrorAction SilentlyContinue
|
||||
if (-not $cpp) {
|
||||
Write-Host "::error::runner missing cppcheck (not on SYSTEM PATH) -- set a machine-level DTU_CPPCHECK, or add its directory to the machine PATH, then re-run"
|
||||
$cmd = Get-Command cppcheck -ErrorAction SilentlyContinue
|
||||
if ($cmd) {
|
||||
$found = $cmd.Source
|
||||
Write-Host "found cppcheck on PATH: $found"
|
||||
} else {
|
||||
$pf = $env:ProgramFiles
|
||||
$pf86 = ${env:ProgramFiles(x86)}
|
||||
if (-not $pf) { $pf = 'C:\Program Files' }
|
||||
if (-not $pf86) { $pf86 = 'C:\Program Files (x86)' }
|
||||
foreach ($cand in @(
|
||||
(Join-Path $pf 'Cppcheck\cppcheck.exe'),
|
||||
(Join-Path $pf86 'Cppcheck\cppcheck.exe'))) {
|
||||
if (Test-Path $cand) { $found = $cand; break }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (-not $found) {
|
||||
Write-Host "::error::runner missing cppcheck (not on SYSTEM PATH, not in the standard install dir)"
|
||||
Write-Host "::error::set a machine-level DTU_CPPCHECK (see scripts/ci_runner_bootstrap.ps1 -SetEnv) or add its directory to the machine PATH, then re-run"
|
||||
exit 1
|
||||
}
|
||||
Write-Host "found cppcheck: $($cpp.Source)"
|
||||
cppcheck --version
|
||||
}
|
||||
$cppDir = Split-Path -Parent $found
|
||||
$env:Path = "$env:Path;$cppDir"
|
||||
# ASCII on purpose: Out-File's PS 5.1 default is UTF-16LE, and other
|
||||
# steps append to this same file -- mixed encodings corrupt it.
|
||||
$cppDir | Out-File -FilePath $env:GITHUB_PATH -Append -Encoding ascii
|
||||
& $found --version 2>&1 | Out-Host
|
||||
|
||||
Write-Host "runner environment check passed"
|
||||
|
||||
@@ -196,6 +200,34 @@ jobs:
|
||||
shell: powershell
|
||||
run: "& $env:PYTHON -s -m pre_commit install --install-hooks"
|
||||
|
||||
- name: 提交信息门禁(gitlint,commit-msg stage)
|
||||
shell: powershell
|
||||
env:
|
||||
DTU_BASE_SHA: ${{ github.event.before }}
|
||||
DTU_PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
DTU_HEAD_SHA: ${{ github.sha }}
|
||||
run: |
|
||||
# Pure ASCII only (see file header).
|
||||
#
|
||||
# WHY THIS STEP EXISTS: gitlint is a `commit-msg` stage hook, and
|
||||
# `pre-commit run --all-files` only runs the `pre-commit` stage -- so
|
||||
# the commit-message rule was enforced locally and NEVER here, while
|
||||
# the job still reported green (17 hooks defined, 16 reported).
|
||||
# pre-commit has no notion of "the commits of this push", so the
|
||||
# script feeds each commit message to the hook one at a time.
|
||||
#
|
||||
# Range: PR -> base..head; push -> event.before..sha, falling back to
|
||||
# the newest commits when `before` is all-zero (new branch) or
|
||||
# unreachable (force push). Merge commits are skipped (git writes them).
|
||||
$base = $env:DTU_PR_BASE_SHA
|
||||
if (-not $base) { $base = $env:DTU_BASE_SHA }
|
||||
& './scripts/ci_gitlint_range.ps1' -BaseSha $base -HeadSha $env:DTU_HEAD_SHA `
|
||||
-MaxCommits 50 -PythonExe $env:PYTHON
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "::error::commit message gate failed (gitlint) -- see the per-commit output above"
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
- name: 决定跳过哪些钩子(Keil 依赖)
|
||||
id: skip
|
||||
shell: powershell
|
||||
@@ -213,14 +245,14 @@ jobs:
|
||||
$map = Get-ChildItem -Path 'MDK-ARM' -Recurse -Filter '*.map' -ErrorAction SilentlyContinue
|
||||
if ($map) {
|
||||
Write-Host "found .map: $($map[0].FullName) -- mem-numbers runs normally"
|
||||
"value=" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
|
||||
"sizegate=on" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
|
||||
"value=" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding ascii
|
||||
"sizegate=on" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding ascii
|
||||
"> Volume gate (mem-numbers): RAN (.map present)" |
|
||||
Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8
|
||||
} else {
|
||||
Write-Host "::warning::no .map found (no Keil build) -- SKIPPING mem-numbers, so the Flash/RAM size gate does NOT run in CI. Size changes are NOT caught here; verify locally with 'python scripts/check_mem_numbers.py --scan' after a Keil build."
|
||||
"value=mem-numbers" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
|
||||
"sizegate=off" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
|
||||
"value=mem-numbers" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding ascii
|
||||
"sizegate=off" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding ascii
|
||||
"> :warning: Volume gate (mem-numbers): **NOT RUN** -- no `.map` (no Keil build)." |
|
||||
Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8
|
||||
}
|
||||
@@ -339,10 +371,10 @@ jobs:
|
||||
}
|
||||
}
|
||||
if ($found) {
|
||||
"value=yes" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
|
||||
"value=yes" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding ascii
|
||||
} else {
|
||||
Write-Host "::warning::runner has no MSVC (VS C++ toolchain) -- skipping L0.5. Install VS BuildTools and re-run."
|
||||
"value=no" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
|
||||
"value=no" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding ascii
|
||||
}
|
||||
|
||||
- name: 运行主机侧单测
|
||||
@@ -350,28 +382,18 @@ jobs:
|
||||
if: steps.msvc.outputs.value == 'yes'
|
||||
shell: powershell
|
||||
run: |
|
||||
# Resolve a working Python first (SYSTEM's PATH python may be unusable).
|
||||
# Order per the DTU_* plan: DTU_PRECOMMIT_PYTHON (explicit; invalid =
|
||||
# hard error) -> PATH python -> repo .venv.
|
||||
$repoRoot = (Get-Location).Path
|
||||
$py = $null
|
||||
$envPy = $env:DTU_PRECOMMIT_PYTHON
|
||||
if ($envPy) {
|
||||
if (-not (Test-Path $envPy)) {
|
||||
Write-Host "::error::DTU_PRECOMMIT_PYTHON is set but does not exist: $envPy"
|
||||
exit 1
|
||||
}
|
||||
$py = $envPy
|
||||
}
|
||||
# Resolve a working Python with the SAME shared probe as L0 (no
|
||||
# -NeedPreCommit: these harnesses are stdlib-only). The old inline
|
||||
# probe here had the same 2-candidate blind spot as L0 -- it only
|
||||
# survived by luck, because the stdlib tests also run on the msys64
|
||||
# python that happens to be on SYSTEM's PATH.
|
||||
$py = & './scripts/ci_find_python.ps1' -WriteEnv
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
if (-not $py) {
|
||||
foreach ($c in @(
|
||||
(Get-Command python -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source),
|
||||
(Join-Path $repoRoot '.venv\Scripts\python.exe')
|
||||
)) {
|
||||
if ($c -and (Test-Path $c)) { $py = $c; break }
|
||||
Write-Host "::error::ci_find_python.ps1 succeeded but returned no path"
|
||||
exit 2
|
||||
}
|
||||
}
|
||||
if (-not $py) { Write-Host "::error::no python found (set DTU_PRECOMMIT_PYTHON or fix the runner PATH)"; exit 1 }
|
||||
$py = "$py".Trim()
|
||||
Write-Host "using python: $py"
|
||||
|
||||
New-Item -ItemType Directory -Force -Path 'ci-artifacts' | Out-Null
|
||||
@@ -423,8 +445,8 @@ jobs:
|
||||
Write-Host "::notice::$agg"
|
||||
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "### L0.5 host-side unit tests" -Encoding utf8
|
||||
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value $agg -Encoding utf8
|
||||
"count=$runnerCount" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
|
||||
"agg=$agg" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
|
||||
"count=$runnerCount" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding ascii
|
||||
"agg=$agg" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding ascii
|
||||
exit $fail
|
||||
|
||||
- name: 归档 L0.5 日志(供下载/排障)
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
- ⚠ **pre-commit 4.x 已移除 `always: true`**(实测对 `local` 钩子只告警 `Unexpected key(s)` 并跳过)→ 想"总是跑"的钩子**必须让 `files:` 覆盖到所有会触发它的文件**,没有兜底开关
|
||||
- **改写型钩子(`trailing-whitespace` + `end-of-file-fixer`)豁免 CubeMX/Keil 生成文件**(配置里 YAML 锚点 `&cubemx-generated`);⚠ **只读检查不受影响**:`cppcheck` / 乱码 / `cubemx-survival` / `task-table` / `mem-numbers` 仍覆盖 `Src/`、`Inc/`
|
||||
- 提交信息:标题 ≤100 字符、**正文每行 ≤80**(`.gitlint` + gitlint 默认)→ 长说明一律用**短标题 + `git commit -F <UTF-8 无 BOM 文件>` 多行正文**(中文长标题极易超限,且 `-m` 会被终端代码页转码破坏)
|
||||
- **CI**(`.gitea/workflows/ci.yml`)只跑 L0 静态门禁 + L0.5 主机侧单测,**刻意不跑 `@build`**(需 Keil 授权)、不含 L1/L2/L3(需真机);本地等价自查 `& '.venv\Scripts\python.exe' scripts/ci_local.py`——**runner 未就绪时这是 L0 的唯一保障**
|
||||
- **CI**(`.gitea/workflows/ci.yml`)只跑 L0 静态门禁 + L0.5 主机侧单测,**刻意不跑 `@build`**(需 Keil 授权)、不含 L1/L2/L3(需真机)。**远程 CI 的全部资料(服务端开关 / runner 部署与保活 / workflow / 排障 / 实测坑)集中在 [`docs/CI/README.md`](docs/CI/README.md),不放本节**——它**只跑在本机一台 runner `win-stm32-ci` 上**,其他机器与项目都不涉及。日常改代码的等价自查是 `& '.venv\Scripts\python.exe' scripts/ci_local.py`(L0 + L0b + L0.5)——**runner 不在线也不影响本地门禁,不能因"已配 CI"放宽本地要求**
|
||||
|
||||
## 平台约束
|
||||
|
||||
@@ -176,4 +176,5 @@
|
||||
| 串口助手 | **`serial-assistant` skill**(命令/判据/陷阱,用到自动加载)、`docs/测试说明/串口助手使用指南.md`、`串口助手设计说明.md` |
|
||||
| UI 架构 / 模拟器 / UI 自动化回归 | **`lvgl-simulator` skill**(构建/自检/报错速查,用到自动加载)、`docs/UI设计/UI_设计架构.md`、`LVGL模拟器部署与使用说明.md`、`UI_自动化测试流程.md`;UI 改动回归走 **`ui-verify` skill** |
|
||||
| 待办与审计发现登记簿 | `docs/待处理内容.md` |
|
||||
| 换机 / 工具路径环境变量 / CI | `docs/环境与部署/` |
|
||||
| 换机 / 工具路径环境变量 | `docs/环境与部署/` |
|
||||
| 远程 CI(Gitea Actions,**只跑本机一台 runner**) | [`docs/CI/README.md`](docs/CI/README.md) |
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
# 远程 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)。
|
||||
|
||||
> ✅ **机器侧遗留已清理(2026-10-12)**:旧方案的 `deploy_runner_task.ps1` 与 `run_runner.cmd`
|
||||
> 已从 `D:\Docker\act_runner\` **删除**(它们跑单次 daemon,首次 Declare 失败就永久停摆,
|
||||
> 机理见指南 §3.2;`check_env.ps1` 里指向它们的提示也已改指 bootstrap)。
|
||||
> 现行部署只有 `ci_runner_bootstrap.ps1` 这一条路。
|
||||
|
||||
> 🔴 **重跑部署前先知道这条**(2026-10-12 实测):`Unregister-ScheduledTask` **不会**带走旧实例
|
||||
> 启动的进程。旧看门狗会作为孤儿继续养着它的 daemon,而那个 daemon 占着 `.runner.lock`,
|
||||
> 于是新看门狗每 10 秒失败一次(`runner file is already locked by another process`)——
|
||||
> **而 runner 在网页上照样显示 online**,CI 看着正常,任务其实谁也没管。
|
||||
> 现在 `-StartNow` 会先清掉这些残留进程再启动(约离线几秒),`-Status` 也会报出重复实例。
|
||||
> **2026-10-12 已实测收敛**:清掉 3 个残留后 `daemon up: pid … (single instance)`,
|
||||
> 监管链 `svchost(任务) → 看门狗 → daemon` 恢复,runner 回 online。
|
||||
|
||||
## 相关文档
|
||||
|
||||
- 测试分层与门禁决策(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 修复随 `master` **合入 `v2-on-ui`**(该分支另有 21 个功能提交;冲突仅 `AGENTS.md` 与 `docs/README.md` 两处,解法是"保留其新版结构 + 把 CI 指针搬进对应位置")。因该分支门禁更多(**26 钩子 / 10 单测套件** vs master 的 18 / 3),指南里写死的数字已改为**规则 + 分分支快照**,见指南 §6 / §7 / §10 |
|
||||
| 2026-10-12 | ✅ 双看门狗缺陷**实测收敛**(管理员 `-StartNow` 清掉 3 个残留 → 单实例、链路恢复、runner online)。另修两处:daemon 日志走 stderr 被 PS 5.1 `2>&1` 包成 ErrorRecord(2 行真日志写成 8 行,每 4 行 3 行噪声)→ 启动器改 `ForEach-Object { "$_" }`;`-Status` 诚实化(非管理员读不到 SYSTEM 进程命令行 → 明说需提权而非谎报"无看门狗";重启循环判据改看进程存活时长,不再靠不轮转日志的尾部字符串报假警)。详见指南 §7.3 |
|
||||
| 2026-10-12 | 🔴 **修复"双看门狗抢 `.runner.lock`"缺陷**:`Unregister-ScheduledTask` 不停旧实例进程 → 孤儿看门狗养着旧 daemon 占锁,新看门狗每 10 秒失败一次,而 runner 网页照样 online、`-Status` 也说"在跑"。现 `-StartNow` 先清残留进程再启动、`-Status` 报重复实例与锁冲突。同日清理机器侧:`config.yaml` 头部错误指引("改用 sc.exe / New-Service")改为看门狗口径,删除已废弃的 `deploy_runner_task.ps1` 与 `run_runner.cmd`,`check_env.ps1` 提示同步改指 bootstrap |
|
||||
| 2026-10-12 | **单一口径清理**:合并稿初版对 4 处分歧"两说并存",现统一为本机实测现状——注册口径(`gitea-runner v4.0.1` / `D:\Docker\act_runner` / `127.0.0.1:3001`,旧 `act_runner v0.2.11` + `C:\act_runner` 段删去可复制命令只留警示)、`config.yaml`(`fetch_timeout: 1m0s`、`timeout: 30m0s`)、常驻方式(只认看门狗,`deploy_runner_task.ps1` 标废弃)、验收数字(L0.5 `288/288`,历史数字标注为快照)。并补"机器侧遗留脚本勿误用"警示 |
|
||||
| 2026-10-12 | **新建本目录**:远程 CI 内容原先散在 `docs/环境与部署/`(两篇互相重叠的指南)、`docs/测试说明/测试开发标准流程.md` §9、`AGENTS.md` 提交门禁节共 4 处,约 2000 行;现合并为单一指南并把 `AGENTS.md` 压成一行指针。动因:远程 CI 只涉及本机一台 runner,与日常开发规范混在一起每次会话都被读,造成困惑 |
|
||||
+1368
File diff suppressed because it is too large
Load Diff
@@ -24,6 +24,7 @@ docs/
|
||||
├── 内存管理/ RAM 预算、分配策略与优化调研
|
||||
├── 问题记录/ 模块陷阱台账、专项审计与项目评审报告
|
||||
├── 环境与部署/ 环境搭建与部署核对清单
|
||||
├── CI/ 远程 CI(Gitea Actions)唯一入口 —— **只涉及本机一台 runner,日常开发不必读**
|
||||
├── UI设计/ UI 架构、页面设计、字体与模拟器
|
||||
├── 硬件平台/ 网表分析报告与语义图
|
||||
├── 芯片手册/ 外部芯片数据手册转录(只读资料)
|
||||
@@ -220,6 +221,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 台账与外部资料
|
||||
|
||||
@@ -268,6 +270,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-11 | 新增文档 | 新建 [`AGENTS编写指南.md`](AGENTS编写指南.md)(好的 AGENTS.md 怎么写:六原则/模板/反模式/维护纪律 + 本仓库瘦身重构案例与外部标杆调研);本索引 §1 目录树与 §7.1b 登记;同日 `AGENTS.md` 按该指南完成宏观重构(补项目概况/目录地图、平台约束分组) |
|
||||
| 2026-10-10 | 新增子系统设计 | 新建 `docs/子系统设计/ADC采样节拍设计说明.md`(ADC 采集链路 1 kHz 硬件定时器触发的 CubeMX 配置规格与代码改造清单,B1 遗留项规划);本索引 §5.1 补登记 |
|
||||
| 2026-10-10 | 新增算法专题 | 新建 `docs/算法/继电保护功能复现规划.md`(保护链路复现总规划);本索引 §1 目录树补 `算法/`、§5 拆为 5.1 功能子系统 / 5.2 算法专题并登记 `算法/` 两篇文档 |
|
||||
|
||||
+1
-1
@@ -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 全绿仍可能误以为体积也守住了。
|
||||
|
||||
**可选处置**:
|
||||
|
||||
+37
-26
@@ -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,36 +528,48 @@ 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。即"单行中文标题 ≤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 / cppcheck / `python -m pre_commit`,缺则 `::error::` + exit 1)→ `pre_commit install --install-hooks` → **决定跳过哪些钩子**(探测 `.map`,写入 `GITHUB_OUTPUT`;无 `.map` 时 `::warning::` 声明体积门禁未跑)→ `pre-commit run --all-files --show-diff-on-failure`(`env: SKIP`,输出 `Tee` 到 `ci-artifacts/l0-pre-commit.log`)→ 打印 `::notice::L0: passed/failed/skipped` 汇总行 → 归档日志说明 |
|
||||
| `l05-host-unit-tests` | `windows` | checkout → **探测 MSVC**(按 VS_ROOTS 找 `Hostx64\x64\cl.exe`)→ 遍历 `test/host/**/run_unit_test.py` 逐个跑(输出 `Tee` 到 `ci-artifacts/l05-<模块>.log`;解析各模块 `N/M PASSED` 汇总行 → 聚合 `::notice::L0.5: K runner(s), A/B assertions passed`;失败 `::error::` 并置退出码);**未探测到 MSVC 时打印醒目 WARNING 并降级跳过**(不硬失败,避免"没装 VS 的 runner 全红") |
|
||||
| `l0-gates` | **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 跳过**(不硬失败,但等于该层裸奔) |
|
||||
|
||||
**结果可见性(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/**` → **纯文档改动不会触发 CI**)
|
||||
与 `pull_request`,另留 `workflow_dispatch` 手动触发;同分支 `cancel-in-progress: true`。
|
||||
两个 job 都写 step-summary + `::notice::` 汇总行,日志落 `ci-artifacts/*.log`。
|
||||
|
||||
触发:`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`
|
||||
|
||||
CI 只在 push / PR 时跑,且需 runner 就绪。**提交前自查、或 runner 还没配好时**,用同一套判定在本地跑:
|
||||
|
||||
```powershell
|
||||
& '.venv\Scripts\python.exe' scripts/ci_local.py # L0 + L0.5(与 CI 等价)
|
||||
& '.venv\Scripts\python.exe' scripts/ci_local.py --l0-only # 只跑 L0
|
||||
& '.venv\Scripts\python.exe' scripts/ci_local.py # L0 + L0b + L0.5(与 CI 等价)
|
||||
& '.venv\Scripts\python.exe' scripts/ci_local.py --l0-only # 只跑 L0(含 L0b 提交信息门禁)
|
||||
& '.venv\Scripts\python.exe' scripts/ci_local.py --l05-only # 只跑 L0.5
|
||||
& '.venv\Scripts\python.exe' scripts/ci_local.py --commits 20 # 提交信息校验最近 20 条
|
||||
```
|
||||
|
||||
- **自动选用装了 pre-commit 的解释器**(优先级:`DTU_PRECOMMIT_PYTHON` 显式指定 → 当前解释器(重建 `.venv` 已自带 pre-commit)→ 仓库 `.venv` → PATH 的 `python`;显式值无效 → 硬报错并指明变量名,不静默回退);
|
||||
仍不可用时退出码 2 并给出修复提示。
|
||||
- **自动选用装了 pre-commit 的解释器**(优先级:`DTU_PRECOMMIT_PYTHON` 显式指定 → 当前解释器(重建 `.venv` 已自带 pre-commit)→ 仓库 `.venv` → PATH 的 `python` → **常见机器级安装位置**;显式值无效 → 硬报错并指明变量名,不静默回退);
|
||||
仍不可用时退出码 2 并列出**全部已尝试路径**。
|
||||
⚠️ 该候选表与 CI 侧 **`scripts/ci_find_python.ps1`** 必须**同序同步**——2026-10-11 的 L0 全灭就是
|
||||
workflow 单方面把候选收窄成"PATH python + 仓库 `.venv`"(runner 上两者都是死路)导致的。
|
||||
- **L0b 提交信息门禁**:与 CI 的 `ci_gitlint_range.ps1` 同语义(`--all-files` 跑不到 commit-msg stage)。
|
||||
- **`.map` 自适应与 CI 完全一致**:本地有 `.map` 就照常跑 `mem-numbers`,没有就跳过。
|
||||
- 退出码:`0` 通过 / `1` 门禁或单测失败 / `2` 环境错误。
|
||||
|
||||
@@ -567,16 +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) |
|
||||
| 缺 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。
|
||||
|
||||
---
|
||||
|
||||
@@ -615,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 缺陷记录) |
|
||||
|
||||
@@ -1,371 +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 以服务方式常驻
|
||||
|
||||
```powershell
|
||||
# 注册为 Windows 服务(管理员权限)
|
||||
.\act_runner.exe daemon install
|
||||
.\act_runner.exe daemon start
|
||||
|
||||
# 或前台调试(先看日志通不通,推荐首次这样跑)
|
||||
.\act_runner.exe daemon
|
||||
```
|
||||
|
||||
注册成功后,回到 **仓库 → Settings → Actions → Runners**,应看到 `win-stm32-ci`
|
||||
状态为 **Idle / 绿色**。
|
||||
|
||||
---
|
||||
|
||||
## 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 关键:PATH 必须对
|
||||
|
||||
CI 的环境自检用的是 `Get-Command python` / `cppcheck`,**依赖 PATH**。
|
||||
装完后**新开一个终端**(或重启 runner 服务)让 PATH 生效,然后自检:
|
||||
|
||||
```powershell
|
||||
python --version # 期望 Python 3.x
|
||||
cppcheck --version # 期望 Cppcheck 2.21.0 或更高
|
||||
python -m pre_commit --version
|
||||
```
|
||||
|
||||
### 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 注册成功 |
|
||||
| 3 | runner 上 `python --version` / `cppcheck --version` / `python -m pre_commit --version` | 三条全通 |
|
||||
| 4 | `git push` 一次(或页面 Run workflow) | Actions 页出现运行中任务 |
|
||||
| 5 | `l0-gates` 日志 | `pre-commit ... Passed`;`::notice::L0: pre-commit hooks -- passed=.. failed=0 skipped=..`;无 `.map` 时见 `::warning::` 体积门禁未跑 |
|
||||
| 6 | `l05-host-unit-tests` 日志 | `::notice::L0.5: 2 runner(s), 139/139 assertions passed, fail=0` |
|
||||
| 7 | job 运行页的 **Summary** 区块 | 出现 "L0 static gates" / "L0.5 host-side unit tests" 小标题与汇总行 |
|
||||
| 8 | 提交记录旁出现 ✅ | CI 全绿 |
|
||||
|
||||
**本地先自查**(与 CI 同一套判定,避免推上去才发现问题):
|
||||
|
||||
```powershell
|
||||
& '.venv\Scripts\python.exe' scripts/ci_local.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 常见故障排查
|
||||
|
||||
| 现象 | 根因 | 处置 |
|
||||
|---|---|---|
|
||||
| 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. 日常维护
|
||||
|
||||
- **升级 runner**:停服务 → 换 `act_runner.exe` → 起服务(注册信息保留在 `config.yaml`);
|
||||
- **改了 `VS_ROOTS` / 工具链探测逻辑**:记得**同步改 workflow 里的探测根目录**(两处一致);
|
||||
- **钩子集变化**(`.pre-commit-config.yaml` 增删钩子):CI 自动跟随,无需改 workflow;
|
||||
- **`@build` / L1 / L2**:永远留在本地,不要往 CI 加(需 Keil 授权与真机);
|
||||
- **runner 长时间不可用**时:CI 只是"多一道网",**提交前的本地 `ci_local.py` 仍是 L0 的唯一保障**,
|
||||
不能因"已经配了 CI"放宽(见 `docs/待处理内容.md` 的部署前置警示块)。
|
||||
|
||||
---
|
||||
|
||||
## 变更记录
|
||||
|
||||
| 日期 | 变更 |
|
||||
|---|---|
|
||||
| 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 拉取 / 端到端验证清单 / 故障排查 / 日常维护) |
|
||||
@@ -1,917 +0,0 @@
|
||||
# Gitea Actions Runner 部署指南(本机 Windows 原生版)
|
||||
|
||||
> **适用环境(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 未运行** → **本指南不使用容器,全程原生**
|
||||
>
|
||||
> **本文只讲 Runner 端**(下载 → 注册 → 配置 → 常驻 → 验证 → 排障)。
|
||||
> Gitea 服务端开关已就绪,无需再动。CI 整体设计见 `docs/测试说明/测试开发标准流程.md` §9。
|
||||
|
||||
> ⚠️ **重要版本变更(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 任务计划程序** —— 见 **§4**。
|
||||
|
||||
---
|
||||
|
||||
## 0. 先理解:这台机器上的拓扑
|
||||
|
||||
**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`**。
|
||||
|
||||
---
|
||||
|
||||
## 1. 下载 gitea-runner
|
||||
|
||||
### 1.1 选对版本
|
||||
|
||||
| 项 | 值 |
|
||||
|---|---|
|
||||
| 架构 | **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`** |
|
||||
|
||||
### 1.2 下载
|
||||
|
||||
```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`)。
|
||||
|
||||
### 1.3 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 原生方式(见 §4)。
|
||||
>
|
||||
> `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)
|
||||
|
||||
```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.3 验证注册
|
||||
|
||||
浏览器刷新 **Settings → Actions → Runners**,应看到:
|
||||
|
||||
| 列 | 期望值 |
|
||||
|---|---|
|
||||
| Name | `win-stm32-ci` |
|
||||
| Labels | `windows`(`:host` 是执行方式,UI 可能只显示 `windows`) |
|
||||
| Status | 🟢 **Idle** |
|
||||
|
||||
---
|
||||
|
||||
## 3. 配置 config.yaml
|
||||
|
||||
**推荐用 `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 # 打印全部可用选项(含注释)
|
||||
```
|
||||
|
||||
**本机已落地的配置**(`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 超时;L0+L0.5 约 1~3 分钟
|
||||
fetch_timeout: 5m0s
|
||||
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."*)
|
||||
|
||||
---
|
||||
|
||||
## 4. 让 Runner 常驻(任务计划程序)
|
||||
|
||||
### 4.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` |
|
||||
|
||||
### 4.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 停掉、再做常驻。
|
||||
|
||||
### 4.2 一键部署脚本(推荐)
|
||||
|
||||
管理员 PowerShell 执行:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File "D:\Docker\act_runner\deploy_runner_task.ps1"
|
||||
```
|
||||
|
||||
脚本流程:提权检查 → 前置文件检查(`gitea-runner.exe` / `config.yaml` / `.runner`)
|
||||
→ **清理旧的无效 Windows 服务**(若存在)→ 生成启动包装 `run_runner.cmd`
|
||||
→ `Register-ScheduledTask`(开机+登录双触发器、SYSTEM 账户、最高权限、崩溃重启)
|
||||
→ 启动任务 → 校验进程存活。
|
||||
|
||||
### 4.3 手动等价命令
|
||||
|
||||
不想用脚本时,管理员 PowerShell 逐条执行:
|
||||
|
||||
```powershell
|
||||
$Dir = "D:\Docker\act_runner"
|
||||
|
||||
# ① 包装脚本(固定工作目录 + stdout 落盘)
|
||||
@"
|
||||
@echo off
|
||||
cd /d "$Dir"
|
||||
"$Dir\gitea-runner.exe" daemon --config "$Dir\config.yaml" >> "$Dir\log\runner-stdout.log" 2>&1
|
||||
"@ | Out-File "$Dir\run_runner.cmd" -Encoding ASCII
|
||||
|
||||
# ② 注册任务
|
||||
$action = New-ScheduledTaskAction -Execute "$Dir\run_runner.cmd" -WorkingDirectory $Dir
|
||||
$trgBoot = New-ScheduledTaskTrigger -AtStartup
|
||||
$trgBoot.Delay = 'PT30S'
|
||||
$trgLogon = New-ScheduledTaskTrigger -AtLogOn
|
||||
$prin = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
|
||||
$sets = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
|
||||
-StartWhenAvailable -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1) `
|
||||
-ExecutionTimeLimit (New-TimeSpan -Seconds 0) -MultipleInstances IgnoreNew
|
||||
|
||||
Register-ScheduledTask -TaskName "GiteaActionsRunner" -Action $action `
|
||||
-Trigger @($trgBoot, $trgLogon) -Principal $prin -Settings $sets `
|
||||
-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` 的地方)。不过仍建议用 `.cmd` 包装再 `cd /d`,
|
||||
> 因为 SYSTEM 账户的默认工作目录不可控。
|
||||
|
||||
### 4.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** 任务才生效。
|
||||
|
||||
> 💡 **`LastTaskResult` 数值解读**(别被非 0 吓到):
|
||||
>
|
||||
> | 值 | 含义 |
|
||||
> |---|---|
|
||||
> | `267009` (`0x41301`) | ✅ **`SCHED_S_TASK_RUNNING` —— 任务正在运行,不是错误**(常驻任务常态值) |
|
||||
> | `0` (`0x0`) | ✅ 上次运行正常结束 |
|
||||
> | `267011` (`0x41303`) | 任务尚未运行过 |
|
||||
> | `267014` (`0x41306`) | 任务被用户终止 |
|
||||
>
|
||||
> **判断 runner 是否健康,看这三项而不是 `LastTaskResult`**:
|
||||
> ① `Get-Process gitea-runner` 有进程;
|
||||
> ② `log\runner-stdout.log` 最新一行是 `declare successfully`;
|
||||
> ③ Gitea 页面 runner 显示 **Idle**(或 MySQL 查 `action_runner.last_online`)。
|
||||
|
||||
### 4.5 ⚠️ 清理遗留的无效服务
|
||||
|
||||
若早前按旧文档建过服务,删掉它(留着只会误导):
|
||||
|
||||
```powershell
|
||||
Stop-Service gitea-runner -Force -ErrorAction SilentlyContinue
|
||||
sc.exe delete gitea-runner
|
||||
```
|
||||
|
||||
> **`gitea-runner` 这个名字将不再作为服务存在。** 之后提到"gitea-runner"
|
||||
> 一律指**计划任务 `GiteaActionsRunner`** 或**进程 `gitea-runner.exe`**。
|
||||
|
||||
### 4.6 ⚠️ 权限要求(本机实测的硬约束)
|
||||
|
||||
**注册计划任务必须用「提权的管理员」Shell**。本机实测:
|
||||
|
||||
| 检查项 | 结果 |
|
||||
|---|---|
|
||||
| 当前用户 | `DESKTOP-KM0OG7Q\Administrator` |
|
||||
| 是否提权(`IsInRole(Administrator)`) | **False** |
|
||||
| 非提权下注册任务 | ❌ `拒绝访问`(`Register-ScheduledTask` 需要 elevated token) |
|
||||
|
||||
> **"是 Administrator 用户" ≠ "有管理员权限"** —— Windows 的 UAC 会把普通登录会话降权。
|
||||
> 必须**右键 PowerShell → 以管理员身份运行**,标题栏会显示「管理员」字样。
|
||||
|
||||
> 💡 计划任务以 **`SYSTEM`** 账户运行,拥有完整权限,可正常访问本地
|
||||
> `127.0.0.1:3001` 的 Gitea。
|
||||
|
||||
---
|
||||
|
||||
## 5. Runner 必须装齐的软件
|
||||
|
||||
Runner 会**直接在本机跑** CI 步骤,所以本机必须有一套工具链。
|
||||
(**你本机是开发机,大概率已经全有** —— 逐条核对下表。)
|
||||
|
||||
| 项 | 检查命令 | 期望 | 缺了怎么办 |
|
||||
|---|---|---|---|
|
||||
| **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` | 任意 | 你已在用 |
|
||||
|
||||
**本机实测状态(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) ✅
|
||||
```
|
||||
|
||||
**→ 四项全齐,无需额外安装。**
|
||||
|
||||
### 5.1 ⚠️ 关键坑:PATH 与运行账户
|
||||
|
||||
**问题的本质**:计划任务以 **`SYSTEM`** 账户运行时,继承的是**系统级 PATH**
|
||||
(注册表 `HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment` 的 `Path`),
|
||||
**不含任何用户级 PATH**。
|
||||
|
||||
- 你在自己的终端里能用 `python`,**不代表 SYSTEM 账户里也能用**;
|
||||
- **装完任何新软件 / 改过系统 PATH 后,必须重启 runner 任务**:
|
||||
|
||||
```powershell
|
||||
Stop-ScheduledTask -TaskName GiteaActionsRunner
|
||||
Start-ScheduledTask -TaskName GiteaActionsRunner
|
||||
```
|
||||
|
||||
> **核对方法**:用 `check_env.ps1` 的「系统 PATH 视角」检查项,
|
||||
> 或以 SYSTEM 身份跑一次 `where python` 验证。
|
||||
|
||||
### 5.1.1 ⚠️⚠️ 关键坑:`pre-commit` 必须在「全局 site-packages」,装到用户态等于没装
|
||||
|
||||
这是 2026-10-02 排查 **L0 job 反复失败**(`::error::no Python with a SYSTEM-visible pre-commit`)的真正根因,
|
||||
也是本环境最隐蔽的一个坑。
|
||||
|
||||
**现象**:`pre-commit` 在你自己的终端里好好的(`pre-commit --version` 有输出),
|
||||
但 CI 的 `l0-gates` job 三个候选 Python 全部探测失败:
|
||||
|
||||
```
|
||||
::error::no Python with a SYSTEM-visible pre-commit. Tried:
|
||||
C:\msys64\ucrt64\bin\python.exe |
|
||||
<workdir>\.venv\Scripts\python.exe |
|
||||
C:\ProgramData\anaconda3\python.exe
|
||||
```
|
||||
|
||||
> 📌 上为 2026-10-02 的历史报错原文。workflow 现已改为
|
||||
> **`DTU_PRECOMMIT_PYTHON`(机器级)→ PATH 的 `python` → 工作区 `.venv`** 顺序(§5.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 的,**根本不存在**。
|
||||
|
||||
### 5.1.2 ⚠️ 关键坑:`doc-links` 钩子「本地过、CI 挂」= 引用了 gitignore 的运行时文件
|
||||
|
||||
**现象**:`文档链接存在性 (D-3/D-7)` 钩子在**本地** `pre-commit run --all-files` 通过,
|
||||
一上 CI 就 `Failed`(exit 1),报的行/路径你在本地明明存在。
|
||||
|
||||
**根因**:CI 的 `actions/checkout@v4` 拉的是**干净克隆**,而钩子校验的是
|
||||
**仓库根相对路径是否存在**(`check_backtick_paths`,见 `scripts/check_doc_links.py`)。
|
||||
若文档引用了 **gitignore 的运行时产物**——
|
||||
如 `.dsh/skills/serial-assistant/scripts/state/meta.json`(运行时未创建,`.gitignore:132` 排除整个 `state/`)——
|
||||
你本机因为跑过工具所以文件在,**克隆里则不存在** → 判为失效引用。
|
||||
|
||||
**修复**:按 `docs/文档规范.md` §3.3 给该行加**状态词**(脚本的 `STATUS_HINTS`:
|
||||
`计划 / 未创建 / 尚未创建 / 已删除 / 删除 / 不存在 / 缺失 / 断链 / 待建 / 待创建`)。
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
> 更一般的教训:**任何校验"文件是否存在/数字是否一致"的钩子,都要在
|
||||
> "无本地产物"的干净克隆里验一次**,别只在开发机上跑。
|
||||
|
||||
### 5.1.3 机器级 `DTU_*` 环境变量(runner 的正式覆盖通道,2026-10-02 起)
|
||||
|
||||
workflow 已改为统一消费 `DTU_*` 接口(见 `本地工具路径环境变量方案.md` §3.3):
|
||||
**显式变量优先 → 自动发现回退**,三个 job 步骤相关的变量:
|
||||
|
||||
| 变量 | 作用 | 未设置时的回退 |
|
||||
|---|---|---|
|
||||
| `DTU_PRECOMMIT_PYTHON` | 指向装有 pre-commit 的 python.exe(L0/L0.5 用) | PATH 的 `python` → 工作区 `.venv`(干净克隆里通常没有) |
|
||||
| `DTU_CPPCHECK` | 指向 cppcheck.exe(L0 钩子用) | SYSTEM 的 PATH(用户级 PATH 不可见) |
|
||||
| `DTU_MSVC_CL` | 指向 x64 `cl.exe`(L0.5 用) | `%ProgramFiles%`/`%ProgramFiles(x86)%` 推导的 VS 标准根自动探测 |
|
||||
|
||||
设置(**必须管理员**;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
|
||||
```
|
||||
|
||||
> 与 §5.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` 用同一套推导(已同步)。
|
||||
|
||||
**你本机实测命中第 1 条**:
|
||||
`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*` 推导,换盘符的标准布局仍命中)
|
||||
|
||||
### 5.3 验证运行器能找到工具链(注册前先跑一遍)
|
||||
|
||||
```powershell
|
||||
& '.venv\Scripts\python.exe' scripts/ci_local.py
|
||||
```
|
||||
|
||||
这条命令跑的就是 CI 的同一套判定。**本地能过,runner 上基本就能过**(前提是 PATH 一致)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 端到端验证
|
||||
|
||||
按顺序做,任一步不过就停在那里查:
|
||||
|
||||
| # | 动作 | 期望 |
|
||||
|---|---|---|
|
||||
| 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 | 提交记录旁 | ✅ 绿灯 |
|
||||
|
||||
> ⚠️ **第 3 步是当前的最大阻塞**:本仓库的 `.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. 排障对照表
|
||||
|
||||
| 现象 | 根因 | 处置 |
|
||||
|---|---|---|
|
||||
| runner 一直 **Idle**,push 后任务**排队不跑** | `--labels` 不是 `windows`(最常见) | 改 `config.yaml` 的 `runner.labels: ["windows:host"]` → 重启任务(§4.4) |
|
||||
| `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` 后第二次快 |
|
||||
| **改了 `.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) |
|
||||
| 任务 `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) |
|
||||
| **脚本报一堆 `源文本中包含无法识别的标记` / `字符串缺少终止符` / 中文显示成 `鐒跺悗鎵ц`** | 🔴 **`.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\deploy_runner_task.ps1" -Raw -Encoding UTF8
|
||||
[System.IO.File]::WriteAllText("D:\Docker\act_runner\deploy_runner_task.ps1", $c, (New-Object System.Text.UTF8Encoding($true)))
|
||||
```
|
||||
|
||||
```bash
|
||||
# 方法 B:直接前缀 BOM 字节(bash/Git Bash)
|
||||
cd "/d/Docker/act_runner"
|
||||
for f in deploy_runner_task.ps1 check_env.ps1; do
|
||||
printf '\xef\xbb\xbf' > "$f.tmp" && cat "$f" >> "$f.tmp" && mv "$f.tmp" "$f"
|
||||
done
|
||||
```
|
||||
|
||||
**校验**:用 PowerShell 解析器(不执行)确认语法干净:
|
||||
|
||||
```powershell
|
||||
foreach ($f in @("deploy_runner_task.ps1","check_env.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 的脚本不受影响**。本目录的 `.ps1` 均含中文注释,现已统一加 BOM。
|
||||
|
||||
| 日志 | 位置 |
|
||||
|---|---|
|
||||
| runner **自身** stdout/stderr | `D:\Docker\act_runner\log\runner-stdout.log` |
|
||||
| **每个 job** 的详细日志 | `D:\Docker\act_runner\log\job\` |
|
||||
|
||||
**看日志**:v4 的 `log` 段**没有 `file` 键**,两种落盘方式:
|
||||
|
||||
```yaml
|
||||
# 方式 A:按 job 落文件(推荐,防上报失败丢日志)
|
||||
log:
|
||||
level: info
|
||||
job:
|
||||
dir: "D:\\Docker\\act_runner\\log\\job"
|
||||
```
|
||||
|
||||
```powershell
|
||||
# 方式 B:前台运行 / 包装脚本里重定向
|
||||
.\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 拉取问题(你的环境很可能命中)
|
||||
|
||||
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 步骤报网络错误,再换成上面的零依赖写法。
|
||||
|
||||
---
|
||||
|
||||
## 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_* 机器级变量(§5.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 不是服务型程序,必须用【任务计划程序】(管理员 PowerShell)
|
||||
powershell -ExecutionPolicy Bypass -File "D:\Docker\act_runner\deploy_runner_task.ps1"
|
||||
|
||||
# 等价手动:
|
||||
# Register-ScheduledTask -TaskName GiteaActionsRunner -Action (
|
||||
# New-ScheduledTaskAction -Execute "D:\Docker\act_runner\run_runner.cmd") `
|
||||
# -Trigger (New-ScheduledTaskTrigger -AtStartup) `
|
||||
# -Principal (New-ScheduledTaskPrincipal -UserId SYSTEM -LogonType ServiceAccount -RunLevel Highest)
|
||||
# Start-ScheduledTask -TaskName GiteaActionsRunner
|
||||
|
||||
# 5) ⚠️⚠️ 把 pre-commit 装到【全局】site-packages(否则 L0 必挂,见 §5.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,显式覆盖、报错直指变量名);
|
||||
# 全局 site-packages 安装保留为未设变量时的兜底。设完同样要重启 runner 任务。
|
||||
# (管理员 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')
|
||||
|
||||
# 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"
|
||||
```
|
||||
|
||||
> **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 单测)。
|
||||
|
||||
**配套脚本**(均在 `D:\Docker\act_runner\`,**均已加 UTF-8 BOM**,可直接在 WinPS 5.1 运行):
|
||||
|
||||
| 脚本 | 用途 | 需要管理员 |
|
||||
|---|---|---|
|
||||
| `check_env.ps1` | 环境自检(Gitea 可达 / 工具链 / MSVC / 任务状态) | 否 |
|
||||
| `deploy_runner_task.ps1` | **一键部署**(自检 + 清旧服务 + 建计划任务 + 启动 + 验证) | **是** |
|
||||
|
||||
> 早前基于 Windows 服务的 `deploy_runner.ps1` / `install_service.ps1` 已删除
|
||||
> —— 那条路走不通(§4.0)。
|
||||
|
||||
---
|
||||
|
||||
## 变更记录
|
||||
|
||||
| 日期 | 变更 |
|
||||
|---|---|
|
||||
| 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 拉取的零依赖改法 |
|
||||
@@ -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
@@ -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`,本表只登记不展开。
|
||||
|
||||
## 使用方式
|
||||
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Pick a Python interpreter that actually works, for the CI jobs (and for
|
||||
local use). Prints diagnostics with Write-Host and emits ONLY the chosen
|
||||
interpreter path on the success stream, so a caller can capture it:
|
||||
|
||||
$py = & './scripts/ci_find_python.ps1' -NeedPreCommit -WriteEnv
|
||||
|
||||
.DESCRIPTION
|
||||
Why this file exists (2026-10-11 CI incident):
|
||||
|
||||
The DTU_* env-var refactor moved the interpreter probe inline into
|
||||
.gitea/workflows/ci.yml and narrowed it to two candidates
|
||||
(PATH python + repo .venv). On the Windows runner BOTH are dead ends:
|
||||
|
||||
* PATH python for the SYSTEM account is C:\msys64\ucrt64\bin\python.exe,
|
||||
which has no pre-commit module;
|
||||
* the repo .venv is gitignored, so it does not exist in a fresh runner
|
||||
workspace at all.
|
||||
|
||||
The interpreter that does work (a machine-wide install such as
|
||||
C:\ProgramData\anaconda3\python.exe) was no longer probed, and no
|
||||
machine-level DTU_PRECOMMIT_PYTHON had ever been set -> every L0 job died
|
||||
in "environment check" before running a single hook.
|
||||
|
||||
So discovery lives here now: one implementation, shared by both CI jobs and
|
||||
runnable offline, and it keeps the well-known machine-wide install
|
||||
locations as fallbacks.
|
||||
|
||||
Probe order (see the DTU_* local-tool-path plan under docs/ -- this file is
|
||||
kept pure ASCII on purpose: PS 5.1 decodes BOM-less files as CP936):
|
||||
1. DTU_PRECOMMIT_PYTHON -- explicit override. If it is set but the file
|
||||
is missing, or it cannot satisfy the requirement, this script FAILS
|
||||
with the variable name and does NOT silently fall back.
|
||||
2. python on PATH
|
||||
3. <repo>\.venv\Scripts\python.exe
|
||||
4. well-known machine-wide installs (Program Files / ProgramData /
|
||||
%LOCALAPPDATA%\Programs\Python), newest version first
|
||||
|
||||
.PARAMETER NeedPreCommit
|
||||
Require the interpreter to expose the pre-commit module (L0 needs it).
|
||||
Probed with `-s` (no user site-packages) on purpose: the runner service
|
||||
runs as SYSTEM, which cannot see a USER-scope site install at all, so only
|
||||
an interpreter that works without user-site is accepted.
|
||||
|
||||
.PARAMETER WriteEnv
|
||||
Append PYTHON=<path> to $env:GITHUB_ENV so later steps can use $env:PYTHON.
|
||||
Written as ASCII: Out-File's PS 5.1 default is UTF-16LE, which appends
|
||||
foreign-encoded lines to a file other steps also append to.
|
||||
|
||||
.OUTPUTS
|
||||
The chosen python.exe path (single line, success stream).
|
||||
|
||||
.NOTES
|
||||
Keep this file pure ASCII -- Windows PowerShell 5.1 decodes BOM-less files
|
||||
as CP936, and non-ASCII bytes break string quoting (same trap as ci.yml).
|
||||
Exit codes: 0 = found, 2 = no usable interpreter / bad DTU_* override.
|
||||
#>
|
||||
param(
|
||||
[switch]$NeedPreCommit,
|
||||
[switch]$WriteEnv
|
||||
)
|
||||
|
||||
# Native probes write to stderr; the runner injects $ErrorActionPreference
|
||||
# ='Stop', which would turn that into a terminating NativeCommandError and
|
||||
# abort the probe loop before the next candidate is tried.
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
function Test-Interpreter {
|
||||
param([string]$Exe, [bool]$RequirePreCommit)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Exe)) { return $false }
|
||||
if (-not (Test-Path -LiteralPath $Exe -PathType Leaf)) { return $false }
|
||||
|
||||
$saved = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
try {
|
||||
if ($RequirePreCommit) {
|
||||
& $Exe -s -m pre_commit --version 2>&1 | Out-Null
|
||||
}
|
||||
else {
|
||||
& $Exe --version 2>&1 | Out-Null
|
||||
}
|
||||
return ($LASTEXITCODE -eq 0)
|
||||
}
|
||||
catch {
|
||||
return $false
|
||||
}
|
||||
finally {
|
||||
$ErrorActionPreference = $saved
|
||||
}
|
||||
}
|
||||
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
$require = [bool]$NeedPreCommit
|
||||
$what = if ($require) { 'pre-commit' } else { 'a runnable interpreter' }
|
||||
|
||||
# --- 1. explicit override: honour it or fail loudly -------------------------
|
||||
$explicit = $env:DTU_PRECOMMIT_PYTHON
|
||||
if (-not [string]::IsNullOrWhiteSpace($explicit)) {
|
||||
$explicit = $explicit.Trim()
|
||||
if (-not (Test-Path -LiteralPath $explicit -PathType Leaf)) {
|
||||
Write-Host "::error::DTU_PRECOMMIT_PYTHON is set but does not exist: $explicit"
|
||||
Write-Host '::error::fix the variable (full path to a python.exe) or unset it to use auto-discovery'
|
||||
exit 2
|
||||
}
|
||||
if (-not (Test-Interpreter -Exe $explicit -RequirePreCommit $require)) {
|
||||
Write-Host "::error::DTU_PRECOMMIT_PYTHON does not provide $what (probed without user-site, i.e. SYSTEM-visible): $explicit"
|
||||
Write-Host '::error::fix the variable, or unset it to use auto-discovery'
|
||||
exit 2
|
||||
}
|
||||
Write-Host "using python (DTU_PRECOMMIT_PYTHON): $explicit"
|
||||
if ($WriteEnv -and $env:GITHUB_ENV) {
|
||||
"PYTHON=$explicit" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding ascii
|
||||
}
|
||||
Write-Output $explicit
|
||||
exit 0
|
||||
}
|
||||
|
||||
# --- 2..4 auto-discovery ----------------------------------------------------
|
||||
$programFiles = $env:ProgramFiles
|
||||
$programFilesX86 = ${env:ProgramFiles(x86)}
|
||||
if ([string]::IsNullOrWhiteSpace($programFiles)) { $programFiles = 'C:\Program Files' }
|
||||
if ([string]::IsNullOrWhiteSpace($programFilesX86)) { $programFilesX86 = 'C:\Program Files (x86)' }
|
||||
|
||||
$candidates = @()
|
||||
|
||||
$onPath = Get-Command python -ErrorAction SilentlyContinue
|
||||
if ($onPath) { $candidates += $onPath.Source }
|
||||
|
||||
$candidates += (Join-Path $repoRoot '.venv\Scripts\python.exe')
|
||||
|
||||
# Newest first: a fresh install is the usual intent.
|
||||
foreach ($ver in @('Python313', 'Python312', 'Python311', 'Python310')) {
|
||||
$candidates += (Join-Path $programFiles "$ver\python.exe")
|
||||
$candidates += (Join-Path $programFilesX86 "$ver\python.exe")
|
||||
if ($env:LOCALAPPDATA) {
|
||||
$candidates += (Join-Path $env:LOCALAPPDATA "Programs\Python\$ver\python.exe")
|
||||
}
|
||||
}
|
||||
|
||||
# Machine-wide distributions (these are what the runner actually had).
|
||||
if ($env:ProgramData) {
|
||||
foreach ($dist in @('anaconda3', 'miniconda3')) {
|
||||
$candidates += (Join-Path $env:ProgramData "$dist\python.exe")
|
||||
}
|
||||
}
|
||||
$candidates += 'C:\ProgramData\anaconda3\python.exe'
|
||||
$candidates += 'C:\ProgramData\miniconda3\python.exe'
|
||||
|
||||
$tried = New-Object System.Collections.Generic.List[string]
|
||||
$chosen = $null
|
||||
foreach ($c in $candidates) {
|
||||
if ([string]::IsNullOrWhiteSpace($c)) { continue }
|
||||
if ($tried.Contains($c)) { continue }
|
||||
$tried.Add($c) | Out-Null
|
||||
if (Test-Interpreter -Exe $c -RequirePreCommit $require) { $chosen = $c; break }
|
||||
}
|
||||
|
||||
if (-not $chosen) {
|
||||
Write-Host "::error::no Python providing $what found (tried `$DTU_PRECOMMIT_PYTHON`, PATH python, repo .venv, and the well-known machine-wide installs)"
|
||||
foreach ($t in $tried) { Write-Host " tried: $t" }
|
||||
Write-Host '::error::note: the runner service runs as SYSTEM, so a USER-scope `pip install --user pre-commit` is invisible to it'
|
||||
Write-Host '::error::fix: set a MACHINE-level DTU_PRECOMMIT_PYTHON (see scripts/ci_runner_bootstrap.ps1 -SetEnv), or install pre-commit into a machine-wide interpreter'
|
||||
exit 2
|
||||
}
|
||||
|
||||
Write-Host "using python (auto-discovered): $chosen"
|
||||
# Out-Host, NOT the pipeline: a native command's stdout joins the success
|
||||
# stream, which would pollute the single-line path the caller captures.
|
||||
& $chosen --version 2>&1 | Out-Host
|
||||
if ($require) { & $chosen -s -m pre_commit --version 2>&1 | Out-Host }
|
||||
|
||||
if ($WriteEnv -and $env:GITHUB_ENV) {
|
||||
"PYTHON=$chosen" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding ascii
|
||||
}
|
||||
|
||||
Write-Output $chosen
|
||||
exit 0
|
||||
@@ -0,0 +1,167 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Commit-message gate for CI: run the `gitlint` pre-commit hook over every
|
||||
commit in a pushed / pull-request range.
|
||||
|
||||
.DESCRIPTION
|
||||
Why this file exists (2026-10-11 CI audit):
|
||||
|
||||
.pre-commit-config.yaml defines 17 hooks, but CI reported 16
|
||||
(passed=15 failed=0 skipped=1). The missing one was `gitlint`: its hook
|
||||
definition declares `stages: [commit-msg]`, while `pre-commit run
|
||||
--all-files` only runs the `pre-commit` stage. So the commit-message rule
|
||||
from AGENTS.md (single-line Chinese title < 100 chars) was enforced on
|
||||
developer machines and NEVER in CI -- a silent gate gap, not a failure.
|
||||
|
||||
pre-commit cannot check "the commits of this push" on its own, so this
|
||||
script feeds each commit message to the hook through
|
||||
`--hook-stage commit-msg --commit-msg-filename <file>`, one commit at a
|
||||
time, and aggregates the result.
|
||||
|
||||
Message files are written INSIDE the workspace (under ci-artifacts/, which
|
||||
is gitignored) for two hard reasons:
|
||||
* pre-commit rejects a message path on another drive than the repo
|
||||
("path is on mount 'C:', start on mount 'D:'" -- measured here);
|
||||
* they must be UTF-8 WITHOUT BOM, because a BOM counts as a character of
|
||||
the title and would distort the 100-char rule.
|
||||
|
||||
.PARAMETER BaseSha
|
||||
Exclusive lower bound. Empty, all-zero (branch creation / deletion), or an
|
||||
unreachable SHA (force push) falls back to the newest -MaxCommits commits.
|
||||
|
||||
.PARAMETER HeadSha
|
||||
Inclusive upper bound. Defaults to HEAD.
|
||||
|
||||
.PARAMETER MaxCommits
|
||||
Upper bound on how many commits are checked, so a big range cannot run for
|
||||
hours. Merge commits are skipped (their messages are generated by git, and
|
||||
the local hook is what guards hand-written ones).
|
||||
|
||||
.PARAMETER PythonExe
|
||||
Interpreter that provides pre-commit. Defaults to $env:PYTHON, then to
|
||||
scripts/ci_find_python.ps1 auto-discovery.
|
||||
|
||||
.OUTPUTS
|
||||
None. Diagnostics go to the host as GitHub workflow commands.
|
||||
Exit codes: 0 = all messages valid, 1 = at least one invalid, 2 = setup error.
|
||||
|
||||
.NOTES
|
||||
Keep this file pure ASCII (PowerShell 5.1 decodes BOM-less files as CP936).
|
||||
#>
|
||||
param(
|
||||
[string]$BaseSha = '',
|
||||
[string]$HeadSha = 'HEAD',
|
||||
[int]$MaxCommits = 50,
|
||||
[string]$PythonExe = ''
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
Push-Location -LiteralPath $repoRoot
|
||||
try {
|
||||
# git emits UTF-8 commit messages; PS 5.1 would otherwise decode them with
|
||||
# the console code page (CP936) and mangle every Chinese title.
|
||||
$savedConsoleEncoding = [Console]::OutputEncoding
|
||||
try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch { }
|
||||
|
||||
try {
|
||||
# --- resolve the interpreter ----------------------------------------
|
||||
if ([string]::IsNullOrWhiteSpace($PythonExe)) { $PythonExe = $env:PYTHON }
|
||||
if ([string]::IsNullOrWhiteSpace($PythonExe)) {
|
||||
$PythonExe = & (Join-Path $PSScriptRoot 'ci_find_python.ps1') -NeedPreCommit
|
||||
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($PythonExe)) {
|
||||
Write-Host '::error::gitlint: no Python with pre-commit available'
|
||||
exit 2
|
||||
}
|
||||
}
|
||||
$PythonExe = "$PythonExe".Trim()
|
||||
Write-Host "gitlint: using python $PythonExe"
|
||||
|
||||
# --- resolve the commit range ---------------------------------------
|
||||
$range = ''
|
||||
$cleanBase = "$BaseSha".Trim()
|
||||
if ($cleanBase -match '^[0-9a-fA-F]{40}$' -and $cleanBase -notmatch '^0+$') {
|
||||
$probe = "$cleanBase^{commit}"
|
||||
git cat-file -e $probe 2>$null
|
||||
if ($LASTEXITCODE -eq 0) { $range = "$cleanBase..$HeadSha" }
|
||||
else { Write-Host "::warning::gitlint: base $cleanBase is not reachable in this clone -- falling back to the newest $MaxCommits commit(s)" }
|
||||
}
|
||||
|
||||
$savedPref = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
if ($range) {
|
||||
$shas = git log --no-merges --format=%H $range 2>$null
|
||||
}
|
||||
else {
|
||||
$shas = git log --no-merges --format=%H -n $MaxCommits $HeadSha 2>$null
|
||||
}
|
||||
$ErrorActionPreference = $savedPref
|
||||
|
||||
$shas = @($shas | Where-Object { $_ -and "$_".Trim() })
|
||||
if ($shas.Count -eq 0) {
|
||||
Write-Host '::notice::gitlint: no non-merge commits in range -- nothing to check'
|
||||
exit 0
|
||||
}
|
||||
if ($shas.Count -gt $MaxCommits) {
|
||||
Write-Host "::warning::gitlint: range has $($shas.Count) commits, checking the newest $MaxCommits"
|
||||
$shas = $shas[0..($MaxCommits - 1)]
|
||||
}
|
||||
|
||||
# --- message files: inside the workspace, UTF-8 without BOM ---------
|
||||
$msgDir = Join-Path $repoRoot 'ci-artifacts\gitlint'
|
||||
New-Item -ItemType Directory -Force -Path $msgDir | Out-Null
|
||||
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
|
||||
|
||||
$checked = 0
|
||||
$failed = 0
|
||||
foreach ($sha in $shas) {
|
||||
$sha = "$sha".Trim()
|
||||
$checked++
|
||||
|
||||
$savedPref = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$body = git log -1 --format=%B $sha 2>$null
|
||||
$ErrorActionPreference = $savedPref
|
||||
$titleLine = (git log -1 --format=%s $sha 2>$null | Select-Object -First 1)
|
||||
|
||||
$msgPath = Join-Path $msgDir "$sha.txt"
|
||||
$text = (($body | Out-String) -replace "`r`n", "`n").TrimEnd("`n")
|
||||
[System.IO.File]::WriteAllText($msgPath, $text, $utf8NoBom)
|
||||
|
||||
Write-Host "----------------------------------------"
|
||||
Write-Host "commit $sha title: $titleLine"
|
||||
|
||||
$savedPref = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
& $PythonExe -s -m pre_commit run --hook-stage commit-msg `
|
||||
--commit-msg-filename $msgPath gitlint 2>&1
|
||||
$rc = $LASTEXITCODE
|
||||
$ErrorActionPreference = $savedPref
|
||||
|
||||
if ($rc -ne 0) {
|
||||
$failed++
|
||||
Write-Host "::error::gitlint: commit $($sha.Substring(0, 8)) rejected -- `"$titleLine`""
|
||||
}
|
||||
}
|
||||
|
||||
$summary = "gitlint: $checked commit message(s) checked, $failed failed"
|
||||
Write-Host "::notice::$summary"
|
||||
if ($env:GITHUB_STEP_SUMMARY) {
|
||||
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value '### Commit message gate (gitlint)' -Encoding utf8
|
||||
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value $summary -Encoding utf8
|
||||
}
|
||||
|
||||
if ($failed -gt 0) {
|
||||
Write-Host '::error::commit message gate failed -- rule: single-line Chinese title, < 100 chars (see docs/ pre-commit usage guide + .gitlint)'
|
||||
exit 1
|
||||
}
|
||||
exit 0
|
||||
}
|
||||
finally {
|
||||
try { [Console]::OutputEncoding = $savedConsoleEncoding } catch { }
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
+118
-3
@@ -8,9 +8,16 @@
|
||||
跑同一套判定,命令与退出码与 CI 一一对应。
|
||||
|
||||
覆盖范围(与 CI 完全一致):
|
||||
L0 静态门禁 = `pre-commit run --all-files`
|
||||
L0 静态门禁 = `pre-commit run --all-files` + 提交信息门禁(gitlint)
|
||||
L0.5 主机侧纯逻辑单测 = `test/host/**/run_unit_test.py`
|
||||
|
||||
⚠️ gitlint 必须**单独跑**(2026-10-11 审计):`stages: [commit-msg]` 写在 gitlint **上游**
|
||||
的 `.pre-commit-hooks.yaml`(项目 `.pre-commit-config.yaml` 里查不到这一行),而
|
||||
`pre-commit run --all-files` 只跑 pre-commit stage —— 它既不在 --all-files 里,
|
||||
CI 也一度漏掉(18 个钩子只报 17 个,且表现为"全绿")。本脚本与 CI 的
|
||||
`scripts/ci_gitlint_range.ps1` 同语义:逐条把最近 N 条非 merge 提交的
|
||||
message 喂给钩子。
|
||||
|
||||
刻意**不含**(与 CI 同理由,见 workflow 文件头):
|
||||
- `@build`(需 Keil 授权;CI 也不跑,真编译是开发者本地红线)
|
||||
- `mem-numbers` 钩子(事实源 `.map` 是 Keil 产物——**本地若存在 .map 就照常跑**,
|
||||
@@ -23,6 +30,7 @@
|
||||
|
||||
汇总行(S3,2026-10-02,对齐 CI 的 ::notice::):
|
||||
[SUMMARY] L0: pre-commit hooks -- passed=N failed=M skipped=K
|
||||
[SUMMARY] gitlint: N commit message(s) checked, M failed
|
||||
[SUMMARY] L0.5: K runner(s), A/B assertions passed, fail=F
|
||||
|
||||
用法(仓库根目录):
|
||||
@@ -30,6 +38,7 @@
|
||||
& '.venv\\Scripts\\python.exe' scripts/ci_local.py --l0-only
|
||||
& '.venv\\Scripts\\python.exe' scripts/ci_local.py --l05-only
|
||||
& '.venv\\Scripts\\python.exe' scripts/ci_local.py --keep-going # 失败也继续跑完
|
||||
& '.venv\\Scripts\\python.exe' scripts/ci_local.py --commits 20 # 提交信息校验条数
|
||||
|
||||
设计依据:`.gitea/workflows/ci.yml`;docs/测试说明/测试开发标准流程.md §9
|
||||
创建日期:2026-10-01
|
||||
@@ -46,6 +55,30 @@ from pathlib import Path
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def _global_python_candidates():
|
||||
"""常见**机器级** Python 安装位置(必须与 scripts/ci_find_python.ps1 同序)。
|
||||
|
||||
两处探测一旦不同步,就会出现"本地能跑、CI 挂"的老毛病:2026-10-11 的
|
||||
L0 全灭就是 workflow 把候选收窄成 PATH python + 仓库 .venv(runner 上两者
|
||||
都是死路),而真正装了 pre-commit 的机器级解释器(如
|
||||
C:\\ProgramData\\anaconda3\\python.exe)不再被探测。
|
||||
"""
|
||||
out = []
|
||||
pf = os.environ.get("ProgramFiles", r"C:\Program Files")
|
||||
pf86 = os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)")
|
||||
local = os.environ.get("LOCALAPPDATA")
|
||||
pd = os.environ.get("ProgramData", r"C:\ProgramData")
|
||||
# 新版本优先:新装的解释器通常才是想要的。
|
||||
for ver in ("Python313", "Python312", "Python311", "Python310"):
|
||||
out.append(os.path.join(pf, ver, "python.exe"))
|
||||
out.append(os.path.join(pf86, ver, "python.exe"))
|
||||
if local:
|
||||
out.append(os.path.join(local, "Programs", "Python", ver, "python.exe"))
|
||||
for dist in ("anaconda3", "miniconda3"):
|
||||
out.append(os.path.join(pd, dist, "python.exe"))
|
||||
return out
|
||||
|
||||
|
||||
def _resolve_python():
|
||||
r"""挑一个真正装了 pre-commit 的解释器。
|
||||
|
||||
@@ -56,6 +89,7 @@ def _resolve_python():
|
||||
2. 当前解释器(sys.executable)
|
||||
3. 仓库 `.venv\Scripts\python.exe`
|
||||
4. PATH 上的 `python`
|
||||
5. 常见机器级安装位置(见 _global_python_candidates)
|
||||
找到即返回 (python_path, note);都找不到则返回 (当前解释器, None) 交由调用方报错。
|
||||
"""
|
||||
explicit = os.environ.get("DTU_PRECOMMIT_PYTHON", "").strip()
|
||||
@@ -80,12 +114,14 @@ def _resolve_python():
|
||||
sys.executable,
|
||||
str(REPO_ROOT / ".venv" / "Scripts" / "python.exe"),
|
||||
shutil.which("python"),
|
||||
]
|
||||
] + _global_python_candidates()
|
||||
seen = set()
|
||||
tried = []
|
||||
for py in candidates:
|
||||
if not py or py in seen or not os.path.isfile(py):
|
||||
continue
|
||||
seen.add(py)
|
||||
tried.append(py)
|
||||
try:
|
||||
cp = subprocess.run([py, "-m", "pre_commit", "--version"],
|
||||
capture_output=True, timeout=60)
|
||||
@@ -94,6 +130,9 @@ def _resolve_python():
|
||||
if cp.returncode == 0:
|
||||
note = None if py == sys.executable else "(自动选用,本解释器无 pre-commit)"
|
||||
return py, note
|
||||
print("[WARN] 未找到装有 pre-commit 的解释器,已尝试:")
|
||||
for py in tried:
|
||||
print(" - {}".format(py))
|
||||
return sys.executable, None
|
||||
|
||||
|
||||
@@ -170,6 +209,75 @@ def run_l0():
|
||||
return rc
|
||||
|
||||
|
||||
def run_gitlint(count):
|
||||
"""提交信息门禁:把最近 N 条非 merge 提交的 message 逐条喂给 gitlint 钩子。
|
||||
|
||||
与 CI 的 scripts/ci_gitlint_range.ps1 同语义(gitlint 声明
|
||||
`stages: [commit-msg]`,`pre-commit run --all-files` 永远跑不到它)。
|
||||
返回退出码(0/1/2)。
|
||||
"""
|
||||
print("")
|
||||
print("=" * 64)
|
||||
print(" L0b 提交信息门禁(gitlint / commit-msg stage,最近 {} 条)".format(count))
|
||||
print("=" * 64)
|
||||
|
||||
def _git(*args):
|
||||
return subprocess.run(["git"] + list(args), cwd=str(REPO_ROOT),
|
||||
capture_output=True, timeout=60)
|
||||
|
||||
try:
|
||||
out = _git("log", "--no-merges", "--format=%H", "-n", str(count), "HEAD")
|
||||
except (OSError, subprocess.TimeoutExpired) as e:
|
||||
print("[ERROR] 无法调用 git:{}".format(e))
|
||||
return 2
|
||||
if out.returncode != 0:
|
||||
print("[ERROR] git log 失败:{}".format(
|
||||
out.stderr.decode("utf-8", errors="replace").strip()))
|
||||
return 2
|
||||
|
||||
shas = [s.strip() for s in out.stdout.decode("utf-8", errors="replace").splitlines()
|
||||
if s.strip()]
|
||||
if not shas:
|
||||
print("[INFO] 无可校验提交 —— 跳过")
|
||||
print("[SUMMARY] gitlint: 0 commit message(s) checked, 0 failed")
|
||||
return 0
|
||||
|
||||
# 消息文件必须写在【仓库内】且【UTF-8 无 BOM】:
|
||||
# - 跨盘符会被 pre-commit 直接拒绝(path is on mount 'C:', start on mount 'D:')
|
||||
# - BOM 会占掉标题 1 个字符,扭曲 .gitlint 的 100 字符上限
|
||||
msg_dir = REPO_ROOT / "ci-artifacts" / "gitlint-local"
|
||||
msg_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
checked = 0
|
||||
failed = 0
|
||||
try:
|
||||
for sha in shas:
|
||||
checked += 1
|
||||
body = _git("log", "-1", "--format=%B", sha)
|
||||
title = _git("log", "-1", "--format=%s", sha)
|
||||
text = body.stdout.decode("utf-8", errors="replace").rstrip("\r\n")
|
||||
title_line = title.stdout.decode("utf-8", errors="replace").strip()
|
||||
|
||||
msg_path = msg_dir / (sha + ".txt")
|
||||
msg_path.write_text(text, encoding="utf-8", newline="\n")
|
||||
|
||||
print("")
|
||||
print("--- {} {}".format(sha[:8], title_line))
|
||||
rc, _ = _run_capture([PY, "-m", "pre_commit", "run",
|
||||
"--hook-stage", "commit-msg",
|
||||
"--commit-msg-filename", str(msg_path),
|
||||
"gitlint"])
|
||||
if rc != 0:
|
||||
failed += 1
|
||||
print("[FAIL] gitlint 拒绝该提交信息:{}".format(title_line))
|
||||
finally:
|
||||
shutil.rmtree(str(msg_dir), ignore_errors=True)
|
||||
|
||||
print("[SUMMARY] gitlint: {} commit message(s) checked, {} failed".format(
|
||||
checked, failed))
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
def run_l05(args):
|
||||
"""L0.5:主机侧单测。返回退出码(0/1/2)。"""
|
||||
print("")
|
||||
@@ -235,6 +343,8 @@ def main(argv=None):
|
||||
g.add_argument("--l05-only", action="store_true", help="只跑 L0.5 主机侧单测")
|
||||
ap.add_argument("--keep-going", action="store_true",
|
||||
help="某步失败也继续跑后续步骤(默认首错即停)")
|
||||
ap.add_argument("--commits", type=int, default=10, metavar="N",
|
||||
help="提交信息门禁(gitlint)校验最近 N 条非 merge 提交(默认 10)")
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
print("=== 本地 CI(等价 .gitea/workflows/ci.yml)===")
|
||||
@@ -263,10 +373,15 @@ def main(argv=None):
|
||||
try:
|
||||
if not args.l05_only:
|
||||
rc = run_l0()
|
||||
if rc != 0 and not args.keep_going:
|
||||
# 提交信息门禁独立于 --all-files(stage 不同),故单独跑一段;
|
||||
# 即使 L0 已失败也照跑,一次看全所有违规项。
|
||||
rc_msg = run_gitlint(args.commits)
|
||||
if rc != 0 or rc_msg != 0:
|
||||
if not args.keep_going:
|
||||
print("")
|
||||
print("[FAIL] L0 未通过 —— 中止(--keep-going 可继续跑 L0.5)")
|
||||
return 1
|
||||
rc = rc or rc_msg
|
||||
if not args.l0_only:
|
||||
rc05 = run_l05(args)
|
||||
rc = rc or rc05
|
||||
|
||||
@@ -0,0 +1,534 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Make the Gitea Actions runner on this machine survive restarts, and give it
|
||||
a toolchain the SYSTEM service account can actually see.
|
||||
|
||||
.DESCRIPTION
|
||||
Why this file exists (2026-10-11 CI audit, the "many jobs never ran" bug):
|
||||
|
||||
Six consecutive runs (16..21, i.e. 12 jobs) produced ZERO log lines. The
|
||||
runner `win-stm32-ci` was simply offline, and two independent causes made
|
||||
that permanent:
|
||||
|
||||
1. NO SUPERVISION. gitea-runner v4.0.1 `daemon` has no service support
|
||||
(v4 removed `daemon install`), and it EXITS when the first Declare
|
||||
call fails. The runner log shows exactly that after a Gitea restart:
|
||||
|
||||
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 ...
|
||||
|
||||
...and the process is gone. So one Gitea restart (or one reboot)
|
||||
silently kills CI until a human notices days later. Queued runs then
|
||||
get cancelled by the NEXT push, because ci.yml sets
|
||||
`concurrency.cancel-in-progress: true` -- which is why the symptom
|
||||
looks like "tasks vanish" rather than "tasks fail".
|
||||
|
||||
2. NO MACHINE-LEVEL TOOLCHAIN. The runner service runs as SYSTEM.
|
||||
pre-commit / cppcheck were only reachable through the interactive
|
||||
user's PATH, and no machine-level DTU_* variable was ever set, so
|
||||
even a live runner died in the L0 environment check.
|
||||
|
||||
This script installs the missing supervision (a watchdog launcher + a
|
||||
scheduled task) and, with -SetEnv, writes the machine-level DTU_* variables
|
||||
by discovering the tools the same way CI does.
|
||||
|
||||
Run elevated, from the repo:
|
||||
|
||||
powershell -ExecutionPolicy Bypass -File scripts\ci_runner_bootstrap.ps1 -SetEnv -StartNow
|
||||
|
||||
.PARAMETER RunnerDir
|
||||
Directory holding gitea-runner.exe / config.yaml / .runner.
|
||||
|
||||
.PARAMETER GiteaUrl
|
||||
Instance address the runner registered against (must match .runner's
|
||||
"address"; the runner talks to it directly, not through the public URL).
|
||||
|
||||
.PARAMETER TaskName
|
||||
Scheduled-task name for the watchdog.
|
||||
|
||||
.PARAMETER SetEnv
|
||||
Discover and write MACHINE-level DTU_PRECOMMIT_PYTHON / DTU_CPPCHECK /
|
||||
DTU_MSVC_CL. Existing values are preserved unless -Force.
|
||||
|
||||
.PARAMETER StartNow
|
||||
Start the task after registering it.
|
||||
|
||||
.PARAMETER Status
|
||||
Only report: task state, daemon process, Gitea reachability.
|
||||
|
||||
.PARAMETER DryRun
|
||||
Build and validate the action / trigger / settings objects and print what
|
||||
WOULD be done, writing and registering nothing. This is also the only way
|
||||
to verify this script without elevation.
|
||||
|
||||
.PARAMETER Force
|
||||
With -SetEnv, overwrite existing machine-level values.
|
||||
|
||||
.OUTPUTS
|
||||
Progress lines. Exit codes: 0 = ok, 1 = precondition failed, 2 = not elevated.
|
||||
|
||||
.NOTES
|
||||
Keep this file pure ASCII (PowerShell 5.1 decodes BOM-less files as CP936).
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$RunnerDir = 'D:\Docker\act_runner',
|
||||
[string]$GiteaUrl = 'http://127.0.0.1:3001',
|
||||
[string]$TaskName = 'GiteaActionsRunner',
|
||||
[switch]$SetEnv,
|
||||
[switch]$StartNow,
|
||||
[switch]$Status,
|
||||
[switch]$DryRun,
|
||||
[switch]$Force
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Info($m) { Write-Host "[INFO] $m" -ForegroundColor Cyan }
|
||||
function Ok($m) { Write-Host "[ OK ] $m" -ForegroundColor Green }
|
||||
function Warn($m) { Write-Host "[WARN] $m" -ForegroundColor Yellow }
|
||||
function Fail($m) { Write-Host "[FAIL] $m" -ForegroundColor Red }
|
||||
|
||||
function Test-Admin {
|
||||
$id = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
(New-Object Security.Principal.WindowsPrincipal($id)
|
||||
).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
}
|
||||
|
||||
function Test-Gitea([string]$url) {
|
||||
try {
|
||||
Invoke-WebRequest -Uri "$url/api/v1/version" -UseBasicParsing -TimeoutSec 10 | Out-Null
|
||||
return $true
|
||||
}
|
||||
catch { return $false }
|
||||
}
|
||||
|
||||
function Get-MachineEnv([string]$name) {
|
||||
$v = [Environment]::GetEnvironmentVariable($name, 'Machine')
|
||||
if ([string]::IsNullOrWhiteSpace($v)) { return $null }
|
||||
return $v
|
||||
}
|
||||
|
||||
function Set-MachineEnv([string]$name, [string]$value, [string]$label) {
|
||||
$cur = Get-MachineEnv $name
|
||||
if ($cur) {
|
||||
if ($cur -ieq $value) { Ok "$label already set: $cur"; return }
|
||||
if (-not $Force) {
|
||||
Warn "$label already set to a different value: $cur (use -Force to overwrite)"
|
||||
return
|
||||
}
|
||||
Warn "$label overwriting $cur -> $value"
|
||||
}
|
||||
[Environment]::SetEnvironmentVariable($name, $value, 'Machine')
|
||||
Ok "$label set (MACHINE scope): $value"
|
||||
}
|
||||
|
||||
function Find-Cppcheck {
|
||||
$pf = $env:ProgramFiles
|
||||
$pf86 = ${env:ProgramFiles(x86)}
|
||||
if ([string]::IsNullOrWhiteSpace($pf)) { $pf = 'C:\Program Files' }
|
||||
if ([string]::IsNullOrWhiteSpace($pf86)) { $pf86 = 'C:\Program Files (x86)' }
|
||||
foreach ($c in @((Join-Path $pf 'Cppcheck\cppcheck.exe'),
|
||||
(Join-Path $pf86 'Cppcheck\cppcheck.exe'))) {
|
||||
if (Test-Path -LiteralPath $c) { return $c }
|
||||
}
|
||||
$cmd = Get-Command cppcheck -ErrorAction SilentlyContinue
|
||||
if ($cmd) { return $cmd.Source }
|
||||
return $null
|
||||
}
|
||||
|
||||
function Find-MsvcCl {
|
||||
$pf = $env:ProgramFiles
|
||||
$pf86 = ${env:ProgramFiles(x86)}
|
||||
if ([string]::IsNullOrWhiteSpace($pf)) { $pf = 'C:\Program Files' }
|
||||
if ([string]::IsNullOrWhiteSpace($pf86)) { $pf86 = 'C:\Program Files (x86)' }
|
||||
$roots = @()
|
||||
foreach ($year in @('2019', '2022', '2026', '18')) {
|
||||
foreach ($edition in @('BuildTools', 'Community', 'Professional', 'Enterprise')) {
|
||||
$roots += (Join-Path $pf86 "Microsoft Visual Studio\$year\$edition")
|
||||
$roots += (Join-Path $pf "Microsoft Visual Studio\$year\$edition")
|
||||
}
|
||||
}
|
||||
foreach ($r in $roots) {
|
||||
if (-not (Test-Path -LiteralPath $r)) { continue }
|
||||
$cl = Get-ChildItem -LiteralPath $r -Recurse -Filter 'cl.exe' -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.FullName -like '*Hostx64\x64*' } |
|
||||
Select-Object -First 1
|
||||
if ($cl) { return $cl.FullName }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
# Builds the task objects in ONE place so -DryRun validates exactly what the
|
||||
# real run registers.
|
||||
#
|
||||
# MEASURED TRAP (2026-10-11, hit on the user's machine): `StartWhenAvailable`
|
||||
# is a TASK SETTINGS property (MSFT_TaskSettings, inherited by
|
||||
# MSFT_TaskSettings3) -- it does NOT exist on triggers. Assigning it to the
|
||||
# trigger aborts the script with a SetValueInvocationException along the lines
|
||||
# of "setting StartWhenAvailable threw an exception: property
|
||||
# 'StartWhenAvailable' is not found on this object" (message localized),
|
||||
# and, worse, that happened AFTER the old task had already been unregistered,
|
||||
# leaving CI with no task at all. Hence: build everything first, only then
|
||||
# unregister the old task.
|
||||
function New-WatchdogTaskPlan {
|
||||
param([string]$Launcher, [string]$WorkDir)
|
||||
|
||||
$action = New-ScheduledTaskAction `
|
||||
-Execute 'powershell.exe' `
|
||||
-Argument "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$Launcher`"" `
|
||||
-WorkingDirectory $WorkDir
|
||||
|
||||
# Boot trigger + a 1 minute grace period: Gitea may still be starting when
|
||||
# the session-0 boot fires (and the watchdog waits for it anyway).
|
||||
$trigger = New-ScheduledTaskTrigger -AtStartup
|
||||
$trigger.Delay = 'PT1M'
|
||||
|
||||
$settings = New-ScheduledTaskSettingsSet `
|
||||
-AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
|
||||
-ExecutionTimeLimit ([TimeSpan]::Zero) `
|
||||
-RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1) `
|
||||
-MultipleInstances IgnoreNew
|
||||
# StartWhenAvailable also covers "the machine was off when this was missed".
|
||||
$settings.StartWhenAvailable = $true
|
||||
|
||||
return [pscustomobject]@{ Action = $action; Trigger = $trigger; Settings = $settings }
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# -Status: read-only report
|
||||
# ---------------------------------------------------------------------------
|
||||
if ($Status) {
|
||||
Info "task $TaskName"
|
||||
$t = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
||||
if ($t) {
|
||||
Ok ("task state: {0}" -f $t.State)
|
||||
$li = Get-ScheduledTaskInfo -TaskName $TaskName -ErrorAction SilentlyContinue
|
||||
if ($li) {
|
||||
Info ("last run: {0} | last result: {1} | next run: {2}" -f `
|
||||
$li.LastRunTime, ('0x{0:X}' -f $li.LastTaskResult), $li.NextRunTime)
|
||||
}
|
||||
}
|
||||
else {
|
||||
Warn "task $TaskName not found"
|
||||
if (-not (Test-Admin)) {
|
||||
Warn ' this session is NOT elevated: a non-admin session cannot always see'
|
||||
Warn ' every task, so re-run -Status elevated before concluding it is missing'
|
||||
}
|
||||
}
|
||||
|
||||
$proc = @(Get-Process | Where-Object { $_.ProcessName -match 'gitea-runner|act_runner' })
|
||||
if ($proc.Count -eq 1) {
|
||||
Ok ("daemon running: pid {0}" -f $proc[0].Id)
|
||||
} elseif ($proc.Count -gt 1) {
|
||||
Warn (" {0} daemon processes -- only ONE can hold .runner.lock, the rest are in a restart loop" -f $proc.Count)
|
||||
Warn ' fix: re-run this script elevated with -StartNow (it clears stale instances first)'
|
||||
} else { Warn 'daemon not running' }
|
||||
|
||||
# A watchdog that cannot get the lock fails every 10s while the runner still
|
||||
# reports ONLINE, so the loop is invisible from the web UI. Count watchdogs and
|
||||
# grep the daemon log for the lock error (measured 2026-10-12).
|
||||
# MEASURED TRAP #3 (2026-10-12): a NON-ELEVATED session cannot read SYSTEM
|
||||
# process command lines -- CommandLine comes back $null, so the filter below
|
||||
# silently matches NOTHING and reporting "no watchdog" would be a lie (this
|
||||
# bit us twice while diagnosing). Say what we cannot see instead.
|
||||
if (-not (Test-Admin)) {
|
||||
Info ' watchdog count skipped: needs elevation (SYSTEM command lines are hidden)'
|
||||
} else {
|
||||
$wd = @(Get-CimInstance Win32_Process -Filter "Name='powershell.exe'" -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.CommandLine -match 'run_runner_watchdog\.ps1' })
|
||||
if ($wd.Count -gt 1) {
|
||||
Warn (" {0} watchdog processes (pids {1}) -- an old one survived a task re-registration" -f `
|
||||
$wd.Count, (($wd | ForEach-Object { $_.ProcessId }) -join ','))
|
||||
Warn ' and is racing the new one for .runner.lock; re-run elevated with -StartNow'
|
||||
} elseif ($wd.Count -eq 1) {
|
||||
Ok ("watchdog running: pid {0}" -f $wd[0].ProcessId)
|
||||
} else {
|
||||
Warn 'no watchdog process found (task not running?)'
|
||||
}
|
||||
}
|
||||
# Restart-loop detection. Do NOT just grep the log tail: the file is never
|
||||
# rotated, so old failures stay in the last lines forever and a HEALTHY runner
|
||||
# keeps "reporting" a stale error (measured false positive 2026-10-12). The
|
||||
# reliable signal is a daemon that keeps dying young -- a process alive longer
|
||||
# than the 10s restart interval cannot be inside that loop. CIM CreationDate is
|
||||
# readable without elevation; Process.StartTime is NOT for SYSTEM processes.
|
||||
$dmon = Get-CimInstance Win32_Process -Filter "Name='gitea-runner.exe'" -ErrorAction SilentlyContinue |
|
||||
Select-Object -First 1
|
||||
$upSec = if ($dmon) { [int]((Get-Date) - $dmon.CreationDate).TotalSeconds } else { -1 }
|
||||
if ($upSec -ge 90) {
|
||||
Ok ("daemon up for {0} min -- no restart loop" -f [int]($upSec / 60))
|
||||
} else {
|
||||
$dlog = Join-Path $RunnerDir 'log\runner-daemon.log'
|
||||
if (Test-Path -LiteralPath $dlog) {
|
||||
$tail = Get-Content -LiteralPath $dlog -Tail 40 -Encoding UTF8 -ErrorAction SilentlyContinue
|
||||
if ($tail -match 'already locked by another process') {
|
||||
Warn ' daemon keeps dying young and the log shows ".runner is already locked'
|
||||
Warn ' by another process" -- a stale instance holds the lock; re-run with -StartNow'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Test-Gitea $GiteaUrl) { Ok "Gitea reachable at $GiteaUrl" }
|
||||
else { Fail "Gitea NOT reachable at $GiteaUrl" }
|
||||
|
||||
foreach ($n in @('DTU_PRECOMMIT_PYTHON', 'DTU_CPPCHECK', 'DTU_MSVC_CL')) {
|
||||
$v = Get-MachineEnv $n
|
||||
if ($v) { Ok ("MACHINE {0} = {1}" -f $n, $v) }
|
||||
else { Warn "MACHINE $n not set (CI auto-discovery still applies; see scripts/ci_find_python.ps1)" }
|
||||
}
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Preconditions
|
||||
# ---------------------------------------------------------------------------
|
||||
if (-not $DryRun -and -not (Test-Admin)) {
|
||||
Fail 'not elevated -- rerun from an elevated PowerShell (this writes a scheduled task and machine env vars)'
|
||||
Fail 'use -DryRun to validate the whole plan (action/trigger/settings) without elevation'
|
||||
exit 2
|
||||
}
|
||||
|
||||
$exe = Join-Path $RunnerDir 'gitea-runner.exe'
|
||||
$cfg = Join-Path $RunnerDir 'config.yaml'
|
||||
$regFile = Join-Path $RunnerDir '.runner'
|
||||
$launcher = Join-Path $RunnerDir 'run_runner_watchdog.ps1'
|
||||
|
||||
foreach ($f in @($exe, $cfg)) {
|
||||
if (-not (Test-Path -LiteralPath $f)) { Fail "missing file: $f"; exit 1 }
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $regFile)) {
|
||||
Fail "not registered: $regFile is missing"
|
||||
Info 'register first, e.g.:'
|
||||
Write-Host " & '$exe' register --no-interactive --instance '$GiteaUrl' --token <RegistrationToken> --name win-stm32-ci --labels 'windows:host'"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# The daemon must talk to the address baked into .runner, not the public URL.
|
||||
$registeredAddress = $null
|
||||
try {
|
||||
$registeredAddress = (Get-Content -LiteralPath $regFile -Raw | ConvertFrom-Json).address
|
||||
}
|
||||
catch { Warn "cannot parse $regFile : $_" }
|
||||
if ($registeredAddress -and $registeredAddress.TrimEnd('/') -ne $GiteaUrl.TrimEnd('/')) {
|
||||
Warn ".runner address is $registeredAddress but -GiteaUrl is $GiteaUrl -- using the .runner value for the readiness probe"
|
||||
$GiteaUrl = $registeredAddress
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 0. build the task plan up front; -DryRun stops here without writing anything
|
||||
# ---------------------------------------------------------------------------
|
||||
$plan = New-WatchdogTaskPlan -Launcher $launcher -WorkDir $RunnerDir
|
||||
|
||||
if ($DryRun) {
|
||||
Info 'dry run -- writing and registering NOTHING'
|
||||
Info ("would write launcher : {0}" -f $launcher)
|
||||
Info ("would run : {0} {1}" -f $plan.Action.Execute, $plan.Action.Arguments)
|
||||
Info ("would trigger : {0} delay={1}" -f $plan.Trigger.CimClass.CimClassName, $plan.Trigger.Delay)
|
||||
Info ("would settings : StartWhenAvailable={0} RestartCount={1} RestartInterval={2} ExecTimeLimit={3} MultipleInstances={4}" -f `
|
||||
$plan.Settings.StartWhenAvailable, $plan.Settings.RestartCount, $plan.Settings.RestartInterval,
|
||||
$plan.Settings.ExecutionTimeLimit, $plan.Settings.MultipleInstances)
|
||||
Info 'would register as : user SYSTEM, RunLevel Highest'
|
||||
if (Test-Admin) { Ok 'elevation: OK (admin)' }
|
||||
else { Warn 'elevation: NOT admin -- the real run must be started from an elevated shell' }
|
||||
if (Test-Gitea $GiteaUrl) { Ok "Gitea reachable at $GiteaUrl" }
|
||||
else { Warn "Gitea not reachable at $GiteaUrl -- the watchdog would keep waiting" }
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. machine-level toolchain (SYSTEM cannot see the interactive user's PATH)
|
||||
# ---------------------------------------------------------------------------
|
||||
if ($SetEnv) {
|
||||
Info 'discovering toolchain for MACHINE scope ...'
|
||||
|
||||
$findPy = Join-Path $PSScriptRoot 'ci_find_python.ps1'
|
||||
if (Test-Path -LiteralPath $findPy) {
|
||||
$py = & $findPy -NeedPreCommit
|
||||
if ($LASTEXITCODE -eq 0 -and $py) {
|
||||
Set-MachineEnv 'DTU_PRECOMMIT_PYTHON' ("$py".Trim()) 'DTU_PRECOMMIT_PYTHON'
|
||||
}
|
||||
else {
|
||||
Warn 'no interpreter with pre-commit found -- install pre-commit into a machine-wide interpreter first'
|
||||
}
|
||||
}
|
||||
else { Warn "not found: $findPy (skipping DTU_PRECOMMIT_PYTHON)" }
|
||||
|
||||
$cpp = Find-Cppcheck
|
||||
if ($cpp) { Set-MachineEnv 'DTU_CPPCHECK' $cpp 'DTU_CPPCHECK' }
|
||||
else { Warn 'cppcheck not found (install Cppcheck.Cppcheck, or set DTU_CPPCHECK manually)' }
|
||||
|
||||
$cl = Find-MsvcCl
|
||||
if ($cl) { Set-MachineEnv 'DTU_MSVC_CL' $cl 'DTU_MSVC_CL' }
|
||||
else { Warn 'MSVC cl.exe (Hostx64\x64) not found -- L0.5 will degrade to "skipped" without it' }
|
||||
|
||||
Info 'machine env vars are read when a PROCESS STARTS: the daemon must be restarted below'
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2. watchdog launcher (the actual fix for "daemon exits and nobody notices")
|
||||
# ---------------------------------------------------------------------------
|
||||
Info "writing watchdog launcher: $launcher"
|
||||
|
||||
$template = @'
|
||||
# AUTO-GENERATED by scripts/ci_runner_bootstrap.ps1 -- do not edit by hand.
|
||||
#
|
||||
# gitea-runner `daemon` exits when its first Declare call fails (no retry, no
|
||||
# service support in v4), so a plain scheduled task is NOT enough: a Gitea
|
||||
# restart would take CI down until a human noticed. This loop
|
||||
# 1) waits until Gitea answers, then
|
||||
# 2) runs the daemon, and
|
||||
# 3) restarts it after a short pause whenever it exits.
|
||||
# Keep this file pure ASCII (PowerShell 5.1 decodes BOM-less files as CP936).
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
$runnerDir = '__RUNNER_DIR__'
|
||||
$runnerExe = Join-Path $runnerDir 'gitea-runner.exe'
|
||||
$runnerCfg = Join-Path $runnerDir 'config.yaml'
|
||||
$logDir = Join-Path $runnerDir 'log'
|
||||
$log = Join-Path $logDir 'runner-watchdog.log'
|
||||
$daemonLog = Join-Path $logDir 'runner-daemon.log'
|
||||
$probeUrl = '__GITEA_URL__/api/v1/version'
|
||||
$maxWaitSec = 1800
|
||||
|
||||
if (-not (Test-Path -LiteralPath $logDir)) {
|
||||
New-Item -ItemType Directory -Force -Path $logDir | Out-Null
|
||||
}
|
||||
|
||||
function Log($m) {
|
||||
$line = '{0} [watchdog] {1}' -f (Get-Date -Format 'o'), $m
|
||||
Add-Content -LiteralPath $log -Value $line -Encoding ascii
|
||||
}
|
||||
|
||||
while ($true) {
|
||||
$waited = 0
|
||||
while ($true) {
|
||||
$ok = $false
|
||||
try {
|
||||
Invoke-WebRequest -Uri $probeUrl -UseBasicParsing -TimeoutSec 10 | Out-Null
|
||||
$ok = $true
|
||||
}
|
||||
catch { $ok = $false }
|
||||
if ($ok) { break }
|
||||
if ($waited -eq 0) { Log "waiting for Gitea at $probeUrl" }
|
||||
Start-Sleep -Seconds 15
|
||||
$waited += 15
|
||||
if ($waited -ge $maxWaitSec) {
|
||||
Log 'Gitea still not answering after 30m -- starting anyway so the real error lands in the log'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
Log 'starting daemon'
|
||||
# MEASURED TRAP (2026-10-12, hit by this very script): in PowerShell 5.1 the
|
||||
# *>> redirection is implemented with Out-File, whose DEFAULT ENCODING IS
|
||||
# UTF-16LE. Appending that into a file whose other lines come from
|
||||
# Add-Content -Encoding ascii leaves ONE FILE WITH TWO ENCODINGS, and the
|
||||
# daemon lines read back as "t i m e = 2 0 2 6 ..." (a NUL after every byte).
|
||||
# Never rely on a redirection's default encoding -- state it explicitly.
|
||||
# utf8 (not ascii) because job/commit text can legitimately be non-ASCII.
|
||||
# MEASURED TRAP #2 (2026-10-12): the daemon writes its log to STDERR, and in
|
||||
# PS 5.1 a native `2>&1` merge wraps every stderr line into an ErrorRecord.
|
||||
# Out-File then renders "gitea-runner.exe : <line>" plus a 4-line CategoryInfo
|
||||
# block -- measured: a 2-line message became 8 lines, so 3 of every 4 lines in
|
||||
# the daemon log were PowerShell noise, in the very file the docs tell people
|
||||
# to read first. Stringifying each pipeline item keeps the text and drops the
|
||||
# decoration (ErrorRecord.ToString() IS the original stderr line) and leaves
|
||||
# $LASTEXITCODE intact. Verified against a cmd.exe wrapper; same result, and
|
||||
# this form avoids cmd's quote-stripping rules for spaced paths.
|
||||
& $runnerExe daemon --config $runnerCfg 2>&1 |
|
||||
ForEach-Object { "$_" } |
|
||||
Out-File -FilePath $daemonLog -Append -Encoding utf8
|
||||
Log ("daemon exited, exit code {0}; restarting in 10s" -f $LASTEXITCODE)
|
||||
Start-Sleep -Seconds 10
|
||||
}
|
||||
'@
|
||||
|
||||
$content = $template.Replace('__RUNNER_DIR__', $RunnerDir).Replace('__GITEA_URL__', $GiteaUrl.TrimEnd('/'))
|
||||
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
|
||||
[System.IO.File]::WriteAllText($launcher, $content, $utf8NoBom)
|
||||
Ok 'launcher written'
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. scheduled task: start at boot, run as SYSTEM, restart if it dies
|
||||
# ---------------------------------------------------------------------------
|
||||
$existing = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
||||
if ($existing) {
|
||||
Info "task $TaskName exists -- unregistering to re-register with the current settings"
|
||||
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false
|
||||
}
|
||||
|
||||
try {
|
||||
Register-ScheduledTask -TaskName $TaskName -Action $plan.Action -Trigger $plan.Trigger `
|
||||
-Settings $plan.Settings -User 'SYSTEM' -RunLevel Highest `
|
||||
-Description 'Gitea Actions runner watchdog (STM32F4-Base CI). Supervises the gitea-runner daemon: waits for Gitea, restarts the daemon when it exits. Generated by scripts/ci_runner_bootstrap.ps1.' |
|
||||
Out-Null
|
||||
}
|
||||
catch {
|
||||
Fail "Register-ScheduledTask failed: $($_.Exception.Message)"
|
||||
if ($existing) {
|
||||
Fail "the previous task was ALREADY removed -- fix the cause and re-run this script immediately"
|
||||
}
|
||||
Fail 'until this succeeds the runner has no auto-start and CI stays dead'
|
||||
exit 1
|
||||
}
|
||||
Ok "scheduled task registered: $TaskName (at boot +1m, as SYSTEM, unlimited runtime, restart x999/1m)"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 4. start it -- but clear stale instances first, or the new watchdog never gets
|
||||
# the .runner lock.
|
||||
#
|
||||
# MEASURED TRAP (2026-10-12): Unregister-ScheduledTask does NOT stop the process
|
||||
# the old instance had started. That orphaned watchdog keeps its daemon alive and
|
||||
# the daemon holds .runner.lock, so the freshly started watchdog fails every 10s:
|
||||
# level=error msg="another gitea-runner process is already using \".runner\"..."
|
||||
# Error: runner file is already locked by another process
|
||||
# The runner still shows ONLINE (the stale daemon serves fine), so CI looks healthy
|
||||
# while the log fills with a restart loop and the task supervises nothing at all.
|
||||
# ---------------------------------------------------------------------------
|
||||
function Stop-StaleRunnerInstances {
|
||||
# Supervisors first: killing the daemon alone lets a live watchdog respawn it
|
||||
# within 10s and the race starts over.
|
||||
$killed = 0
|
||||
Get-CimInstance Win32_Process -Filter "Name='powershell.exe'" -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.CommandLine -match 'run_runner_watchdog\.ps1' } |
|
||||
ForEach-Object {
|
||||
Info (" stopping stale watchdog pid {0}" -f $_.ProcessId)
|
||||
Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue
|
||||
$killed++
|
||||
}
|
||||
Get-Process -Name 'gitea-runner' -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
Info (" stopping stale daemon pid {0} (releases .runner.lock)" -f $_.Id)
|
||||
Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue
|
||||
$killed++
|
||||
}
|
||||
return $killed
|
||||
}
|
||||
|
||||
if ($StartNow) {
|
||||
if (-not (Test-Gitea $GiteaUrl)) {
|
||||
Warn "Gitea is not answering at $GiteaUrl right now -- the watchdog will keep waiting"
|
||||
}
|
||||
$stale = Stop-StaleRunnerInstances
|
||||
if ($stale -gt 0) {
|
||||
Warn "$stale stale process(es) stopped; the runner is offline for a few seconds"
|
||||
Start-Sleep -Seconds 2
|
||||
}
|
||||
Start-ScheduledTask -TaskName $TaskName
|
||||
Start-Sleep -Seconds 12 # the watchdog probes Gitea, then execs the daemon
|
||||
$d = @(Get-Process -Name 'gitea-runner' -ErrorAction SilentlyContinue)
|
||||
if ($d.Count -eq 1) {
|
||||
Ok ("daemon up: pid {0} (single instance, holds .runner.lock)" -f $d[0].Id)
|
||||
} elseif ($d.Count -eq 0) {
|
||||
Warn 'no daemon yet -- the watchdog may still be waiting for Gitea; check the watchdog log'
|
||||
} else {
|
||||
Warn ("{0} daemons running -- only one can hold the lock; inspect and kill the extras" -f $d.Count)
|
||||
}
|
||||
Ok "daemon log: Get-Content '$(Join-Path $RunnerDir 'log\runner-daemon.log')' -Tail 20"
|
||||
Ok "watchdog log: Get-Content '$(Join-Path $RunnerDir 'log\runner-watchdog.log')' -Tail 20"
|
||||
Info 'verify on the web UI: repo -> Settings -> Actions -> Runners -> win-stm32-ci should turn Idle'
|
||||
}
|
||||
|
||||
Ok 'done'
|
||||
exit 0
|
||||
Reference in New Issue
Block a user