修复CI门禁三因:runner掉线致12个job零日志、L0探测回归全灭、gitlint在CI从不执行
1) runner 掉线:run 16~21 共 12 个 job 零日志。gitea-runner v4 的
daemon 无服务支持、首次 Declare 失败即退出进程,Gitea/机器重启
一次 CI 就静默停摆;叠加 cancel-in-progress 后症状是"任务凭空
消失"。新增 scripts/ci_runner_bootstrap.ps1:等 Gitea 就绪 +
崩溃自拉起看门狗 + 开机计划任务 + -SetEnv 写 MACHINE 级 DTU_*。
2) L0 自 33a4aa4 起在 runner 上必挂:Python 候选被收窄成 PATH python
(msys64,无 pre-commit) + 仓库 .venv(gitignore),cppcheck 目录兜底
亦被删,而机器级 DTU_* 从未设置。探测收进 ci_find_python.ps1
(两 job 共用、可离线自测、恢复机器级安装兜底)。
3) gitlint 声明 stages:[commit-msg],而 --all-files 只跑 pre-commit
stage → 17 个钩子定义 CI 只报 16 个且全绿。新增
ci_gitlint_range.ps1 逐条校验本次范围,ci_local.py 增 L0b 段。
另:GITHUB_ENV/PATH/OUTPUT 写入显式 -Encoding ascii(PS 5.1 的
Out-File -Append 默认 UTF-16LE,会把带 NUL 的行追加进 UTF-8 文件)
This commit is contained in:
+126
-105
@@ -43,6 +43,34 @@
|
||||
# 那些由 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` 定义 17 个钩子,CI 只报 16 个(`passed=15 skipped=1`)——
|
||||
# 缺的正是 gitlint:其钩子声明 `stages: [commit-msg]`,而 `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 +125,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"
|
||||
exit 1
|
||||
$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 }
|
||||
}
|
||||
}
|
||||
Write-Host "found cppcheck: $($cpp.Source)"
|
||||
cppcheck --version
|
||||
}
|
||||
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
|
||||
}
|
||||
$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 +199,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 +244,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 +370,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 +381,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 +444,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 日志(供下载/排障)
|
||||
|
||||
@@ -34,7 +34,10 @@ STM32F407ZGTx @168MHz(HSE 8MHz → PLL M=4/N=168/P=2);**仅 Keil MDK-ARM v
|
||||
- 提交信息:**单行中文标题 <100 字符**(gitlint),UTF-8 无 BOM + `git commit -F <文件>`(防终端转码破坏)
|
||||
- 代码规范 V1.0:小写下划线命名(`g_`/`s_`/`p_`/`a_` 前缀)、头文件保护宏 `XXX_H`、常量在右、文件头模块注释块 → `docs/代码规范/`
|
||||
- 全库扫描 `pre-commit run --all-files`;详见 `docs/代码规范/pre-commit使用说明.md`
|
||||
- **CI(Gitea Actions,2026-10-01 T-6)**:`.gitea/workflows/ci.yml` 在 push/PR 跑 **L0 静态门禁 + L0.5 主机侧单测**(Windows runner);**不含 L1/L2/L3**(需真机+ST-LINK)、**刻意不跑 `@build`**(需 Keil 授权)、无 `.map` 时自适应跳过 `mem-numbers`。本地等价自查:`& '.venv\Scripts\python.exe' scripts/ci_local.py`(**runner 未就绪时这是 L0 的唯一保障**,不能因"已配 CI"放宽本地门禁)。详见 `docs/测试说明/测试开发标准流程.md` §9
|
||||
- **CI(Gitea Actions,2026-10-01 T-6)**:`.gitea/workflows/ci.yml` 在 push/PR 跑 **L0 静态门禁 + gitlint 提交信息门禁 + L0.5 主机侧单测**(Windows runner);**不含 L1/L2/L3**(需真机+ST-LINK)、**刻意不跑 `@build`**(需 Keil 授权)、无 `.map` 时自适应跳过 `mem-numbers`。本地等价自查:`& '.venv\Scripts\python.exe' scripts/ci_local.py`(**runner 未就绪时这是 L0 的唯一保障**,不能因"已配 CI"放宽本地门禁)。详见 `docs/测试说明/测试开发标准流程.md` §9
|
||||
- **`pre-commit run --all-files` 跑不到 `gitlint`**(2026-10-11 实测):该钩子声明 `stages: [commit-msg]`,而 `--all-files` 只跑 `pre-commit` stage → **17 个钩子定义、CI 只报 16 个、且表现为全绿**(提交信息门禁曾在 CI 完全空转)。现由 `scripts/ci_gitlint_range.ps1`(CI)与 `ci_local.py` 的 **L0b** 段(本地)逐条校验;**增删钩子后必须核对"定义数 == 日志条数 + gitlint"**
|
||||
- **CI runner 必须有看门狗**(2026-10-11 实测):`gitea-runner` v4 的 `daemon` **无服务支持、首次 Declare 失败就直接退出进程**(Gitea 重启一次即停摆,run 16~21 共 12 个 job 零日志;叠加 `cancel-in-progress` 后症状是"任务凭空消失"而非失败)。体检 `& '.\scripts\ci_runner_bootstrap.ps1' -Status`;部署 `-SetEnv -StartNow`(看门狗 + 开机计划任务 + MACHINE 级 `DTU_PRECOMMIT_PYTHON`/`DTU_CPPCHECK`/`DTU_MSVC_CL`——runner 以 SYSTEM 跑,看不见用户级 PATH/site)
|
||||
- **写 `GITHUB_ENV`/`GITHUB_PATH`/`GITHUB_OUTPUT` 必须显式 `-Encoding ascii`**(2026-10-11 实测):PS 5.1 的 `Out-File -Append` **默认 UTF-16LE**,会把 `42 00 42 00 3D 00` 这种带 NUL 的行追加进 runner 以 UTF-8 建的同一文件里
|
||||
|
||||
## 平台约束
|
||||
|
||||
|
||||
+20
-5
@@ -530,11 +530,20 @@ flowchart LR
|
||||
> → `python scripts/check_mem_numbers.py --scan`。详见
|
||||
> [`../环境与部署/CI配置指南.md`](../环境与部署/CI配置指南.md) §0.0。
|
||||
|
||||
> 🔴 **`gitlint` 属 `commit-msg` stage,`--all-files` 永远跑不到它(2026-10-11 审计)**:
|
||||
> `.pre-commit-config.yaml` 定义 **17** 个钩子,而 CI 日志只报 **16** 个(`passed=15 skipped=1`)——
|
||||
> 缺的正是 gitlint(其钩子声明 `stages: [commit-msg]`,而 `pre-commit run --all-files` 只跑
|
||||
> `pre-commit` stage)。即"单行中文标题 <100 字符"这条门禁**本地有、CI 无**,且表现为**全绿**
|
||||
> 而非失败,所以长期无人发现。现由 **`scripts/ci_gitlint_range.ps1`** 逐条校验本次 push/PR
|
||||
> 引入的提交(PR 用 `base..head`;push 用 `event.before..sha`,`before` 全零/不可达时回退最近
|
||||
> N 条;跳过 merge 提交)。本地等价:`ci_local.py` 的 **L0b** 段。
|
||||
|
||||
### 9.2 两个 job
|
||||
|
||||
| job | `runs-on` | 步骤要点 |
|
||||
|---|---|---|
|
||||
| `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` 汇总行 → 归档日志说明 |
|
||||
| `l0-gates` | `windows` | checkout(`fetch-depth: 0`,gitlint/cubemx 需完整历史)→ 环境自检(**Python 探测走 `scripts/ci_find_python.ps1`**、cppcheck 按 `DTU_CPPCHECK` → PATH → 标准安装目录,缺则 `::error::` + exit 1)→ `pre_commit install --install-hooks` → **提交信息门禁 `scripts/ci_gitlint_range.ps1`**(见上方 🔴)→ **决定跳过哪些钩子**(探测 `.map`,写入 `GITHUB_OUTPUT`;无 `.map` 时 `::warning::` 声明体积门禁未跑)→ `pre-commit run --all-files --show-diff-on-failure`(`env: SKIP`,输出 `Tee` 到 `ci-artifacts/l0-pre-commit.log`)→ 打印 `::notice::L0: passed/failed/skipped` 汇总行 → 归档日志说明 |
|
||||
|
||||
| `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 全红") |
|
||||
|
||||
**结果可见性(S3,2026-10-02)**:两个 job 均写 `$GITHUB_STEP_SUMMARY`(job 运行页的
|
||||
@@ -551,13 +560,17 @@ step-summary 达成。
|
||||
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` 环境错误。
|
||||
|
||||
@@ -570,6 +583,8 @@ CI 只在 push / PR 时跑,且需 runner 就绪。**提交前自查、或 runn
|
||||
|---|---|
|
||||
| Gitea Actions | 实例开启 `[actions] ENABLED = true`(site admin 配置) |
|
||||
| Windows runner | 已注册且 **labels 含 `windows`**;装有 Python 3.x、cppcheck、pre-commit、MSVC(VS2019/2022 C++ 工具链 + Windows SDK) |
|
||||
| **runner 必须有看门狗** | `gitea-runner` v4 的 `daemon` **无服务支持、且首次 Declare 失败就直接退出进程** → Gitea 重启一次,CI 就静默停摆(2026-10-11 实测:run 16~21 共 12 个 job 零日志)。用 **`scripts/ci_runner_bootstrap.ps1`** 装"等 Gitea 就绪 + 崩溃自拉起"的看门狗 + 计划任务 |
|
||||
| 机器级工具变量 | runner 以 **SYSTEM** 运行,看不见交互用户的 PATH/用户级 site。用 `ci_runner_bootstrap.ps1 -SetEnv` 写入 **MACHINE** 级 `DTU_PRECOMMIT_PYTHON` / `DTU_CPPCHECK` / `DTU_MSVC_CL` |
|
||||
| 缺 MSVC 时 | L0.5 降级为 WARNING 跳过(不硬失败),但**等于该层裸奔**,须尽快补装 |
|
||||
|
||||
> 📖 **配置步骤 / 排障**(服务端开关、runner 注册命令、PATH 与 MSVC 路径要求、
|
||||
|
||||
+98
-19
@@ -160,20 +160,54 @@ cache:
|
||||
enabled: true
|
||||
```
|
||||
|
||||
### 2.4 以服务方式常驻
|
||||
### 2.4 常驻方式:**必须用看门狗,不能只起 daemon**(2026-10-11 实测重写)
|
||||
|
||||
> 🔴 **`daemon install` 在 v4 已不存在**:本机 runner 是 `gitea-runner v4.0.1`,命令集只有
|
||||
> `bug-report / cache-server / config / daemon / exec / register`,**没有任何 Windows 服务
|
||||
> 管理代码**(`daemon` 是纯控制台程序,无法向 SCM 回报 `SERVICE_RUNNING`)。
|
||||
> 旧版本文写的 `act_runner.exe daemon install` 在本机**不可用**。
|
||||
|
||||
> 🔴🔴 更关键的坑:**`daemon` 在首次 Declare 失败时直接退出进程**(不重试)。
|
||||
> runner 日志(`D:\Docker\act_runner\log\runner-stdout.log`)实测:
|
||||
> ```
|
||||
> level=error msg="fail to invoke Declare" error="unavailable:
|
||||
> dial tcp 127.0.0.1:3001: connectex: ... actively refused it."
|
||||
> Error: unavailable: dial tcp 127.0.0.1:3001 ...
|
||||
> ```
|
||||
> 之后**没有**任何重试记录——进程已经没了。于是 **Gitea 重启一次 / 机器重启一次,
|
||||
> CI 就静默停摆**:2026-10-08 起 runner 掉线,run **16~21 共 12 个 job 一行日志都没产生**;
|
||||
> 又因 `ci.yml` 的 `concurrency.cancel-in-progress: true`,队列里的 run 会被**下一次 push 直接取消**
|
||||
> (16~20 = `cancelled`、21 = 长期 `queued`)——症状看起来是"任务凭空消失",而不是"任务失败",
|
||||
> 极难从 UI 反推根因。
|
||||
|
||||
**正确做法:一条命令装好"等 Gitea 就绪 + 崩溃自拉起 + 开机自启"**(管理员权限,仓库根目录):
|
||||
|
||||
```powershell
|
||||
# 注册为 Windows 服务(管理员权限)
|
||||
.\act_runner.exe daemon install
|
||||
.\act_runner.exe daemon start
|
||||
|
||||
# 或前台调试(先看日志通不通,推荐首次这样跑)
|
||||
.\act_runner.exe daemon
|
||||
powershell -ExecutionPolicy Bypass -File scripts\ci_runner_bootstrap.ps1 -SetEnv -StartNow
|
||||
```
|
||||
|
||||
它做三件事(细节见脚本头注释):
|
||||
|
||||
| 动作 | 内容 |
|
||||
|---|---|
|
||||
| ① 机器级工具变量 | 按 CI 同一套探测写 **MACHINE** 级 `DTU_PRECOMMIT_PYTHON` / `DTU_CPPCHECK` / `DTU_MSVC_CL`(已存在则跳过,`-Force` 才覆盖) |
|
||||
| ② 看门狗启动器 | 生成 `D:\Docker\act_runner\run_runner_watchdog.ps1`:先轮询 `<GiteaUrl>/api/v1/version` 直到 Gitea 应答,再起 `daemon`;daemon 退出 → 记日志 → 10s 后重拉,**永不放弃** |
|
||||
| ③ 计划任务 | `GiteaActionsRunner`:开机 +1 分钟触发、以 **SYSTEM** + 最高权限运行、**运行时限=不限**、失败每 1 分钟重启(×999)、不允许多实例 |
|
||||
|
||||
随时体检(只读,不改任何东西):
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts\ci_runner_bootstrap.ps1 -Status
|
||||
```
|
||||
|
||||
输出会告诉你:计划任务是否存在、daemon 是否在跑、Gitea 是否可达、三个机器级变量是否已设。
|
||||
|
||||
注册成功后,回到 **仓库 → Settings → Actions → Runners**,应看到 `win-stm32-ci`
|
||||
状态为 **Idle / 绿色**。
|
||||
|
||||
> ⚠️ **`.runner` 里的 `address` 是 runner 实际连接的地址**(本机 `http://127.0.0.1:3001`),
|
||||
> 与 `ROOT_URL`(`http://gitea.bitnasdaq.vip/`)不是一回事;bootstrap 会以 `.runner` 为准。
|
||||
|
||||
---
|
||||
|
||||
## 3. Runner 软件环境:装什么
|
||||
@@ -189,10 +223,35 @@ runner 是一台干净的 Windows,**下面这些必须装**,否则 job 会
|
||||
| Keil / ST-LINK | ⛔ 不需要 | CI 刻意不跑 `@build` | — |
|
||||
| 开发板 / 串口 | ⛔ 不需要 | L1/L2/L3 不上 CI | — |
|
||||
|
||||
### 3.1 关键:PATH 必须对
|
||||
### 3.1 关键:变量必须对 **SYSTEM** 可见(不是"对你可见")
|
||||
|
||||
CI 的环境自检用的是 `Get-Command python` / `cppcheck`,**依赖 PATH**。
|
||||
装完后**新开一个终端**(或重启 runner 服务)让 PATH 生效,然后自检:
|
||||
runner 以 **SYSTEM 服务/计划任务**运行,它看到的 PATH 与 site-packages **和你登录时完全不同**:
|
||||
|
||||
| 你以为 | SYSTEM 实际看到 | 后果 |
|
||||
|---|---|---|
|
||||
| `python` 在 PATH 里 | PATH 上是 `C:\msys64\ucrt64\bin\python.exe`,**没有 pre-commit** | L0 环境自检失败 |
|
||||
| `pip install --user pre-commit` 装好了 | **用户级 site 对 SYSTEM 完全不可见**(换任何 python 路径都没用) | 同上 |
|
||||
| 仓库里有 `.venv` | `.venv` 已 `.gitignore`,runner 全新工作区**根本没有** | 同上 |
|
||||
| `cppcheck` 在 PATH 里 | 机器 PATH 无 `C:\Program Files\Cppcheck` | L0 环境自检失败 |
|
||||
|
||||
CI 侧的探测统一走 **`scripts/ci_find_python.ps1`**(两个 job 共用、可离线自测):
|
||||
|
||||
```
|
||||
DTU_PRECOMMIT_PYTHON(显式;设了就必须有效,不静默回退)
|
||||
-> PATH python -> 仓库 .venv -> 常见机器级安装(Program Files\Python3xx、
|
||||
%LOCALAPPDATA%\Programs\Python\Python3xx、%ProgramData%\anaconda3|miniconda3)
|
||||
```
|
||||
|
||||
cppcheck 同理:`DTU_CPPCHECK` → PATH → `%ProgramFiles%\Cppcheck\cppcheck.exe`
|
||||
(及 `%ProgramFiles(x86)%\Cppcheck\cppcheck.exe`)。
|
||||
|
||||
**推荐做法**:跑一次 bootstrap,把机器级变量固化下来(探测逻辑与 CI 完全一致):
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts\ci_runner_bootstrap.ps1 -SetEnv
|
||||
```
|
||||
|
||||
手工自检(三条都必须对 SYSTEM 成立,而不只是对你的终端成立):
|
||||
|
||||
```powershell
|
||||
python --version # 期望 Python 3.x
|
||||
@@ -200,6 +259,9 @@ cppcheck --version # 期望 Cppcheck 2.21.0 或更高
|
||||
python -m pre_commit --version
|
||||
```
|
||||
|
||||
> ⚠️ 改了机器级变量后**必须重启 runner**(计划任务:`Stop-ScheduledTask` + `Start-ScheduledTask`)
|
||||
> ——环境变量只在**进程启动时**读取,daemon 不重启就吃不到新值。
|
||||
|
||||
### 3.2 MSVC 的路径要求(探测逻辑)
|
||||
|
||||
`l05-host-unit-tests` 的 MSVC 探测优先读机器级 `DTU_MSVC_CL`(x64 `cl.exe` 完整路径;
|
||||
@@ -316,13 +378,15 @@ github.com 会在 checkout 步骤卡住/失败。三种处置:
|
||||
| # | 检查 | 期望 |
|
||||
|---|---|---|
|
||||
| 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 全绿 |
|
||||
| 2 | Settings → Actions → Runners 里 runner 为 **Idle** | runner 注册成功且**在线**(`offline` = 后面全都不会跑,先做第 3 步) |
|
||||
| 3 | `scripts\ci_runner_bootstrap.ps1 -Status` | 计划任务已注册 / daemon 在跑 / Gitea 可达 / 三个 `DTU_*` 已设——**四项全绿** |
|
||||
| 4 | runner 上 `python --version` / `cppcheck --version` / `python -m pre_commit --version` | 三条全通(且对 SYSTEM 成立,见 §3.1) |
|
||||
| 5 | `git push` 一次(或页面 Run workflow) | Actions 页出现运行中任务 |
|
||||
| 6 | `l0-gates` 日志 | `using python (…)` + `runner environment check passed`;`::notice::L0: pre-commit hooks -- passed=.. failed=0 skipped=..`;无 `.map` 时见 `::warning::` 体积门禁未跑 |
|
||||
| 7 | `l0-gates` 的 **提交信息门禁** 步骤 | `::notice::gitlint: N commit message(s) checked, 0 failed`(**这一行以前根本不存在**,见 §6) |
|
||||
| 8 | `l05-host-unit-tests` 日志 | `::notice::L0.5: 3 runner(s), 278/278 assertions passed, fail=0` |
|
||||
| 9 | job 运行页的 **Summary** 区块 | 出现 "L0 static gates" / "Commit message gate (gitlint)" / "L0.5 host-side unit tests" 小标题与汇总行 |
|
||||
| 10 | 提交记录旁出现 ✅ | CI 全绿 |
|
||||
|
||||
**本地先自查**(与 CI 同一套判定,避免推上去才发现问题):
|
||||
|
||||
@@ -336,6 +400,11 @@ github.com 会在 checkout 步骤卡住/失败。三种处置:
|
||||
|
||||
| 现象 | 根因 | 处置 |
|
||||
|---|---|---|
|
||||
| 运行状态是 **`cancelled`** 且**一行日志都没有** | runner 掉线时 run 只在队列里等,**下一次 push 触发 `cancel-in-progress` 把它们取消**——所以表现为"任务凭空消失"而不是"任务失败" | 先按下一行修 runner;被取消的 run 可在页面 Re-run,或重新 push |
|
||||
| Runners 页 runner 显示 **`offline`** | daemon 进程不在:`gitea-runner` v4 的 `daemon` **首次 Declare 失败就直接退出**(Gitea 重启/机器重启后无人拉起) | §2.4 用 `ci_runner_bootstrap.ps1` 装看门狗 + 计划任务;`-Status` 复查 |
|
||||
| `l0-gates` 报 `no Python with a SYSTEM-visible pre-commit` | runner 是 SYSTEM:PATH 上的 python 无 pre-commit、用户级 site 不可见、`.venv` 未入库(§3.1 三重坑) | `ci_runner_bootstrap.ps1 -SetEnv` 写机器级 `DTU_PRECOMMIT_PYTHON`,或把 pre-commit 装进机器级解释器;**改完必须重启 runner** |
|
||||
| `l0-gates` 报 `runner missing cppcheck` | 机器 PATH 无 Cppcheck 且无机器级 `DTU_CPPCHECK` | `-SetEnv`(会自动发现 `C:\Program Files\Cppcheck`)或装 `Cppcheck.Cppcheck` |
|
||||
| **钩子条数对不上**(定义 17、CI 只报 16) | `gitlint` 声明 `stages: [commit-msg]`,`pre-commit run --all-files` **跑不到它**,且表现为全绿——静默空转 | 已由 `scripts/ci_gitlint_range.ps1` 单独校验;**以后增删钩子必须核对"定义数 == 日志条数 + gitlint"** |
|
||||
| push 后 Actions 页**空白/无任务** | ① 服务端开关未开 ② workflow 没 push 到远程 ③ 只改了 `docs/**`(被 `paths-ignore` 滤掉) | 逐条核对 §1 / §4.1 / §4.2 |
|
||||
| 任务一直 **Waiting / 排队** | 没有 label 匹配 `windows` 的 runner | 检查 `--labels "windows:host"`;Settings→Runners 是否 Idle |
|
||||
| `l0-gates` 报 `::error::runner 缺少:Python, cppcheck` | runner 没装或不在 PATH | 装完后**重启 runner 服务**(新进程才吃到新 PATH) |
|
||||
@@ -353,9 +422,17 @@ github.com 会在 checkout 步骤卡住/失败。三种处置:
|
||||
|
||||
## 7. 日常维护
|
||||
|
||||
- **升级 runner**:停服务 → 换 `act_runner.exe` → 起服务(注册信息保留在 `config.yaml`);
|
||||
- **改了 `VS_ROOTS` / 工具链探测逻辑**:记得**同步改 workflow 里的探测根目录**(两处一致);
|
||||
- **每周(或每次改完 Gitea/机器重启后)体检**:`scripts\ci_runner_bootstrap.ps1 -Status`——
|
||||
计划任务 / daemon 进程 / Gitea 可达 / 三个机器级 `DTU_*`,四项全绿才算 CI 活着;
|
||||
- **升级 runner**:`Stop-ScheduledTask GiteaActionsRunner` → 换 `gitea-runner.exe` →
|
||||
`Start-ScheduledTask GiteaActionsRunner`(注册信息在 `.runner`,配置在 `config.yaml`);
|
||||
若换了目录/地址,重跑 `ci_runner_bootstrap.ps1` 重新生成看门狗与计划任务;
|
||||
- **改了工具链探测逻辑**:Python 侧只有一处事实源 **`scripts/ci_find_python.ps1`**
|
||||
(两个 job 共用),本地 `scripts/ci_local.py` 的候选表须与它**同序**;
|
||||
MSVC 探测仍是 workflow 内联 + `test/host/common/unit_test_harness.py` **两处一致**;
|
||||
- **钩子集变化**(`.pre-commit-config.yaml` 增删钩子):CI 自动跟随,无需改 workflow;
|
||||
但**必须核对条数**——`pre-commit run --all-files` 只跑 `pre-commit` stage,
|
||||
`commit-msg` stage 的钩子(gitlint)不在其中,靠 `ci_gitlint_range.ps1` 单独守(见 §6);
|
||||
- **`@build` / L1 / L2**:永远留在本地,不要往 CI 加(需 Keil 授权与真机);
|
||||
- **runner 长时间不可用**时:CI 只是"多一道网",**提交前的本地 `ci_local.py` 仍是 L0 的唯一保障**,
|
||||
不能因"已经配了 CI"放宽(见 `docs/待处理内容.md` 的部署前置警示块)。
|
||||
@@ -366,6 +443,8 @@ github.com 会在 checkout 步骤卡住/失败。三种处置:
|
||||
|
||||
| 日期 | 变更 |
|
||||
|---|---|
|
||||
| 2026-10-11 | **修"很多任务不执行"(runner 静默停摆)+ 补 gitlint 门禁**:① §2.4 重写——`gitea-runner` v4 的 `daemon` **无服务支持、首次 Declare 失败即退出进程**(日志实证),Gitea/机器重启一次 CI 就停摆,**run 16~21 共 12 个 job 零日志**(`cancelled`/`queued`,叠加 `cancel-in-progress` 后表现为"任务凭空消失"而非失败);新增 **`scripts/ci_runner_bootstrap.ps1`**(等 Gitea 就绪 + 崩溃自拉起的看门狗 + 开机计划任务 + `-SetEnv` 写机器级 `DTU_*` + `-Status` 只读体检)。② §3.1 重写——SYSTEM 可见性三重坑(PATH python 是 msys64 无 pre-commit / 用户级 site 对 SYSTEM 不可见 / `.venv` 已 gitignore)是 run 14~15 L0 全灭根因(`33a4aa4` 把候选收窄成这两个、又删了 cppcheck 目录兜底,而机器级 `DTU_*` 从未设置);探测收进 **`scripts/ci_find_python.ps1`**(两 job 共用、可离线自测、失败列出全部候选)并恢复机器级安装兜底。③ 新增 **gitlint 提交信息门禁**:钩子定义 **17** 个而 CI 只报 **16** 个,缺的正是声明 `stages: [commit-msg]` 的 gitlint(`--all-files` 跑不到,且表现为全绿 → 长期无人发现)→ **`scripts/ci_gitlint_range.ps1`** 逐条校验本次 push/PR 范围(消息文件须写在仓库内 + UTF-8 无 BOM)。④ `GITHUB_ENV/PATH/OUTPUT` 写入统一显式 `-Encoding ascii`(PS 5.1 `Out-File -Append` 默认 **UTF-16LE**,实测落盘 `42 00 42 00 42 00 3D 00 31 00`)。§5 清单 8→10 条、§6 排障表 +5 行、§7 维护项重写 |
|
||||
|---|---|
|
||||
| 2026-10-02 | **S3 + S14**:新增 §0.0「CI 不覆盖的体积门禁」醒目声明;新增 §4.4「产物存档与结果汇总」(零依赖 `Tee-Object` + `$GITHUB_STEP_SUMMARY` + `::notice::` 汇总行,不用 `actions/upload-artifact`;记录 `$ErrorActionPreference='Stop'` 与 `2>&1 \| Tee-Object` 的陷阱及处置)。§5 验证清单(7→8 条)与 §6 排障表同步更新 |
|
||||
| 2026-10-01 | **修正 `DEFAULT_ACTIONS_URL` 说明**(原 §1.2 示例误导):新增 §1.2.1 明确它控制的是 **action 下载源**(默认 `github.com`;`self` = 从自己实例拉且需先镜像 action),**不是填自己的 Gitea 地址**;§4.3 同步修正(原文误写"gitea.com 代理"),并补零依赖写法为内网首选。依据:Gitea 官方 FAQ + config-cheat-sheet |
|
||||
| 2026-10-01 | 初版:Gitea Actions 配置全流程(服务端开关 / Windows runner 注册 / runner 软件环境 / workflow 位置与触发 / 内网 action 拉取 / 端到端验证清单 / 故障排查 / 日常维护) |
|
||||
|
||||
@@ -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
|
||||
}
|
||||
+120
-6
@@ -8,9 +8,15 @@
|
||||
跑同一套判定,命令与退出码与 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]`,
|
||||
而 `pre-commit run --all-files` 只跑 pre-commit stage —— 它既不在 --all-files 里,
|
||||
CI 也一度漏掉(17 个钩子只报 16 个,且表现为"全绿")。本脚本与 CI 的
|
||||
`scripts/ci_gitlint_range.ps1` 同语义:逐条把最近 N 条非 merge 提交的
|
||||
message 喂给钩子。
|
||||
|
||||
刻意**不含**(与 CI 同理由,见 workflow 文件头):
|
||||
- `@build`(需 Keil 授权;CI 也不跑,真编译是开发者本地红线)
|
||||
- `mem-numbers` 钩子(事实源 `.map` 是 Keil 产物——**本地若存在 .map 就照常跑**,
|
||||
@@ -23,6 +29,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 +37,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 +54,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 +88,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 +113,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 +129,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 +208,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 +342,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 +372,15 @@ def main(argv=None):
|
||||
try:
|
||||
if not args.l05_only:
|
||||
rc = run_l0()
|
||||
if rc != 0 and not args.keep_going:
|
||||
print("")
|
||||
print("[FAIL] L0 未通过 —— 中止(--keep-going 可继续跑 L0.5)")
|
||||
return 1
|
||||
# 提交信息门禁独立于 --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,352 @@
|
||||
<#
|
||||
.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 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]$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
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# -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) } else { Warn 'task not registered' }
|
||||
|
||||
$proc = Get-Process | Where-Object { $_.ProcessName -match 'gitea-runner|act_runner' }
|
||||
if ($proc) { Ok ("daemon running: pid(s) " + ($proc.Id -join ',')) } else { Warn 'daemon not running' }
|
||||
|
||||
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 (Test-Admin)) {
|
||||
Fail 'not elevated -- rerun from an elevated PowerShell (this writes a scheduled task and machine env vars)'
|
||||
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
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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-stdout.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'
|
||||
& $runnerExe daemon --config $runnerCfg *>> $log
|
||||
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
|
||||
}
|
||||
|
||||
$action = New-ScheduledTaskAction `
|
||||
-Execute 'powershell.exe' `
|
||||
-Argument "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$launcher`"" `
|
||||
-WorkingDirectory $RunnerDir
|
||||
|
||||
# -AtStartup needs the "System" account; a boot trigger plus StartWhenAvailable
|
||||
# also covers "machine was off when the task was created".
|
||||
$trigger = New-ScheduledTaskTrigger -AtStartup
|
||||
$trigger.Delay = 'PT1M'
|
||||
$trigger.StartWhenAvailable = $true
|
||||
|
||||
$settings = New-ScheduledTaskSettingsSet `
|
||||
-AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
|
||||
-ExecutionTimeLimit ([TimeSpan]::Zero) `
|
||||
-RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1) `
|
||||
-MultipleInstances IgnoreNew
|
||||
|
||||
Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger `
|
||||
-Settings $settings -User 'SYSTEM' -RunLevel Highest `
|
||||
-Description 'Gitea Actions runner watchdog (STM32F4-Base CI). Supervises gitea-runner daemon: waits for Gitea, restarts the daemon when it exits. Generated by scripts/ci_runner_bootstrap.ps1.' |
|
||||
Out-Null
|
||||
Ok "scheduled task registered: $TaskName (at boot +1m, as SYSTEM, unlimited runtime, restart x999/1m)"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 4. start it
|
||||
# ---------------------------------------------------------------------------
|
||||
if ($StartNow) {
|
||||
if (-not (Test-Gitea $GiteaUrl)) {
|
||||
Warn "Gitea is not answering at $GiteaUrl right now -- the watchdog will keep waiting"
|
||||
}
|
||||
Start-ScheduledTask -TaskName $TaskName
|
||||
Ok "task started; watch: Get-Content '$(Join-Path $RunnerDir 'log\runner-stdout.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