fix(ci): run 块按点源语义加固——原生 stderr 变终止性错误曾致两 job 3 秒亡
CI / L0 静态门禁 (pre-commit) (push) Failing after 6m43s
CI / L0.5 主机侧单测 (MSVC) (push) Successful in 2m39s

run 44(本分支上一次推送)把上一版原生检出打回原形:L0 用了 3 s、L0.5 用了
2 s 双双失败,日志里是 error: No such remote 'origin' 被包成 NativeCommandError
后整步终止。根因在我上一版:runner 执行 run: 块的方式是 powershell -command
点源那个文件,会话继承的 $ErrorActionPreference 是 Stop,于是原生程序往 stderr
写的任意一行都会被 PS 5.1 包成终止性错误。我写的 git remote get-url origin
2>$null 正是这一类,而 2>$null 不算摊平,反而让它先把整步炸掉。

修法两件事一起做:块首显式 $ErrorActionPreference = Continue(不依赖继承);
所有 git 调用走同一个漏斗 Invoke-DtuGit,把 stderr 摊平成字符串再决定是否打印。
探针顺手换成 git config --get remote.origin.url:远端不存在时它只返回非零码、
不往 stderr 写,从源头不再触发这类错误。

更阴的一条教训是我的测法本身。第一版复现器用 -File 包一层点源,结果四条失败
路径全测成 rc=0。用最小矩阵把语义钉死后:exit 1 写在被点源的文件里时,-File
直跑 = 1;-File 包一层点源 = 0 且继续执行后续语句;-Command 点源(runner 的真
实用法)= 1。也就是说只要我一直用 -File 测,检出失败也会一路伪装成绿,这个洞
比上面那个 bug 更坏。现在统一用 runner 的真实语义测:powershell -NoProfile
-Command 点源块体文件,且前置 $ErrorActionPreference = Stop。

换测法后的实测(全部跑在从 ci.yml 抽出的那份字节上,不是另抄的副本):
- 点源 + Stop、全新工作区:ExitCode=0、checkout ok: c1e7e8c / commits=669、
  dirty_files=0、150.9 s(修复前同一模式就是 run 44 的 3 秒亡);
- DTU_REPO_SHA 空 / DTU_REPO_URL 空 / sha 不在远端 / 坏令牌:ExitCode 均 1,
  坏令牌 0.5 s 返回 unable to get password,不挂起、不烧 job 超时;
- 复用工作区:1.6 s,dirty_files=0,被忽略文件保留,未跟踪垃圾清除;
- 结构面:13 步、两 job 第 1 步逐字一致、uses: 计数 0、run 块非 ASCII 计数 0、
  无 BOM 无 CR;check yaml Passed;check_doc_links --all 180 篇、无法判定 0。

文档同笔落地(教训要跟它救的那段代码一起进历史):指南 §5.3 补 run 44 实录与
退出码矩阵、实测表补 G/H 两行、§7 补 job 几秒就亡一行、两份变更记录各加一行;
remote-ci-ops skill §10 补 run: 块的正确测法。另记一个运维事实:推到已存在
的远端分支会正常建 run(run 44 即如此,推送后约 4 分钟入库,我先前把轮询窗口
取得太短误判成没建),以后验 workflow 不再需要手动 dispatch 与服务凭据。
下一轮可能红的两处我先标出来,红了正是我们要的证据:SYSTEM 下的 ui-golden
(历史上败在 32 位 MSBuild 与 VCTargetsPath)与 test-coverage(缺
DTU_MS_COVERAGE,走自动发现)。
This commit is contained in:
2026-10-27 12:52:56 +08:00
parent c1e7e8c42c
commit ae80bb8e34
4 changed files with 210 additions and 126 deletions
+166 -126
View File
@@ -135,95 +135,115 @@ jobs:
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.
# WHY NOT a "uses:" action: 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. 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.
# 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.
# $ErrorActionPreference is set to Continue EXPLICITLY. The runner executes a
# run: block by dot-sourcing it into a session, so an inherited Stop turns any
# raw native stderr line into a TERMINATING NativeCommandError. Measured
# 2026-10-27, run 44 / task-72: a benign "error: No such remote 'origin'" from
# a probe killed the whole job 3 seconds in, before any gate could run.
$ErrorActionPreference = 'Continue'
function Invoke-DtuGit {
# Single funnel for git. Two reasons it must be a funnel, not bare calls:
# 1) native stderr has to be flattened into plain strings (see above), and
# 2) $LASTEXITCODE has to stay readable by the caller afterwards.
# Returns Code + lines and lets the caller decide what to print.
param([string[]]$G)
$lines = & git @G 2>&1 | ForEach-Object { [string]$_ }
return [pscustomobject]@{ Code = $LASTEXITCODE; Lines = @($lines) }
}
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 }
# Never let a fetch wait for a human: the runner has no console, and a
# credential prompt would burn the whole job timeout while looking like a hung
# CI. Anonymous fetch works here (the repository is public; measured HTTP 200
# on /info/refs?service=git-upload-pack), so CI must not depend on a credential
# store it cannot answer.
$env:GIT_TERMINAL_PROMPT = '0'
$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 }
$r = Invoke-DtuGit -G @('init', '-q', '.')
if ($r.Code -ne 0) { Write-Host '::error::git init failed'; $r.Lines | ForEach-Object { Write-Host " $_" }; 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
# "git remote get-url" writes to stderr when the remote is absent -- that is
# the exact line that killed run 44. Ask the config instead: an absent key
# prints nothing and only sets a non-zero code, so nothing escapes as an error.
$probe = Invoke-DtuGit -G @('config', '--get', 'remote.origin.url')
$url = ($probe.Lines -join '').Trim()
if ($probe.Code -eq 0 -and $url) {
$r = Invoke-DtuGit -G @('remote', 'set-url', 'origin', $env:DTU_REPO_URL)
} else {
git remote add origin $env:DTU_REPO_URL
$r = Invoke-DtuGit -G @('remote', 'add', 'origin', $env:DTU_REPO_URL)
}
if ($LASTEXITCODE -ne 0) { Write-Host '::error::configuring remote origin failed'; exit 1 }
if ($r.Code -ne 0) { Write-Host '::error::configuring remote origin failed'; $r.Lines | ForEach-Object { Write-Host " $_" }; 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'
# 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).
$r = Invoke-DtuGit -G @('config', 'remote.origin.fetch', '+refs/heads/*:refs/remotes/origin/*')
if ($r.Code -ne 0) { Write-Host '::error::setting remote.origin.fetch failed'; exit 1 }
# 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
# 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 that 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 { "$_" }
$f = Invoke-DtuGit -G ($auth + @('fetch', '--prune', '--no-tags', 'origin'))
if ($f.Code -ne 0) {
$f.Lines | ForEach-Object { Write-Host " $_" }
Write-Host ('::error::git fetch origin failed (exit ' + $f.Code + ')')
exit 1
}
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 }
# Some event 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.
$want = $env:DTU_REPO_SHA + '^{commit}'
$chk = Invoke-DtuGit -G @('cat-file', '-e', $want)
if ($chk.Code -ne 0) {
Write-Host 'sha not covered by the branch refspec -- fetching the event ref explicitly'
$f2 = Invoke-DtuGit -G ($auth + @('fetch', '--prune', '--no-tags', 'origin', $env:DTU_REPO_REF))
if ($f2.Code -ne 0) { $f2.Lines | ForEach-Object { Write-Host " $_" } }
$chk = Invoke-DtuGit -G @('cat-file', '-e', $want)
}
if ($chk.Code -ne 0) {
Write-Host ('::error::' + $env:DTU_REPO_SHA + ' is not present in the fetched history')
Write-Host ' (refusing to run the gates against a tree we did not verify)'
exit 1
}
$c = Invoke-DtuGit -G @('checkout', '-q', '--detach', $env:DTU_REPO_SHA)
if ($c.Code -ne 0) { $c.Lines | ForEach-Object { Write-Host " $_" }; Write-Host '::error::detached checkout failed'; exit 1 }
$rr = Invoke-DtuGit -G @('reset', '--hard', '-q', $env:DTU_REPO_SHA)
if ($rr.Code -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))
$cl = Invoke-DtuGit -G @('clean', '-ffd', '-q')
if ($cl.Code -ne 0) { $cl.Lines | ForEach-Object { Write-Host " $_" }; Write-Host '::error::git clean failed'; exit 1 }
$head = (Invoke-DtuGit -G @('rev-parse', '--short', 'HEAD')).Lines -join ''
$ncommits = (Invoke-DtuGit -G @('rev-list', '--count', 'HEAD')).Lines -join ''
$dirty = @(Invoke-DtuGit -G @('status', '--porcelain')).Lines.Count
Write-Host ('checkout ok: ' + $head + ' | commits=' + $ncommits + ' | dirty_files=' + $dirty)
- name: 环境自检(Python / cppcheck / pre-commit)
shell: powershell
run: |
@@ -503,95 +523,115 @@ jobs:
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.
# WHY NOT a "uses:" action: 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. 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.
# 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.
# $ErrorActionPreference is set to Continue EXPLICITLY. The runner executes a
# run: block by dot-sourcing it into a session, so an inherited Stop turns any
# raw native stderr line into a TERMINATING NativeCommandError. Measured
# 2026-10-27, run 44 / task-72: a benign "error: No such remote 'origin'" from
# a probe killed the whole job 3 seconds in, before any gate could run.
$ErrorActionPreference = 'Continue'
function Invoke-DtuGit {
# Single funnel for git. Two reasons it must be a funnel, not bare calls:
# 1) native stderr has to be flattened into plain strings (see above), and
# 2) $LASTEXITCODE has to stay readable by the caller afterwards.
# Returns Code + lines and lets the caller decide what to print.
param([string[]]$G)
$lines = & git @G 2>&1 | ForEach-Object { [string]$_ }
return [pscustomobject]@{ Code = $LASTEXITCODE; Lines = @($lines) }
}
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 }
# Never let a fetch wait for a human: the runner has no console, and a
# credential prompt would burn the whole job timeout while looking like a hung
# CI. Anonymous fetch works here (the repository is public; measured HTTP 200
# on /info/refs?service=git-upload-pack), so CI must not depend on a credential
# store it cannot answer.
$env:GIT_TERMINAL_PROMPT = '0'
$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 }
$r = Invoke-DtuGit -G @('init', '-q', '.')
if ($r.Code -ne 0) { Write-Host '::error::git init failed'; $r.Lines | ForEach-Object { Write-Host " $_" }; 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
# "git remote get-url" writes to stderr when the remote is absent -- that is
# the exact line that killed run 44. Ask the config instead: an absent key
# prints nothing and only sets a non-zero code, so nothing escapes as an error.
$probe = Invoke-DtuGit -G @('config', '--get', 'remote.origin.url')
$url = ($probe.Lines -join '').Trim()
if ($probe.Code -eq 0 -and $url) {
$r = Invoke-DtuGit -G @('remote', 'set-url', 'origin', $env:DTU_REPO_URL)
} else {
git remote add origin $env:DTU_REPO_URL
$r = Invoke-DtuGit -G @('remote', 'add', 'origin', $env:DTU_REPO_URL)
}
if ($LASTEXITCODE -ne 0) { Write-Host '::error::configuring remote origin failed'; exit 1 }
if ($r.Code -ne 0) { Write-Host '::error::configuring remote origin failed'; $r.Lines | ForEach-Object { Write-Host " $_" }; 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'
# 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).
$r = Invoke-DtuGit -G @('config', 'remote.origin.fetch', '+refs/heads/*:refs/remotes/origin/*')
if ($r.Code -ne 0) { Write-Host '::error::setting remote.origin.fetch failed'; exit 1 }
# 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
# 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 that 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 { "$_" }
$f = Invoke-DtuGit -G ($auth + @('fetch', '--prune', '--no-tags', 'origin'))
if ($f.Code -ne 0) {
$f.Lines | ForEach-Object { Write-Host " $_" }
Write-Host ('::error::git fetch origin failed (exit ' + $f.Code + ')')
exit 1
}
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 }
# Some event 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.
$want = $env:DTU_REPO_SHA + '^{commit}'
$chk = Invoke-DtuGit -G @('cat-file', '-e', $want)
if ($chk.Code -ne 0) {
Write-Host 'sha not covered by the branch refspec -- fetching the event ref explicitly'
$f2 = Invoke-DtuGit -G ($auth + @('fetch', '--prune', '--no-tags', 'origin', $env:DTU_REPO_REF))
if ($f2.Code -ne 0) { $f2.Lines | ForEach-Object { Write-Host " $_" } }
$chk = Invoke-DtuGit -G @('cat-file', '-e', $want)
}
if ($chk.Code -ne 0) {
Write-Host ('::error::' + $env:DTU_REPO_SHA + ' is not present in the fetched history')
Write-Host ' (refusing to run the gates against a tree we did not verify)'
exit 1
}
$c = Invoke-DtuGit -G @('checkout', '-q', '--detach', $env:DTU_REPO_SHA)
if ($c.Code -ne 0) { $c.Lines | ForEach-Object { Write-Host " $_" }; Write-Host '::error::detached checkout failed'; exit 1 }
$rr = Invoke-DtuGit -G @('reset', '--hard', '-q', $env:DTU_REPO_SHA)
if ($rr.Code -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))
$cl = Invoke-DtuGit -G @('clean', '-ffd', '-q')
if ($cl.Code -ne 0) { $cl.Lines | ForEach-Object { Write-Host " $_" }; Write-Host '::error::git clean failed'; exit 1 }
$head = (Invoke-DtuGit -G @('rev-parse', '--short', 'HEAD')).Lines -join ''
$ncommits = (Invoke-DtuGit -G @('rev-list', '--count', 'HEAD')).Lines -join ''
$dirty = @(Invoke-DtuGit -G @('status', '--porcelain')).Lines.Count
Write-Host ('checkout ok: ' + $head + ' | commits=' + $ncommits + ' | dirty_files=' + $dirty)
- name: 探测 MSVC(VS BuildTools / Community 等)
id: msvc
shell: powershell