From ae80bb8e34e7c6534cd63f723f445ab057d715c2 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Tue, 27 Oct 2026 12:52:56 +0800 Subject: [PATCH] =?UTF-8?q?fix(ci):=20run=20=E5=9D=97=E6=8C=89=E7=82=B9?= =?UTF-8?q?=E6=BA=90=E8=AF=AD=E4=B9=89=E5=8A=A0=E5=9B=BA=E2=80=94=E2=80=94?= =?UTF-8?q?=E5=8E=9F=E7=94=9F=20stderr=20=E5=8F=98=E7=BB=88=E6=AD=A2?= =?UTF-8?q?=E6=80=A7=E9=94=99=E8=AF=AF=E6=9B=BE=E8=87=B4=E4=B8=A4=20job=20?= =?UTF-8?q?3=20=E7=A7=92=E4=BA=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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,走自动发现)。 --- .dsh/skills/remote-ci-ops/SKILL.md | 10 + .gitea/workflows/ci.yml | 292 ++++++++++++++++------------- docs/CI/README.md | 1 + docs/CI/远程CI完整指南.md | 33 ++++ 4 files changed, 210 insertions(+), 126 deletions(-) diff --git a/.dsh/skills/remote-ci-ops/SKILL.md b/.dsh/skills/remote-ci-ops/SKILL.md index 4e1dcd18..42509b3b 100644 --- a/.dsh/skills/remote-ci-ops/SKILL.md +++ b/.dsh/skills/remote-ci-ops/SKILL.md @@ -207,6 +207,16 @@ gitlint 专属:消息文件必须写在**仓库内**(跨盘符被 pre-commit $b=[System.IO.File]::ReadAllBytes($p); @($b|?{$_ -gt 127}).Count # 必须 0 $e=$null; [System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path $p),[ref]$null,[ref]$e)|Out-Null; @($e).Count # 必须 0 +# run: 块:必须按 runner 的语义测——点源(10-27 用 run 44 换来的两条教训) +# runner 执行方式: powershell -command . '<块体>' ← 只有这个是真语义 +$tmp = '.gitea/_blk_probe.ps1' # 用 python 从 ci.yml 抽出某步 run 正文写到这里,别另抄一份 +& powershell.exe -NoProfile -Command "`$ErrorActionPreference='Stop'; . (Resolve-Path $tmp)" +# 教训一:块被点源进会话,$ErrorActionPreference 继承为 Stop 时,原生程序往 stderr +# 写一行就变成**终止性** NativeCommandError,`2>` 重定向挡不住(`2>` 加 `$null` 也不行)。 +# 所以块首显式 `$ErrorActionPreference='Continue'`,原生调用一律摊平成字符串。 +# 教训二:`exit 1` 的退出码语义随调用方式变——`-File` 直跑=1;`-File` 包一层点源=**0 且继续执行**; +# `-Command` 点源=1。用 `-File` 测会把失败全测成 rc=0(我第一版就栽在这,四条失败路径全绿)。 + # .yml:可解析 + 全部 run: 块纯 ASCII + 无 BOM + LF & '.venv\Scripts\python.exe' -c "import yaml;d=yaml.safe_load(open(r'.gitea/workflows/ci.yml',encoding='utf-8'));print(list(d))" diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 2f748f89..d2a663c5 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -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/). 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/). 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 diff --git a/docs/CI/README.md b/docs/CI/README.md index 5c9b8e23..ee356bdc 100644 --- a/docs/CI/README.md +++ b/docs/CI/README.md @@ -86,6 +86,7 @@ | 日期 | 变更 | |---|---| +| 2026-10-27 | run 44(推送到已存在分支会正常建 run)暴露的教训写进指南 §5.3:run: 块是被**点源**执行的,`$ErrorActionPreference` 继承 Stop 时原生 stderr 会终止整步;且测 run: 块只能用 `-Command ". 文件"`(`-File` 包点源会把失败测成 rc=0)。§7 补「几秒就亡」一行,skill §10 补测法 | | 2026-10-27 | 指南 §5.3 **整节重写并落地**:workflow 两个 job 的 `uses:` 归零(原生 git 全量检出,六项实测 A/B/C/D/E/F + 两条自查命令见 §5.3);§1.2.1 第三行按 1.25.4 实际行为改正(自定义 `DEFAULT_ACTIONS_URL` 已无效,旧写法会把人引向一个不存在的开关);§7 的 checkout 行改为“看见这症状=有人把 `uses:` 加回来了”。另:`scripts/ci_runner_bootstrap.ps1` 的 MSVC 探测改为 `vswhere` + 工具集钉世代(默认 14.51,与覆盖率基线同版本),`-SetEnv` 开始成对写 `DTU_MS_COVERAGE` | | 2026-10-27 | 新增指南 §4.4「覆盖率工具链(mscoverage)与 VS2026 BuildTools」并加进上面的目录表。当天另修掉 `cppcheck` 两根真红(`ci/remote-ci` 的 `5869c03`)——此前 L0 的 28 次红里混着**三类**:runner 缺覆盖率组件、外网拉 `actions/checkout` 抖动、真代码问题。**别再一律当成环境噪声**,按 §7 逐条定性 | | 2026-10-12 | 🐛 **首次真跑出 run**:`L0.5` **成功**(`10 runner(s), 647/647, fail=0`,与本地逐字一致);`L0` **假失败**——gitlint 因 runner 无 git 身份而 `exit 254`(`Missing git configuration: please set user.name`),旧文案把它说成"提交信息违规",且该步 `exit 1` 连带让 26 个钩子全部没跑。已在 `ci_gitlint_range.ps1` 用 `GIT_CONFIG_*` 注入一次性身份(不写文件)并区分 254/1 退出码。另记:**新建分支的首次 push 不触发 run**,需 `workflow_dispatch` 或再推一个非文档提交。详见指南 §7 | diff --git a/docs/CI/远程CI完整指南.md b/docs/CI/远程CI完整指南.md index 15799a3a..55c7fdf7 100644 --- a/docs/CI/远程CI完整指南.md +++ b/docs/CI/远程CI完整指南.md @@ -1047,6 +1047,8 @@ set-url origin /.git` → 完整 refspec `+refs/heads/*: | D | 灌一个坏令牌 | rc=1,`fatal: unable to get password from user`(**快速失败**,没挂住) | | E | 执行**从 `ci.yml` 抽出的**正文 | rc=0、与 A 同数;**零 ErrorRecord 噪声**;首次全量拉取 172 s(18.65 MiB / loopback) | | F | 同一工作区再跑一次 | rc=0、秒级——**稳态 CI 不重复付全量代价**,只有工作区全新时才慢 | +| G | **点源 + `$ErrorActionPreference` 为 Stop(= runner 语义)**,全新工作区 | 修复后 rc=0、`checkout ok: c1e7e8c / commits=669`、首拉 168 s;**修复前同一模式 3 s 亡**(就是 run 44)——这一行才算真正的门 | +| H | 点源模式下的四条失败路径 | `DTU_REPO_SHA` 空 / `DTU_REPO_URL` 空 / sha 不在远端 / 坏令牌 → **ExitCode 全 = 1**(成功与复用 = 0)。A–F 那批是用 `-File` 测的,G/H 起才换成语义正确的点源测法 | > ⚠ **一条未证的边界**:`pull_request` 也是本 workflow 的触发器,而 PR 事件的 `github.ref` 是 > `refs/pull//merge`,**不在**我用的分支 refspec 内 → 会走「再显式 fetch 事件 ref」那条兜底。 > 这台实例**至今 0 个 PR**(库里 `pull_request` 计数 = 0),`git ls-remote 'refs/pull/*'` 也返回空, @@ -1056,6 +1058,35 @@ set-url origin /.git` → 完整 refspec `+refs/heads/*: > 不受影响)。同一条备注也适用于 `${{ github.server_url }}`:原生检出与 `checkout@v4` 用的是**同一个** > server_url 拼法,**可达性假设没有变化**——变的只是不再联网 github.com。 +> 🔴 **run 44 真机首验栽了一次,两条教训都属于「本地照不出来」那一类**(10-27 10:38 推上 +> `ci/remote-ci` 之后两个 job 分别 **3 s / 2 s 亡**,日志原文如下): +> +> ``` +> git : error: No such remote 'origin' +> + CategoryInfo : NotSpecified: (...) [], RemoteException +> + FullyQualifiedErrorId : NativeCommandError +> ##[error]Process completed with exit code 1. +> ``` +> +> ① **`2>$null` 不算摊平**。runner 把 `run:` 块**点源**进会话(`shell: powershell -command +> . 那个文件`),会话的 `$ErrorActionPreference` 是 Stop 时,原生程序**任何写到 stderr 的一行** +> 都会被 PS 5.1 包成**终止性** `NativeCommandError`——`2>$null` 不但没吞掉它,反而让它先把整 +> 个步骤炸掉(`git remote get-url` 在远端不存在时正是往 stderr 写)。修法两条一起做:块首 +> **显式** `$ErrorActionPreference = Continue`;所有原生 git 调用统一走一个把 stderr 摊平成 +> 字符串的漏斗(现在这份就是这么写的)。 +> ② **测法必须复现「点源」,否则连失败都测不出来**。把 `exit 1` 写在被点源的文件里时, +> 三种调用方式的实测语义如下: +> +> | 调用方式 | `exit 1` 之后 | 进程退出码 | +> |---|---|---| +> | `powershell -File a.ps1` | 进程结束 | 1 | +> | `powershell -File b.ps1`,而 b 里点源 a | **继续执行 b 的后续语句** | **0** | +> | `powershell -Command ". a.ps1"` ← **runner 的用法** | 进程结束 | **1** | +> +> 我第一版复现器恰好用了中间那种,于是把四条失败路径全测成 `rc=0`(**假警报**,同时也 +> 照不出教训 ①)。**要验 workflow 里的 `run:` 块,只能用 `-Command` 点源**,并且最好显式 +> 把 `$ErrorActionPreference` 前置成 Stop——那才是 runner 的真实语义。 + **自查(改 workflow 前后都跑,第二条是内联换来的同步义务)**: ```powershell @@ -1175,6 +1206,7 @@ date +%s # 对比 last_online | 任务一直 **Waiting / 排队** | 没有 label 匹配 `windows` 的 runner | 检查 `--labels "windows:host"`;Settings→Runners 是否 Idle | | `l0-gates` 报 `::error::runner 缺少:Python, cppcheck` | runner 没装或不在 PATH | 装完后**重启 runner 服务**(新进程才吃到新 PATH) | | `Prepare all required actions` 或 `checkout` 卡住/超时(~45 s 后整 job 亡、零钩子输出) | 有人把 `uses:` 加回了 workflow —— 该阶段**先于任何步骤**无条件联网,手工 curl 可达不代表 runner 可达 | 本仓库 2026-10-27 起已无 `uses:`(§5.3 有六项实测与自查命令);自查:`Select-String .gitea/workflows/ci.yml -Pattern '^\s*-?\s*uses:'` 应为空 | +| job **3~5 秒就亡**、日志里是 `CategoryInfo` + `FullyQualifiedErrorId : NativeCommandError`,而同一条命令在本地全绿 | `run:` 块被**点源**进会话且 `$ErrorActionPreference` 继承为 Stop → 原生程序的 stderr 行变成**终止性**错误(`2>$null` 挡不住) | 块首显式设 `Continue`,原生调用一律摊平(`2>&1` 配 `ForEach-Object` 那类漏斗);复现方法见 §5.3 的 run 44 实录 | | `l05` 日志出现 `::warning::runner 未安装 MSVC` | VS 装在非默认路径或未装 | 见 §4.2(默认路径 / 同步改两处根目录) | | `mem-numbers` 显示 `Skipped` | 正常(CI 无 Keil,无 `.map`) | 无需处理;但注意 **CI 未守体积门禁**(见 §0.0),改代码后本地 `@build` + `check_mem_numbers.py --scan` 兜底 | | L0.5 显示 `2 runner(s), X/Y ...` 但 X.log` | @@ -1467,6 +1499,7 @@ powershell -ExecutionPolicy Bypass -File scripts\ci_runner_bootstrap.ps1 -DryRun | 日期 | 变更 | |---|---| +| 2026-10-27 | 🔴 **run 44 真机首验失败一次 → 两条「本地照不出来」的教训写进 §5.3**:把原生检出差上去推到 `ci/remote-ci`(10:38)后两个 job 分别 3 s / 2 s 亡,日志是 `error: No such remote 'origin'` 被包成 `NativeCommandError` 终止整步。教训①:runner 用 `powershell -command . '<文件>'` **点源**执行 run: 块,会话继承 `$ErrorActionPreference=Stop` 时原生程序的 stderr 一行就是**终止性错误**,`2>` 重定向挡不住——修法:块首显式 `Continue` + 所有 git 调用走摊平漏斗(另把 `git remote get-url` 换成 `git config --get`,缺 key 时不写 stderr)。教训②:**测法必须复现点源**——实测 `exit 1` 语义矩阵:`-File` 直跑=1 / `-File` 包点源=**0 且继续执行** / `-Command` 点源=1;我第一版复现器用了中间那种,把四条失败路径全测成 rc=0(假警报,也照不出教训①)。换点源测法后:全新工作区 150 s `checkout ok: c1e7e8c / commits=669`、四条失败路径 ExitCode 全 1、复用工作区 1.6 s 且 gitignored 文件保留。§5.3 实测表补 G/H 两行、§7 补「几秒就亡」一行、`remote-ci-ops` skill §10 补测法。另记:本仓 push 到**已存在**的远端分支会正常建 run(run 44),而新建分支首推不建(已三次复现) | | 2026-10-27 | ✂️ **CI 侧两处根治 + 更正我上一条的一处推断**:① **workflow 去掉 `uses:`**——两个 job 的第 1 步换成原生 git 全量检出(完整 refspec + 分离 HEAD + `reset --hard` + `clean -ffd`、令牌只走 per-invocation `-c http.extraheader`、`GIT_TERMINAL_PROMPT=0` 防挂死、目标 sha 不在历史里就 `::error::` 硬失败而不是默默构建错树)。动因是 run 36/41/42 三次 **`Prepare all required actions` 阶段 ~45 s 外网超时把整个 job 打死、零钩子输出**,而同一分钟手工 `curl` 返回 200。§5.3 整节重写,含六项实测(A/B/C/D/E/F——关键是 E:**把正文从 `ci.yml` 里抽出来跑**,测的就是文件里那份字节)与两条自查命令;§7 的 checkout 行同步改为“看见此症状=有人把 `uses:` 加回来了”。② **`scripts/ci_runner_bootstrap.ps1` 的 `Find-MsvcCl` 重写**:`vswhere -all -products *` 取实例(**能看见非默认盘**)+ 工具集版本降序 + `-MsvcToolset` 前缀**钉住世代**(默认 `14.51.`,与覆盖率基线同版本;钉不上连 WARN 再回落最新,绝不静默)+ 新增 `-MsvcLatest`;`-SetEnv` 增写 `DTU_MS_COVERAGE`(须与 `DTU_MSVC_CL` 成对改)。③ **更正**:先前写“旧探测会把机器级变量覆回 C 盘 14.29”——本机实情是 C 盘 VS2019 已于 **01:00 卸载**(`…\2019` 只剩空目录、C 盘 x64 `cl.exe` 实测 0 个),旧 `-SetEnv` 在本机是**找不到 → 只 Warn → 不写**,于是变量一直指着失效路径(CI 的 L0.5 走显式通道即硬失败,不做静默回退)。实测新逻辑:候选 3 个(14.44/14.51/14.52,全在 E 盘)→ 默认命中 **14.51.36231**,`-Latest` 给 14.52,`-MsvcToolset 99.` 走告警回落。④ §1.2.1 表第三行按 1.25.4 实际行为改正(自定义 `DEFAULT_ACTIONS_URL` 取值已无效)——旧写法会把下一个排障的人引向一个不存在的开关 | | 2026-10-27 | 🧯 **拆穿「L0 恒红 = 环境噪声」这个想当然 + 覆盖率工具链补齐**:① `cppcheck` 恒红**不是**版本漂移——本机与 runner 同机、同 exe(2.21.0)、同参数复现即红,两根 `uninitializedVariable`(`s_cmd_con` 在空信息体分支把未初始化 `body` 传下去、`s_spont_step` 的 `blen` 作用域过宽)是真代码问题,已在 `ci/remote-ci` 的 `5869c03` 修掉(`@build` 0/0,`Code 231712→231732`)。② 新增 §4.4:`Microsoft.CodeCoverage.Console.exe` **只随 VS 分发**(NuGet 两候选包实测 .exe 数为 0 / 只有老版 `CodeCoverage.exe`),本机补装 VS2026 BuildTools 18.11 于 **E 盘**后仍因非默认盘不被看见,须显式机器级 `DTU_MSVC_CL`(指 14.51 = 与 `scripts/test_coverage_baseline.json` 同版本);三条管理员命令、`-Force` 陷阱、`Restart-Service Schedule` 判据一并写入。③ 排障教训:**页面时长与本地对不上时以 `commit_status` + runner 日志为准**——页面那条「L0 Failing after 6m49s」在库里是 run 26 的 409 s(cppcheck 真红),同一 sha 另有一次 5 s 的 checkout 超时,**两者不是一回事**,别拿一个解释另一个 | | 2026-10-25 | 🔒 **A-5 残余销项联动(§0.0 重写)**:体积门禁由"CI 不守"改为**同码凭证强制**——本地 `@build` 0/0 后 `ram_baseline.py --update` 盖 `scripts/size_attestation.json`,L0 作业末尾 `check_size_gate.py --ci` 复核同码性 + 红线复算(runner 无需 Keil);主干 push 拦红、其余事件 warn-only;`mem-budget` 补进 SKIP 名单诚实显示。指纹取 git 索引 blob(跨 checkout 稳定,修 7c4628b);三项仍守不住的口径唯一正文 = 脚本 docstring |