perf(ci): TL bypass cache 让 caller 真秒过 (weekly 才真装)#901
Conversation
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)
PR Review: perf(ci): TL bypass cache 让 caller 真秒过 (weekly 才真装)审查摘要整体评价: 方案思路清晰——绕开 🔴 阻塞问题 (1)1. warmup-tl 的 verification step 在 cache hit 时仍联网且会 fail,阻塞所有 caller文件: PR 中 warmup-tl 的 verification step 没有 # cache hit 路径下也跑一遍, 防 cache 内 TL 损坏 (理论不会, 防御性).
- name: Verify TL mirror reachable (no silent fallback to local db)
run: |
out=$(tlmgr update --self --list 2>&1) || true
...
if echo "$out" | grep -qiE "Unable to download|not verified"; then
exit 1
fi问题链:
建议: - name: Verify TL mirror reachable (fallback path only)
if: steps.bypass-cache.outputs.cache-hit != 'true'
run: |
...与 🟡 重要建议 (1)2. caller 端
|
…ify 只跑 cache miss caller 端 Export PATH 的 `tlmgr version` sanity check 没有 || true 兜底, 在 Windows runner 上必 fail: TL 的 tlmgr 是 tlmgr.bat, msys2 bash 不按 PATHEXT 补后缀, "$TL_BIN/tlmgr" 直接退 127. PR #901 上 5 个 Windows caller 全卡这. 修法 (PR #901 review #2): Export PATH 加 id: export-path + continue-on-error, 3 个 fallback step (try1 / try2 / Verify mirror) 的 if 同时 OR steps.export-path.outcome == 'failure'. Windows 路径名问题 + cache 损坏两种 场景都能自动接管走 setup-texlive fallback. warmup 的 Verify TL mirror reachable 没限 cache miss, mirror 抖动时会把 warmup 拖 fail, 下游 5 caller 全被 needs: warmup-tl 阻塞 — 这正是 bypass cache 最想避免的. 加 if: cache-hit != 'true' (PR #901 review #1). 顺手清掉 test.yml 多余空行 (PR #901 review #3).
🔍 PR 增量审查
第二个 commit 已修复前次 review 指出的 warmup verification cache-hit 联网问题和 caller export-path 自愈问题,方案整体设计清晰,fallback 路径完备。发现 1 个重要建议。
|
上个 commit 的 continue-on-error 自愈只是不让 step fail, 没解决 Windows "$TL_BIN/tlmgr" 找不到的根本问题: 每个 Windows caller cache hit 后 sanity check 必失败 → outcome=failure → 触发 setup-texlive fallback → 重装 TL. 实测 Windows 小包 (zhnumber/CJKpunct/zhlineskip) ~4min vs ubuntu 同包 ~25s, 差的 3-4min 全在重装 — PR #901 bypass cache 在 Windows 上等于废了. 修法: matrix 显式给出 tlmgr-exe (ubuntu/macos=tlmgr, windows=tlmgr.bat), sanity check 用 "$TL_BIN/${{ matrix.tlmgr-exe }}" version. Windows cache hit 真能 sanity check 过 → 不走 fallback → 秒过. continue-on-error 兜底 cache 损坏的极端情况保留. caller + warmup 两处都改.
|
这下看起来对了。windows 也能秒过 tl-install 的阶段了。 |
🔍 PR 增量审查 (第二轮)上轮审查的 3 个问题(warmup verification 缺 🟡 重要建议 (1)1. ISO 年-周计算用
|
| 问题 | 修复 commit | 状态 |
|---|---|---|
warmup verification 缺 if 守卫 |
5f8f982c |
✅ 正确 |
| caller Export PATH 无自愈 | 5f8f982c |
✅ 正确 |
Windows tlmgr.bat 假阳 fallback |
9395664a |
✅ 正确 |
✅ 整体评价
方案成熟度高,三个 commit 迭代逻辑清晰。核心设计(bypass cache + weekly key + fallback + Windows 显式 tlmgr-exe)正确,边界情况处理到位。%G vs %Y 是唯一建议在合入前顺手修的点,其余小建议可后续优化。
🤖 Generated with Claude Code
…rify tl1/2 守卫) PR #901 第二轮 bot review 三项: - 重要: ISO 年-周 `%Y-W%V` 跨年错 key. `%V` 是 ISO 周号, `%Y` 是日历年, 错配 导致跨年那周 (2025-12-29 周一: ISO=2026-W01, %Y=2025) 产生 `2025-W01` 而 非 `2026-W01`, 与下一周 `2026-W02` key 不连续. 改 `%G-W%V` (%G 是 ISO 年). 每年 1-3 天的假阳 cache miss 修掉. - 小: warmup Export PATH 改 step 级 continue-on-error: true (替 `|| true`). cache 真损坏时 warmup 日志能看到 failure annotation, debug 友好. - 小: caller Verify TL mirror reachable 触发条件加 tl1/tl2 success 守卫. 避 免 cache miss + try1/2 都 fail 时 verify 报 "tlmgr not found" 噪音掩盖真 正的 install fail.
PR #901 审查结果📋 审查摘要结论: APPROVE — 设计完善,代码质量好,注释详尽。发现 1 个重要问题需修复,1 个小建议。
总体评价PR 设计思路清晰:绕开 🟡 重要 (1)
if: (steps.bypass-cache.outputs.cache-hit != 'true' || steps.export-path.outcome == 'failure') && (steps.tl1.outcome == 'success' || steps.tl2.outcome == 'success')但 caller 的 "Install TeX Live (try 2, illinois mirror, fallback)" step 没有 对比 建议: 给 caller try 2 加 💡 小建议 (1)warmup-tl warmup-tl 的 "Export PATH (cache hit fast path)" step 没有
当前设计决策(warmup cache hit 时不走 self-healing 路径)是正确的,这只是风格建议。 ✅ 亮点
🤖 Generated with Claude Code |
上个 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 增量审查 (第三轮)
两个新 commit 精准修复了上轮 review 指出的全部问题,PR 整体质量成熟,可以合入。 ✅ 上轮问题修复确认
🔍 最终 diff 全量确认对 PR 最终状态做了全量复核,各关键路径均正确:
💡 遗留小建议 (不阻塞)上轮提到的 warmup 整体评价5 个 commit 的迭代链清晰,每个 commit 都精准回应了 review 反馈。方案从设计到实现都很成熟:
预期效果: caller 端 TL setup 从 70-600s → ~2s (cache hit),总 wall-clock 进一步缩短约 50%。 🤖 Generated with Claude Code |
之前 font cache key 用 hashFiles('.github/workflows/_test-package.yml').
该 yml 任何改动都让 key 变 — PR #901 已经改了 6 次 yml, 每次都让 15 个
job (5 caller × 3 OS) 全部重 download ~170MB Noto Sans + Serif. 而字体
URL 一字未动, cache 完全应该命中.
修法: 抽出 .github/font-urls.txt 列两条 URL, cache key 改 hashFiles 它,
Install Noto fonts step 也从这个文件逐行 curl — key 和实际 download 同源,
保证 "URL 改 → cache invalidate + 重 download" 完全同步, 避免 key 和 URL
脱钩造成 cache hit 但内容不对.
实测 28281536073 run 日志确认:
Cache not found for input keys: ctex-kit-fonts-Linux-e9c5622...-v1
curl -LO "https://github.com/notofonts/noto-cjk/.../03_NotoSansCJK-OTC.zip"
即 cache 没命中, 字体每次都重下.
🔍 PR 增量审查
方案成熟,之前审查提出的阻塞/重要问题已全部在后续 commit 修复。6 个 commit 迭代清晰,当前代码逻辑完备。 已确认修复项
|
上一个 commit 把字体 URL 抽到 .github/font-urls.txt 后, Windows runner checkout 时把 LF 转 CRLF, while read 把行尾 \r 一起读进 url, curl 拿到 "https://...zip\r" 报 "Failed to extract a filename" exit 3, 5 caller × 1 OS 全 fail. 修法: read 后 \`printf %s | tr -d '\r'\` 把 CR 干掉. 不动 .gitattributes, 避免给别人 clone 引入额外配置漂移.
🔍 PR 审查
设计扎实、文档详尽的 CI 性能优化。绕开 setup-texlive 内部 immutable cache 限制,用自管
|
…ad 防御) 第四轮 bot review APPROVE 但提了 3 项 hardening, 都修了: - 重要: test.yml paths-filter `_all` 加 `.github/font-urls.txt`. 否则未来 只改字体 URL 的 PR (例升级 Noto Sans 版本) 不触发任何 caller, 字体改 动合入前没 CI 验证. - 小 #1: while IFS= read -r url 加 `|| [ -n "$url" ]` 兜底, 防 trailing newline 缺失时丢最后一行. 当前 git 保证有 newline 不出事, 纯防御. - 小 #2: case 模式 `\#*` 改 `'#'*`. # 在 case 里不需要转义, 风格统一.
🔍 PR 审查
方案设计扎实,绕开
|
…ify 只跑 cache miss caller 端 Export PATH 的 `tlmgr version` sanity check 没有 || true 兜底, 在 Windows runner 上必 fail: TL 的 tlmgr 是 tlmgr.bat, msys2 bash 不按 PATHEXT 补后缀, "$TL_BIN/tlmgr" 直接退 127. PR #901 上 5 个 Windows caller 全卡这. 修法 (PR #901 review #2): Export PATH 加 id: export-path + continue-on-error, 3 个 fallback step (try1 / try2 / Verify mirror) 的 if 同时 OR steps.export-path.outcome == 'failure'. Windows 路径名问题 + cache 损坏两种 场景都能自动接管走 setup-texlive fallback. warmup 的 Verify TL mirror reachable 没限 cache miss, mirror 抖动时会把 warmup 拖 fail, 下游 5 caller 全被 needs: warmup-tl 阻塞 — 这正是 bypass cache 最想避免的. 加 if: cache-hit != 'true' (PR #901 review #1). 顺手清掉 test.yml 多余空行 (PR #901 review #3).
上个 commit 的 continue-on-error 自愈只是不让 step fail, 没解决 Windows "$TL_BIN/tlmgr" 找不到的根本问题: 每个 Windows caller cache hit 后 sanity check 必失败 → outcome=failure → 触发 setup-texlive fallback → 重装 TL. 实测 Windows 小包 (zhnumber/CJKpunct/zhlineskip) ~4min vs ubuntu 同包 ~25s, 差的 3-4min 全在重装 — PR #901 bypass cache 在 Windows 上等于废了. 修法: matrix 显式给出 tlmgr-exe (ubuntu/macos=tlmgr, windows=tlmgr.bat), sanity check 用 "$TL_BIN/${{ matrix.tlmgr-exe }}" version. Windows cache hit 真能 sanity check 过 → 不走 fallback → 秒过. continue-on-error 兜底 cache 损坏的极端情况保留. caller + warmup 两处都改.
…rify tl1/2 守卫) PR #901 第二轮 bot review 三项: - 重要: ISO 年-周 `%Y-W%V` 跨年错 key. `%V` 是 ISO 周号, `%Y` 是日历年, 错配 导致跨年那周 (2025-12-29 周一: ISO=2026-W01, %Y=2025) 产生 `2025-W01` 而 非 `2026-W01`, 与下一周 `2026-W02` key 不连续. 改 `%G-W%V` (%G 是 ISO 年). 每年 1-3 天的假阳 cache miss 修掉. - 小: warmup Export PATH 改 step 级 continue-on-error: true (替 `|| true`). cache 真损坏时 warmup 日志能看到 failure annotation, debug 友好. - 小: caller Verify TL mirror reachable 触发条件加 tl1/tl2 success 守卫. 避 免 cache miss + try1/2 都 fail 时 verify 报 "tlmgr not found" 噪音掩盖真 正的 install fail.
上个 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 漏了, 这次补齐.
之前 font cache key 用 hashFiles('.github/workflows/_test-package.yml').
该 yml 任何改动都让 key 变 — PR #901 已经改了 6 次 yml, 每次都让 15 个
job (5 caller × 3 OS) 全部重 download ~170MB Noto Sans + Serif. 而字体
URL 一字未动, cache 完全应该命中.
修法: 抽出 .github/font-urls.txt 列两条 URL, cache key 改 hashFiles 它,
Install Noto fonts step 也从这个文件逐行 curl — key 和实际 download 同源,
保证 "URL 改 → cache invalidate + 重 download" 完全同步, 避免 key 和 URL
脱钩造成 cache hit 但内容不对.
实测 28281536073 run 日志确认:
Cache not found for input keys: ctex-kit-fonts-Linux-e9c5622...-v1
curl -LO "https://github.com/notofonts/noto-cjk/.../03_NotoSansCJK-OTC.zip"
即 cache 没命中, 字体每次都重下.
PR #901 把 caller TL setup 砍到秒级后, ctex 测试 wall-clock 瓶颈完全转移 到 Phase 1 单 engine 串行. 实测 (PR #901 master run): pdftex: 2m56s xetex: 3m50s uptex: 2m42s luatex: 8m18s luatex 一个 engine 比其余三个加起来还慢, 完全主导 wall-clock. 修法: 新增 LUATEX_BUCKETS env (默认 1, 即旧行为). 设 N>1 时: - 按 testfiledir 字母序均匀切 N 个 bucket - 各 bucket 独立 workdir + l3build --first/--last 跑自己范围 - 与其他 engine 一起后台并行 luatex bucket 间不会冲突 (各 workdir 独立, build/check 不重叠), 字母序 切分跟 l3build 内部 globbing 后 sort 的顺序一致, 任何 testfile 增删 都自动重新分布. 实际启用 (LUATEX_BUCKETS=2) 在下一个 commit 的 test.yml 里.
实测 (PR #901 master run) 4 engine wall-clock: pdftex 2m56s | xetex 3m50s | uptex 2m42s | luatex 8m18s luatex 主导 Phase 1, 其他 engine 跑完空等近 5min. LUATEX_BUCKETS=2: luatex 切 2 桶并行, 单桶 ~4min, Phase 1 总 wall-clock 预期 max(xetex 3m50s, luatex/b1 ~4m, luatex/b2 ~4m) ≈ 4min, ctex caller 整体 ~5min, 拍平到 6min 目标内. 总并发进程数 3 engine + 2 bucket = 5, 4 vCPU 略超载, 但 tex 启动 + I/O 占大头, 实测加速明显. 5 进程同时跑也只是峰值, 各 engine 完成时间差很大 (uptex 最快 ~3min 结束就释放资源).
test.yml caller 端通过 weekly bypass cache 把 setup-texlive 从 ~70s 砍到 ~2s (PR #901). 同一仓库的 release.yml / release-ctan-upload.yml 仍走老路: 直接 跑 setup-texlive-action, 没用 cache, 一次 release 多花 60-70s 的 TL setup, 而且暴露在 mirror 抖动下 (跟 PR #901 修过的老坑同源). 修法: 两个 release workflow 都加 PR #901 同款 3 step 链: 1. Compute TL cache week (ISO `%G-W%V`, 跟 test.yml changes job 同源) 2. TL bypass cache (restore), key = tl-bypass-Linux-2026-<week> 3. Export PATH (cache hit fast path) + continue-on-error 自愈 旧 setup-texlive step 加 `if: cache-hit != 'true' || export-path.outcome == 'failure'` 作 fallback. cache key 跟 test.yml weekly cache 共享, schedule 每 周一 12:00 UTC 跑 test.yml warmup-tl 刷新这份 cache, release 触发时直接命中. - release.yml: 跑 l3build ctan 需全套 tl_packages → 直接复用全套 cache. - release-ctan-upload.yml: 只需 l3build + luatex (minimal), 但 cache restore 全套 ~10s 仍比真装 minimal ~30s 快, 且不依赖 mirror 通畅. ubuntu runner, tlmgr 不是 tlmgr.bat, 不需要 Windows 那套 matrix.tlmgr-exe hack.
PR #892 重构后, zhlineskip.dtx driver 用 `NotoSerifCJKsc-Medium.otf` 这种 带 .otf 后缀的字体名, fontspec 会**优先在 typesetdir 找单文件**, 然后才退 到 system fonts. 所以 build.lua docinit_hook 不得不从 mirrors.ctan.org curl 下载 5 个 OTF 单文件. 这跟 xeCJK / ctex 的做法不一致 — 它们都用 fontconfig 风格 (例 `Noto Serif CJK SC`), 直接走系统 OTC 集合, 不需要 docinit 下载. PR #903 后 release.yml 仍失败, 根因正是这个不一致: CI 上 mirror.latigo.net (mirrors.ctan.org 随机 redirect) 偶尔抖动, fetchdocsupp curl --fail 直接 报错. mirror 抖动是 PR #901 已经踩过的老坑. 改法: 把 setCJK* 三组改成 fontconfig 风格 + 显式 weight 保原视觉: setCJKmainfont: `Noto Serif CJK SC` + Medium/Black/Bold setCJKsansfont: `Noto Sans CJK SC` + Regular/Bold setCJKmonofont: `Noto Sans CJK SC` + Regular/Bold (跟原 sans 同源) 字体 weight 跟原 .otf 文件配置一致 (Main: Medium/Black/Bold, Sans/Mono: Regular/Bold), 手册视觉无差异. build.lua docinit_hook 删字体下载放下个 commit.
test.yml caller 端通过 weekly bypass cache 把 setup-texlive 从 ~70s 砍到 ~2s (PR #901). 同一仓库的 release.yml / release-ctan-upload.yml 仍走老路: 直接 跑 setup-texlive-action, 没用 cache, 一次 release 多花 60-70s 的 TL setup, 而且暴露在 mirror 抖动下 (跟 PR #901 修过的老坑同源). 修法: 两个 release workflow 都加 PR #901 同款 3 step 链: 1. Compute TL cache week (ISO `%G-W%V`, 跟 test.yml changes job 同源) 2. TL bypass cache (restore), key = tl-bypass-Linux-2026-<week> 3. Export PATH (cache hit fast path) + continue-on-error 自愈 旧 setup-texlive step 加 `if: cache-hit != 'true' || export-path.outcome == 'failure'` 作 fallback. cache key 跟 test.yml weekly cache 共享, schedule 每 周一 12:00 UTC 跑 test.yml warmup-tl 刷新这份 cache, release 触发时直接命中. - release.yml: 跑 l3build ctan 需全套 tl_packages → 直接复用全套 cache. - release-ctan-upload.yml: 只需 l3build + luatex (minimal), 但 cache restore 全套 ~10s 仍比真装 minimal ~30s 快, 且不依赖 mirror 通畅. ubuntu runner, tlmgr 不是 tlmgr.bat, 不需要 Windows 那套 matrix.tlmgr-exe hack.
PR #903 加了 inconsolata 到 tl_packages, PR #904 接入 cache restore 后, force- push zhlineskip-v1.0f tag 触发 release.yml 仍 fail: cache hit fast path 用了 周一 schedule 刷的老 cache (那时还没 inconsolata), Inconsolatazi4 找不到. PR #901 weekly cache 设计的 trade-off: schedule 周一刷新, 周内 PR 改 tl_packages 不会立即翻 cache. 但这场景下, 新加包跟新 release 撞同一周就炸. 修法: 4 个 cache key (test.yml warmup-tl, _test-package.yml caller, release.yml, release-ctan-upload.yml) 都拼上 hashFiles('.github/tl_packages'). tl_packages 改一行 → 全部 4 处 key 变 → cache miss 走 fallback 装最新, 然后 新 cache save 含新包, 后续 release 直接命中. 副作用: 加新包的 PR merge 后, master 上下个 schedule 跑 warmup-tl (周一 12:00 UTC) 之前所有 caller 都得 cache miss 走 fallback ~70s. 对 release-only 的包 (inconsolata) 影响极小 (test caller 不用), 但对常规包 (例 cm-unicode 之类 通用) 影响一周内所有 PR. 实测可接受 — tl_packages 改频率极低, 历次也就 PR #883 (LaTeX 2026-06-01) / PR #903 (inconsolata) 两次.
背景
PR #899 之后 CI 主要瓶颈仍在 caller 端 setup-texlive ~70s/job 跑 tlmgr update, mirror 抖动时单 step 可飙到 10+min. 实测 master 上 run 28280000443 的
test-ctex / on macos跑了 17min32s, 其中 setup-texlive try 1 单独 632s (10.5min,mirror.ctan.orgauto-redirect 选到慢 mirror), try 2 又 94s.根因: setup-texlive-action 内部用
actions/cache但设计是 "同 primaryKey immutable" — 一旦保存就不更新. warmup-tl 跑了 update-all 也无法回写到原 primaryKey, caller restore 时永远拿到 update 前的 baseline, 必须自己再跑一次 tlmgr update.改动
绕开 setup-texlive 内部 cache, 自己用
actions/cache@v6缓存整个 TEXDIR 到 weekly key. caller 直接 restore + export PATH, 完全跳过 setup-texlive-action.工作机制
Weekly schedule 对齐 ISO 周边界
schedule 从周五挪到周一 UTC 12:00 (北京时间 20:00). ISO 周从周一 00:00 UTC 开始 — 周一一过
tl-cache-week变化, cache key 翻新, 当天 12:00 UTC schedule 跑刷新 cache. 周内 PR/push 享用 cache hit.平台 bin 目录
echo $TEXDIR/bin/<platform> >> $GITHUB_PATH:bin/x86_64-linuxbin/universal-darwin(Apple Silicon, universal binary)bin/windowssetup-texlive 源码确认 (packages/action/src/runs/main/index.ts) 它只调用
tlmgr.path.add()添加 PATH, 没其它 env 注册.关键参数
cache: false— 关掉它自带的actions/cache, 我们用自己的 bypass-cache.update-all-packages: true仅在 cache miss fallback 路径生效.预期加速
cache hit 路径下:
cache miss 路径下:
实际加速 ~70s/caller × 15 caller = ~1050s 减少 (并发取 max, wall-clock 预计 ~6-7min 总, 比 PR #899 的 ~13min 再砍 ~50%).
Test plan
runner.temp/setup-texlive-action/<version>+bin/<platform>会失效. 用注释绑死这个假设.合入后观察 1-2 周确认 cache hit 率.