diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 700b9c46..28b9d91f 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -14,6 +14,17 @@ # 无 `@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(见 CI配置指南 §4.3); +# 零依赖方案下日志留在 runner 工作区,UI 通过 step-summary 即可排障。 +# ⚠️ `ci-artifacts/` 已 gitignore(运行时产物)。 +# # 触发:push(除纯文档路径外)与 pull_request。 # # 🔴 shell 用 `powershell` 而非 `pwsh`(2026-10-02 实测修正): @@ -156,13 +167,27 @@ jobs: 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 + "sizegate=on" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + "> Volume gate (mem-numbers): RAN (.map present)" | + Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 } else { - Write-Host "::notice::no .map found (no Keil build) -- skipping mem-numbers hook" + 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 + "> :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) @@ -170,13 +195,55 @@ jobs: 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" } - & $env:PYTHON -s -m pre_commit run --all-files --show-diff-on-failure - if ($LASTEXITCODE -ne 0) { - Write-Host "::error::L0 static gates failed (see output above)" - exit $LASTEXITCODE + $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 则跳过) # ------------------------------------------------------------------ @@ -213,6 +280,7 @@ jobs: } - name: 运行主机侧单测 + id: l05 if: steps.msvc.outputs.value == 'yes' shell: powershell run: | @@ -229,6 +297,8 @@ jobs: if (-not $py) { Write-Host "::error::no python found"; exit 1 } 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 @@ -237,22 +307,74 @@ jobs: 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 "================================" - & $py $r.FullName - if ($LASTEXITCODE -ne 0) { - Write-Host "::error::unit test failed: $($r.FullName) (exit $LASTEXITCODE)" + $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 + "agg=$agg" | Out-File -FilePath $env:GITHUB_OUTPUT -Append 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 diff --git a/.gitignore b/.gitignore index 1d709809..b06ee784 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,7 @@ docs/images/*.png # Serial assistant (tools/serial_tool) runtime state (daemon log/meta, local runtime artifacts) tools/serial_tool/state/ + +# CI 运行时产物(.gitea/workflows/ci.yml 的 S3 步骤在 runner 工作区落盘的日志; +# 零依赖存档方案——落在工作区目录而非上传 artifact,故必须忽略以免误提交) +ci-artifacts/ diff --git a/docs/测试说明/测试开发标准流程.md b/docs/测试说明/测试开发标准流程.md index 47fa4723..e7c44970 100644 --- a/docs/测试说明/测试开发标准流程.md +++ b/docs/测试说明/测试开发标准流程.md @@ -457,17 +457,29 @@ flowchart LR 1. **不跑 `@build`(Keil UV4)**——需本机 Keil 授权,runner 上不具备。L0 的 cppcheck + 代码规范 已覆盖"编译前的静态问题",真编译仍是开发者的本地红线(`AGENTS.md`)。 -2. **强制跑 `mem-numbers` 钩子**——它的事实源是 `MDK-ARM/**/*.map`(Keil 构建产物,已 `.gitignore`), - 无 `@build` 即无 `.map`。故 workflow 在**检测不到 `.map` 时自动跳过该钩子** - (`SKIP=mem-numbers`,钩子显示 `Skipped` 而非 `Passed`——是"不适用",不是"通过"); - 有 `.map` 的 runner(如自建带 Keil 的机器)则照常跑。 +2. **`mem-numbers` 钩子按 `.map` 有无自适应跳过**——它的事实源是 `MDK-ARM/**/*.map` + (Keil 构建产物,已 `.gitignore`),无 `@build` 即无 `.map`。故 workflow 在**检测不到 + `.map` 时自动跳过该钩子**(`SKIP=mem-numbers`,钩子显示 `Skipped` 而非 `Passed`—— + 是"不适用",不是"通过");有 `.map` 的 runner(如自建带 Keil 的机器)则照常跑。 + +> 🔴 **门禁盲区(S14,2026-10-02)**:跳过 `mem-numbers` 意味着 **CI 不守 Flash/RAM 体积门禁**—— +> 新增任务/栈/缓冲/代码导致的体积增长**不会在 CI 被拦**。故该跳过以 `::warning::` 明示 +> (不是弱化的 notice)并写入 step-summary。**兜底必须靠本地**:改代码后 `@build`(0/0) +> → `python scripts/check_mem_numbers.py --scan`。详见 +> [`../环境与部署/CI配置指南.md`](../环境与部署/CI配置指南.md) §0.0。 ### 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`)→ `pre-commit run --all-files --show-diff-on-failure`(`env: SKIP`) | -| `l05-host-unit-tests` | `windows` | checkout → **探测 MSVC**(按 VS_ROOTS 找 `Hostx64\x64\cl.exe`)→ 遍历 `test/host/**/run_unit_test.py` 逐个跑(失败 `::error::` 并置退出码);**未探测到 MSVC 时打印醒目 WARNING 并降级跳过**(不硬失败,避免"没装 VS 的 runner 全红") | +| `l0-gates` | `windows` | checkout(`fetch-depth: 0`,gitlint/cubemx 需完整历史)→ 环境自检(Python / cppcheck / `python -m pre_commit`,缺则 `::error::` + exit 1)→ `pre_commit install --install-hooks` → **决定跳过哪些钩子**(探测 `.map`,写入 `GITHUB_OUTPUT`;无 `.map` 时 `::warning::` 声明体积门禁未跑)→ `pre-commit run --all-files --show-diff-on-failure`(`env: SKIP`,输出 `Tee` 到 `ci-artifacts/l0-pre-commit.log`)→ 打印 `::notice::L0: passed/failed/skipped` 汇总行 → 归档日志说明 | +| `l05-host-unit-tests` | `windows` | checkout → **探测 MSVC**(按 VS_ROOTS 找 `Hostx64\x64\cl.exe`)→ 遍历 `test/host/**/run_unit_test.py` 逐个跑(输出 `Tee` 到 `ci-artifacts/l05-<模块>.log`;解析各模块 `N/M PASSED` 汇总行 → 聚合 `::notice::L0.5: K runner(s), A/B assertions passed`;失败 `::error::` 并置退出码);**未探测到 MSVC 时打印醒目 WARNING 并降级跳过**(不硬失败,避免"没装 VS 的 runner 全红") | + +**结果可见性(S3,2026-10-02)**:两个 job 均写 `$GITHUB_STEP_SUMMARY`(job 运行页的 +**Summary** 区块直接可见)+ `::notice::` 汇总行;日志落 `ci-artifacts/*.log`(runner 工作区, +已 gitignore)。**刻意不用 `actions/upload-artifact`**(内网可能拉不到外层 action,见 +`CI配置指南.md` §4.3/§4.4)——零依赖方案下,"不查 MySQL / 不翻磁盘即可排障"的目标由 +step-summary 达成。 触发:`push`(`paths-ignore: docs/** / **/*.md / .workbuddy-ai/**`)与 `pull_request`,另留 `workflow_dispatch` 手动触发。 `concurrency: group: ci-${{ github.ref }}` + `cancel-in-progress: true`(同分支旧运行自动取消,省 runner)。 @@ -537,6 +549,7 @@ CI 只在 push / PR 时跑,且需 runner 就绪。**提交前自查、或 runn | 日期 | 变更 | |---|---| +| 2026-10-02 | **S3 + S14**:§9.1 第 2 条改正(原文"强制跑 `mem-numbers`"自相矛盾 → 改为"按 `.map` 有无自适应跳过")并加 🔴 门禁盲区声明;§9.2 补 S3 结果可见性(`Tee-Object` 落盘 + `$GITHUB_STEP_SUMMARY` + `::notice::` 汇总行,不用 `actions/upload-artifact`)。另:§4.1 登记 `test/host/sys_clock/` 新单测(S1-a) | | 2026-10-01 | 新增 **§9 CI 持续集成(Gitea Actions,T-6)**:`.gitea/workflows/ci.yml`(`l0-gates` + `l05-host-unit-tests`,Windows runner);**L0 + L0.5 上 CI、L1/L2/L3 留本地**;刻意不跑 `@build`、`.map` 缺失时自适应跳过 `mem-numbers`;新增 `scripts/ci_local.py` 本地等价命令(§4.1 登记);§1 补 CI 说明;`docs/待处理内容.md` T-6 销项 | | 2026-10-01 | §3.6 新增 **`test/run_all_suites.py`** 多套件回归编排(`try/finally` 无条件还原套件开关;T-5);`test/README.md` 同步 | | 2026-10-01 | §3.2 断言宏改为**集中内核**:新增 `test/board/test_common.h`(`test_stats_t` + `TEST_CHECK_IMPL`/`TEST_REPORT_IMPL`),4 套件各自用一行宏绑定自身统计变量(T-4;零行为变化、零 RAM 增量、`@build` 0/0,四套件均 0/0 验证)。附 `test_common.h` 不 include `dbg_log.h` 的原因(`DBG_TAG` 冻结 → `#47-D`) | diff --git a/docs/环境与部署/CI配置指南.md b/docs/环境与部署/CI配置指南.md index c1a5186b..e23b9fbf 100644 --- a/docs/环境与部署/CI配置指南.md +++ b/docs/环境与部署/CI配置指南.md @@ -7,6 +7,25 @@ --- +## ⚠️ 0.0 CI 不覆盖的体积门禁(必读,S14) + +> **CI 不守 Flash/RAM 体积门禁。** `mem-numbers` 钩子的事实源是 Keil 构建产物 +> `MDK-ARM/**/*.map`(已 gitignore),而 CI 刻意不跑 `@build` → **无 `.map` → 该钩子被跳过**。 +> +> 后果:**新增任务 / 栈 / 缓冲 / 代码导致的体积增长(或文档内存数字失真)不会在 CI 被拦下。** +> +> 兜底(每次改代码后必做): +> ```powershell +> & "MDK-ARM\build.bat" # 0/0 且生成 .map +> & '.venv\Scripts\python.exe' scripts/check_mem_numbers.py --scan +> ``` +> 若 CI 允许在带 Keil 的 runner 上跑,则该钩子会在有 `.map` 时**照常执行**(workflow 自适应)。 +> +> CI 运行时该跳过以 **`::warning::`** 明示(不是弱化的 notice),并写入 job 的 +> **step-summary** 醒目行,避免被忽略。 + +--- + ## 0. 全景:CI 需要三样东西就位 ``` @@ -258,6 +277,35 @@ github.com 会在 checkout 步骤卡住/失败。三种处置: > 方案 3 无需任何外部 action 与任何 `DEFAULT_ACTIONS_URL` 配置;代价是失去 checkout 的缓存优化。 > 参考:Gitea FAQ「Where will the runner download scripts when using actions such as `actions/checkout@v4`?」 +### 4.4 产物存档与结果汇总(S3,2026-10-02) + +**目标**:CI 失败时**不查 MySQL / 不翻 runner 磁盘**,在 Gitea 页面即可看到完整输出与摘要。 + +**零依赖方案**(内网首选,**不用 `actions/upload-artifact`**): + +| 手段 | 说明 | +|---|---| +| `Tee-Object` 落盘 | 两个 job 把关键输出 `Tee` 到 `ci-artifacts/l0-pre-commit.log`、`ci-artifacts/l05-<模块>.log`(**留在 runner 工作区**,已 gitignore) | +| `$GITHUB_STEP_SUMMARY` | 写入 job 的 **step-summary**(Gitea UI 运行页直接可见,无需下钻日志) | +| `::notice::` 汇总行 | 控制台醒目行,便于日志检索 | + +**汇总行格式**: + +| 层 | 汇总行 | +|---|---| +| L0 | `L0: pre-commit hooks -- passed=N failed=M skipped=K (log: ci-artifacts\l0-pre-commit.log)` | +| L0.5 | `L0.5: runner(s), / assertions passed, fail=`(逐模块另有 `::notice::<模块>: N/M PASSED (failed=0)`) | + +> ⚠️ **`ci-artifacts/` 已加入 `.gitignore`**:它是 runner 工作区的运行时产物,不要提交。 +> +> ⚠️ **若后续改为 `actions/upload-artifact`**(需确认实例能拉该 action):把 `Tee` 落盘 +> 保留即可,上传步骤叠加在其后;本方案的落盘文件即上传源。 + +**PowerShell 陷阱(已在 workflow 内处理)**:runner 注入 `$ErrorActionPreference='Stop'`, +而 `& native 2>&1 | Tee-Object` 会把 native 的 stderr 变成 ErrorRecord → 在 `Stop` 下**整段中止**。 +故 pipeline 前后需 `Save/Restore` 该偏好为 `Continue`(与"环境自检"步骤同法)。 +`$LASTEXITCODE` 在 `Tee-Object` 之后**仍保留** native 退出码(实测),可直接用于判定。 + --- ## 5. 端到端验证清单 @@ -270,9 +318,10 @@ github.com 会在 checkout 步骤卡住/失败。三种处置: | 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`;无 `.map` 时 `mem-numbers` 显示 `Skipped` | -| 6 | `l05-host-unit-tests` 日志 | `=== Task Watchdog Tests: 39/39 PASSED (failed=0) ===` | -| 7 | 提交记录旁出现 ✅ | CI 全绿 | +| 5 | `l0-gates` 日志 | `pre-commit ... Passed`;`::notice::L0: pre-commit hooks -- passed=.. failed=0 skipped=..`;无 `.map` 时见 `::warning::` 体积门禁未跑 | +| 6 | `l05-host-unit-tests` 日志 | `::notice::L0.5: 2 runner(s), 139/139 assertions passed, fail=0` | +| 7 | job 运行页的 **Summary** 区块 | 出现 "L0 static gates" / "L0.5 host-side unit tests" 小标题与汇总行 | +| 8 | 提交记录旁出现 ✅ | CI 全绿 | **本地先自查**(与 CI 同一套判定,避免推上去才发现问题): @@ -291,7 +340,10 @@ github.com 会在 checkout 步骤卡住/失败。三种处置: | `l0-gates` 报 `::error::runner 缺少:Python, cppcheck` | runner 没装或不在 PATH | 装完后**重启 runner 服务**(新进程才吃到新 PATH) | | `checkout` 步骤卡住/超时 | 拉不到 `actions/checkout@v4`(内网) | 见 §4.3 | | `l05` 日志出现 `::warning::runner 未安装 MSVC` | VS 装在非默认路径或未装 | 见 §3.2(默认路径 / 同步改两处根目录) | -| `mem-numbers` 显示 `Skipped` | 正常(CI 无 Keil,无 `.map`) | 无需处理;是"不适用"不是"失败" | +| `mem-numbers` 显示 `Skipped` | 正常(CI 无 Keil,无 `.map`) | 无需处理;但注意 **CI 未守体积门禁**(见 §0.0),改代码后本地 `@build` + `check_mem_numbers.py --scan` 兜底 | +| L0.5 显示 `2 runner(s), X/Y ...` 但 X.log` | +| L0.5 报 `produced no '=== ... N/M PASSED ===' summary line` | 该模块运行器未输出汇总行(改了 harness 或运行器崩溃) | 核对 `test/host/common/unit_test_harness.py` 的 `summary_marker` 与该模块 `TestBuild` 是否一致 | +| `Tee-Object` 步骤报 `TerminatorExpectedAtEndOfString` / 字符串缺少终止符 | `run:` 块混入了非 ASCII(GBK 解码错位) | 见文件头 🔴🔴;`run:` 内必须纯 ASCII | | `l05` 报 `fatal error C1083: 无法打开包括文件 "stdio.h"` | Windows SDK 缺失 | 装 VS 时勾"使用 C++ 的桌面开发"+ SDK | | 单测退出码 2 | 环境错误(多为无 MSVC) | 同上 | | `pre-commit` 首次运行久 | 首次要下载各钩子的环境 | 正常,runner 缓存后会快 | @@ -313,5 +365,6 @@ github.com 会在 checkout 步骤卡住/失败。三种处置: | 日期 | 变更 | |---|---| +| 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 拉取 / 端到端验证清单 / 故障排查 / 日常维护) | diff --git a/scripts/ci_local.py b/scripts/ci_local.py index 3381f0e5..e51b68e4 100644 --- a/scripts/ci_local.py +++ b/scripts/ci_local.py @@ -21,6 +21,10 @@ 1 = 有门禁/单测失败 2 = 环境错误(缺 pre-commit / python 模块等) +汇总行(S3,2026-10-02,对齐 CI 的 ::notice::): + [SUMMARY] L0: pre-commit hooks -- passed=N failed=M skipped=K + [SUMMARY] L0.5: K runner(s), A/B assertions passed, fail=F + 用法(仓库根目录): & '.venv\\Scripts\\python.exe' scripts/ci_local.py & '.venv\\Scripts\\python.exe' scripts/ci_local.py --l0-only @@ -33,6 +37,7 @@ import argparse import os +import re import subprocess import sys from pathlib import Path @@ -90,6 +95,27 @@ def _run(cmd, env=None): return cp.returncode +def _run_capture(cmd, env=None): + """跑子进程,输出**既直通终端又捕获**(对齐 CI 的 Tee-Object)。 + + 返回 (退出码, 全部输出文本)。用于 L0.5 解析各模块汇总行(S3 汇总)。 + """ + print("$ " + " ".join(str(c) for c in cmd)) + cp = subprocess.run(cmd, cwd=str(REPO_ROOT), env=env, + capture_output=True) + try: + text = cp.stdout.decode("utf-8") + except UnicodeDecodeError: + text = cp.stdout.decode("utf-8", errors="replace") + sys.stdout.write(text) + if cp.stderr: + try: + sys.stderr.write(cp.stderr.decode("utf-8")) + except UnicodeDecodeError: + sys.stderr.write(cp.stderr.decode("utf-8", errors="replace")) + return cp.returncode, text + + def run_l0(): """L0:pre-commit 全部钩子。返回退出码(0/1/2)。""" print("") @@ -105,10 +131,20 @@ def run_l0(): print("[INFO] mem-numbers 照常运行") else: print("[INFO] 未找到 .map(未做 Keil 构建)-> 跳过 mem-numbers(与 CI 一致)") + print("[INFO] :warning: 体积门禁未执行——改代码后请本地 @build + " + "check_mem_numbers.py --scan 兜底") skip = env.get("SKIP", "") env["SKIP"] = (skip + "," if skip else "") + "mem-numbers" - return _run([PY, "-m", "pre_commit", "run", "--all-files"], env=env) + rc, text = _run_capture([PY, "-m", "pre_commit", "run", "--all-files"], env=env) + # 汇总行(对齐 CI 的 ::notice::):统计钩子 Passed/Failed/Skipped + lines = text.splitlines() + passed = sum(1 for l in lines if l.rstrip().endswith("Passed")) + failed = sum(1 for l in lines if l.rstrip().endswith("Failed")) + skipd = sum(1 for l in lines if l.rstrip().endswith("Skipped")) + print("[SUMMARY] L0: pre-commit hooks -- passed={} failed={} skipped={}".format( + passed, failed, skipd)) + return rc def run_l05(args): @@ -124,11 +160,28 @@ def run_l05(args): return 0 rc_all = 0 + runner_count = 0 + sum_pass = 0 + sum_total = 0 for r in runners: + runner_count += 1 rel = r.relative_to(REPO_ROOT) print("") print("--- 运行:{} ---".format(rel)) - rc = _run([PY, str(r)]) + rc, text = _run_capture([PY, str(r)]) + # 解析模块自身汇总行,如 "=== System Clock Tests: 100/100 PASSED (failed=0) ===" + m = None + for line in text.splitlines(): + mm = re.search(r":\s*(\d+)/(\d+)\s+PASSED\s+\(failed=(\d+)\)", line) + if mm: + m = mm + if m: + sum_pass += int(m.group(1)) + sum_total += int(m.group(2)) + print("[SUMMARY] {}: {}/{} PASSED (failed={})".format( + r.parent.name, m.group(1), m.group(2), m.group(3))) + else: + print("[WARN] {} 未输出 '=== ... N/M PASSED ===' 汇总行".format(rel)) if rc != 0: print("[FAIL] {}(退出码 {})".format(rel, rc)) if rc == 2: @@ -139,6 +192,9 @@ def run_l05(args): break else: print("[OK] {}".format(rel)) + + print("[SUMMARY] L0.5: {} runner(s), {}/{} assertions passed, fail={}".format( + runner_count, sum_pass, sum_total, rc_all)) return rc_all