381 lines
19 KiB
YAML
381 lines
19 KiB
YAML
# 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
|
||
#
|
||
# 本地等价命令(与 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.
|
||
# On this box SYSTEM's `python` is C:\msys64\...\python.exe (3.14.5,
|
||
# no pre-commit). Probe the same order as scripts/ci_local.py:
|
||
# PATH python -> .venv -> anaconda3
|
||
#
|
||
# 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.
|
||
$candidates = @(
|
||
(Get-Command python -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source),
|
||
(Join-Path $repoRoot '.venv\Scripts\python.exe'),
|
||
'C:\ProgramData\anaconda3\python.exe'
|
||
)
|
||
$py = $null
|
||
$saved = $ErrorActionPreference
|
||
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. Tried: $($candidates -join ' | ')"
|
||
Write-Host "::error::pre-commit is probably installed only in a USER site (invisible to SYSTEM)."
|
||
Write-Host "::error::fix (elevated): powershell -ExecutionPolicy Bypass -File D:\Docker\act_runner\install_precommit_global.ps1"
|
||
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 -----------------------------------------------------
|
||
# cppcheck is often installed with a USER-scope PATH entry, which
|
||
# SYSTEM cannot see. Fall back to its default install dir.
|
||
$cpp = Get-Command cppcheck -ErrorAction SilentlyContinue
|
||
if ($cpp) {
|
||
Write-Host "found cppcheck: $($cpp.Source)"
|
||
} elseif (Test-Path 'C:\Program Files\Cppcheck\cppcheck.exe') {
|
||
$dir = 'C:\Program Files\Cppcheck'
|
||
$env:Path = "$env:Path;$dir"
|
||
$dir | Out-File -FilePath $env:GITHUB_PATH -Append
|
||
Write-Host "cppcheck not on PATH; added $dir (User-scope PATH is invisible to SYSTEM)"
|
||
} else {
|
||
Write-Host "::error::runner missing cppcheck -- install it, then re-run"
|
||
exit 1
|
||
}
|
||
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: |
|
||
# Keep in sync with VS_ROOTS in test/host/common/unit_test_harness.py
|
||
$roots = @(
|
||
'C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools',
|
||
'C:\Program Files\Microsoft Visual Studio\2022\BuildTools',
|
||
'C:\Program Files\Microsoft Visual Studio\2022\Community',
|
||
'C:\Program Files\Microsoft Visual Studio\2022\Professional'
|
||
)
|
||
$found = $false
|
||
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).
|
||
$repoRoot = (Get-Location).Path
|
||
$py = $null
|
||
foreach ($c in @(
|
||
(Get-Command python -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source),
|
||
(Join-Path $repoRoot '.venv\Scripts\python.exe'),
|
||
'C:\ProgramData\anaconda3\python.exe'
|
||
)) {
|
||
if ($c -and (Test-Path $c)) { $py = $c; break }
|
||
}
|
||
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
|
||
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
|