# 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(见 CI配置指南 §4.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」同源;改脚本时务必守住。 # # ⚠️ 前置(部署到 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. $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 # # 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' 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 "using python: $py" & $py --version & $py -s -m pre_commit --version "PYTHON=$py" | Out-File -FilePath $env:GITHUB_ENV -Append # --- 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. $envCpp = $env:DTU_CPPCHECK if ($envCpp) { if (-not (Test-Path $envCpp)) { Write-Host "::error::DTU_CPPCHECK is set but does not exist: $envCpp" 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 } 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 } Write-Host "found cppcheck: $($cpp.Source)" cppcheck --version } Write-Host "runner environment check passed" - name: 安装 pre-commit 钩子到本仓库 shell: powershell run: "& $env:PYTHON -s -m pre_commit install --install-hooks" - 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 "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 "::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) 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 } 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 } - name: 运行主机侧单测 id: l05 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 } 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 } } } if (-not $py) { Write-Host "::error::no python found (set DTU_PRECOMMIT_PYTHON or fix the runner PATH)"; 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 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 "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