Files
STM32F4-Base/scripts/ci_gitlint_range.ps1
wandering1 ea10dbae53
CI / L0 静态门禁 (pre-commit) (push) Successful in 3m41s
CI / L0.5 主机侧单测 (MSVC) (push) Successful in 33s
修 gitlint 门禁在 CI 空转:runner 无 git 身份致 254 被误报成信息违规
task 31(首次真跑出的 L0)失败与提交信息无关:actions/checkout 把 HOME
指向每次任务的临时目录,SYSTEM 账号没有全局 git 身份,gitlint 在读到任何
规则之前就以 Missing git configuration: please set user.name 退出——254
是配置错误,不是规则违规。旧脚本还把失败原因写死成"标题超 100 字符",加
上该步 exit 1,后面 26 个钩子一个都没跑。

改三处:① 先探测身份,缺失则用 GIT_CONFIG_COUNT/KEY/VALUE 注入一次性身
份,只进子进程环境、不写任何文件,开发者机器不受影响;② 254 与真实违规
分开报(2 = 门禁没跑起来,1 = 信息确实违规),不再替 gitlint 猜规则;
③ 钩子输出改走 ForEach-Object { "$_" },避免 PS 5.1 把原生 stderr 包成
ErrorRecord 污染日志。

实测:空 HOME 下同一条命令改前 254、改后 0 failed;正常环境 10 条提交全
过;脚本纯 ASCII、解析 0 错。同批文档:§7 排障表新增本坑与"新建分支首
次 push 不触发 run",两份变更记录各加一行。
2026-10-12 19:17:11 +08:00

213 lines
9.5 KiB
PowerShell

<#
.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):
CI always reports ONE hook fewer than .pre-commit-config.yaml defines
(measured 2026-10-12: master 18 defined / 17 reported, v2-on-ui 26 / 25).
The missing one is `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 (title
< 100 chars, body lines <= 80) 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"
# --- git identity guard ---------------------------------------------
# Measured on CI 2026-10-12 (task 31): actions/checkout points HOME at
# a per-job temp dir and the SYSTEM account has no global identity, so
# gitlint aborted with "Missing git configuration: please set
# user.name" and exit code 254 BEFORE reading a single rule. Every push
# failed this gate no matter how good the message was, and the log made
# it look like a message violation.
# Inject a throwaway identity through GIT_CONFIG_* (git >= 2.31): it is
# inherited by the child git processes only, so NO file is written and
# a developer machine keeps its own identity untouched. gitlint needs
# an identity merely to start; it does not judge the author.
$savedPref = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
$idName = git config user.name 2>$null
$idMail = git config user.email 2>$null
$ErrorActionPreference = $savedPref
if (-not "$idName".Trim() -or -not "$idMail".Trim()) {
if ($env:GIT_CONFIG_COUNT) {
Write-Host '::warning::gitlint: GIT_CONFIG_* already set by the caller -- leaving it alone'
} else {
$env:GIT_CONFIG_COUNT = '2'
$env:GIT_CONFIG_KEY_0 = 'user.name'
$env:GIT_CONFIG_VALUE_0 = 'dtu-ci'
$env:GIT_CONFIG_KEY_1 = 'user.email'
$env:GIT_CONFIG_VALUE_1 = 'dtu-ci@invalid.local'
Write-Host '::warning::gitlint: no git identity here -- injected a throwaway one via GIT_CONFIG_* (needed only so gitlint can start)'
}
}
# --- 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
$gateBroken = $false
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'
# ForEach-Object { "$_" } unwraps the ErrorRecords that PS 5.1's
# 2>&1 builds for every native stderr line, without disturbing
# $LASTEXITCODE (docs/CI guide, section 7.3).
$hookOut = & $PythonExe -s -m pre_commit run --hook-stage commit-msg `
--commit-msg-filename $msgPath gitlint 2>&1 | ForEach-Object { "$_" }
$rc = $LASTEXITCODE
$ErrorActionPreference = $savedPref
$hookOut | ForEach-Object { Write-Host $_ }
if ($rc -eq 254 -or ($hookOut -match 'Missing git configuration')) {
# The gate itself could not run. Never report that as a
# message violation -- it is a runner/environment fault and
# needs a different fix (see the identity guard above).
Write-Host "::error::gitlint: GATE BROKEN (exit $rc, git config error) at $($sha.Substring(0, 8)) -- NOT a message violation"
$gateBroken = $true
} elseif ($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 ($gateBroken) {
Write-Host '::error::commit message gate could not RUN -- fix the runner git identity; do NOT read this as a message violation'
exit 2
}
if ($failed -gt 0) {
Write-Host '::error::commit message gate failed -- see the gitlint output above for the exact rule id (rules in .gitlint; AGENTS.md: title <= 100 chars, body lines <= 80)'
exit 1
}
exit 0
}
finally {
try { [Console]::OutputEncoding = $savedConsoleEncoding } catch { }
}
}
finally {
Pop-Location
}