维护记录
完成的工作
- 把 CI 从「一堆散落在 YAML 里的 shell」改成「薄 YAML +
bin/*.sh」 ✅- 触发原因:用户认为当前工作流不健康(诊断结论:仓库有两个作者 —— 用户和 CI 里的 bot,根因是本地不跑 CI 会跑的 fixer,典型事故是
0a651bd 一次误改 230 个文件、b68a395 改名 225 篇后要 51aa901 补 25 篇 aliases) - 用户口径(原话):「将 github action 重写,CI 中运行的脚本后续以本地 shell 脚本的方式编写,除非是 CI 环境或者是 github 强制依赖的操作,否则都在本地的 shell 中完成」,随后澄清「脚本还是在 github action 上运行,不是本地运行,只是接触了 yaml 编写工作流的依赖」
- 于是设计成:YAML 只声明触发条件 /
permissions / environment / 第三方 action / 工具链安装 / concurrency,逻辑全部在 bin/*.sh,本地跑同一个脚本就复现 CI 的结果
- 新增
bin/ 脚本层(11 个文件,bash 3.2 兼容 —— macOS 自带 bash 3.2 没有 **、关联数组、mapfile)✅bin/lib.sh:被 source 的底座(定位仓库根、把 vendored 的 .tools/deno/bin 前置进 PATH、log/ok/warn/fail、run_step + CHECK_FAILED + report 的「失败不中断、最后汇总」累加器、shell_syntax_check、sha256_of)bin/preflight.sh(--check 只读 / --fix 就地修 / --re-stage 修完重新入 index,五步:① 文章文件名 ② Markdown 格式 ③ front matter ④ 单测 ⑤ 构建产物新鲜度)、bin/validate-posts.sh、bin/test.sh(含全部 shell 文件的 bash -n 语法检查)、bin/build.sh、bin/build-wasm.sh、bin/artifacts.sh、bin/check-links.sh、bin/publish-autofix.sh(CI 专用,本地跑会被拒绝)bin/hooks/pre-commit(preflight --fix --re-stage)与 bin/hooks/pre-push(preflight --check)+ bin/install-hooks.sh(写成 core.hooksPath=bin/hooks,所以 hooks 是版本化文件,升级 / 回滚不用重装)
- 四个 workflow 全部瘦成壳 ✅
deploy.yml:checkout(去掉多余的 submodules: true,本仓库没有 .gitmodules)→ setup-deno(v2.x → 钉死 '2.9.5')→ deno install -g -A npm:prettier@3.9.6 → actions-hugo → ./bin/preflight.sh --fix → ./bin/build.sh → 上传 Pages → ./bin/publish-autofix.shtest-scripts.yml / validate-posts.yml / check-dead-links.yml 同样只剩一行调用(./bin/test.sh / ./bin/validate-posts.sh / ./bin/check-links.sh),paths 里补上 bin/**
- 新增「构建产物新鲜度」检查(本仓库最容易忘的两件事)✅
bin/artifacts.sh:图片侧检查 assets/src/bg/*.png → static/bg/*.avif(源图缺产物 = 硬失败并提示跑 deno task optimize-images;只比 mtime 才 warn,避免新克隆时误报),wasm 侧读指纹清单比对- 新产物
themes/sdttttt-paper/assets/wasm/particles.sha256:sha256sum 格式(故意不用 JSON,bash 3.2 里解析 JSON 不值当)记「产物 1 行 + 源文件 4 行」。CI 不装 Rust,所以靠这份指纹就能发现「改了 wasm/** 却忘了重建」;本地有 cargo 时 --rebuild 会重编译 + cmp 做最强校验 - 实测指纹
cac4cbe7… particles.wasm,与独立验证的 sha256 完全一致(32062 字节);--rebuild 重编译 0.27s、逐字节一致 ⇒ 产物可复现
scripts/rename-posts.ts 新增 --check 只读门禁 ✅- 语义:一个字节都不许改,但「有待改文件」本身就是失败(退出码 1),用来在本地 / CI 断言「当前内容 == CI 会产出的内容」;
deno.json 新增 rename-posts-check task - skipped 明细在
--check 下压成一行(否则每次刷 225 行「已是新格式」) scripts/__tests__/rename-posts-execute.test.ts 新增 2 个 CLI 端到端测试(不合规范 → 退出码 1 且文件不动;已规范化 → 退出码 0)
- 文档同步:
AGENTS.md(新增 bin/ 目录说明与「CI 与本地检查」一节、提交规范指向 ./bin/preflight.sh、wasm 重建要连指纹一起提交)、README.md(常用命令加 ./bin/preflight.sh / ./bin/install-hooks.sh,补一句「workflow 只是壳,CI 跑的是 bin/*.sh」)✅ - 验证:
./bin/preflight.sh(只读)与 ./bin/preflight.sh --fix 都在 1.9s 内五步全绿,退出码 0;deno task test 31 passed (142 steps);hugo --minify 通过;./bin/publish-autofix.sh 在本地如设计般拒绝执行(退出码 2)
遇到的问题
- bash 3.2 +
set -u 下 $VAR 紧跟全角字符会被当成变量名的一部分 ⚠️ - ok "产物已更新:$WASM_ARTIFACT(…" 直接报 WASM_ARTIFACT(: unbound variable(bin/build-wasm.sh: line 23),因为 ( 的多字节序列被当成了名字里的字符。全仓库 grep 出 9 处(preflight.sh:35/55/58、check-links.sh:24、artifacts.sh:65/90/94/135、build-wasm.sh:23),统一改成 ${VAR}(。附注:位置参数 $1 后面跟全角是安全的(bash 只取一位数字),字母变量名才有这个坑 --check 第一版会真的改名 ⚠️ - 加了 --check 解析却忘了让它隐含 dry-run,于是 /tmp/checkprobe 里实测:打印「--check:需要重命名 1 篇文章」之后紧接着「✓ 重命名 1 篇文章成功」并 exit 0 —— 一个号称只读的门禁把文件改了。修法是 dryRun = --dry-run || --dryRun || --check,并补了「文件名必须一个都不动」的端到端测试守住它- 本机没有全局
deno ⚠️ - deno: command not found,但仓库里有 vendored 的 .tools/deno/bin/deno(2.9.5)。bin/lib.sh 因此负责把 .tools/deno/bin 前置进 PATH —— 顺带也解决了 prettier:.tools/deno/bin/prettier 是 deno 的 shim,自己要求 PATH 上有 deno bash -n 只吃第一个文件 ⚠️ - bash -n bin/*.sh 静默只检查了第一个,语法错误会漏。shell_syntax_check() 改为逐个文件循环
下次建议
./bin/install-hooks.sh 还没有执行:它会写用户的 git config core.hooksPath,属于动本地配置,等确认后再装- 尚未实跑的路径:
bin/publish-autofix.sh(本地会拒绝,只能在 CI 里验)、bin/check-links.sh(要打外网)、bin/build.sh 单独跑、preflight --re-stage;lib.sh 的 sha256_stream 目前没人调用(疑似死代码,下次清理时确认) - 「工作流不健康」还剩四条没有落地也没有被否决的修法:① 文件名与正文 hash 解耦(现在改正文就会改 URL,只能靠
aliases 兜)② front matter 只留一份真相(文件与 Hugo 缓存 / 索引之间的重复)③ themes/sdttttt-paper/assets/js/*.js(1557 行)零测试 ④ 本地 git 身份是 github-actions[bot],用户自己的提交也挂在 bot 名下 - 本次改动已随同一次提交推送:
ci: 把 workflow 逻辑搬进 bin/ 脚本层
第二轮跟进(同日,用 gh 看过 CI 之后)
- 先看线上跑得怎么样 ✅ -
440b746 上四个 workflow 全 success(Hugo on GitHub Pages / Check Dead Links / Validate Posts / Test Scripts);build 日志里 bin/preflight.sh 五步全绿(ok preflight 通过(fix))、hugo 建 270 页、Commit auto-fix results: ok 没有需要提交的改动 —— bin/publish-autofix.sh 正确 no-op,「两个作者」的问题第一次没有发作。唯一的告警是 Node 20 弃用(跑在 Node 24 上)和 ubuntu-latest 的迁移预告 - 升 action 大版本(就是那条 Node 20 告警)✅ - 四个 workflow 里的
actions/checkout@v4→v7,加上 deploy 专用的 actions/configure-pages@v5→v6、actions/upload-pages-artifact@v3→v5、actions/deploy-pages@v4→v5。逐个核过 breaking change:checkout v6 只是把 persist-credentials 改存 $RUNNER_TEMP,v7 新增的 allow-unsafe-pr-checkout 只影响 pull_request_target / workflow_run 的 fork checkout;v4 的 upload-pages-artifact 不再打包 dotfiles —— 实测 find public -name '.*' 0 个,对我们无影响。setup-deno 与 peaceiris/actions-hugo 已是最新 - 死链检查拆出「被服务器拦住」 ✅ -
scripts/check-dead-links.ts 新增 BLOCKED_STATUSES = new Set([401, 403, 429, 451])、isBlockedStatus()、partitionByStatus(),先报死链(stderr、退出码 1)再报「N 个链接被服务器拦住、没能验证(不算死链)」;bin/check-links.sh 在「没有死链、但有被拦住」时也写一条 job summary。本地实跑:20 条告警 → 3 条真死链 + 17 条被拦住(13 条百度百科 ?fromModule=lemma_inlink 反爬、StackOverflow、2 条 github.com/user-attachments、1 条 451) - 反爬域名不参与判定 ✅ - 同一批百度百科链接在同一份代码上时而 403、时而 404(并发 6 个请求打到同一家站时偶发 404),于是「真死链」数量每次跑都不一样。新增
ANTI_CRAWL_HOSTS = {baike.baidu.com} + isAntiCrawlHost():这些域名下拿到的任何状态都归「没能验证」 - 收掉最后一条真死链(归档允许清单) ✅ -
scripts/check-dead-links.ts 新增 ARCHIVED_URLS + isArchived():已知失效、且正文里已注明「仅作存档」的链接连请求都不发,单列一栏「已归档」,不计入死链、不改退出码。清单自身也有测试守着 —— 每条都必须在正文里真的出现、且带一句说明,否则就成了「悄悄放行一条死链」 - 日志噪声与死代码 ✅ -
rename-posts 默认只打一行「跳过 N 篇(…加 –verbose 看明细)」(以前刷 229 行);删掉 bin/lib.sh 里从未被调用的 sha256_stream - 修两个站内 404(都是写在 changelog 正文里的历史 URL)✅ -
content/changelog/_index.md 新建,用 aliases: ["/maintenance/"] 给旧地址留活路;content/posts/20260817-文章的变化-277.md 补全 5 条历史 alias(05hog4 → 03wwo2 → -jtc → -1e69 → -dqo → -277,用 YAML 块式写) - 顺手修掉
addAlias 的一个 latent bug ✅ - scripts/rename-posts.ts 里识别「aliases: + 缩进数组」的正则认不出 Prettier 把方括号也折成多行的写法([ 与 ] 各占一行),会落到块式分支、插出 aliases: + - "…" + [ … ] 这种非法 YAML —— 仓库里已经有 2 个文件是这个形态(content/posts/20230912-关于查找自己想要的软件的问题-pyx.md、content/posts/20250504-C中的指针运算-类型转换以及函数指针-dsq.md),下一次改名就会让 Hugo 构建失败。修法:正则的 (.*) 换成 ([\s\S]*?)、[ \t]+\[ 放宽成 [ \t]*\[;scripts/lib/frontmatter.ts 同步支持折行数组(并在 parseScalar 丢掉尾逗号产生的空项);两处各补测试 - 验证:
./bin/preflight.sh 五步全绿(33 passed / 150 steps,其中 --fix --re-stage 这条路 —— 正是 pre-commit hook 的命令 —— 也跑通了);hugo --minify → 270 页 / 250 aliases;public/maintenance/index.html 已生成且跳 /changelog/;./bin/check-links.sh → 2 条死链(就是本次刚修的两个站内 404,部署后即消失)+ 17 条没能验证 + 1 条已归档 - 部署后复验 ✅ -
de9b9ea 推上去后四个 workflow 全 success(Hugo on GitHub Pages 1m7s、Check Dead Links / Validate Posts / Test Scripts 均 ~13s);actions/checkout@v7 生效后 日志里再也找不到 Node 20 弃用字样;两个站内 URL 现在都返回 200(https://sdttttt.online/maintenance/、https://sdttttt.online/posts/20260817-文章的变化-1e69/);手动 gh workflow run check-dead-links.yml 重跑一次 → 0 条死链 + 17 条没能验证 + 1 条已归档,不再挂 ::warning::
遇到的问题(第二轮)
partitionByStatus 第一版把 200 的链接也算进死链 ⚠️ - 只按 isBlockedStatus(status) 分组、忘了原来那句 !statuses.get(url)!.ok,于是「能打开」的链接也进了 dead 组。新加的 3 个单测当场抓住(ok 的链接两组都不进 失败,actual 里混进了那个 ok.example 的链接),补一句 if (entry.ok) continue; 后全绿- 给 section 加
_index.md 会把标题清空 ⚠️ - 写完 content/changelog/_index.md(只为了 aliases)之后构建出的标题变成 <title>- 海边。把文件移开重建才确认:原本的 “Changelogs” 是 Hugo 按目录名生成的,一旦有了 _index.md 就改从文件取。所以必须在文件里显式写 title: "Changelogs" —— 这种「加了文件反而丢东西」的改动,只能靠改动前后各构建一次对比来发现 - Prettier 会把长 alias 数组折成多行 ⚠️ - 5 条 alias 写成 inline 时 Prettier 会折成
[ / 各项 / ] 各占一行,而那正是上面那个 latent bug 的触发形态。改用 YAML 块式(aliases: + - "…")后 Prettier 保留原样,addAlias 与 parseFrontMatter 也都认 - 同一批链接每次跑出来的死链数量不一样 ⚠️ - 第一次跑 3 死链 + 17 没能验证,第二次变成 2 + 16:变的是那条
baike.baidu.com/item/UDP/571511(并发 6 个请求打到同一家反爬站时它偶尔回 404;curl 与 deno 单独打都稳定 403)。这类看人下菜的回的码不能当成链接坏了 —— 所以有了 ANTI_CRAWL_HOSTS - 在 changelog 里写例子 URL 会被自己的死链检查抓 ⚠️ - 记录「新测试抓住了混进来的
ok.example 链接」之后,check-links 当场把那个 .example 域名报成死链(DNS 失败)。行内代码块拦不住(正则只是停在反引号前),要么反引号也不写 scheme,要么放进围栏代码块
下次建议(第二轮)
- 两条站内 404 已在部署后复验为 200(Hugo 的 alias 页是 200 + meta refresh,死链脚本看的就是 200)
- 剩下的都是「工作流不健康」那条线上的老账(见第一轮的下次建议):① 文件名与正文 hash 解耦(现在改正文就改 URL,只能靠
aliases 兜)② front matter 只留一份真相 ③ themes/sdttttt-paper/assets/js/*.js(1557 行)零测试 ④ 本地 git 身份是 github-actions[bot]。本次已与用户确认:四条都先不做,留档待议 check-dead-links.yml 的 push 触发保留(paths 收窄为 content/**、scripts/check-dead-links.ts、bin/check-links.sh、自身):内容改了顺手查一次外链,这次的两条站内 404 就是它抓出来的。只有每周 cron + 手动触发则为备选./bin/install-hooks.sh 按用户要求不装(不动本地 git config core.hooksPath);hooks 文件本身已在仓库里,需要时手动跑一次即可