From 66c27c7b62554d7e7ea7f6df8b4cd8e03b7c0d87 Mon Sep 17 00:00:00 2001 From: zqlit Date: Mon, 5 Oct 2026 12:18:53 +0800 Subject: [PATCH] =?UTF-8?q?fix(ci):=20CDN=20=E5=88=B7=E6=96=B0=E6=B8=85?= =?UTF-8?q?=E5=8D=95=E6=94=B9=E4=B8=BA=E3=80=8C=E5=9B=BA=E5=AE=9A=E5=85=A5?= =?UTF-8?q?=E5=8F=A3=E9=A1=B5=20+=20=E6=9C=AC=E6=AC=A1=E6=94=B9=E5=8A=A8?= =?UTF-8?q?=E8=BF=87=E7=9A=84=E5=86=85=E5=AE=B9=E9=A1=B5=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 又拍云对 HTML 的 max-age 是 8 天,而流水线只刷固定白名单(/ sitemap rss archives posts comment links)。不在清单里的页面改动后,源站已更新、CDN 仍发 旧副本——本次 about.html 就卡住了(源站 47935B 含新板块,CDN 仍是 41454B)。 - 新增 scripts/purge_list.js:读 git diff 找出改动的 content/*.md,按 slug/url/目录名/文件名推输出页,并校验 public/ 下确实存在才加入 - .cnb.yml 刷新步骤改用它,脚本失败时兜底只刷首页 - git diff 必须带 -z / core.quotepath=false,否则非 ASCII 路径会被转义引号包裹, 校验必然失败(本次踩过) --- .cnb.yml | 23 ++++++++----- scripts/purge_list.js | 80 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 8 deletions(-) create mode 100644 scripts/purge_list.js diff --git a/.cnb.yml b/.cnb.yml index 9484f531..ea6925d4 100644 --- a/.cnb.yml +++ b/.cnb.yml @@ -113,14 +113,21 @@ } trap record EXIT - # ★ 必须刷固定入口页:只刷首页会让 sitemap.xml / rss.xml / archives.html / - # posts.html 一直发 CDN 上的旧副本(2026-10-01 实测复现:源站 22812B、 - # CDN 仍是 7/24 的 22592B,手动 purge 后立刻变新)。 - { echo "https://usj.cc/" - for p in sitemap.xml rss.xml archives.html posts.html comment.html links.html; do - echo "https://usj.cc/$p" - done - } > /tmp/purge.txt + # 刷新清单 = 固定入口页 + 本次改动过的内容页,由 scripts/purge_list.js 生成。 + # + # ★ 为什么必须刷固定入口页:只刷首页会让 sitemap.xml / rss.xml / + # archives.html / posts.html 一直发 CDN 上的旧副本(2026-10-01 实测复现: + # 源站 22812B、CDN 仍是 7/24 的 22592B,手动 purge 后立刻变新)。 + # ★ 为什么要动态部分:又拍云对 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;拿不到(浅克隆等)就自动降级为只剩固定入口页。 + 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 - name: 刷新多吉云 CDN diff --git a/scripts/purge_list.js b/scripts/purge_list.js new file mode 100644 index 00000000..5f3a417e --- /dev/null +++ b/scripts/purge_list.js @@ -0,0 +1,80 @@ +#!/usr/bin/env node +/** + * 生成「本次部署需要刷新的 CDN URL 清单」(每行一个 URL,输出到 stdout) + * + * 背景(2026-10-05 实测): + * 又拍云 CDN 对 HTML 是 max-age=691200(8 天),流水线里只刷固定入口页 + * (/ sitemap rss archives posts comment links)。于是**不在白名单里的页面 + * 即使源站已更新,CDN 仍会发最多 8 天的旧副本**:本次 about.html 就是这样 + * 卡在旧副本上(源站 47935B / CDN 41454B)。 + * + * 策略:固定入口页(保底,逻辑不变)+ 本次改动过的 content/*.md 对应的输出页。 + * - 输出页靠「frontmatter 的 url / slug / 目录名 / 文件名」推导,并**校验 + * public/ 下确实存在该文件**才加入,避免刷不存在的 URL(刷了也无害,但脏)。 + * - 拿不到 git 基线(单提交仓库 / 浅克隆)时自动降级:只输出固定入口页。 + * + * 用法:node scripts/purge_list.js [BASE_REF] BASE_REF 默认 HEAD~1 + */ +const { execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +const SITE = 'https://usj.cc'; + +// 固定入口页:首页 / 聚合页 / 站点地图(只刷首页会让后面这些一直发旧副本) +const FIXED = [ + '/', + 'sitemap.xml', + 'rss.xml', + 'archives.html', + 'posts.html', + 'comment.html', + 'links.html', + 'about.html', + 'circles.html', + 'tiaozhuan.html', +]; + +function sh(cmd) { + try { + return execSync(cmd, { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8' }); + } catch { + return ''; + } +} + +// ★ 必须用 -z:git 默认会把非 ASCII 路径写成 "\344\270\255..." 的带引号转义形式, +// 那种字符串在 fs.existsSync 上永远查不到,动态部分会静默失效。 +const base = process.argv[2] || 'HEAD~1'; +let changed = sh(`git -c core.quotepath=false diff --name-only -z ${base} HEAD`).split('\0').filter(Boolean); +if (!changed.length) changed = sh('git -c core.quotepath=false diff --name-only -z HEAD').split('\0').filter(Boolean); + +const urls = new Set(FIXED.map((p) => SITE + '/' + p.replace(/^\//, ''))); + +for (const f of changed) { + if (!/^content\/.*\.md$/.test(f) || !fs.existsSync(f)) continue; + const src = fs.readFileSync(f, 'utf8'); + const fm = (src.match(/^---\r?\n([\s\S]*?)\r?\n---/) || [])[1] || ''; + const grab = (k) => { + const m = fm.match(new RegExp('^' + k + ':[ \\t]*["\']?([^"\'\\s]+)', 'm')); + return m ? m[1] : ''; + }; + + const cands = []; + const rawUrl = grab('url'); + if (rawUrl) cands.push(rawUrl.replace(/^\/+|\/+$/g, '') + '.html'); + const slug = grab('slug'); + if (slug) cands.push(slug + '.html'); + const name = path.basename(f, '.md'); + const parent = path.basename(path.dirname(f)); + if (name === 'index') cands.push(parent + '.html'); // 目录型内容(如 content/posts/xxx/index.md) + else cands.push(name + '.html'); + + for (const c of cands) { + const rel = c.replace(/^\/+/, ''); + if (!rel || rel.includes('..')) continue; + if (fs.existsSync(path.join('public', rel))) urls.add(SITE + '/' + rel); + } +} + +process.stdout.write([...urls].join('\n') + '\n');