diff --git a/.cnb.yml b/.cnb.yml index ea6925d4..0850f4f4 100644 --- a/.cnb.yml +++ b/.cnb.yml @@ -113,7 +113,10 @@ } trap record EXIT - # 刷新清单 = 固定入口页 + 本次改动过的内容页,由 scripts/purge_list.js 生成。 + # 刷新清单 = 固定入口页 + (纯内容改动 ? 改动内容页 : 全站 HTML), + # 由 scripts/purge_list.js 生成。判定理由见脚本头部注释:只要产物里的资源 + # 文件名可能变(非纯内容改动),就必须全量刷,否则旧 HTML 会引用被 + # `upx sync --delete` 删掉的旧指纹资源而断链。 # # ★ 为什么必须刷固定入口页:只刷首页会让 sitemap.xml / rss.xml / # archives.html / posts.html 一直发 CDN 上的旧副本(2026-10-01 实测复现: @@ -121,14 +124,27 @@ # ★ 为什么要动态部分:又拍云对 HTML 的 max-age 是 691200(8 天), # **不在清单里的页面即使源站已更新,CDN 也会发最多 8 天的旧副本** # (2026-10-05 实测:about.html 源站 47935B 已含新板块,CDN 仍发 41454B 旧版)。 - # 脚本靠 `git diff BASE HEAD` 找出改动的 content/*.md,按 slug/url/文件名 - # 推出输出页并校验 public/ 下确实存在,避免刷不存在的 URL。 - # 基线取 HEAD~1;拿不到(浅克隆等)就自动降级为只剩固定入口页。 + # 脚本靠 `git diff BASE HEAD` 找出改动文件:只含 content/ 时按 slug/url/ + # 文件名推出改动内容页并校验 public/ 下确实存在;否则(含主题/脚本等) + # 全量输出 public 下所有 .html。 + # 基线取 HEAD~1,按 push / 定时任务分别兜底(见脚本头部注释)。 node scripts/purge_list.js > /tmp/purge.txt || true # 兜底:脚本要是整个失败,至少保证固定入口页仍被刷 [ -s /tmp/purge.txt ] || printf 'https://usj.cc/\n' > /tmp/purge.txt - echo "刷新清单 $(wc -l < /tmp/purge.txt) 条" - upx purge --list /tmp/purge.txt + echo "刷新清单 $(wc -l < /tmp/purge.txt | tr -d ' ') 条" + # ★ 分批:又拍云 URL 刷新「单次 ≤50 个、每分钟 ≤600 个」。清单在全量刷新时 + # 可达 1300+ 条,一次性提交会被拒(且 allowFailure 下静默失败)——2026-10-05 + # 实测踩到过。按 50 条/批 + 批间 6s(≈500 条/分钟)稳妥推进。 + TOTAL=$(wc -l < /tmp/purge.txt | tr -d ' ') + awk -v n=50 'NR % n == 1 { c++ } { print > sprintf("/tmp/pchunk_%03d", c) }' /tmp/purge.txt + n=0 + for f in /tmp/pchunk_*; do + n=$((n + 1)) + echo " → 批次 $n($(wc -l < "$f" | tr -d ' ') 条)" + upx purge --list "$f" || echo " ⚠️ 批次 $n 刷新失败(不阻断流水线)" + sleep 6 + done + echo "✅ 又拍云 CDN 刷新完成:$TOTAL 条 / $n 批" - name: 刷新多吉云 CDN allowFailure: true diff --git a/scripts/purge_list.js b/scripts/purge_list.js index 808faac9..068e315c 100644 --- a/scripts/purge_list.js +++ b/scripts/purge_list.js @@ -9,11 +9,12 @@ * 卡在旧副本上(源站 47935B / CDN 41454B)。 * * 策略:固定入口页(保底,逻辑不变) - * + 本次改动过的 content/*.md 对应的输出页 - * + **全局性改动(themes/layouts/assets/static/data、hugo/config)时,追加全站所有 HTML**。 + * + **非「纯内容改动」时,追加全站所有 HTML** + * + 纯内容改动(diff 只含 content/**)时,只追加改动内容对应的输出页 * - 输出页靠「frontmatter 的 url / slug / 目录名 / 文件名」推导,并**校验 * public/ 下确实存在该文件**才加入,避免刷不存在的 URL(刷了也无害,但脏)。 - * - 拿不到 git 基线(单提交仓库 / 浅克隆)时自动降级:只输出固定入口页。 + * - 拿不到 git 基线(单提交仓库 / 浅克隆)时:push 事件按「全量」处理(宁多刷 + * 不漏刷,代价只是刷新耗时),定时任务只输出固定入口页。 * * 用法:node scripts/purge_list.js [BASE_REF] BASE_REF 默认 HEAD~1 */ @@ -69,15 +70,18 @@ function walkHtml(dir, out = []) { const urls = new Set(FIXED.map((p) => SITE + '/' + p.replace(/^\//, ''))); -// ★ 全局性改动 → 刷新**全部** HTML(2026-10-05 新增) -// 为什么必须这么做:模板用 resources.Fingerprint 给 JS/CSS 生成带 hash 的文件名, -// 而 upx sync 带 --delete(远端不存在的文件会被删)。主题一改,page-only / core 等 -// 包的文件名就变了 → 老指纹文件被删。文章页的 HTML 若还留在 CDN(TTL 8 天), -// 它引用的就是**已被删掉的旧 JS** → 评论区/交互直接断链。 -// 所以凡是会改变「全站 HTML 引用资源」的改动,都必须让所有 HTML 重新拉取。 -const GLOBAL_DIRS = /^(themes|layouts|assets|static|data|archetypes)\//; -const GLOBAL_FILES = /^(hugo|config)\.(toml|yaml|yml|json)$/; -const isGlobal = changed.some((f) => GLOBAL_DIRS.test(f) || GLOBAL_FILES.test(f)); +// ★ 全量刷新判定:**只有「纯内容改动」才走精细刷新,其它一律全量** +// 为什么必须这么严:模板用 resources.Fingerprint 给 JS/CSS 生成带 hash 的 +// 文件名,而 upx sync 带 --delete(远端不存在的文件会被删)。只要产物的资源 +// 文件名变了,CDN 上还在的旧 HTML(TTL 8 天)就会引用**已被删掉的旧文件** +// → 评论区/交互直接断链。所以凡是产物可能变的改动,都得让 HTML 重新拉取。 +// 为什么不用「dirs 白名单」:2026-10-05 踩过 —— 主题改动与 purge 规则改动分成 +// 两个 commit 时,后一个 commit 的 diff 里没有主题文件 → 被判成非全局 → 文章页 +// 漏刷。改成「非 content 即全量」后,不再依赖改动被切进哪个 commit。 +const isCron = process.env.CNB_IS_CRONEVENT === 'true'; +const isContentOnly = changed.length > 0 && changed.every((f) => /^content\//.test(f)); +const isGlobal = changed.length > 0 ? !isContentOnly : !isCron; + if (isGlobal) { for (const p of walkHtml('public')) { const rel = p.replace(/^public[\\/]/, '').split(path.sep).join('/'); @@ -85,6 +89,12 @@ if (isGlobal) { } } +// 诊断输出(进构建日志,便于事后核对判定是否正确) +process.stderr.write( + `[purge] base=${base} 变更=${changed.length} 条 纯内容=${isContentOnly} ` + + `全量=${isGlobal}${changed.length ? '' : '(无基线)'} → 清单=${urls.size} 条\n` +); + for (const f of changed) { if (!/^content\/.*\.md$/.test(f) || !fs.existsSync(f)) continue; const src = fs.readFileSync(f, 'utf8');