842d711ed6
用户裁定「我不想看到编号」。测试验证层级这一族的整族清退分三道并行:本笔是**代码与 CI 道** (CI 步骤与门禁输出面),代码注释面另走、文档面由文档道走。 口径(旧 → 语义名 → ASCII 段名,段名只进日志/旗标/job id/文件名): L0 静态门禁 static | L0a 钩子解释器预检 preflight | L0b 提交信息门禁 commitmsg L0c 体积门禁 size | L0.5 主机侧纯逻辑单测 host-unit | L0.5c 覆盖率 coverage - scripts/ci_local.py 43 站点:段标题与 [SUMMARY] 标签改 ASCII 段名(static: / host-unit:),函数 run_l0/run_l05 → run_static_gates/run_host_unit_tests, 收尾「已跑:」列表用段名。旗标改 --static-only / --host-unit-only,**旧旗标 --l0-only / --l05-only 保留为 argparse 别名**(肌肉记忆与既有脚本片段不断档, 实测 --help 两名列出)。 - .gitea/workflows/ci.yml 42 站点:job id l0-gates → static-gates、 l05-host-unit-tests → host-unit-tests;步骤名与 step-summary 标题改语义名; 日志名 l0-pre-commit.log → static-pre-commit.log、l05-<模块>.log → host-unit-<模块>.log;::error::/::notice:: 文案同步。yaml.safe_load 核过 两个 job id 与步骤名。 host-unit-tests;步骤名与 step-summary 标题改语义名;日志名 l0-pre-commit.log → static-pre-commit.log、l05-<模块>.log → host-unit-<模块>.log;::error::/::notice:: 文案同步。yaml.safe_load 核过两个 job id 与步骤名。 - 门禁脚本 18 站点:check_registration(登记簿理由串 7)、check_test_coverage 5、 README 8、check_generated_fresh 2、ram_baseline 2、check_size_gate 1、 ci_find_python.ps1 2、ci_runner_bootstrap.ps1 2、.pre-commit-config.yaml 2、 requirements.txt 1(保持纯 ASCII)。 - skills 7 站点:task-worktree(含 [SUMMARY] 样例两行)、mem-budget、ui-verify、 task_worktree.py 的实跑标签。 - AGENTS.md 3 站点:CI 段「只跑静态门禁 + 主机侧纯逻辑单测」、自查命令段、 「2026-10-11 静态门禁全灭」事件名。 - 归档:编号治理历史.md 新增 §10 换算表 + **撞族警示**(同形 L0/L1/L2/L3 还属 UI 设计 令牌层 / UI 菜单层级 / 采集链 / 链路时延段 / CubeMX 三层 / 硬件位号与 FTP 应答字面量 等六个族,其中 6 处在字符串字面量里,改了就是改行为——故全程只按行内置换、先判族)。 - docs/产品需求 §7.4 测试分层表:去号 + 顺带修掉上一批**错族替换疤**(该表 L1/L2 行曾被 采集链层名「语义取数与有效性 / 全周傅氏相量」「保护判据」顶替,现按内容改写为板端测试 套件 / 主机侧回归两行)。同文档 §1.5 G5 判据行同步。 门禁:ci_local --help 两面旗标并列;py_compile 7 个 .py 全过;yaml 解析通过; check_ids_new --worktree rc=0;git diff --check 干净;17 个改动文件 FFFD=0、无 CR、 BOM 与原文件一致(含 requirements.txt 纯 ASCII)。本族无机器识别面(单字母+数字是 新增行门禁的自述盲区),口径来自自建逐行扫描器,复扫本族形态归零。 - 交接说明:文档道 teammate 在改 CI 指南时中断,剩余 9 处双写疤与 29 处 CI 标识 (job id / 日志名 / 汇总行字面量)由 Lead 接手改完,逐处仍走断言式行内置换。
561 lines
32 KiB
YAML
561 lines
32 KiB
YAML
# Gitea Actions CI —— STM32F4-Base 持续集成
|
||
#
|
||
# 覆盖范围(**只做不依赖硬件的层**,见 docs/测试说明/测试开发标准流程.md §1/§9):
|
||
# 静态门禁 static = pre-commit 全部钩子(等价本地 `pre-commit run --all-files`)
|
||
# 主机侧纯逻辑单测 host-unit = test/host/**(MSVC 离线编译,零硬件)
|
||
#
|
||
# ⛔ 板端测试套件(test/board/,需真机 + ST-LINK)/ 主机侧回归(@test 闭环)/ 人工目检
|
||
# **不上 CI** —— 需要开发板与烧录器,只能在本地跑。
|
||
#
|
||
# 明确不做的两件事(都是刻意为之):
|
||
# 1. `@build`(Keil UV4)—— 需本机 Keil 授权,runner 上不具备;且静态门禁的 cppcheck +
|
||
# 代码规范已覆盖"编译前的静态问题",真编译仍是开发者的本地红线(AGENTS.md)。
|
||
# 2. `mem-numbers` / `mem-budget` 两根钩子 —— 事实源都是 `MDK-ARM/**/*.map`(Keil
|
||
# 构建产物、已 gitignore),无 `@build` 即无 `.map`。故本 workflow 在**检测不到
|
||
# .map 时自动跳过它们**(见下方 "决定跳过哪些钩子" 步骤),有 .map 的 runner 照常跑。
|
||
#
|
||
# 🔴 体积门禁:盲区已关闭为**强制路径**(2026-10-25 起;原先只做到"声明")
|
||
# 跳过钩子只解释了"runner 没有 Keil",没解决**"没跑 = 通过"**:钩子在无 .map 时
|
||
# 自适应 SKIP 并返回 0,于是 CI 全绿仍可能被读成"体积也守住了"。现由静态门禁作业**最后
|
||
# 一步** `python scripts/check_size_gate.py --ci` 在**不需要 Keil** 的前提下补两条判据:
|
||
# · 同码性:`scripts/size_attestation.json` 的源码指纹必须等于本次检出树的指纹,
|
||
# 而该凭证只能由"新鲜 .map + 当时未超线"的真实链接测量写出(见 --stamp 三道前置)
|
||
# ——等价于"这份代码至少被 Keil 链接并核算过一次";
|
||
# · 红线:该次测量记录的受管区占比 ≤92%,与 `mem-budget` 同一常数同一口径。
|
||
# 强制落点 = **主干 push**(唯一能保证"这就是要发出去的那份"的位置);其余事件降级为
|
||
# `::warning::`——任务分支上的代码尚未定型,拦红只制造噪音,还会诱发多条道同时
|
||
# 抢写 scripts/ 台账。仍守不住的(**口径正文唯一**在 scripts/check_size_gate.py 的
|
||
# 模块 docstring,此处不重复):绝对数字由本地真实构建产生(runner 无 Keil 许可证)、
|
||
# 编译器选项(uvprojx,UV4 每次改写且不入库)不在指纹范围内。
|
||
#
|
||
# 产物存档与结果可见性(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 的静态门禁 **全部死在"环境自检",一个钩子都没跑**。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 时主机侧单测步骤会**降级为提示并跳过**(不硬失败),但会打印醒目 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:
|
||
# ------------------------------------------------------------------
|
||
# 静态门禁(pre-commit 钩子全集)
|
||
# ------------------------------------------------------------------
|
||
static-gates:
|
||
name: 静态门禁 (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 static-gate 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 the static-gate job 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 / mem-budget fact source is the Keil build artifact .map
|
||
# (gitignored). No .map => those hooks cannot run => skip them EXPLICITLY.
|
||
#
|
||
# 2026-10-25: two honesty fixes here.
|
||
# 1) mem-budget used NOT to be listed, yet it self-skips inside the script
|
||
# when .map is missing (exit 0) -- so on a Keil-less runner it reported
|
||
# "Passed" while checking nothing. A gate that fakes green is worse than
|
||
# a gate that is visibly skipped: skip it here so the report says Skipped.
|
||
# 2) "skipped" alone is still the blind spot, so the compensation is a
|
||
# real step, not only a warning: see the "size gate" step at the end of
|
||
# this job (scripts/check_size_gate.py --ci, no Keil needed: attestation
|
||
# parity + 92% re-check on the recorded numbers). Boundary and rationale
|
||
# live in that script docstring (single source of truth).
|
||
$map = Get-ChildItem -Path 'MDK-ARM' -Recurse -Filter '*.map' -ErrorAction SilentlyContinue
|
||
if ($map) {
|
||
Write-Host "found .map: $($map[0].FullName) -- size hooks run normally"
|
||
"value=" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding ascii
|
||
"sizegate=on" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding ascii
|
||
"> Volume gates (mem-numbers, mem-budget): 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 and mem-budget, so absolute Flash/RAM numbers are NOT recomputed here."
|
||
Write-Host "::warning::that gap is no longer silent: the final step of this job runs scripts/check_size_gate.py --ci (forced path: attestation parity + 92% budget re-check). Locally: scripts/ci_local.py size step."
|
||
"value=mem-numbers,mem-budget" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding ascii
|
||
"sizegate=off" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding ascii
|
||
"> :warning: Volume gates (mem-numbers, mem-budget): **NOT RUN** -- no `.map` (no Keil build); compensated by the size-gate step below." |
|
||
Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8
|
||
}
|
||
|
||
- name: 运行静态门禁(--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\static-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 = "static: pre-commit hooks -- passed=$passed failed=$failed skipped=$skipd (log: $log)"
|
||
Write-Host "::notice::$summary"
|
||
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "### 静态门禁 (static gates)" -Encoding utf8
|
||
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value $summary -Encoding utf8
|
||
|
||
if ($rc -ne 0) {
|
||
Write-Host "::error::static gates failed (see output above and $log)"
|
||
exit $rc
|
||
}
|
||
|
||
- name: 归档静态门禁日志(供下载/排障)
|
||
if: always()
|
||
shell: powershell
|
||
run: |
|
||
# Result visibility: 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\static-pre-commit.log') {
|
||
$size = (Get-Item 'ci-artifacts\static-pre-commit.log').Length
|
||
Write-Host "archived: ci-artifacts/static-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/static-pre-commit.log`` ($size bytes on runner)" -Encoding utf8
|
||
}
|
||
Write-Host "::notice::static-gate artifacts kept on runner at: ci-artifacts\static-pre-commit.log"
|
||
|
||
# ------------------------------------------------------------
|
||
# 强制体积门禁(2026-10-25):不需要 Keil,也不需要 .map
|
||
# 放在静态门禁作业**最后**且 if: always()——静态门禁失败时这条结论同样要看得到,
|
||
# 而它自己失败会让整个作业变红(这正是"强制"的含义)。
|
||
- name: 体积门禁(强制路径:同码凭证 + 92% 红线,无需 Keil)
|
||
if: always()
|
||
shell: powershell
|
||
env:
|
||
GATE_REF: ${{ github.ref }}
|
||
GATE_EVENT: ${{ github.event_name }}
|
||
run: |
|
||
# Pure ASCII only (see file header).
|
||
#
|
||
# WHY THIS STEP EXISTS: mem-numbers / mem-budget read the Keil .map and
|
||
# self-skip (exit 0) without it, so "not measured" used to look like
|
||
# "passed" -- that is exactly the blind spot ("CI all green does not
|
||
# mean the size budget held"). Merely declaring it did not close it.
|
||
#
|
||
# This step enforces a check that needs NO Keil and NO .map:
|
||
# * parity -- scripts/size_attestation.json (a committed attestation
|
||
# that --stamp refuses to write unless the .map is present, newer
|
||
# than every source file in scope, and inside budget) must
|
||
# fingerprint-match THIS checked-out tree => this exact code was
|
||
# linked for real at least once;
|
||
# * budget -- the region percentages that measurement recorded must
|
||
# stay within the same 92% line mem-budget uses.
|
||
#
|
||
# Enforcement point = trunk push, the only ref guaranteed to be the code
|
||
# we ship. Task branches / WIP pull requests run --warn-only instead: the
|
||
# code there is still moving, and blocking it would push every lane to
|
||
# edit the same scripts/ ledger file at the same time.
|
||
New-Item -ItemType Directory -Force -Path 'ci-artifacts' | Out-Null
|
||
$log = 'ci-artifacts\size-gate.log'
|
||
if (-not $env:PYTHON) {
|
||
# The environment probe step already failed in that case, so the job is
|
||
# red anyway; do not mask that with a second, unrelated error.
|
||
Write-Host "::warning::PYTHON unresolved (env step failed) -- size gate cannot run"
|
||
exit 0
|
||
}
|
||
$trunks = @('refs/heads/v2-on-ui', 'refs/heads/master', 'refs/heads/main')
|
||
$enforce = ($env:GATE_EVENT -eq 'push') -and ($trunks -contains $env:GATE_REF)
|
||
if ($enforce) {
|
||
$mode = @()
|
||
$label = 'ENFORCING (trunk push)'
|
||
} else {
|
||
$mode = @('--warn-only')
|
||
$label = 'advisory (non-trunk event)'
|
||
}
|
||
$saved = $ErrorActionPreference
|
||
$ErrorActionPreference = 'Continue'
|
||
& $env:PYTHON -s scripts/check_size_gate.py --ci @mode 2>&1 |
|
||
Tee-Object -FilePath $log
|
||
$rc = $LASTEXITCODE
|
||
$ErrorActionPreference = $saved
|
||
|
||
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "" -Encoding utf8
|
||
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "### Volume gate (forced path)" -Encoding utf8
|
||
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "size gate: rc=$rc mode=$label (attestation parity + 92% re-check; no Keil)" -Encoding utf8
|
||
Write-Host "::notice::size gate: rc=$rc mode=$label (log: $log)"
|
||
if ($rc -ne 0) {
|
||
Write-Host "::error::volume gate FAILED: this tree has no matching real Keil measurement, or the measured regions exceed the 92% budget."
|
||
Write-Host "::error::fix: run the Keil build locally (0 errors 0 warnings), then 'python scripts/ram_baseline.py --update', and commit scripts/size_attestation.json (scripts/ram_baseline.tsv is refreshed by the same command). Rationale/boundary: scripts/check_size_gate.py docstring."
|
||
exit $rc
|
||
}
|
||
|
||
# ------------------------------------------------------------------
|
||
# 主机侧纯逻辑单测(零硬件;缺 MSVC 则跳过)
|
||
# ------------------------------------------------------------------
|
||
host-unit-tests:
|
||
name: 主机侧单测 (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 host-side unit tests. 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 the static-gate job (no
|
||
# -NeedPreCommit: these harnesses are stdlib-only). The old inline
|
||
# probe here had the same 2-candidate blind spot as the static-gate job -- 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 the static-gate steps for details.
|
||
$savedPref = $ErrorActionPreference
|
||
foreach ($r in $runners) {
|
||
$runnerCount++
|
||
$name = $r.Directory.Name
|
||
$log = "ci-artifacts\host-unit-$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 = "host-unit: $runnerCount runner(s), $sumPass/$sumTotal assertions passed, fail=$fail"
|
||
Write-Host "::notice::$agg"
|
||
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "### 主机侧纯逻辑单测 (host-unit)" -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: 归档主机侧单测日志(供下载/排障)
|
||
if: always()
|
||
shell: powershell
|
||
run: |
|
||
# Result visibility: same zero-dependency archival as the static-gate job.
|
||
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 (host-side unit tests skipped or failed early)"
|
||
}
|
||
|
||
- name: 主机侧单测跳过说明
|
||
if: steps.msvc.outputs.value == 'no'
|
||
shell: powershell
|
||
run: |
|
||
Write-Host "host-side unit tests 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 "### 主机侧纯逻辑单测 (host-unit)" -Encoding utf8
|
||
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value ":warning: **NOT RUN** -- runner has no MSVC (VS C++ toolchain)." -Encoding utf8
|
||
exit 0
|