Skip to content

perf(ci): TL bypass cache 让 caller 真秒过 (weekly 才真装)#901

Merged
Liam0205 merged 8 commits into
masterfrom
feat/manual-tl-install
Jun 27, 2026
Merged

perf(ci): TL bypass cache 让 caller 真秒过 (weekly 才真装)#901
Liam0205 merged 8 commits into
masterfrom
feat/manual-tl-install

Conversation

@Liam0205

@Liam0205 Liam0205 commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

背景

PR #899 之后 CI 主要瓶颈仍在 caller 端 setup-texlive ~70s/job 跑 tlmgr update, mirror 抖动时单 step 可飙到 10+min. 实测 master 上 run 28280000443test-ctex / on macos 跑了 17min32s, 其中 setup-texlive try 1 单独 632s (10.5min, mirror.ctan.org auto-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.

工作机制

changes job: output tl-cache-week = "2026-W26" (ISO 年-周)

warmup-tl × 3 OS:
  1. actions/cache@v6 restore key=tl-bypass-<os>-2026-<week>
     - hit: 只 echo bin >> $GITHUB_PATH, 秒过
     - miss: 跑 setup-texlive (3 mirror retry) + verification
  2. job 末尾 actions/cache@v6 自动 save (cache miss 时)

caller × 5 pkg × 3 OS:
  1. actions/cache@v6 restore 同 key
     - hit (~99% 时间): echo bin >> $GITHUB_PATH, 跳所有 setup-texlive, 秒过
     - miss (cache eviction / weekly 边界 race): fallback 到 setup-texlive
                                                 (2 mirror retry + verification)

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:

  • ubuntu-latest: bin/x86_64-linux
  • macos-latest: bin/universal-darwin (Apple Silicon, universal binary)
  • windows-latest: bin/windows

setup-texlive 源码确认 (packages/action/src/runs/main/index.ts) 它只调用 tlmgr.path.add() 添加 PATH, 没其它 env 注册.

关键参数

  • caller / warmup 内 setup-texlive 都加 cache: false — 关掉它自带的 actions/cache, 我们用自己的 bypass-cache.
  • update-all-packages: true 仅在 cache miss fallback 路径生效.

预期加速

cache hit 路径下:

  • caller setup-texlive: 70s+ → ~2s (echo PATH + tlmgr version sanity check)
  • warmup-tl: 类似, ~2s

cache miss 路径下:

实际加速 ~70s/caller × 15 caller = ~1050s 减少 (并发取 max, wall-clock 预计 ~6-7min 总, 比 PR #899 的 ~13min 再砍 ~50%).

Test plan

  • cache miss 路径 (本 PR 首次 push): warmup save 成功, caller fallback 成功
  • cache hit 路径 (第二次 push): caller skip setup-texlive, 秒过
  • schedule 触发 (周一 UTC 12:00): warmup 用新 key 刷 cache
  • cache miss recovery: 手动 evict cache 或 bump TL_VERSION 后 PR 仍能跑通

⚠️ 风险点

  1. cache eviction: GH cache 7 天未访问自动 evict. 我们 weekly schedule + 周内 PR 频繁访问, 不会触发. 万一触发, caller fallback 路径兜底.
  2. 首次合入: 合入后第一周内, weekly schedule 还没跑, cache 由第一次 PR/push 的 warmup-tl 自动 fill. 该 push 上的 caller 也会 fallback (因为 caller 和 warmup 并行但同 cache key — warmup save 在 job 末, caller 启动时 warmup 可能还没 save 完).
  3. 平台 bin 路径变化: 若 setup-texlive 未来改装路径, 我们的 runner.temp/setup-texlive-action/<version> + bin/<platform> 会失效. 用注释绑死这个假设.

合入后观察 1-2 周确认 cache hit 率.

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)
@github-actions

Copy link
Copy Markdown
Contributor

PR Review: perf(ci): TL bypass cache 让 caller 真秒过 (weekly 才真装)

审查摘要

整体评价: 方案思路清晰——绕开 setup-texlive-action 内部 "同 primaryKey immutable" 的 cache 设计缺陷,自建 weekly bypass cache,让 caller 端 70-600s 降到秒级。PR 描述详尽,风险分析充分。发现 1 个阻塞问题和 1 个重要建议。


🔴 阻塞问题 (1)

1. warmup-tl 的 verification step 在 cache hit 时仍联网且会 fail,阻塞所有 caller

文件: test.yml warmup-tl "Verify TL mirror reachable"

PR 中 warmup-tl 的 verification step 没有 if 条件,cache hit 时也会运行:

      # 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

问题链:

  1. cache hit → warmup 本该秒过,但 verification 仍执行 tlmgr update --self --list(联网)
  2. mirror 不可达 → grep 命中 "Unable to download" → exit 1warmup-tl job fail
  3. 所有 caller 都 needs: warmup-tl全部被阻塞
  4. 这恰恰是 bypass cache 最想解决的场景(mirror 抖动时 caller 不受影响),却因 warmup verification 把 failure 传递回去了

建议:

      - name: Verify TL mirror reachable (fallback path only)
        if: steps.bypass-cache.outputs.cache-hit != 'true'
        run: |
          ...

_test-package.yml 中 caller 的 verification step 保持一致(PR 已正确加了 if)。如果确实想在 cache hit 时也做防御性检查,应改用 continue-on-error: true 或只做本地 tlmgr version 检查(不联网)。


🟡 重要建议 (1)

2. caller 端 tlmgr version sanity check 无 fallback 到 setup-texlive

文件: _test-package.yml "Export PATH" step

    - name: Export PATH (cache hit fast path, skip setup-texlive)
      if: steps.bypass-cache.outputs.cache-hit == 'true'
      run: |
        ...
        "$TL_BIN/tlmgr" version        # ← 无 || true,失败则 step fail

vs warmup 中:

        "$TL_BIN/tlmgr" version || true   # ← 有 || true

如果 cache 损坏(虽然极少),caller 端 tlmgr version 失败后 step 报错,但后续 fallback steps 的 if: steps.bypass-cache.outputs.cache-hit != 'true' 不会触发——因为 cache-hit 仍然是 'true',只是内容损坏。job 会直接 fail 且无法自愈。

建议: 考虑给 Export PATH step 加 id: export-pathcontinue-on-error: true,让 fallback 条件同时检查 steps.bypass-cache.outputs.cache-hit != 'true' || steps.export-path.outcome == 'failure',使 cache 损坏时能自动回退到 setup-texlive。


💡 小建议 (1)

3. test.yml 多余空行

文件: test.yml L283

verification step 后面多了一个空行(连续两个空行),风格上与文件其他位置不一致。


✅ 做得好的地方

  • 方案设计: 绕开 setup-texlive 的 cache 缺陷,自建 weekly key,思路正确
  • fallback 完备: caller 端 cache miss 时完整保留原有 3 mirror retry 路径
  • ISO 周对齐: schedule 从周五挪到周一,紧贴 ISO 周边界翻新 cache key
  • cache: false: 正确关闭 setup-texlive 内部 cache,避免两层 cache 冲突
  • PR 文档: 工作机制、平台 bin 路径、风险分析写得非常充分

结论: 需要修复 warmup-tl verification 在 cache hit 时的联网 + fail 问题后可合入。

🤖 Generated with Claude Code

…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).
@github-actions

Copy link
Copy Markdown
Contributor

🔍 PR 增量审查

项目 结果
结论 💬 COMMENT
审查范围 bf99570..5f8f982 (两个 commit 均已审查)
审查截止 5f8f982c3d97502ff31b9d580cdd389db3e13089

第二个 commit 已修复前次 review 指出的 warmup verification cache-hit 联网问题和 caller export-path 自愈问题,方案整体设计清晰,fallback 路径完备。发现 1 个重要建议。

🟠 重要建议 (1)

  • 文件: test.yml L138

  • 问题: ISO 年-周计算使用 %Y-W%V,但 %Y 是日历年而非 ISO 年。在年底/年初边界会产生错误 key:例如 2024-12-30(周一)是 ISO 2025-W01,但 %Y-W%V 输出 2024-W01。这意味着:

    1. 年底最后几天与年初第一天可能生成不同的 cache key(同一 ISO 周内 key 不一致)
    2. 跨年时 cache key 与上一年 W01 碰撞(2024-W01 既可能是 2024 年初也可能是 2024 年底)

    虽然后果只是偶尔 cache miss 走 fallback(不会破坏构建),但一年有 1-3 天会命中。

  • 建议: 改为 %G-W%V%G 是 ISO 周对应的年份(ISO year),与 %V 配套使用:

    week="$(TZ=UTC date +'%G-W%V')"

✅ 做得好的地方

  • 第二个 commit 精准修复了前次 review 的两个问题:warmup verification 加 if: cache-hit != 'true' 限制到 cache miss 路径;caller export-path 加 id + continue-on-error + 下游 fallback OR outcome == 'failure' 自愈
  • 方案设计成熟:绕开 setup-texlive 的 "同 primaryKey immutable" cache 缺陷,自建 weekly bypass cache
  • caller fallback 路径完备,cache 损坏 / Windows tlmgr.bat 路径问题均有自愈机制
  • PR 描述和代码注释详尽,风险分析充分
  • .gitignore 顺手清理 actionlint 二进制

上个 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 两处都改.
@Liam0205

Copy link
Copy Markdown
Contributor Author

这下看起来对了。windows 也能秒过 tl-install 的阶段了。

@github-actions

Copy link
Copy Markdown
Contributor

🔍 PR 增量审查 (第二轮)

上轮审查的 3 个问题(warmup verification 缺 if 守卫 / caller Export PATH 无自愈 / Windows tlmgr.bat)已在 commit 5f8f982c9395664a 中全部修复,修法合理。以下是对最终 diff 的增量审查。


🟡 重要建议 (1)

1. ISO 年-周计算用 %Y 而非 %G,跨年边界会产生错误 key

文件: test.yml changes job "Compute active packages"

当前:

week="$(TZ=UTC date +'%Y-W%V')"

%V 是 ISO 8601 周号,但 %Y 是日历年。在年末/年初边界二者会不一致:

  • 2025-12-29 (周一): ISO 周 = 2026-W01,但 %Y-W%V = 2025-W01 ← 错误
  • 2026-01-05 (周一): %Y-W%V = 2026-W02 ← 正确

同一次 run 内 warmup 和 caller 共享同一个 key,不会出 bug。但跨年那周的 key 与下周不连续(2025-W012026-W02,跳过了 2026-W01),导致不必要的 cache miss。

建议: 用 %G(ISO 8601 年)替代 %Y

week="$(TZ=UTC date +'%G-W%V')"

严重程度: 低。每年只影响 ~1 周,且 fallback 路径兜底。但修复成本极低(一行改动),建议一并改掉。


💡 小建议 (2)

2. warmup Export PATH 的 || true 吞掉了 sanity check 失败信号

文件: test.yml warmup-tl "Export PATH (cache hit fast path)"

"$TL_BIN/${{ matrix.tlmgr-exe }}" version || true

warmup 端用 || true 意味着即使 cache 损坏、tlmgr version 失败,warmup 仍然 success。下游 caller 有 continue-on-error: true + fallback 自愈,所以功能上没问题。

但这会让调试更难:warmup 日志全绿,实际 cache 坏了,caller 全部走 fallback 重装却不知道为什么。如果 warmup 也改用 step 级 continue-on-error: true(去掉 || true),可以在 warmup 日志中看到 warning annotation,更方便定位。

不阻塞合入,作为后续优化。

3. caller fallback Verify step 的触发条件可加 setup-texlive 成功判断

文件: _test-package.yml "Verify TL mirror reachable (fallback path only)"

if: steps.bypass-cache.outputs.cache-hit != 'true' || steps.export-path.outcome == 'failure'

cache miss 且 try 1/2 都 failure 时,setup-texlive 实际没装上,verify step 里 tlmgr 不在 PATH 会报错。不影响正确性(后续测试步骤同样会 fail),但错误消息不太友好。可考虑加 && (steps.tl1.outcome == 'success' || steps.tl2.outcome == 'success') 更精确触发。

不阻塞,可后续 hardening。


✅ 上轮问题修复确认

问题 修复 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.
@github-actions

Copy link
Copy Markdown
Contributor

PR #901 审查结果

📋 审查摘要

结论: APPROVE — 设计完善,代码质量好,注释详尽。发现 1 个重要问题需修复,1 个小建议。

  • 关键问题 (阻塞): 0
  • 重要建议: 1
  • 小建议: 1

总体评价

PR 设计思路清晰:绕开 setup-texlive-action 内部 actions/cache 的 "同 primaryKey immutable" 限制,用自定义 actions/cache@v6 按 weekly key 缓存整个 TEXDIR,让 caller 从 ~70s+ 的 tlmgr update 降到 ~2s 的 PATH export。fallback 路径、Windows tlmgr.bat 处理、ISO 年-周 %G-W%V(避免跨年 %Y-W%V 错 key)、schedule 对齐周一 ISO 周边界等关键决策都合理且注释充分。


🟡 重要 (1)

_test-package.yml caller try 2 缺少 id: tl2,verify step 退化

_test-package.yml verify step 的新 if 条件引用了 steps.tl2.outcome

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 没有 id: tl2(原文件就没有,PR diff 也没加)。这导致 steps.tl2.outcome 始终为空串,verify step 在 "tl1 失败 + tl2 成功" 路径下不会运行——丧失了对 tlmgr silent fallback 的检测能力。

对比 test.yml warmup-tl 的 try 2 已正确声明 id: tl2(line 209),caller 端遗漏了。

建议: 给 caller try 2 加 id: tl2


💡 小建议 (1)

warmup-tl Export PATH step 建议加 id

warmup-tl 的 "Export PATH (cache hit fast path)" step 没有 id,而 caller 的同名 step 有 id: export-path。虽然 warmup 不需要 self-healing(actions/cache@v6 在 cache-hit 时不会 re-save,重装也写不回),但加个 id 有利于:

  • 与 caller 的对称性和可读性
  • GitHub Actions UI 中日志追踪(failure annotation 能引用 step id)

当前设计决策(warmup cache hit 时不走 self-healing 路径)是正确的,这只是风格建议。


✅ 亮点

  1. Windows tlmgr.bat 处理: matrix.tlmgr-exe 的方案比 continue-on-error 盲兜底好——避免 Windows 每次假阳 fallback 退化成重装
  2. %G-W%V 而非 %Y-W%V: 正确处理了跨年周边界,注释也清楚解释了 2025-12-29 → 2026-W01 的错配场景
  3. cache: false 关闭内部 cache: 避免双重缓存冲突,简洁有效
  4. 文档与注释: 每个关键决策都有充分的 inline 注释和 PR body 解释,维护友好

🤖 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 漏了, 这次补齐.
@github-actions

Copy link
Copy Markdown
Contributor

🔍 PR 增量审查 (第三轮)

项目 结果
结论 ✅ APPROVE
审查范围 9395664a..5ed8c29d (2 commits)
审查截止 5ed8c29d563560b4673815c24f38d60ddd00c69b

两个新 commit 精准修复了上轮 review 指出的全部问题,PR 整体质量成熟,可以合入。


✅ 上轮问题修复确认

问题 修复 commit 状态
%Y-W%V 跨年错 key → 改 %G-W%V 404cfd22 ✅ 正确,注释详细解释了 2025-12-29 场景
warmup Export PATH || true 吞信号 → step 级 continue-on-error 404cfd22 ✅ 正确,failure annotation 可见
caller Verify step 缺 tl1/tl2 success guard 404cfd22 ✅ 加了 (steps.tl1.outcome == 'success' || steps.tl2.outcome == 'success')
caller try 2 缺 id: tl2 导致 verify 条件失效 5ed8c29d ✅ 补齐,修复了 404cfd22 引入的回归

🔍 最终 diff 全量确认

对 PR 最终状态做了全量复核,各关键路径均正确:

  • cache hit 快速路径: warmup 和 caller 都通过 $TL_BIN/${{ matrix.tlmgr-exe }} version 做 sanity check,Windows 用 tlmgr.bat 显式后缀避免 msys2 PATHEXT 问题 ✅
  • cache miss fallback: caller 通过 steps.bypass-cache.outputs.cache-hit != 'true' || steps.export-path.outcome == 'failure' 双条件触发,cache 损坏也能自愈 ✅
  • ISO 年-周: %G-W%V%V 配套,跨年边界正确 ✅
  • schedule: 周一 UTC 12:00 紧贴 ISO 周边界翻新 cache key ✅
  • cache: false: warmup + caller 共 4 处 setup-texlive-action 调用均关闭内部 cache ✅
  • tl-cache-week 透传: changes job 计算 → 5 个 caller 全部接收 ✅
  • verify 守卫: caller 端 (tl1.outcome == 'success' || tl2.outcome == 'success') 防 "command not found" 噪音;warmup 端靠 try 3 无 continue-on-error 自然守卫 ✅

💡 遗留小建议 (不阻塞)

上轮提到的 warmup Export PATHid 问题仍存在。当前设计决策是合理的(warmup cache hit 时不需要 self-healing,因为 actions/cache 不会重写同一 key),但加 id 可提升与 caller 的对称性和日志可追踪性。可作为后续 hardening。


整体评价

5 个 commit 的迭代链清晰,每个 commit 都精准回应了 review 反馈。方案从设计到实现都很成熟:

  1. 根因分析正确: setup-texlive 的 "同 primaryKey immutable" 是真正瓶颈
  2. 方案选择合理: 自建 weekly bypass cache 而非 hack 上游
  3. 边界处理完备: Windows tlmgr.bat、ISO 跨年周、cache 损坏自愈、mirror 抖动 fallback
  4. 注释与文档充分: 每个 if 条件和设计决策都有 inline 注释解释 why

预期效果: 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 没命中, 字体每次都重下.
@github-actions

Copy link
Copy Markdown
Contributor

🔍 PR 增量审查

项目 结果
结论 ✅ APPROVE
审查截止 1feec0e5b4ce5f04d6821d1d2e091dcb4cca7cbf

方案成熟,之前审查提出的阻塞/重要问题已全部在后续 commit 修复。6 个 commit 迭代清晰,当前代码逻辑完备。

已确认修复项

原问题 修复 commit 状态
warmup verification 在 cache hit 时仍联网并可能 fail 阻塞 caller commit 1 (初版已含 if: cache-hit != 'true')
caller export-path 无 fallback (cache 损坏时 job 直接 fail) commit 2 id: export-path + continue-on-error + fallback OR 条件
Windows tlmgr.bat 路径问题导致 cache hit 假阳 fallback commit 3 matrix 显式 tlmgr-exe
%Y-W%V 跨年错 key commit 4%G-W%V
caller try 2 缺 id: tl2 导致 verify 条件失效 commit 5 补齐
font cache key 被 yml 无关改动掀翻 commit 6 抽出 font-urls.txt

🟢 小建议 (2)

1. warmup cache hit + cache 损坏时无自愈

文件: test.yml warmup-tl

warmup 的 install steps 只检查 steps.bypass-cache.outputs.cache-hit != 'true',不像 caller 那样额外 OR steps.export-path.outcome == 'failure'。如果 cache hit 但内容损坏,warmup export-path continue-on-error 静默通过,但不会重装 TL — 损坏的 cache 会持续到下周 key 翻新,期间所有 caller 每次都走 fallback 重装。

PR body 已明确记录这个设计取舍(caller fallback 兜底),当前可接受。如果后续想优化,可以给 warmup install steps 也加 || steps.export-path.outcome == 'failure' 条件,让 warmup 在检测到损坏时主动重装并覆盖 cache。

2. font-urls.txt 读取时 \#* 模式

文件: _test-package.yml L209

case "$url" in ''|\#*) continue ;; esac

\#case 里等价于 #,转义非必要(不影响正确性)。极小的风格点,不需要改。

✅ 做得好的地方

  • 方案完整: bypass cache + fallback 双路径设计,cache miss 100% 兼容原流程
  • Windows 处理到位: tlmgr-exe matrix 变量彻底解决 msys2 bash PATHEXT 问题
  • ISO 周年对齐: %G-W%V 正确处理跨年边界
  • font cache 解耦: font-urls.txt 让 cache key 与实际下载 URL 同源,避免 yml 无关改动掀翻 cache
  • 迭代质量高: 6 个 commit 逐步修复问题,每个 commit message 都清晰记录了问题根因和修法
  • PR 文档详尽: 工作机制、平台路径、风险分析、测试计划齐全

上一个 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 引入额外配置漂移.
@github-actions

Copy link
Copy Markdown
Contributor

🔍 PR 审查

项目 结果
结论 ✅ APPROVE
审查截止 7c2bfbc84230b4ef2fd6c2da7144be03debc58db

设计扎实、文档详尽的 CI 性能优化。绕开 setup-texlive 内部 immutable cache 限制,用自管 actions/cache@v6 + weekly key 实现 caller 秒过,fallback 路径完善。7 个 commit 迭代修复了 Windows tlmgr.bat、ISO 年-周 %G-W%Vid: tl2 缺失、font cache key 稳定性等问题,演进过程清晰。

🟠 重要建议 (1)

  • 文件: .github/workflows/test.ymlpaths-filter _all
  • 问题: .github/font-urls.txt 未加入 paths-filter 的 _all 公共改动列表。若 PR 仅改动 font-urls.txt(如升级字体版本),dorny/paths-filter 不会匹配任何包 → 所有 caller job 不触发 → 字体下载变更在合入前得不到 CI 验证。
  • 建议: 在 _all 锚中加 - '.github/font-urls.txt',与 _test-package.yml 并列。字体 URL 变更频率极低,但一旦改了必须跑 CI 验 download 能不能通。

🟢 小问题 (2)

  1. 文件: .github/workflows/_test-package.ymlInstall Noto fonts while-read 循环

    • while IFS= read -r url 对文件末尾无 newline 的情况会丢失最后一行。当前 font-urls.txt 由 git 保证有 trailing newline 所以没问题,但防御性写法可以改为 while IFS= read -r url || [ -n "$url" ]; do。优先级很低。
  2. 文件: .github/workflows/test.ymlwarmup cache hit + export-path 失败场景

    • warmup 在 cache hit 时如果 export-path 失败(cache 损坏),后续 install 步骤全部 if: cache-hit != 'true' 不会触发,warmup 无法自愈修复 cache(actions/cache hit 时不 re-save)。该场景下所有 caller 会各自 export-path 失败 → 各自 fallback 重装,功能正确但性能退化一整周。PR 描述的风险点已覆盖此场景,记录在此仅作备忘。如需增强可在 warmup 加一个 if: cache-hit == 'true' && export-path.outcome == 'failure' 的显式 re-install + actions/cache/save 步骤,但考虑到 cache 损坏概率极低,当前设计完全可接受。

审查覆盖: 全部 4 个变更文件(font-urls.txt_test-package.ymltest.yml.gitignore)× 7 个 commit,重点验证了 cache key 一致性、fallback 条件逻辑、Windows 平台兼容、ISO 周计算正确性。

…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 里不需要转义, 风格统一.
@github-actions

Copy link
Copy Markdown
Contributor

🔍 PR 审查

项目 结果
结论 ✅ APPROVE
审查截止 4ff25041f3a19b637985fa02692c0097285dc3cf

方案设计扎实,绕开 setup-texlive-action 的 “同 primaryKey immutable” cache 缺陷,自建 weekly bypass cache,caller 端 70-600s → 秒过。8 个 commit 迭代修复了 Windows tlmgr.bat 路径、%G-W%V ISO 跨年 key、font cache key 解耦、tl2 id 缺失等问题,当前代码质量良好。

✅ 审查要点确认

cache 机制

  • warmup-tl 用 actions/cache@v6 save 整个 TEXDIR 到 tl-bypass-<OS>-<TL_VERSION>-<year-week> key,caller restore 后只需 echo bin >> GITHUB_PATH,设计正确
  • cache: false 关闭 setup-texlive 内部 cache,避免两层冲突 ✅
  • tl-cache-weekchanges job 统一计算 (TZ=UTC date +%G-W%V),透传所有 caller,保证跨 OS key 一致 ✅

fallback 自愈

  • caller Export PATH: id: export-path + continue-on-error: true,fallback 条件 OR steps.export-path.outcome == failure,cache 损坏时自动接管 ✅
  • Windows: matrix 显式 tlmgr-exe: tlmgr.bat,sanity check 用显式路径而非裸 tlmgr,不会假阳触发 fallback ✅
  • warmup Verify step 加 if: cache-hit != true,cache hit 时不联网不 fail,不阻塞下游 ✅
  • caller Verify step 加 tl1/tl2 outcome success 守卫,避免 "command not found" 噪音 ✅
  • caller try 2 补齐 id: tl2,修复 verify 条件空引用 ✅

ISO 周处理

  • %G-W%V(ISO 年 + ISO 周号)替代 %Y-W%V,修复跨年 1-3 天假阳 cache miss ✅

font cache 优化

  • 抽出 .github/font-urls.txt,cache key 用其 hash,URL 不变则 cache 不翻 ✅
  • while IFS= read -r url + tr -d CR 处理 Windows CRLF ✅
  • paths-filter _all 加入 .github/font-urls.txt,未来只改字体 URL 的 PR 也能触发 CI ✅

schedule

  • 从周五改周一 UTC 12:00,紧贴 ISO 周边界翻新 cache ✅

🟢 小建议 (1)

  • 文件: test.yml warmup Export PATH
  • 观察: warmup 的 Export PATH step 没有 id(caller 有 id: export-path)。当前不影响正确性(warmup 不需要 fallback 逻辑),但加个 id 有助于日后 debug 时在日志中引用 step outcome。纯风格建议,不阻塞。

@Liam0205 Liam0205 merged commit 7b1d1c4 into master Jun 27, 2026
21 checks passed
Liam0205 added a commit that referenced this pull request Jun 27, 2026
…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).
Liam0205 added a commit that referenced this pull request Jun 27, 2026
上个 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 两处都改.
Liam0205 added a commit that referenced this pull request Jun 27, 2026
…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.
Liam0205 added a commit that referenced this pull request Jun 27, 2026
上个 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 漏了, 这次补齐.
Liam0205 added a commit that referenced this pull request Jun 27, 2026
之前 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 没命中, 字体每次都重下.
@Liam0205 Liam0205 deleted the feat/manual-tl-install branch June 27, 2026 07:38
Liam0205 added a commit that referenced this pull request Jun 27, 2026
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 里.
Liam0205 added a commit that referenced this pull request Jun 27, 2026
实测 (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 结束就释放资源).
Liam0205 added a commit that referenced this pull request Jun 27, 2026
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.
Liam0205 added a commit that referenced this pull request Jun 27, 2026
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.
Liam0205 added a commit that referenced this pull request Jun 27, 2026
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.
Liam0205 added a commit that referenced this pull request Jun 27, 2026
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) 两次.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant