修复CI门禁三因:runner掉线致12个job零日志、L0探测回归全灭、gitlint在CI从不执行
1) runner 掉线:run 16~21 共 12 个 job 零日志。gitea-runner v4 的
daemon 无服务支持、首次 Declare 失败即退出进程,Gitea/机器重启
一次 CI 就静默停摆;叠加 cancel-in-progress 后症状是"任务凭空
消失"。新增 scripts/ci_runner_bootstrap.ps1:等 Gitea 就绪 +
崩溃自拉起看门狗 + 开机计划任务 + -SetEnv 写 MACHINE 级 DTU_*。
2) L0 自 33a4aa4 起在 runner 上必挂:Python 候选被收窄成 PATH python
(msys64,无 pre-commit) + 仓库 .venv(gitignore),cppcheck 目录兜底
亦被删,而机器级 DTU_* 从未设置。探测收进 ci_find_python.ps1
(两 job 共用、可离线自测、恢复机器级安装兜底)。
3) gitlint 声明 stages:[commit-msg],而 --all-files 只跑 pre-commit
stage → 17 个钩子定义 CI 只报 16 个且全绿。新增
ci_gitlint_range.ps1 逐条校验本次范围,ci_local.py 增 L0b 段。
另:GITHUB_ENV/PATH/OUTPUT 写入显式 -Encoding ascii(PS 5.1 的
Out-File -Append 默认 UTF-16LE,会把带 NUL 的行追加进 UTF-8 文件)
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Commit-message gate for CI: run the `gitlint` pre-commit hook over every
|
||||
commit in a pushed / pull-request range.
|
||||
|
||||
.DESCRIPTION
|
||||
Why this file exists (2026-10-11 CI audit):
|
||||
|
||||
.pre-commit-config.yaml defines 17 hooks, but CI reported 16
|
||||
(passed=15 failed=0 skipped=1). The missing one was `gitlint`: its hook
|
||||
definition declares `stages: [commit-msg]`, while `pre-commit run
|
||||
--all-files` only runs the `pre-commit` stage. So the commit-message rule
|
||||
from AGENTS.md (single-line Chinese title < 100 chars) was enforced on
|
||||
developer machines and NEVER in CI -- a silent gate gap, not a failure.
|
||||
|
||||
pre-commit cannot check "the commits of this push" on its own, so this
|
||||
script feeds each commit message to the hook through
|
||||
`--hook-stage commit-msg --commit-msg-filename <file>`, one commit at a
|
||||
time, and aggregates the result.
|
||||
|
||||
Message files are written INSIDE the workspace (under ci-artifacts/, which
|
||||
is gitignored) for two hard reasons:
|
||||
* pre-commit rejects a message path on another drive than the repo
|
||||
("path is on mount 'C:', start on mount 'D:'" -- measured here);
|
||||
* they must be UTF-8 WITHOUT BOM, because a BOM counts as a character of
|
||||
the title and would distort the 100-char rule.
|
||||
|
||||
.PARAMETER BaseSha
|
||||
Exclusive lower bound. Empty, all-zero (branch creation / deletion), or an
|
||||
unreachable SHA (force push) falls back to the newest -MaxCommits commits.
|
||||
|
||||
.PARAMETER HeadSha
|
||||
Inclusive upper bound. Defaults to HEAD.
|
||||
|
||||
.PARAMETER MaxCommits
|
||||
Upper bound on how many commits are checked, so a big range cannot run for
|
||||
hours. Merge commits are skipped (their messages are generated by git, and
|
||||
the local hook is what guards hand-written ones).
|
||||
|
||||
.PARAMETER PythonExe
|
||||
Interpreter that provides pre-commit. Defaults to $env:PYTHON, then to
|
||||
scripts/ci_find_python.ps1 auto-discovery.
|
||||
|
||||
.OUTPUTS
|
||||
None. Diagnostics go to the host as GitHub workflow commands.
|
||||
Exit codes: 0 = all messages valid, 1 = at least one invalid, 2 = setup error.
|
||||
|
||||
.NOTES
|
||||
Keep this file pure ASCII (PowerShell 5.1 decodes BOM-less files as CP936).
|
||||
#>
|
||||
param(
|
||||
[string]$BaseSha = '',
|
||||
[string]$HeadSha = 'HEAD',
|
||||
[int]$MaxCommits = 50,
|
||||
[string]$PythonExe = ''
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
Push-Location -LiteralPath $repoRoot
|
||||
try {
|
||||
# git emits UTF-8 commit messages; PS 5.1 would otherwise decode them with
|
||||
# the console code page (CP936) and mangle every Chinese title.
|
||||
$savedConsoleEncoding = [Console]::OutputEncoding
|
||||
try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch { }
|
||||
|
||||
try {
|
||||
# --- resolve the interpreter ----------------------------------------
|
||||
if ([string]::IsNullOrWhiteSpace($PythonExe)) { $PythonExe = $env:PYTHON }
|
||||
if ([string]::IsNullOrWhiteSpace($PythonExe)) {
|
||||
$PythonExe = & (Join-Path $PSScriptRoot 'ci_find_python.ps1') -NeedPreCommit
|
||||
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($PythonExe)) {
|
||||
Write-Host '::error::gitlint: no Python with pre-commit available'
|
||||
exit 2
|
||||
}
|
||||
}
|
||||
$PythonExe = "$PythonExe".Trim()
|
||||
Write-Host "gitlint: using python $PythonExe"
|
||||
|
||||
# --- resolve the commit range ---------------------------------------
|
||||
$range = ''
|
||||
$cleanBase = "$BaseSha".Trim()
|
||||
if ($cleanBase -match '^[0-9a-fA-F]{40}$' -and $cleanBase -notmatch '^0+$') {
|
||||
$probe = "$cleanBase^{commit}"
|
||||
git cat-file -e $probe 2>$null
|
||||
if ($LASTEXITCODE -eq 0) { $range = "$cleanBase..$HeadSha" }
|
||||
else { Write-Host "::warning::gitlint: base $cleanBase is not reachable in this clone -- falling back to the newest $MaxCommits commit(s)" }
|
||||
}
|
||||
|
||||
$savedPref = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
if ($range) {
|
||||
$shas = git log --no-merges --format=%H $range 2>$null
|
||||
}
|
||||
else {
|
||||
$shas = git log --no-merges --format=%H -n $MaxCommits $HeadSha 2>$null
|
||||
}
|
||||
$ErrorActionPreference = $savedPref
|
||||
|
||||
$shas = @($shas | Where-Object { $_ -and "$_".Trim() })
|
||||
if ($shas.Count -eq 0) {
|
||||
Write-Host '::notice::gitlint: no non-merge commits in range -- nothing to check'
|
||||
exit 0
|
||||
}
|
||||
if ($shas.Count -gt $MaxCommits) {
|
||||
Write-Host "::warning::gitlint: range has $($shas.Count) commits, checking the newest $MaxCommits"
|
||||
$shas = $shas[0..($MaxCommits - 1)]
|
||||
}
|
||||
|
||||
# --- message files: inside the workspace, UTF-8 without BOM ---------
|
||||
$msgDir = Join-Path $repoRoot 'ci-artifacts\gitlint'
|
||||
New-Item -ItemType Directory -Force -Path $msgDir | Out-Null
|
||||
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
|
||||
|
||||
$checked = 0
|
||||
$failed = 0
|
||||
foreach ($sha in $shas) {
|
||||
$sha = "$sha".Trim()
|
||||
$checked++
|
||||
|
||||
$savedPref = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$body = git log -1 --format=%B $sha 2>$null
|
||||
$ErrorActionPreference = $savedPref
|
||||
$titleLine = (git log -1 --format=%s $sha 2>$null | Select-Object -First 1)
|
||||
|
||||
$msgPath = Join-Path $msgDir "$sha.txt"
|
||||
$text = (($body | Out-String) -replace "`r`n", "`n").TrimEnd("`n")
|
||||
[System.IO.File]::WriteAllText($msgPath, $text, $utf8NoBom)
|
||||
|
||||
Write-Host "----------------------------------------"
|
||||
Write-Host "commit $sha title: $titleLine"
|
||||
|
||||
$savedPref = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
& $PythonExe -s -m pre_commit run --hook-stage commit-msg `
|
||||
--commit-msg-filename $msgPath gitlint 2>&1
|
||||
$rc = $LASTEXITCODE
|
||||
$ErrorActionPreference = $savedPref
|
||||
|
||||
if ($rc -ne 0) {
|
||||
$failed++
|
||||
Write-Host "::error::gitlint: commit $($sha.Substring(0, 8)) rejected -- `"$titleLine`""
|
||||
}
|
||||
}
|
||||
|
||||
$summary = "gitlint: $checked commit message(s) checked, $failed failed"
|
||||
Write-Host "::notice::$summary"
|
||||
if ($env:GITHUB_STEP_SUMMARY) {
|
||||
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value '### Commit message gate (gitlint)' -Encoding utf8
|
||||
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value $summary -Encoding utf8
|
||||
}
|
||||
|
||||
if ($failed -gt 0) {
|
||||
Write-Host '::error::commit message gate failed -- rule: single-line Chinese title, < 100 chars (see docs/ pre-commit usage guide + .gitlint)'
|
||||
exit 1
|
||||
}
|
||||
exit 0
|
||||
}
|
||||
finally {
|
||||
try { [Console]::OutputEncoding = $savedConsoleEncoding } catch { }
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
Reference in New Issue
Block a user