Files
STM32F4-Base/.gitea/workflows/ci.yml
T
wandering1 2fdc5f6dda
CI / L0 静态门禁 (pre-commit) (push) Failing after 1m10s
CI / L0.5 主机侧单测 (MSVC) (push) Successful in 31s
修 ci.yml 注释写死的钩子数:改为"报告数 = 定义数 − 1"
钩子数在两个分支上不同(master 18、v2-on-ui 26),注释里写死 18/17 会让
另一分支上的读者按错数字去核对,正是本轮文档清理里修掉的同类漂移。头部
注释改为讲规则并给分分支实测快照;run 块内那行同步改成纯 ASCII 的规则
表述,避免增删钩子后再次过期。

同时作为非文档路径的普通 push:新建分支 v2-on-ui-ci 的那次 push 没有产生
run(runner 在线且空闲,job 日志最新仍是 10-03),怀疑新建分支的 push 拿
不到变更文件集、被 paths-ignore 滤空,用这次提交验证。

验证:YAML 可解析、全部 run 块纯 ASCII、无 BOM、LF;pre-commit 对该文件
通过。
2026-10-12 19:04:09 +08:00

483 lines
26 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Gitea Actions CI —— STM32F4-Base 持续集成
#
# 覆盖范围(**只做不依赖硬件的层**,见 docs/测试说明/测试开发标准流程.md §1/§9):
# L0 静态门禁 = pre-commit 全部钩子(等价本地 `pre-commit run --all-files`)
# L0.5 主机侧纯逻辑单测 = test/host/**(MSVC 离线编译,零硬件)
#
# ⛔ L1 板端套件(test/board/,需真机 + ST-LINK)/ L2 @test 闭环 / L3 人工目检
# **不上 CI** —— 需要开发板与烧录器,只能在本地跑。
#
# 明确不做的两件事(都是刻意为之):
# 1. `@build`(Keil UV4)—— 需本机 Keil 授权,runner 上不具备;且 L0 的 cppcheck +
# 代码规范已覆盖"编译前的静态问题",真编译仍是开发者的本地红线(AGENTS.md)。
# 2. `mem-numbers` 钩子 —— 它的事实源是 `MDK-ARM/**/*.map`(Keil 构建产物、已 gitignore),
# 无 `@build` 即无 `.map`。故本 workflow 在**检测不到 .map 时自动跳过该钩子**
# (见下方 "决定跳过哪些钩子" 步骤),有 .map 的 runner(如自建带 Keil 的机器)则照常跑。
#
# 🔴 门禁盲区声明(S14,2026-10-02):跳过 `mem-numbers` 意味着 **CI 不守 Flash/RAM 体积门禁**。
# 体积变更(新增任务/缓冲/代码)不会在 CI 被拦下;必须靠**本地 `@build` 后**
# `python scripts/check_mem_numbers.py --scan` 兜底。故该跳过以 `::warning::` 明示
# (不是弱化的 `::notice::`),并写入 job step-summary 的醒目行。
#
# 产物存档(S3,2026-10-02):两个 job 把关键输出 `Tee-Object` 到 `ci-artifacts/*.log`,
# 并写 $GITHUB_STEP_SUMMARY(Gitea UI 直接可见)+ `::notice::` 汇总行。
# **刻意不用 `actions/upload-artifact`**——内网可能拉不到外层 action(见 docs/CI/远程CI完整指南.md §5.3);
# 零依赖方案下日志留在 runner 工作区,UI 通过 step-summary 即可排障。
# ⚠️ `ci-artifacts/` 已 gitignore(运行时产物)。
#
# 触发:push(除纯文档路径外)与 pull_request。
#
# 🔴 shell 用 `powershell` 而非 `pwsh`(2026-10-02 实测修正):
# `pwsh` = PowerShell 7,**不是 Windows 自带**;本机 runner 只有
# Windows PowerShell 5.1(`C:\Windows\System32\WindowsPowerShell\v1.0\`),
# 用 `shell: pwsh` 会直接报 `Cannot find: pwsh in PATH` → job 失败(跑不到任何业务逻辑)。
# `shell: powershell` 在任意 Windows 上都存在。
#
# 🔴🔴 `run:` 脚本内容必须【纯 ASCII】(2026-10-02 第二次实测修正):
# runner 把每个 `run:` 块写入临时 `.ps1`(`...\act\workflow\N.ps1`)再交给
# Windows PowerShell 5.1 执行,而该临时文件**无 BOM** → PS 5.1 按 **GBK/CP936** 解码。
# 脚本里的 UTF-8 中文字节会与相邻引号错位 → 报
# `TerminatorExpectedAtEndOfString` / `字符串缺少终止符: "` / `缺少右"}"`
# (报错行号正是含中文的那一行,极易误判为逻辑错误)。
# **故本文件所有 `run:` 块内零非 ASCII**(中文仅出现在 YAML 的 name:/注释里——
# 那些由 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` 里 gitlint 那一条**永远不进 CI 报告**(报告数 = 定义数 − 1;
# 2026-10-12 实测 `master` 18 定义 / 17 报告、`v2-on-ui` 26 定义 / 25 报告)——
# 缺的正是 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`),且装有:
# Python 3.x、cppcheck、pre-commit、MSVC(VS2019/2022 的 C++ 工具链 + Windows SDK)
# - 缺 MSVC 时 L0.5 步骤会**降级为提示并跳过**(不硬失败),但会打印醒目 WARNING
#
# 🔧 runner 机器级环境变量(可选项覆盖;runner 以 SYSTEM 服务运行,用户级变量不可见):
# DTU_PRECOMMIT_PYTHON / DTU_CPPCHECK / DTU_MSVC_CL —— 未设置时按 PATH / 标准 VS 布局自动发现。
# 变量显式设置但值无效时 job 直接报变量名并失败(不静默回退)。配置方法见
# docs/环境与部署/本地工具路径环境变量方案.md §5.3。
#
# 本地等价命令(与 CI 同一套判定,见 scripts/ci_local.py):
# & '.venv\Scripts\python.exe' scripts/ci_local.py
name: CI
on:
push:
branches: ['**']
paths-ignore:
- 'docs/**'
- '**/*.md'
- '.workbuddy-ai/**'
pull_request:
paths-ignore:
- 'docs/**'
- '**/*.md'
- '.workbuddy-ai/**'
workflow_dispatch:
# 同一分支的旧运行自动取消,省 runner
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
# ------------------------------------------------------------------
# L0:静态门禁(pre-commit 钩子全集)
# ------------------------------------------------------------------
l0-gates:
name: L0 静态门禁 (pre-commit)
runs-on: windows
steps:
- uses: actions/checkout@v4
with:
# 钩子要对比基线与历史(doc-links 等读工作区即可,但 gitlint/cubemx 需完整历史)
fetch-depth: 0
- name: 环境自检(Python / cppcheck / pre-commit)
shell: powershell
run: |
# 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.
# --- pick a Python that actually has pre-commit -------------------
# 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.
#
# 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::ci_find_python.ps1 succeeded but returned no path"
exit 2
}
$py = "$py".Trim()
# --- cppcheck -----------------------------------------------------
# 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
}
$found = $envCpp
Write-Host "using cppcheck from DTU_CPPCHECK: $found"
} else {
$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
}
$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"
- name: 安装 pre-commit 钩子到本仓库
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: the reported hook count is always
# (defined count - 1), so a silently missing hook is invisible.
# Recount that difference whenever hooks are added or removed.
# 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
run: |
# mem-numbers fact source is the Keil build artifact .map (gitignored).
# No .map => this hook cannot run => skip (not "pass", but "not applicable").
#
# S14 (2026-10-02): make this GAP loud. mem-numbers is the ONLY gate that
# checks docs/memory numbers against the real .map (Flash/RAM size). When
# it is skipped, CI effectively does NOT guard the size budget -- a silent
# ::notice:: was too easy to miss. So:
# - emit ::warning:: (not ::notice::) so it shows in the run summary
# - write a machine-readable marker file for the summary step
# - append a bold line to the job step-summary
$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 -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 -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
}
- name: 运行 L0 门禁(--all-files)
shell: powershell
env:
SKIP: ${{ steps.skip.outputs.value }}
run: |
# Pure ASCII only (see file header). Capture output to an artifact file
# AND to the console (Tee-Object), so a failed run is debuggable from the
# Gitea UI without inspecting runner disk.
#
# CAUTION: the runner injects $ErrorActionPreference='Stop'. With `2>&1 |`,
# a native command's stderr becomes an ErrorRecord -> under 'Stop' that
# would ABORT the pipeline. Relax the preference around the pipeline and
# restore it afterwards (same pattern as the env-check step above).
New-Item -ItemType Directory -Force -Path 'ci-artifacts' | Out-Null
$log = 'ci-artifacts\l0-pre-commit.log'
if ($env:SKIP) { Write-Host "SKIP=$env:SKIP" }
$saved = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
& $env:PYTHON -s -m pre_commit run --all-files --show-diff-on-failure 2>&1 |
Tee-Object -FilePath $log
$rc = $LASTEXITCODE
$ErrorActionPreference = $saved
# Summarize hook results: count Passed / Failed / Skipped lines.
$lines = Get-Content $log -ErrorAction SilentlyContinue
$passed = @($lines | Select-String -Pattern 'Passed$').Count
$failed = @($lines | Select-String -Pattern 'Failed$').Count
$skipd = @($lines | Select-String -Pattern 'Skipped$').Count
$summary = "L0: pre-commit hooks -- passed=$passed failed=$failed skipped=$skipd (log: $log)"
Write-Host "::notice::$summary"
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "### L0 static gates" -Encoding utf8
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value $summary -Encoding utf8
if ($rc -ne 0) {
Write-Host "::error::L0 static gates failed (see output above and $log)"
exit $rc
}
- name: 归档 L0 日志(供下载/排障)
if: always()
shell: powershell
run: |
# S3: zero-dependency artifact archival. We do NOT use
# actions/upload-artifact (inner networks may not fetch outer actions);
# instead we keep the log on the runner AND surface its path + tail in
# the step summary, so the UI is self-sufficient for triage.
if (Test-Path 'ci-artifacts\l0-pre-commit.log') {
$size = (Get-Item 'ci-artifacts\l0-pre-commit.log').Length
Write-Host "archived: ci-artifacts/l0-pre-commit.log ($size bytes)"
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "" -Encoding utf8
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "Archived log: ``ci-artifacts/l0-pre-commit.log`` ($size bytes on runner)" -Encoding utf8
}
Write-Host "::notice::L0 artifacts kept on runner at: ci-artifacts\l0-pre-commit.log"
# ------------------------------------------------------------------
# L0.5:主机侧纯逻辑单测(零硬件;缺 MSVC 则跳过)
# ------------------------------------------------------------------
l05-host-unit-tests:
name: L0.5 主机侧单测 (MSVC)
runs-on: windows
steps:
- uses: actions/checkout@v4
- name: 探测 MSVC(VS BuildTools / Community 等)
id: msvc
shell: powershell
run: |
# MSVC discovery per the DTU_* plan, kept in sync with
# test/host/common/unit_test_harness.py:
# DTU_MSVC_CL (explicit x64 cl.exe; invalid = hard error)
# -> auto-probe standard VS roots derived from %ProgramFiles%
# / %ProgramFiles(x86)% (2019 / 2022 / 2026, all editions)
$found = $false
$envCl = $env:DTU_MSVC_CL
if ($envCl) {
if (-not (Test-Path $envCl)) {
Write-Host "::error::DTU_MSVC_CL is set but does not exist: $envCl"
exit 1
}
if ($envCl -notlike '*bin\Hostx64\x64\cl.exe') {
Write-Host "::error::DTU_MSVC_CL must be the x64 Host compiler (path ending bin\Hostx64\x64\cl.exe): $envCl"
exit 1
}
$found = $true
Write-Host "using cl.exe from DTU_MSVC_CL: $envCl"
} else {
$pf = ${env:ProgramFiles}
$pf86 = ${env:ProgramFiles(x86)}
if (-not $pf) { $pf = 'C:\Program Files' }
if (-not $pf86) { $pf86 = 'C:\Program Files (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\2026\BuildTools'),
(Join-Path $pf 'Microsoft Visual Studio\2026\Community'),
(Join-Path $pf 'Microsoft Visual Studio\18\BuildTools'),
(Join-Path $pf 'Microsoft Visual Studio\18\Community'),
(Join-Path $pf 'Microsoft Visual Studio\18\Professional'),
(Join-Path $pf 'Microsoft Visual Studio\18\Enterprise')
)
foreach ($r in $roots) {
if (Test-Path $r) {
$cl = Get-ChildItem -Path $r -Recurse -Filter 'cl.exe' -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -like '*Hostx64\x64*' } | Select-Object -First 1
if ($cl) { $found = $true; Write-Host "found cl.exe: $($cl.FullName)"; break }
}
}
}
if ($found) {
"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 -Encoding ascii
}
- name: 运行主机侧单测
id: l05
if: steps.msvc.outputs.value == 'yes'
shell: powershell
run: |
# 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) {
Write-Host "::error::ci_find_python.ps1 succeeded but returned no path"
exit 2
}
$py = "$py".Trim()
Write-Host "using python: $py"
New-Item -ItemType Directory -Force -Path 'ci-artifacts' | Out-Null
# Iterate test/host/*/run_unit_test.py (each module ships its own runner via common harness)
$runners = Get-ChildItem -Path 'test/host' -Recurse -Filter 'run_unit_test.py' |
Sort-Object FullName
if (-not $runners) {
Write-Host "::warning::no test/host/**/run_unit_test.py found -- skipping"
exit 0
}
$fail = 0
$runnerCount = 0
$sumPass = 0
$sumTotal = 0
# Relax preference around each `2>&1 | Tee-Object` pipeline (native stderr
# becomes ErrorRecord -> 'Stop' would abort). See L0 step for details.
$savedPref = $ErrorActionPreference
foreach ($r in $runners) {
$runnerCount++
$name = $r.Directory.Name
$log = "ci-artifacts\l05-$name.log"
Write-Host "================================"
Write-Host "run: $($r.FullName)"
Write-Host "================================"
$ErrorActionPreference = 'Continue'
& $py $r.FullName 2>&1 | Tee-Object -FilePath $log
$rc = $LASTEXITCODE
$ErrorActionPreference = $savedPref
# Parse the module's own summary line, e.g.
# "=== System Clock Tests: 100/100 PASSED (failed=0) ==="
$m = Select-String -Path $log -Pattern ': (\d+)/(\d+) PASSED \(failed=(\d+)\)' -ErrorAction SilentlyContinue |
Select-Object -Last 1
if ($m) {
$sumPass += [int]$m.Matches[0].Groups[1].Value
$sumTotal += [int]$m.Matches[0].Groups[2].Value
Write-Host ("::notice::{0}: {1}" -f $name, $m.Matches[0].Value.Trim())
} else {
Write-Host "::warning::$name produced no '=== ... N/M PASSED ===' summary line"
}
if ($rc -ne 0) {
Write-Host "::error::unit test failed: $($r.FullName) (exit $rc)"
$fail = 1
}
}
$agg = "L0.5: $runnerCount runner(s), $sumPass/$sumTotal assertions passed, fail=$fail"
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 -Encoding ascii
"agg=$agg" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding ascii
exit $fail
- name: 归档 L0.5 日志(供下载/排障)
if: always()
shell: powershell
run: |
# S3: same zero-dependency archival as L0.
if (Test-Path 'ci-artifacts') {
$logs = Get-ChildItem -Path 'ci-artifacts' -Filter '*.log' -ErrorAction SilentlyContinue
foreach ($f in $logs) {
Write-Host "archived: $($f.FullName) ($($f.Length) bytes)"
}
$names = ($logs | ForEach-Object { '``' + $_.Name + '``' }) -join ', '
if ($names) {
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "" -Encoding utf8
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "Archived logs on runner: $names" -Encoding utf8
}
} else {
Write-Host "no ci-artifacts directory (L0.5 skipped or failed early)"
}
- name: L0.5 跳过说明
if: steps.msvc.outputs.value == 'no'
shell: powershell
run: |
Write-Host "L0.5 skipped: runner has no MSVC."
Write-Host "Local equivalent: & '.venv\Scripts\python.exe' scripts/ci_local.py"
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "### L0.5 host-side unit tests" -Encoding utf8
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value ":warning: **NOT RUN** -- runner has no MSVC (VS C++ toolchain)." -Encoding utf8
exit 0