fix(ci): 去掉 actions/checkout 外网依赖,MSVC 探测改 vswhere 并钉住工具集世代

两个 job 的第 1 步原先是 `uses: actions/checkout@v4`。`uses:` 的解析发生在
Prepare all required actions 阶段——先于任何步骤、无条件联网(默认 github.com),
且每个 job 各自解析进自己的 .cache/act/<hash>、跨 job 不复用。这条链在本机实测
不稳:run 36(10-21)/ 41(10-25)/ 42(10-26)三次都是 ~45 s 后整 job 死亡、
零钩子输出,而同一分钟手工 curl 同一地址返回 200——手工可达 ≠ CI 可用(解析器
跑在 SYSTEM 下)。Gitea 1.25.4 又去掉了自定义 DEFAULT_ACTIONS_URL,服务端没有
可拧的键,唯一归本仓库管的修法就是不再依赖它:两个 job 第 1 步换成原生 git 全量检出。

关键选择(依据与实测表见 docs/CI/远程CI完整指南.md 同文 §5.3,此处只记结论):
- 完整 refspec + 全量,不是 --depth 1:提交信息门禁要解 base..head 的 range;
  且目标 sha 不在历史里就 ::error:: 硬失败,绝不默默构建一棵错的树;
- clean -ffd 而非 -ffdx:-x 会删被忽略文件,而 runner 工作区跨运行复用它们;
- GIT_TERMINAL_PROMPT=0 + 空 credential.helper:否则认证挑战会等人类输入,
  把 30 分钟 job 超时白烧掉,表现得像 CI 挂了;
- 令牌只走 per-invocation -c http.extraheader(Basic + x-access-token,与
  checkout@v4 同拼法),不落 .git/config、不回显;仓库公开时匿名即可;
- 不抓 tag:本仓库 tag 数 0、无脚本用 describe --tags(实测)。

另修 scripts/ci_runner_bootstrap.ps1 的 Find-MsvcCl。旧版只扫 %ProgramFiles%
派生根且取首个命中:本机 C 盘 VS2019 已于今日 01:00 卸载(C 盘 x64 cl.exe 实测
0 个),旧版因此是「找不到 → 只 Warn → 不写」,机器级变量一直指着那条失效路径,
而 harness 对「显式设置但无效」是硬失败、不做自动回退。现在改为 vswhere 取实例
(能看见非默认盘)+ 工具集版本降序 + -MsvcToolset 前缀钉住世代(默认 14.51.,
与覆盖率基线同版本;钉不上连 WARN 再回落最新,绝不静默)+ -MsvcLatest;
-SetEnv 与 -Status 同步纳入 DTU_MS_COVERAGE。

验证(内层一律 powershell.exe 5.1,忠实复现 runner 执行方式):临时工作区六项
A/B/C/D/E/F——E 是执行**从 ci.yml 里抽出的正文**,测的就是文件那份字节。
A rc=0、commits=665、refspec 完整、分离 HEAD;B 复用工作区后 dirty_files=0、
被忽略文件保留、17ece86..HEAD 解出 18 个提交;C rc=1 且 HEAD 不动;D 坏令牌
fatal: unable to get password 秒失败;令牌改造后再跑 ANON/BOGUS/ANON2=0/1/0。
ci_local 全量:L0.5 = 40 runner、3188/3188 断言、fail=0(MSVC 14.51);
test-coverage 与 ui-golden 均 Passed。.ps1 非 ASCII 字节 0、语法错误 0、无 BOM、
LF;ci.yml 无 BOM 无 CR、13 步、0 个 run 块含非 ASCII、uses: 计数 0。
This commit is contained in:
2026-10-27 08:00:14 +08:00
parent 5869c0307b
commit 2d18193d38
2 changed files with 338 additions and 27 deletions
+198 -7
View File
@@ -125,11 +125,105 @@ jobs:
name: L0 静态门禁 (pre-commit)
runs-on: windows
steps:
- uses: actions/checkout@v4
with:
# 钩子要对比基线与历史(doc-links 等读工作区即可,但 gitlint/cubemx 需完整历史)
fetch-depth: 0
- name: 检出源码(原生 git;不依赖 github.com 上的 action)
shell: powershell
env:
DTU_REPO_URL: ${{ github.server_url }}/${{ github.repository }}.git
DTU_REPO_REF: ${{ github.ref }}
DTU_REPO_SHA: ${{ github.sha }}
DTU_REPO_TOKEN: ${{ github.token }}
run: |
# Pure ASCII only (see file header).
#
# WHY NOT `uses: actions/checkout`: the runner resolves every `uses:` by
# contacting the actions URL during "Prepare all required actions", BEFORE
# the first step runs. On this intranet runner that call intermittently dies
# with a connect timeout -- runs 36 (10-21) / 41 (10-25) / 42 (10-26) each
# burned ~45 s and produced ZERO hook output, while a manual curl to the same
# URL minutes later returned 200. Manual reachability is not CI health: the
# resolver runs as SYSTEM and the runner caches nothing across jobs (each job
# re-resolves into its own .cache/act/<hash>). Gitea 1.25.4 also removed the
# custom DEFAULT_ACTIONS_URL knob, so the only fix this repo owns is to stop
# depending on the public resolver at all.
#
# KEEP IN SYNC with the same step in the other job. This logic deliberately
# does NOT live in scripts/*.ps1 (unlike the Python / MSVC probes): the file
# is not checked out yet at the moment it would be needed -- chicken and egg.
#
# NOTE on $ErrorActionPreference: left at Continue on purpose. Under Windows
# PowerShell 5.1 a native command that writes to stderr while EAP is Stop
# raises NativeCommandError -- git legitimately writes progress to stderr.
if (-not $env:DTU_REPO_SHA) { Write-Host '::error::DTU_REPO_SHA is empty'; exit 1 }
if (-not $env:DTU_REPO_URL) { Write-Host '::error::DTU_REPO_URL is empty'; exit 1 }
$ws = (Get-Location).Path
Write-Host "workspace: $ws"
if (-not (Test-Path -LiteralPath (Join-Path $ws '.git'))) {
git init -q .
if ($LASTEXITCODE -ne 0) { Write-Host '::error::git init failed'; exit 1 }
}
$rem = git remote get-url origin 2>$null
if ($LASTEXITCODE -eq 0 -and $rem) {
git remote set-url origin $env:DTU_REPO_URL
} else {
git remote add origin $env:DTU_REPO_URL
}
if ($LASTEXITCODE -ne 0) { Write-Host '::error::configuring remote origin failed'; exit 1 }
# Full refspec, NOT --depth 1: the commit-message gate resolves base..head
# ranges and several gates diff against history, so a shallow clone turns
# them into "unknown revision" failures. Tags are skipped on purpose: this
# repository has zero tags and no script uses `git describe --tags`
# (measured 2026-10-27).
git config remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*'
# Never let a fetch wait for a human: the runner has no console and a
# credential prompt would burn the whole 30 m job timeout while looking like
# a hung CI. Terminal prompts off + no credential helper means an auth
# problem fails immediately with a message instead of blocking. Anonymous
# fetch works here (the repository is public; measured HTTP 200 on
# /info/refs?service=git-upload-pack), so CI must not start depending on a
# credential store it cannot answer.
$env:GIT_TERMINAL_PROMPT = '0'
# The token, when the instance provides one, travels only as a per-invocation
# -c override: it never lands in .git/config and is never echoed in logs.
# Anonymous fetch works too (the repository is public), so CI does not start
# depending on token availability.
$auth = @('-c', 'credential.helper=', '-c', 'credential.interactive=false')
if ($env:DTU_REPO_TOKEN) {
# Same scheme actions/checkout uses against Gitea git-http: Basic with a
# placeholder user and the token as the password. (Bearer is the GitHub
# *API* style, not the git-http style.) Honest caveat: this branch CANNOT
# be verified here -- the repository is public, so an anonymous fetch
# succeeds and any token we fabricate locally only proves a bad one is
# rejected. If the repository ever goes private this is the first line to
# check; the failure mode is a loud rc=1, never a silent fallback.
$pair = 'x-access-token:' + $env:DTU_REPO_TOKEN
$b64 = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($pair))
$auth += @('-c', ('http.extraheader=AUTHORIZATION: Basic ' + $b64))
}
# PS 5.1 wraps each native stderr line into an ErrorRecord, turning two real
# log lines into eight of CategoryInfo/FullyQualifiedErrorId noise. Flattening
# with "$_" keeps the log readable; $LASTEXITCODE is unaffected.
$fetchArgs = $auth + @('fetch', '--prune', '--no-tags', 'origin')
& git @fetchArgs 2>&1 | ForEach-Object { "$_" }
if ($LASTEXITCODE -ne 0) { Write-Host "::error::git fetch origin failed (exit $LASTEXITCODE)"; exit 1 }
# Some refs (PR merge commits, refs outside refs/heads) are not covered by the
# branch refspec -- fall back to an explicit ref fetch, then INSIST the wanted
# SHA is really present, rather than silently building the wrong tree.
git rev-parse --verify -q ($env:DTU_REPO_SHA + '^{commit}') 2>$null | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Host 'sha not covered by the branch refspec -- fetching the event ref explicitly'
$refArgs = $auth + @('fetch', '--prune', '--no-tags', 'origin', $env:DTU_REPO_REF)
& git @refArgs 2>&1 | ForEach-Object { "$_" }
}
git rev-parse --verify -q ($env:DTU_REPO_SHA + '^{commit}') 2>$null | Out-Null
if ($LASTEXITCODE -ne 0) { Write-Host ('::error::' + $env:DTU_REPO_SHA + ' is not present in the fetched history'); exit 1 }
& git checkout -q --detach $env:DTU_REPO_SHA 2>&1 | ForEach-Object { "$_" }
if ($LASTEXITCODE -ne 0) { Write-Host '::error::detached checkout failed'; exit 1 }
git reset --hard -q $env:DTU_REPO_SHA
if ($LASTEXITCODE -ne 0) { Write-Host '::error::reset --hard failed'; exit 1 }
# -ffd, deliberately not -ffdx: -x would delete IGNORED files, and the runner
# reuses those across runs (pip / pre-commit caches live in the workspace).
git clean -ffd -q
Write-Host ('checkout ok: ' + (git rev-parse --short HEAD) + ' | commits=' + (git rev-list --count HEAD) + ' | dirty_files=' + ((git status --porcelain | Measure-Object).Count))
- name: 环境自检(Python / cppcheck / pre-commit)
shell: powershell
run: |
@@ -399,8 +493,105 @@ jobs:
name: L0.5 主机侧单测 (MSVC)
runs-on: windows
steps:
- uses: actions/checkout@v4
- name: 检出源码(原生 git;与 l0-gates 同一实现口径)
shell: powershell
env:
DTU_REPO_URL: ${{ github.server_url }}/${{ github.repository }}.git
DTU_REPO_REF: ${{ github.ref }}
DTU_REPO_SHA: ${{ github.sha }}
DTU_REPO_TOKEN: ${{ github.token }}
run: |
# Pure ASCII only (see file header).
#
# WHY NOT `uses: actions/checkout`: the runner resolves every `uses:` by
# contacting the actions URL during "Prepare all required actions", BEFORE
# the first step runs. On this intranet runner that call intermittently dies
# with a connect timeout -- runs 36 (10-21) / 41 (10-25) / 42 (10-26) each
# burned ~45 s and produced ZERO hook output, while a manual curl to the same
# URL minutes later returned 200. Manual reachability is not CI health: the
# resolver runs as SYSTEM and the runner caches nothing across jobs (each job
# re-resolves into its own .cache/act/<hash>). Gitea 1.25.4 also removed the
# custom DEFAULT_ACTIONS_URL knob, so the only fix this repo owns is to stop
# depending on the public resolver at all.
#
# KEEP IN SYNC with the same step in the other job. This logic deliberately
# does NOT live in scripts/*.ps1 (unlike the Python / MSVC probes): the file
# is not checked out yet at the moment it would be needed -- chicken and egg.
#
# NOTE on $ErrorActionPreference: left at Continue on purpose. Under Windows
# PowerShell 5.1 a native command that writes to stderr while EAP is Stop
# raises NativeCommandError -- git legitimately writes progress to stderr.
if (-not $env:DTU_REPO_SHA) { Write-Host '::error::DTU_REPO_SHA is empty'; exit 1 }
if (-not $env:DTU_REPO_URL) { Write-Host '::error::DTU_REPO_URL is empty'; exit 1 }
$ws = (Get-Location).Path
Write-Host "workspace: $ws"
if (-not (Test-Path -LiteralPath (Join-Path $ws '.git'))) {
git init -q .
if ($LASTEXITCODE -ne 0) { Write-Host '::error::git init failed'; exit 1 }
}
$rem = git remote get-url origin 2>$null
if ($LASTEXITCODE -eq 0 -and $rem) {
git remote set-url origin $env:DTU_REPO_URL
} else {
git remote add origin $env:DTU_REPO_URL
}
if ($LASTEXITCODE -ne 0) { Write-Host '::error::configuring remote origin failed'; exit 1 }
# Full refspec, NOT --depth 1: the commit-message gate resolves base..head
# ranges and several gates diff against history, so a shallow clone turns
# them into "unknown revision" failures. Tags are skipped on purpose: this
# repository has zero tags and no script uses `git describe --tags`
# (measured 2026-10-27).
git config remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*'
# Never let a fetch wait for a human: the runner has no console and a
# credential prompt would burn the whole 30 m job timeout while looking like
# a hung CI. Terminal prompts off + no credential helper means an auth
# problem fails immediately with a message instead of blocking. Anonymous
# fetch works here (the repository is public; measured HTTP 200 on
# /info/refs?service=git-upload-pack), so CI must not start depending on a
# credential store it cannot answer.
$env:GIT_TERMINAL_PROMPT = '0'
# The token, when the instance provides one, travels only as a per-invocation
# -c override: it never lands in .git/config and is never echoed in logs.
# Anonymous fetch works too (the repository is public), so CI does not start
# depending on token availability.
$auth = @('-c', 'credential.helper=', '-c', 'credential.interactive=false')
if ($env:DTU_REPO_TOKEN) {
# Same scheme actions/checkout uses against Gitea git-http: Basic with a
# placeholder user and the token as the password. (Bearer is the GitHub
# *API* style, not the git-http style.) Honest caveat: this branch CANNOT
# be verified here -- the repository is public, so an anonymous fetch
# succeeds and any token we fabricate locally only proves a bad one is
# rejected. If the repository ever goes private this is the first line to
# check; the failure mode is a loud rc=1, never a silent fallback.
$pair = 'x-access-token:' + $env:DTU_REPO_TOKEN
$b64 = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($pair))
$auth += @('-c', ('http.extraheader=AUTHORIZATION: Basic ' + $b64))
}
# PS 5.1 wraps each native stderr line into an ErrorRecord, turning two real
# log lines into eight of CategoryInfo/FullyQualifiedErrorId noise. Flattening
# with "$_" keeps the log readable; $LASTEXITCODE is unaffected.
$fetchArgs = $auth + @('fetch', '--prune', '--no-tags', 'origin')
& git @fetchArgs 2>&1 | ForEach-Object { "$_" }
if ($LASTEXITCODE -ne 0) { Write-Host "::error::git fetch origin failed (exit $LASTEXITCODE)"; exit 1 }
# Some refs (PR merge commits, refs outside refs/heads) are not covered by the
# branch refspec -- fall back to an explicit ref fetch, then INSIST the wanted
# SHA is really present, rather than silently building the wrong tree.
git rev-parse --verify -q ($env:DTU_REPO_SHA + '^{commit}') 2>$null | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Host 'sha not covered by the branch refspec -- fetching the event ref explicitly'
$refArgs = $auth + @('fetch', '--prune', '--no-tags', 'origin', $env:DTU_REPO_REF)
& git @refArgs 2>&1 | ForEach-Object { "$_" }
}
git rev-parse --verify -q ($env:DTU_REPO_SHA + '^{commit}') 2>$null | Out-Null
if ($LASTEXITCODE -ne 0) { Write-Host ('::error::' + $env:DTU_REPO_SHA + ' is not present in the fetched history'); exit 1 }
& git checkout -q --detach $env:DTU_REPO_SHA 2>&1 | ForEach-Object { "$_" }
if ($LASTEXITCODE -ne 0) { Write-Host '::error::detached checkout failed'; exit 1 }
git reset --hard -q $env:DTU_REPO_SHA
if ($LASTEXITCODE -ne 0) { Write-Host '::error::reset --hard failed'; exit 1 }
# -ffd, deliberately not -ffdx: -x would delete IGNORED files, and the runner
# reuses those across runs (pip / pre-commit caches live in the workspace).
git clean -ffd -q
Write-Host ('checkout ok: ' + (git rev-parse --short HEAD) + ' | commits=' + (git rev-list --count HEAD) + ' | dirty_files=' + ((git status --porcelain | Measure-Object).Count))
- name: 探测 MSVC(VS BuildTools / Community 等)
id: msvc
shell: powershell