perf(ci): 四阶段 DAG + warmup-tl 预热 + path filter + 字体缓存 + 时区/mirror 韧性#899
Conversation
3 个优化点 + 1 个细节, 都在 test.yml + .gitignore: 1. **5 包独立 matrix job**: 原结构是单 job 串行跑 Test ctex → xeCJK → zhnumber → CJKpunct → zhlineskip. 改为 matrix os × pkg = 15 个独立 job 并行, fail-fast: false. wall-clock 取最慢一个 = ctex on windows ~8.5min, 比原来 single-job 三 OS max(ctex=8.5min + 其他 4 包串行) 总 ~10-13min 显著快. 2. **PR 上按修改包过滤**: 新增 changes job, 用 dorny/paths-filter@v4 检测改了哪些包. 依赖反查: ctex 依赖 xeCJK + zhnumber, 后者改了 也跑 ctex. 公共改 (.github/workflows/test.yml, scripts/check-parallel.sh, support/**, Makefile) 让 5 个全跑. push / schedule / workflow_dispatch 触发不过滤, 全跑回归保险. 3. **字体缓存**: actions/cache@v6 缓存 ~/.cache/ctex-kit-fonts/, key 含本 yml hash, URL 改 (升级字体版本) 自动失效. 估计每 job 省 1min. 4. **新增 test-result 汇总 job**: branch protection 只盯它就够, 不必列 15 个 matrix slot. scripts/__pycache__/ 加 .gitignore (之前 commit 时漏 ignore).
actionlint 揭示: GH Actions 在 strategy expansion 前求值 job-level if, 那时 matrix context 不可用. 上一版用 (matrix.pkg == 'ctex' && ...) 直接让 workflow startup fail (0 jobs registered). 修法: changes job 新增 Compute active packages step, 把 PR 上 path filter 命中的 pkg 拼成 JSON 数组 (push/schedule/dispatch 触发时直接 给全 5 个). test matrix 用 fromJSON(needs.changes.outputs.packages) 动态生成 pkg 维度. 附带 if: != '[]' guard: PR 上若没改任何相关包 (例如纯改 README), 整个 test matrix 跳过, test-result job 视为 skipped.
之前 paths-ignore 含 '.github/workflows/**', 设计是让 dependabot 改 非主测 workflow 时不跑测试. 但代价: 改 test.yml 自身的 PR 也被 ignore, **workflow 改动无法在该 PR 上验证**. 改为只 ignore 'agentic-*.yml' (agentic-pr-review / agentic-patrol / agentic-llmdoc-updater 三个), test.yml / release.yml / release-ctan-upload.yml / lint-test-files.yml 等核心 workflow 改了 立刻触发 CI 验证. (本 PR 之前 push 没启动主测就是被 .github/workflows/** ignore 拦了)
PR #899 上 5/5 windows job 都死在 "Install Noto fonts" step. log 显示 mkdir -p ~/.cache/ctex-kit-fonts 在 line 1 没报错, 但 cd ~/.cache/ctex-kit-fonts 在 line 3 报 "No such file or directory". 同样的 ~ expansion 两次结果 不一致 — msys2 bash 在 Windows GitHub Actions 上的诡异 bug. 修法: 改用 ${{ github.workspace }}/.font-cache 绝对路径, 不依赖 ~: - actions/cache@v6 的 path - Install Noto fonts step 内 FONT_CACHE=$(github.workspace)/.font-cache - ubuntu / macos 上 workspace 是 Linux 路径, 也是绝对路径, 无副作用
setup-texlive-action 自带 cache (TL 包目录), 但 update-all-packages 默认 true 让每次都跑 tlmgr update (~1min/job × 15 job = 浪费). 改: update-all-packages 仅在 schedule / workflow_dispatch 触发时为 true. PR / push 走 cache baseline, 节省 ~1min/job (并发取 max 大概 1-2min wall-clock). 代价: PR / push 上 TL 不再同步 TLnet 最新版. schedule 每周五跑一次 update-all 把 baseline 推进, workflow_dispatch 维护者强制刷新.
PR Review: perf(ci): DAG 并行 + PR path filter + 字体缓存审查结论: REQUEST_CHANGES — 整体架构思路优秀 (DAG 并行 + path filter + 字体缓存三管齐下),但有 1 个阻塞问题需解决。 Critical (1)🔴
|
bot review 揭示 PR 上 9/15 job 失败的根因: 仓库 .tlg baseline 是在
update-all-packages: true (TLnet 最新) 下 save 的, 我上一 commit 把
update-all 改为只 schedule 触发, PR/push 走 cache baseline (旧版 TL),
TL 包行为漂移 → .log vs .tlg 不匹配, 全包都炸.
修法: update-all-packages 改回 true. 节省 1min 不值 PR CI 不一致.
顺手处理 review 一个 nit:
- 字体 zip 解压后 rm -f, 减小 cache 体积 ~200MB.
bot 提到的 branch protection 迁移已写进 PR description.
幽灵 ${{ matrix.pkg }} on ${{ matrix.os }} job 是 GitHub Actions 已知
matrix 启动行为, 非本 PR 问题, 不修.
PR #899 review 提到 ${{ matrix.pkg }} on ${{ matrix.os }} 残留一个 cancelled "幽灵" job. 根因: GitHub Actions 在 strategy expansion **前** 注册 placeholder check, 用未渲染的模板字符串作 name, expansion 后再 cancel. 这是 GH Actions 已知行为, 但能通过架构绕开. 修法: 拆 matrix.pkg 维度, 改为 5 个独立 caller job (test-ctex / test-xeCJK / ...) 各自 `uses: ./.github/workflows/_test-package.yml`. - 每个 caller job 名字静态 ("test-ctex" 等), 无 placeholder. - reusable workflow 内只剩 matrix.os 维度, 3 个 OS, 全静态 include. - changes job 输出 5 个 bool, 每个 caller `if: needs.changes.outputs.<pkg>`. - test-result needs 5 个 caller, 检查各自 result (success/skipped 都 OK). bot 说"非本 PR 问题不修", 但用户判定"不能留任何形式的技术债", 修. llmdoc/reference/build-and-test.md 同步新架构.
🔍 PR 增量审查
上次审查的 3 个问题全部处理到位:
|
PR #899 review nit: 字体 install 逻辑已移入 _test-package.yml (该文件 有自己的 env 声明), test.yml 顶层 env 块仅含 NOTO_SANS_URL / NOTO_SERIF_URL, 没人引用, 死代码. 删.
🔍 PR 增量审查
增量仅 1 个 commit:删除
|
bot review nit: L246+ 的详细说明已更新, 但 L213 的摘要段还在写 "同一 job 中依次进入 ctex/xeCJK/..." 旧串行结构. 改成 "5 个独立 caller job, 各自 uses _test-package.yml 在 3 OS 上并行".
🔍 PR 增量审查
增量仅 1 个 commit 历次 review 提出的所有问题均已处理:
代码良好,无新问题。CI 当前 pending( 🤖 Generated with Claude Code |
setup-texlive-action 自身无 retry / mirror-list / timeout 选项 (其 v4 action.yml 仅 9 个 input, 全部网络问题需自己处理). PR #899 上多次 windows job 因 install-tl 下载 192.17.3.223:443 (ctan.math.illinois.edu) ETIMEDOUT 而 FAILED_TO_DOWNLOAD, 不是我们代码 bug 但拖累 test-result. 修法: 包 3 次 retry — try 1/2 都 continue-on-error: true, 后一个 if 前一个 outcome == 'failure'; try 3 不带 continue-on-error, 真失败才让 job 挂. setup-texlive-action 内部自己有 cache, 重试时若网络已恢复就 直接走 cache hit 路径, 不会重复装 (cache 是 actions/cache 持久化的, 跨 step 共享).
🔍 PR 增量审查
3 个新增 commit 都是合理的收尾/加固工作:死代码清理、文档同步、TeX Live 安装重试。整体架构
|
| Commit | 评价 |
|---|---|
273ae74 删 test.yml 顶层死 env |
✅ 正确清理,NOTO_*_URL 已移入 _test-package.yml |
ed593d9 llmdoc L213 摘要同步 |
✅ 描述从旧串行结构更新为 5 caller 架构,保持文档一致 |
5abee18 TeX Live 3 次 retry |
✅ 模式正确:try1/2 带 continue-on-error,try3 不带,真失败才挂 job |
亮点
- retry 模式干净:
continue-on-error+ 条件if级联,比 shell 内for i in 1 2 3; do更清晰,且每次 retry 都有独立的 step 状态可在 CI UI 中查看 _test-package.ymlpaths-filter 已包含自身:_allanchor 中加入了_test-package.yml,改 reusable workflow 也会触发全包测试- 字体 cache key 更新为
_test-package.yml:hashFiles指向正确的文件(URL 定义所在文件),而非旧的test.yml
🤖 Generated with Claude Code
bot review 揭示 2 个问题: 1. [Critical] reusable workflow 内 github.event_name 永远是 'workflow_call', 不是顶层触发事件. L131 的 schedule 判定永远走 -H, --show-saves 失效. 修法: workflow_call 加 event-name input, 5 个 caller 透传 github.event_name. reusable workflow 内用 inputs.event-name 判定. 2. [Insight] PR 上 5 caller × 3 OS = 15 job 同时启动, cache 没填好时 全部并发 cache miss → 都跑 install-tl → 同 mirror 被 5 路并发轰炸 → 偶发 ETIMEDOUT 让其中一个 fail. 修法: 新 warmup-tl job 收敛到每 OS 1 个 setup-texlive, 各 caller needs warmup-tl 串行. cache 一旦填好, 15 caller 100% 走 cache hit (~10-20s/job), 完全消除 mirror ETIMEDOUT 风险. weekly schedule 的 update-all 也挪到 warmup-tl: schedule / workflow_dispatch 触发时 update-all = true 推 baseline; PR/push 走 cache. caller 内 update-all 仍 true 保险 (走 cache hit, 廉价). warmup-tl 自己保留 3 次 retry (try1/2 continue-on-error, try3 真 失败让 job 挂), 因为它**唯一会真装 install-tl** 的地方, 必须能应对 首次冷启动的 ETIMEDOUT. caller 内的 retry 不再需要, 删掉.
🔍 PR 增量审查
上次审查的 Critical( 整体架构演进为四阶段 DAG:
|
| Commit | 评价 |
|---|---|
e809b91 warmup-tl + event-name 透传 |
✅ 修复 Critical:inputs.event-name 替代 github.event_name,--show-saves 在 schedule 触发时正确生效 |
架构亮点
- warmup-tl 设计精确: 收敛
install-tl到每 OS 1 个 job,3 次 retry 仅在此处保留(唯一冷启动入口),caller 简化为单步无 retry - event-name 透传模式: reusable workflow 的标准解法,注释清晰说明了 GH Actions 的已知行为
_test-package.yml注释更新到位: 准确解释了update-all-packages: true为何在 caller 中是廉价操作paths-ignore收窄到agentic-*.yml: 关键修复 — 确保test.yml/_test-package.yml自身改动能触发 CI 自验
合并前提醒
- Branch protection 迁移: 合并后需将 status check 从旧的
on ubuntu-latest等 3 个改为test-result1 个(PR 描述中已提及) - Windows warmup-tl 仍在 pending: 建议等此轮 CI 全绿后再合并
🤖 Generated with Claude Code
1. [Critical] PR #899 上 warmup-tl on windows 3 次 retry 同 mirror (ctan.math.illinois.edu) 都 ETIMEDOUT 192.17.3.223:443. bot review 提的 🟢 #2 完全应验了 — 同 mirror 长时间不可达时 3 次都炸. 修法: try 2 换 https://ftp.fau.de/ctan/systems/texlive/tlnet (德国), try 3 换 https://mirror.ctan.org/systems/texlive/tlnet (全球自动重 定向). 三个 mirror 同时不可用的概率极低. 2. [Critical] 之前 test-result 只看 5 个 test-* result, warmup-tl 失败 → 5 个 test-* SKIPPED → test-result 判 skipped 为 OK → 错误绿了. 修法: needs 加 warmup-tl, 检查它 result. warmup 失败 → test-result 失败 → branch protection 正确反映 CI 状态.
🔍 PR 增量审查
5 个增量 commit 质量扎实,解决了上轮审查后发现的实际 CI 问题。架构从 增量改动总结
|
PR #899 实测 windows caller 即便 cache key 一致也 install-tl ETIMEDOUT. 根因: setup-texlive-action 默认 version=latest, action 入口先跑 resolveVersion → checkRemoteVersion(repo) 联网查"TLnet 现在是什么版本", **这一步早于 cache restore**. 联网到 illinois mirror 失败 → 整个 action 挂. 修法: 4 处 setup-texlive-action 调用都加 version: '2026' 显式 pin, 跳 过 remote check. cache 命中后无任何联网, 真正实现"caller 100% 走 cache". bot 提的"两个 🟢"里第二个(retry 同 mirror 风险)+ 这个 remote-check 联网 盲点合在一起就是 windows 链路问题. 现在彻底关上.
🔍 PR 增量审查
新增 1 个 commit:在全部 4 处 全量审阅
|
PR #899 实测各 caller 跑完后 cache hit, 但仍跑 ~70s 'Updating packages' 58 个包 — setup-texlive-action 设计是"primary key cache 命中时不 save", update 后状态没回写, 每个 caller 自己重 update. 修法: warmup-tl 设 env SETUP_TEXLIVE_ACTION_FORCE_UPDATE_CACHE=1, 强制 setup-texlive 把 update 后的 TEXDIR 保到 **uniqueKey** (= primaryKey + random uuid). 因 GH actions/cache restore 用 restoreKeys 数组按 primaryKey prefix 模糊匹配, caller 不设此 env 时 cache restore 仍能 命中 warmup 的 uniqueKey, 拿到已 update 状态. caller 端 update-all-packages: false, 不再重跑 update. warmup-tl 自身永远 update-all=true (无视触发器), 保证 cache 内容与 仓库 .tlg save 时的 TL baseline 一致. 实测预期: 每 caller 省 ~70s, wall-clock ~9min → ~7min. 源码确认 (TeX-Live/setup-texlive-action packages/action/src/cache.ts): - SETUP_TEXLIVE_ACTION_FORCE_UPDATE_CACHE 是官方 env, 触发 this.#forceUpdate = true - forceUpdate 时 save 用 uniqueKey, 否则 primaryKey - restore 用 restoreKeys = [primaryKey, oldPrimaryKey, ...], 按 prefix 匹配, 能命中 uniqueKey
🔍 PR 增量审查
7 个增量提交解决了上次审查后的实测问题,架构从"5 caller 直连"进化到
|
PR #899 上一 commit 042442f 让 caller update-all=false 试图复用 warmup update 后的 cache, 实测 3 个 caller 测试 fail (xeCJK macos / ubuntu, zhlineskip ubuntu) 因 baseline 漂移. 根因: setup-texlive-action 用 actions/cache, GH cache 设计是 primaryKey 精确优先. warmup 用 SETUP_TEXLIVE_ACTION_FORCE_UPDATE_CACHE=1 保到 uniqueKey (= primaryKey + uuid). caller restoreKeys 列表中 primaryKey 精确命中老的 (无 uuid) entry — 那是 update 前的 TL baseline. caller 拿到旧 baseline + 不 update → 跟仓库 .tlg (在 update 后 save 的) 不一致. 修法: caller 回退 update-all=true. 每 caller 跑 70s tlmgr update 是 setup-texlive cache 设计的固有代价, 无合理方案绕开 (绕开需手动 install-tl + 自己管 actions/cache + 多 OS 适配, 400+ 行 yml). 总 wall-clock 仍优 旧 13min (现 ~9min). 如要进一步消除 70s, 见 follow-up issue (手动 install-tl 路径).
🔍 PR 增量审查
增量仅 1 个 commit:回退 caller 端
|
实测 PR #899 上 hook 报 "✓ all CI checks passed, no new review activity" 但 bot 实际已在 push 后写了评论, 漏检. 根因: head_committed_at 用 git show -s --format=%cI 输出本地 timezone (+08:00, 因为我在 CST 时区 commit). GitHub API created_at / submittedAt 返回 UTC (Z 后缀). jq 字符串比较按字母序: "2026-06-27T04:23:05Z" # bot UTC 评论 (push 后) "2026-06-27T12:05:26+08:00" # head commit 本地时间 (实际等价 UTC 04:05) 字符串排序 Z < +, 04: < 12:, 所以 bot 评论被判为 < head commit, 误判为 "push 前的旧评论". 修法: 用 TZ=UTC + git show --format=%cd --date=iso-strict-local. git 默认 %cI 是 commit timezone 不受 TZ env 影响, 必须用 --date=iso-strict-local 配合 TZ=UTC 才让 git 重新格式化为 UTC Z 后缀. 实测: 本地 commit 时间 12:21:03 +08:00 → TZ=UTC git show -s --format=%cd --date=iso-strict-local → 2026-06-27T04:21:03Z 现在 jq 比较两边都是 UTC Z 字符串, 字母序 = 时间序, 比较正确.
🔍 PR 增量审查
上次审查后新增 9 个 commit,涵盖:warmup-tl 预热 job 引入、event-name 透传修复、多 mirror retry、version pin、FORCE_UPDATE_CACHE 尝试与回退、死 env 清理、githook 时区修复。整体改动逻辑清晰,迭代过程中每个 bug 都有对应修复且注释充分,最终架构 changes → warmup-tl → 5 caller → test-result 四阶段稳固。 🟡 重要建议 (1)
🟢 小问题 (2)
亮点
🤖 Generated with Claude Code |
bot review 跨多轮累计建议 + 实测 CI 失败问题, 一次性处理: 1. [Critical, CI fail] windows test-xeCJK update 阶段 mirror 抖动 → tlmgr 用本地老 db → cache 内 TL revision 不与 .tlg baseline 一致 → environ01.xetex.diff fail. 修法: caller 加 2 次 retry 换 mirror (try1 illinois, try2 fau.de), 各 timeout 15min. 2. [Important] version: '2026' 硬编码 4 处 → 提取 TL_VERSION 顶层 env. workflow_call inputs 不能直接引用顶层 env, 由 changes job 输出 tl-version 字段透传给 5 caller, 再透传到 _test-package.yml input. 3. [Important] warmup-tl 注释说 "实测每 caller 省 ~70s" 但实际不生效, 与 _test-package.yml caller update-all=true 矛盾. 去掉 FORCE_UPDATE_CACHE env (实测无效, 留着误导) + 改写"历史"段如实记录 GH cache primaryKey 精确优先导致 FORCE_UPDATE_CACHE 思路失败. 4. [Nit] warmup retry timeout 30/30/30 → 10/10/30, 让换 mirror 反应快. 5. [Doc] llmdoc 补 阶段 0.5 warmup-tl + caller retry 描述 + TL_VERSION 设计 + FORCE_UPDATE_CACHE 历史. 三阶段说成四阶段. 不修 (bot 标记非阻塞): - dorny/paths-filter@v4 没 pin SHA: 整仓 action 都没 pin, 改一处不协调, 若要全仓 pin 是独立 PR.
🔍 PR 增量审查
上次审查后新增 1 个 commit,处理了前几轮 review 累积的 5 项建议。所有改动合理,前轮标记的重要建议(
|
PR #899 实测: hook 报 'CI FAILED warmup-tl on \${{ matrix.os }}' → exit 1. 但该 check 是 GH Actions matrix 在 strategy expansion 前注册的 placeholder, conclusion 永远是 cancelled (expansion 完成后被 cancel), 不代表真失败. 之前的 dedupe 逻辑 (group_by(.name)) 救不了, 因为 placeholder name 含 未渲染的 \${{ matrix.X }} 字面, 与真实 matrix job (如 'warmup-tl on ubuntu-latest') 是不同 name, 单独成组. 解法: 按 name 含 '\${{' 字面 过滤掉这类 placeholder. 注意当前 PR yml 已经修了 matrix.pkg 维度的幽灵 (拆 5 caller job), 但 warmup-tl 仍用 matrix.os 维度, 还是会产生 'warmup-tl on \${{ matrix.os }}' placeholder. 这是 GH Actions 固有 行为, 不可能彻底消除 placeholder; 只能在消费侧过滤.
🔍 PR 增量审查
增量仅 1 个 commit,修复
|
| 前序问题 | 状态 |
|---|---|
| update-all-packages: false 导致 baseline 漂移 | ✅ 已回退为 true |
| 字体 zip 增大 cache 体积 | ✅ 解压后 rm -f *.zip |
| 幽灵 cancelled job | ✅ 拆 5 caller job 消除 |
| event_name 在 reusable workflow 内恒为 workflow_call | ✅ inputs.event-name 透传 |
| mirror ETIMEDOUT 间歇失败 | ✅ warmup-tl 3 mirror retry + caller 2 retry |
| TL version 硬编码 4 处 | ✅ 提取 TL_VERSION 顶层 env |
| FORCE_UPDATE_CACHE 实测不生效 | ✅ 已移除,注释如实记录历史 |
| branch protection 迁移 | ✅ PR 描述已说明 |
| check-pr-ci.sh 假阳性 | ✅ 本次 commit 修复 |
最终架构 changes → warmup-tl → 5 caller → test-result 四阶段清晰,代码注释充分,llmdoc 同步更新。
🟢 小问题 (1)
- 文件:
.github/workflows/test.ymlL312 - 问题: dorny/paths-filter@v4 未 pin commit SHA,存在 supply chain 风险
- 建议: 与仓库其他 action 一致(均未 pin SHA),可作为独立 PR 统一处理,非本 PR 阻塞项
🤖 Generated with Claude Code
…allback PR #899 实测: caller setup-texlive try 1 (illinois mirror) 报 success, 但实际 tlmgr 在 'Unable to download the checksum of the remote TeX Live database' 时 silent 用本地老 db, setup-texlive 看 tlmgr 退码 0 认为 update 成功 → step success → retry 不触发. 结果 cache 内 TL 比仓库 .tlg baseline 老 → footnote01 / environ01 等测试 fail. 修法 (双管齐下): 1. caller try 1 换 mirror.ctan.org 全球自动重定向 (而非 pin illinois 单点), GH runner 网络好时它会自动选最快 mirror. illinois 留 try 2 兜底 (mirror.ctan.org 极端不可用时). 2. 加 verification step (caller + warmup-tl 各一个): setup-texlive 完成后跑 tlmgr update --self --list, grep 输出"Unable to download| not verified" 字样, 命中就 exit 1 强 fail. 这样 tlmgr silent fallback 时, 我们能在 setup-texlive step 后立即检测出来, step fail 后 rerun --failed 可以人工重跑 (网络抖动通常 transient).
🔍 PR 增量审查
增量 1 个 commit 修复思路正确——
|
PR #899 上 5/5 windows job 都死在 "Install Noto fonts" step. log 显示 mkdir -p ~/.cache/ctex-kit-fonts 在 line 1 没报错, 但 cd ~/.cache/ctex-kit-fonts 在 line 3 报 "No such file or directory". 同样的 ~ expansion 两次结果 不一致 — msys2 bash 在 Windows GitHub Actions 上的诡异 bug. 修法: 改用 ${{ github.workspace }}/.font-cache 绝对路径, 不依赖 ~: - actions/cache@v6 的 path - Install Noto fonts step 内 FONT_CACHE=$(github.workspace)/.font-cache - ubuntu / macos 上 workspace 是 Linux 路径, 也是绝对路径, 无副作用
bot review 揭示 PR 上 9/15 job 失败的根因: 仓库 .tlg baseline 是在
update-all-packages: true (TLnet 最新) 下 save 的, 我上一 commit 把
update-all 改为只 schedule 触发, PR/push 走 cache baseline (旧版 TL),
TL 包行为漂移 → .log vs .tlg 不匹配, 全包都炸.
修法: update-all-packages 改回 true. 节省 1min 不值 PR CI 不一致.
顺手处理 review 一个 nit:
- 字体 zip 解压后 rm -f, 减小 cache 体积 ~200MB.
bot 提到的 branch protection 迁移已写进 PR description.
幽灵 ${{ matrix.pkg }} on ${{ matrix.os }} job 是 GitHub Actions 已知
matrix 启动行为, 非本 PR 问题, 不修.
PR #899 review 提到 ${{ matrix.pkg }} on ${{ matrix.os }} 残留一个 cancelled "幽灵" job. 根因: GitHub Actions 在 strategy expansion **前** 注册 placeholder check, 用未渲染的模板字符串作 name, expansion 后再 cancel. 这是 GH Actions 已知行为, 但能通过架构绕开. 修法: 拆 matrix.pkg 维度, 改为 5 个独立 caller job (test-ctex / test-xeCJK / ...) 各自 `uses: ./.github/workflows/_test-package.yml`. - 每个 caller job 名字静态 ("test-ctex" 等), 无 placeholder. - reusable workflow 内只剩 matrix.os 维度, 3 个 OS, 全静态 include. - changes job 输出 5 个 bool, 每个 caller `if: needs.changes.outputs.<pkg>`. - test-result needs 5 个 caller, 检查各自 result (success/skipped 都 OK). bot 说"非本 PR 问题不修", 但用户判定"不能留任何形式的技术债", 修. llmdoc/reference/build-and-test.md 同步新架构.
PR #899 review nit: 字体 install 逻辑已移入 _test-package.yml (该文件 有自己的 env 声明), test.yml 顶层 env 块仅含 NOTO_SANS_URL / NOTO_SERIF_URL, 没人引用, 死代码. 删.
setup-texlive-action 自身无 retry / mirror-list / timeout 选项 (其 v4 action.yml 仅 9 个 input, 全部网络问题需自己处理). PR #899 上多次 windows job 因 install-tl 下载 192.17.3.223:443 (ctan.math.illinois.edu) ETIMEDOUT 而 FAILED_TO_DOWNLOAD, 不是我们代码 bug 但拖累 test-result. 修法: 包 3 次 retry — try 1/2 都 continue-on-error: true, 后一个 if 前一个 outcome == 'failure'; try 3 不带 continue-on-error, 真失败才让 job 挂. setup-texlive-action 内部自己有 cache, 重试时若网络已恢复就 直接走 cache hit 路径, 不会重复装 (cache 是 actions/cache 持久化的, 跨 step 共享).
1. [Critical] PR #899 上 warmup-tl on windows 3 次 retry 同 mirror (ctan.math.illinois.edu) 都 ETIMEDOUT 192.17.3.223:443. bot review 提的 🟢 #2 完全应验了 — 同 mirror 长时间不可达时 3 次都炸. 修法: try 2 换 https://ftp.fau.de/ctan/systems/texlive/tlnet (德国), try 3 换 https://mirror.ctan.org/systems/texlive/tlnet (全球自动重 定向). 三个 mirror 同时不可用的概率极低. 2. [Critical] 之前 test-result 只看 5 个 test-* result, warmup-tl 失败 → 5 个 test-* SKIPPED → test-result 判 skipped 为 OK → 错误绿了. 修法: needs 加 warmup-tl, 检查它 result. warmup 失败 → test-result 失败 → branch protection 正确反映 CI 状态.
PR #899 实测 windows caller 即便 cache key 一致也 install-tl ETIMEDOUT. 根因: setup-texlive-action 默认 version=latest, action 入口先跑 resolveVersion → checkRemoteVersion(repo) 联网查"TLnet 现在是什么版本", **这一步早于 cache restore**. 联网到 illinois mirror 失败 → 整个 action 挂. 修法: 4 处 setup-texlive-action 调用都加 version: '2026' 显式 pin, 跳 过 remote check. cache 命中后无任何联网, 真正实现"caller 100% 走 cache". bot 提的"两个 🟢"里第二个(retry 同 mirror 风险)+ 这个 remote-check 联网 盲点合在一起就是 windows 链路问题. 现在彻底关上.
PR #899 实测各 caller 跑完后 cache hit, 但仍跑 ~70s 'Updating packages' 58 个包 — setup-texlive-action 设计是"primary key cache 命中时不 save", update 后状态没回写, 每个 caller 自己重 update. 修法: warmup-tl 设 env SETUP_TEXLIVE_ACTION_FORCE_UPDATE_CACHE=1, 强制 setup-texlive 把 update 后的 TEXDIR 保到 **uniqueKey** (= primaryKey + random uuid). 因 GH actions/cache restore 用 restoreKeys 数组按 primaryKey prefix 模糊匹配, caller 不设此 env 时 cache restore 仍能 命中 warmup 的 uniqueKey, 拿到已 update 状态. caller 端 update-all-packages: false, 不再重跑 update. warmup-tl 自身永远 update-all=true (无视触发器), 保证 cache 内容与 仓库 .tlg save 时的 TL baseline 一致. 实测预期: 每 caller 省 ~70s, wall-clock ~9min → ~7min. 源码确认 (TeX-Live/setup-texlive-action packages/action/src/cache.ts): - SETUP_TEXLIVE_ACTION_FORCE_UPDATE_CACHE 是官方 env, 触发 this.#forceUpdate = true - forceUpdate 时 save 用 uniqueKey, 否则 primaryKey - restore 用 restoreKeys = [primaryKey, oldPrimaryKey, ...], 按 prefix 匹配, 能命中 uniqueKey
PR #899 上一 commit 042442f 让 caller update-all=false 试图复用 warmup update 后的 cache, 实测 3 个 caller 测试 fail (xeCJK macos / ubuntu, zhlineskip ubuntu) 因 baseline 漂移. 根因: setup-texlive-action 用 actions/cache, GH cache 设计是 primaryKey 精确优先. warmup 用 SETUP_TEXLIVE_ACTION_FORCE_UPDATE_CACHE=1 保到 uniqueKey (= primaryKey + uuid). caller restoreKeys 列表中 primaryKey 精确命中老的 (无 uuid) entry — 那是 update 前的 TL baseline. caller 拿到旧 baseline + 不 update → 跟仓库 .tlg (在 update 后 save 的) 不一致. 修法: caller 回退 update-all=true. 每 caller 跑 70s tlmgr update 是 setup-texlive cache 设计的固有代价, 无合理方案绕开 (绕开需手动 install-tl + 自己管 actions/cache + 多 OS 适配, 400+ 行 yml). 总 wall-clock 仍优 旧 13min (现 ~9min). 如要进一步消除 70s, 见 follow-up issue (手动 install-tl 路径).
实测 PR #899 上 hook 报 "✓ all CI checks passed, no new review activity" 但 bot 实际已在 push 后写了评论, 漏检. 根因: head_committed_at 用 git show -s --format=%cI 输出本地 timezone (+08:00, 因为我在 CST 时区 commit). GitHub API created_at / submittedAt 返回 UTC (Z 后缀). jq 字符串比较按字母序: "2026-06-27T04:23:05Z" # bot UTC 评论 (push 后) "2026-06-27T12:05:26+08:00" # head commit 本地时间 (实际等价 UTC 04:05) 字符串排序 Z < +, 04: < 12:, 所以 bot 评论被判为 < head commit, 误判为 "push 前的旧评论". 修法: 用 TZ=UTC + git show --format=%cd --date=iso-strict-local. git 默认 %cI 是 commit timezone 不受 TZ env 影响, 必须用 --date=iso-strict-local 配合 TZ=UTC 才让 git 重新格式化为 UTC Z 后缀. 实测: 本地 commit 时间 12:21:03 +08:00 → TZ=UTC git show -s --format=%cd --date=iso-strict-local → 2026-06-27T04:21:03Z 现在 jq 比较两边都是 UTC Z 字符串, 字母序 = 时间序, 比较正确.
bot review 跨多轮累计建议 + 实测 CI 失败问题, 一次性处理: 1. [Critical, CI fail] windows test-xeCJK update 阶段 mirror 抖动 → tlmgr 用本地老 db → cache 内 TL revision 不与 .tlg baseline 一致 → environ01.xetex.diff fail. 修法: caller 加 2 次 retry 换 mirror (try1 illinois, try2 fau.de), 各 timeout 15min. 2. [Important] version: '2026' 硬编码 4 处 → 提取 TL_VERSION 顶层 env. workflow_call inputs 不能直接引用顶层 env, 由 changes job 输出 tl-version 字段透传给 5 caller, 再透传到 _test-package.yml input. 3. [Important] warmup-tl 注释说 "实测每 caller 省 ~70s" 但实际不生效, 与 _test-package.yml caller update-all=true 矛盾. 去掉 FORCE_UPDATE_CACHE env (实测无效, 留着误导) + 改写"历史"段如实记录 GH cache primaryKey 精确优先导致 FORCE_UPDATE_CACHE 思路失败. 4. [Nit] warmup retry timeout 30/30/30 → 10/10/30, 让换 mirror 反应快. 5. [Doc] llmdoc 补 阶段 0.5 warmup-tl + caller retry 描述 + TL_VERSION 设计 + FORCE_UPDATE_CACHE 历史. 三阶段说成四阶段. 不修 (bot 标记非阻塞): - dorny/paths-filter@v4 没 pin SHA: 整仓 action 都没 pin, 改一处不协调, 若要全仓 pin 是独立 PR.
PR #899 实测: hook 报 'CI FAILED warmup-tl on \${{ matrix.os }}' → exit 1. 但该 check 是 GH Actions matrix 在 strategy expansion 前注册的 placeholder, conclusion 永远是 cancelled (expansion 完成后被 cancel), 不代表真失败. 之前的 dedupe 逻辑 (group_by(.name)) 救不了, 因为 placeholder name 含 未渲染的 \${{ matrix.X }} 字面, 与真实 matrix job (如 'warmup-tl on ubuntu-latest') 是不同 name, 单独成组. 解法: 按 name 含 '\${{' 字面 过滤掉这类 placeholder. 注意当前 PR yml 已经修了 matrix.pkg 维度的幽灵 (拆 5 caller job), 但 warmup-tl 仍用 matrix.os 维度, 还是会产生 'warmup-tl on \${{ matrix.os }}' placeholder. 这是 GH Actions 固有 行为, 不可能彻底消除 placeholder; 只能在消费侧过滤.
上个 commit 给 Verify TL mirror reachable 的 if 加 tl1/tl2 success 守卫 (`(tl1.outcome == 'success' || tl2.outcome == 'success')`), 但 caller try 2 一直没声明 id, 导致 steps.tl2.outcome 始终空串. "tl1 失败 + tl2 成功" 路径 下 verify 不会跑, 丧失对 tlmgr silent fallback (Unable to download checksum → 退码 0 用本地老 db) 的检测能力, 这正是 PR #899 修的洞. PR #901 第三轮 bot review 抓到的回归. warmup test.yml 的 try 2 一直有 id: tl2, caller _test-package.yml 漏了, 这次补齐.
PR #899 实测 caller 端 setup-texlive 平均 70s+, mirror 抖动时单 step 可飙到 10+min (master test-ctex 17min 就是这样). 因为 setup-texlive 内部 actions/cache 设计 "同 primaryKey immutable", warmup 跑了 update 也不能回写, caller 永远要自己再跑一次 tlmgr update --all (~70s) + 看 mirror 脸色. 绕开方案: 我们自己用 actions/cache@v6 缓存整个 TEXDIR 到 weekly key. - key: tl-bypass-<runner.os>-<TL_VERSION>-<year-week ISO> - warmup-tl save: 跑完 setup-texlive (cache: false 关内部 cache) + verification, job 末尾 actions/cache@v6 自动 save TEXDIR. - caller restore: actions/cache@v6 restore TEXDIR + echo bin 路径到 $GITHUB_PATH. 完全跳过 setup-texlive-action, 不联网, 秒过. - cache miss (weekly 边界 race / cache eviction): caller fallback 到 setup-texlive-action (3 mirror retry + verification). 时序: weekly schedule 改周一 UTC 12:00 (从周五挪过来), 紧贴 ISO 周 边界 (周一 00:00 UTC 翻周). 周一一过 cache key 翻新, 12:00 schedule 跑刷新 TL cache, 周内 PR/push 享用 cache hit. setup-texlive 源码确认 (TeX-Live/setup-texlive-action packages/action/ src/runs/main/index.ts): 它只调用 tlmgr.path.add() 把 TEXDIR/bin/<platform> 加入 PATH, 没其它 env 注册. 所以 caller 跳过 setup-texlive 后只需自己 echo "$TEXDIR/bin/<platform>" >> $GITHUB_PATH 即可. 平台 bin 目录: - ubuntu-latest: bin/x86_64-linux - macos-latest: bin/universal-darwin (Apple Silicon, universal binary) - windows-latest: bin/windows 预期加速: caller 端 70-600s/job → 几秒. 总 wall-clock 进一步缩短. 附带: - .gitignore 加 actionlint (本地 lint 二进制, 别 commit)
上个 commit 给 Verify TL mirror reachable 的 if 加 tl1/tl2 success 守卫 (`(tl1.outcome == 'success' || tl2.outcome == 'success')`), 但 caller try 2 一直没声明 id, 导致 steps.tl2.outcome 始终空串. "tl1 失败 + tl2 成功" 路径 下 verify 不会跑, 丧失对 tlmgr silent fallback (Unable to download checksum → 退码 0 用本地老 db) 的检测能力, 这正是 PR #899 修的洞. PR #901 第三轮 bot review 抓到的回归. warmup test.yml 的 try 2 一直有 id: tl2, caller _test-package.yml 漏了, 这次补齐.
背景
PR #898 把 ctex 单包 4-engine 并行跑 (~20→8min), 但 CI workflow 整体仍受限于:
Test ctex→xeCJK→ ... →zhlineskip)本 PR 把 CI 从单 job 串行重构成四阶段 DAG, 全链路加固.
架构
改动
1. 5 个 caller job + reusable workflow
test.yml拆出 5 个 caller job (test-ctex/test-xeCJK/test-zhnumber/test-CJKpunct/test-zhlineskip), 各自uses: ./.github/workflows/_test-package.yml. reusable workflow 内只剩matrix.os维度 (ubuntu/macos/windows).为何不用
matrix.pkg维度: GH Actions 在 strategy expansion 前注册 placeholder check 用未渲染${{ matrix.pkg }} on ...作 name 然后 cancel, 留下"幽灵"job. 拆 5 caller job 用静态 name 彻底绕开.2.
changesjob + PR path filterdorny/paths-filter@v4检测 PR 改动:ctex/**xeCJK/**zhnumber/**CJKpunct/**zhlineskip/**test.yml,_test-package.yml,scripts/check-parallel.sh,support/**,Makefile)push / schedule / workflow_dispatch 触发不过滤, 全跑保险.
changes同时输出tl-version透传给 caller (workflow_call input 不能直接引用顶层 env).3.
warmup-tljob (cache 预热)每 OS 1 个 job 跑 setup-texlive. 收敛 install-tl 请求, 避免 5 caller × 3 OS = 15 路并发轰炸 mirror.
3 mirror retry: try 1
illinois(10min) → try 2fau.de(10min) → try 3mirror.ctan.org自动重定向 (30min). 短超时让换 mirror 反应快.4. caller mirror retry + verification
caller 端 setup-texlive 也 2 次 retry: try 1
mirror.ctan.org(auto-redirect, 15min), try 2illinois兜底 (15min).新增 verification step (caller + warmup 各一个) —
tlmgr update --self --list 2>&1 | grep "Unable to download\|not verified". tlmgr 在 mirror 不通时静默用本地老 db, setup-texlive 步骤退码 0 看不出, retry 不触发. verification step 把这种 silent fallback 检出, 强 fail 让 `rerun --failed` 可重跑.5. `version: '2026'` 提取 `TL_VERSION` 顶层 env
setup-texlive 默认 `version: latest`, 它的 `resolveVersion → checkRemoteVersion` 早于 cache restore, mirror 不通时即便 cache 命中也让 install-tl 步骤 ETIMEDOUT. 显式 pin `version` 跳过这步.
通过 `TL_VERSION` 顶层 env → `changes.outputs.tl-version` → workflow_call input 透传, TL 年度发版只需改一处.
6. 字体缓存
`actions/cache@v6` 缓存 `$GITHUB_WORKSPACE/.font-cache/` (zip 解完即删, 只留 ttc,
200MB→100MB). 用 workspace 绝对路径避开 msys2 `~` expansion 在 Windows 上的 bug. cache key 含 `_test-package.yml` hash, URL 改自动失效.7. `test-result` 汇总 job
`needs: [warmup-tl, test-ctex, test-xeCJK, test-zhnumber, test-CJKpunct, test-zhlineskip]`. 把 warmup 也算进 needs, 防 warmup 失败 → caller 全 skipped → result 误绿. branch protection 只盯这一个 check.
8. `paths-ignore` 收窄
旧 ignore 含 `.github/workflows/**`, 改 workflow 自身的 PR 不跑 CI. 收窄到 `agentic-.yml` 三个, 让核心 workflow (test.yml / release.yml / lint-test-files.yml) 改动立即触发 CI 自验.
9. `_test-package.yml` 新增 `event-name` input
reusable workflow 内 `github.event_name` 永远是 `workflow_call`, 拿不到顶层触发事件. 5 个 caller 透传 `event-name: ${{ github.event_name }}`, reusable workflow 内用 `inputs.event-name` 替代, schedule 触发的 `--show-saves` 行为正确生效.
10. `check-pr-ci.sh` 修两个假阳
11. 其它
实测加速
Workflow 总 wall-clock: master ~17min → PR ~13min (~25% 缩短).
PR 改单包 (例 CJKpunct) 时只跑相关 caller × 3 OS, ~1.5min, ~10× 加速.
旧 status check 名:
新 status check 名:
`release.yml` 的 gating 用 `actions/workflows/test.yml/runs` 看整体 workflow conclusion, 不受 matrix 改名影响.
失败重试历史 (踩坑记录)
每个都通过 commit message + 注释 + llmdoc 记录, 防再踩.