优化: - initArtalk 加节点级幂等守卫(mount.__atkInited,失败回滚), 消除 artalk.html/mypjax 多入口导致的 4× comments + 4× pv 重复请求 - fetch 包装层对无 body 的 GET/HEAD 去掉多余 content-type, 消掉 Artalk 对 GET /comments 的 CORS 预检(按完整 URL 缓存, 每篇文章都重付一次 RTT)。实测评论列表 +1150~1565ms → +626ms 修复: - 评论列表骨架屏改注入 .atk-list-body(原骨架在 #Comments 内, 被 Artalk.init 清空),list-loaded/故障/12s 兜底移除 - 友链申请弹窗抽成 modules/friendlink.js 进 page-only bundle (原内联脚本在 #pjax-container 外,pjax 后 flApplyOpenModal 未定义) - toast 统一走本地化 Message.js/Qmsg 适配层(保留 window.Toast/showToast 旧 API) 文档: - README / 架构总览 更新;新增 评论加载优化方案.md
303 lines
15 KiB
YAML
303 lines
15 KiB
YAML
# =====================================================================
|
||
# CNB 流水线 | 由 .github/workflows/deploy.yml 迁移(2026-10-04)
|
||
# =====================================================================
|
||
# 为什么迁过来反而更简单:
|
||
# 1. 构建节点在国内(腾讯云)→ 又拍云「境内直传」恢复。
|
||
# 原版因 runner 在境外(Gitea 在境外 VPS),直传又拍云恒失败被 if:false
|
||
# 停用,改用「COS 中转 + 广州机 sync.sh 每分钟轮询」兜底。CNB 让这条
|
||
# 整链失去存在理由(见 deploy.yml 第 290 行那段注释的「复活条件」)。
|
||
# 2. 不需要 artifact 传递:同一 pipeline 的 stages 顺序执行、共享工作目录,
|
||
# public/ 天然传给后续 stage —— 连带 upload/download-artifact 在 Gitea 上
|
||
# 那两个坑(v4 不支持 / v3 finalize 500)一起消失。
|
||
# 3. 不需要 github.server_url 那 8 处兼容分支。
|
||
# 4. 未迁「图片优化 + 反向 commit」:原版每轮都白跑(15 张老图处理不掉),
|
||
# 还会把优化结果反写回仓库。如需保留,照搬 scripts/optimize_images.js。
|
||
# 5. 通知只保留邮件:原版三套冗余(TG / 飞书 / 邮件),现收敛成邮件一套。
|
||
# 成功 → stages 最后一个任务;失败 → failStages。
|
||
# 发信由 scripts/send_mail.js 零依赖手写(net/tls 直连 SMTP,465 隐式 TLS),
|
||
# 对应原版的 dawidd6/action-send-mail,两者都走 QQ 邮箱。
|
||
# 注意原版**失败通知没有邮件**(只有 TG/飞书),失败邮件是这次补上的。
|
||
# 6. 又拍云保持「非阻断但如实上报」:原版是 continue-on-error: true,
|
||
# 国内线路直传失败不得拖垮境外线路。这里用 allowFailure + .ci_status
|
||
# 落盘记录,邮件里逐条如实显示,不做假绿。
|
||
#
|
||
# ⚠️ 落地前必做
|
||
# 1. 建一个 CNB「密钥仓库」(新建仓库时类型选「密钥仓库」;它禁止 clone、
|
||
# 只能在 Web 界面编辑,所以必须手动建)。命名 zqlit/blog-secrets,
|
||
# 在 Web 界面新建 secrets.yml,内容照抄本仓库根目录的 cnb-secrets.yml
|
||
# (该文件已 gitignore,只作粘贴草稿,绝不入库)。需注入 10 项:
|
||
# UPYUN_SERVICE / UPYUN_OPERATOR / UPYUN_PASSWORD
|
||
# DOGECLOUD_ACCESS_KEY / DOGECLOUD_SECRET_KEY / CDN_URL_LIST
|
||
# EDGEONE_API_TOKEN / RSS_API_TOKEN
|
||
# MAIL_USERNAME / MAIL_PASSWORD ← QQ 邮箱 + SMTP 授权码
|
||
# ⚠️ 又拍云服务名在旧 Gitea 里存的 secret 名是 UPYUN_BUCKET,
|
||
# 迁到 CNB 必须改叫 UPYUN_SERVICE(imports 注入的就是变量名本身)。
|
||
# 2. 密钥仓库若与本仓库不在同一组织,定时任务可能被跨仓管控拦下,
|
||
# 需在「组织设置 → 仓库管控」放行。首跑留意 cron 那条有没有报 imports 403。
|
||
# 3. 首次跑重点观察两个未知数:
|
||
# a) 又拍云 v0 接口从 CNB 出口的真实表现(本方案的立论基础)
|
||
# b) EdgeOne 从国内上传 ~372MB 的耗时(原为境外同区上传)
|
||
# 建议首跑把这两步暂时注释掉,只验「Hugo 构建」跑通,再逐步放开。
|
||
# 4. 邮件默认 smtp.qq.com:465;要换服务商就加 env(SMTP_HOST / SMTP_PORT /
|
||
# SMTP_FROM_NAME),脚本自带 QQ 的默认值。
|
||
# =====================================================================
|
||
|
||
# 公用 stage 片段:push 与定时任务共用(顶层以 . 开头的 key 不会被当成分支名)
|
||
.common-stages: &common-stages
|
||
- name: Hugo 构建
|
||
timeout: 15m
|
||
script: |
|
||
set -eu
|
||
# 用一份落盘的状态文件在各 stage 之间传话(原版靠 needs.*.outputs),
|
||
# 邮件通知最后统一读它。同一 pipeline 的 stages 共享工作目录。
|
||
CI_STATUS="$PWD/.ci_status"
|
||
: > "$CI_STATUS"
|
||
|
||
hugo version
|
||
|
||
# ── /conf 快照(评论首屏优化)────────────────────────────────────
|
||
# 构建期拉一次 Artalk 的 /conf 落到 data/,Hugo 会内联进每个页面。
|
||
# 前端据此对匿名 /conf 请求本地合成响应 —— Artalk 内部 conf 与评论列表
|
||
# 是严格串行的,省掉这一跳等于首屏少一整个跨境往返。
|
||
# 服务端这份 conf 只由 settings 表决定(唯一因人而异的 imgUpload 由前端
|
||
# 按登录态放行真实请求),所以匿名访客的快照人人相同。
|
||
# 拉取失败一律降级:删文件 → 模板不内联 → 前端自动走原生远程请求。
|
||
# 绝不让它拖垮构建(在 if 条件里失败不会触发 set -e)。
|
||
mkdir -p data
|
||
if curl -fsS --max-time 30 --retry 1 --retry-delay 2 \
|
||
"https://api.200181.xyz/api/v2/conf" -o data/artalk_conf.json \
|
||
&& python3 -c "import json;json.load(open('data/artalk_conf.json',encoding='utf-8'))"; then
|
||
echo "✅ conf 快照就绪 $(wc -c < data/artalk_conf.json | tr -d ' ') 字节"
|
||
else
|
||
rm -f data/artalk_conf.json
|
||
echo "⚠️ conf 快照拉取失败,已降级为运行时远程拉取"
|
||
fi
|
||
|
||
python3 scripts/add_draft_to_hidden.py
|
||
rm -rf public resources/_gen
|
||
hugo --minify --gc
|
||
BUILD_HASH=$(find public -type f -print0 | sort -z | xargs -0 sha256sum | sha256sum | awk '{print $1}')
|
||
echo "$BUILD_HASH" > public/.build_hash
|
||
echo "$BUILD_HASH" > public/build_hash.txt
|
||
echo "$BUILD_HASH" > .build_hash.out
|
||
echo "build=ok" >> "$CI_STATUS"
|
||
echo "✅ 构建完成 hash=$(printf '%.12s' "$BUILD_HASH") 文件数=$(find public -type f | wc -l | tr -d ' ')"
|
||
|
||
- name: 同步到又拍云(境内源站)
|
||
timeout: 30m
|
||
# 对应原版的 continue-on-error: true —— 国内线路直传失败不该拖垮境外线路,
|
||
# 但失败必须被如实记录(写进 .ci_status,邮件里会显示 ❌)。
|
||
allowFailure: true
|
||
script: |
|
||
set -eu
|
||
CI_STATUS="$PWD/.ci_status"
|
||
ST=ok
|
||
record() {
|
||
rc=$?
|
||
if [ "$rc" -ne 0 ]; then ST=fail; fi
|
||
echo "upyun_sync=$ST" >> "$CI_STATUS"
|
||
exit "$rc"
|
||
}
|
||
trap record EXIT
|
||
|
||
# 原版这里是 if:false(境外直传必挂)。CNB 节点在国内,直接恢复。
|
||
# 首次仍保守用 -w 5 且不加 --strong,稳定后再考虑调高并发。
|
||
upx login "$UPYUN_SERVICE" "$UPYUN_OPERATOR" "$UPYUN_PASSWORD"
|
||
upx sync public/ / -w 5 --delete
|
||
upx put public/.build_hash /__build_hash || true
|
||
echo "✅ 又拍云同步完成"
|
||
|
||
- name: 刷新又拍云 CDN
|
||
allowFailure: true
|
||
script: |
|
||
set -eu
|
||
CI_STATUS="$PWD/.ci_status"
|
||
ST=ok
|
||
record() {
|
||
rc=$?
|
||
if [ "$rc" -ne 0 ]; then ST=fail; fi
|
||
echo "upyun_purge=$ST" >> "$CI_STATUS"
|
||
exit "$rc"
|
||
}
|
||
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
|
||
upx purge --list /tmp/purge.txt
|
||
|
||
- name: 刷新多吉云 CDN
|
||
allowFailure: true
|
||
script: |
|
||
set -eu
|
||
CI_STATUS="$PWD/.ci_status"
|
||
ST=ok
|
||
record() {
|
||
rc=$?
|
||
if [ "$rc" -ne 0 ]; then ST=fail; fi
|
||
echo "dogecloud=$ST" >> "$CI_STATUS"
|
||
exit "$rc"
|
||
}
|
||
trap record EXIT
|
||
|
||
# 复用仓库里的 scripts/refresh_cdn.js(零外部依赖,仅用 node 内置模块)
|
||
node scripts/refresh_cdn.js
|
||
|
||
- name: 部署 EdgeOne(境外线路)
|
||
timeout: 20m
|
||
script: |
|
||
set -eu
|
||
CI_STATUS="$PWD/.ci_status"
|
||
ST=ok
|
||
record() {
|
||
rc=$?
|
||
if [ "$rc" -ne 0 ]; then ST=fail; fi
|
||
echo "edgeone=$ST" >> "$CI_STATUS"
|
||
exit "$rc"
|
||
}
|
||
trap record EXIT
|
||
|
||
test -n "$EDGEONE_API_TOKEN" || { echo "❌ 缺 EDGEONE_API_TOKEN"; exit 1; }
|
||
npx edgeone pages deploy ./public -n hugo-blog -t "$EDGEONE_API_TOKEN" --area overseas
|
||
echo "✅ EdgeOne 部署完成"
|
||
|
||
- name: 上报部署状态
|
||
allowFailure: true
|
||
script: |
|
||
set -eu
|
||
H=$(cat .build_hash.out)
|
||
curl -s --max-time 20 -X POST \
|
||
"https://api.200181.xyz/api/deploy-status?token=${RSS_API_TOKEN}" \
|
||
-H "Content-Type: application/json" \
|
||
-d "{\"hash\":\"$H\",\"phase\":\"built\"}" || true
|
||
echo "ℹ️ 已上报 built: $(printf '%.12s' "$H")"
|
||
|
||
# ── 成功通知(放在 stages 末尾)────────────────────────────────────
|
||
# 任一前置 stage 真失败会直接跳 failStages,这个 stage 就不会跑,
|
||
# 所以「成功邮件」只在真的全绿(含又拍云)时发出。
|
||
- name: 发送邮件通知(成功)
|
||
allowFailure: true
|
||
script: |
|
||
set -eu
|
||
CI_STATUS="$PWD/.ci_status"
|
||
st() { case "${1:-}" in ok) printf '✅ 成功';; fail) printf '❌ 失败';; *) printf '⏭️ 未执行';; esac; }
|
||
stv() { sed -n "s/^$1=//p" "$CI_STATUS" 2>/dev/null | tail -1; }
|
||
H=$(cat .build_hash.out 2>/dev/null || echo unknown)
|
||
if [ "${CNB_IS_CRONEVENT:-false}" = "true" ]; then TRIGGER=定时任务; else TRIGGER=代码推送; fi
|
||
|
||
{
|
||
printf '🎉 部署成功\n'
|
||
printf '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n'
|
||
printf '📦 项目信息\n'
|
||
printf ' 项目: %s\n' "${CNB_REPO_SLUG:-zqlit/blog}"
|
||
printf ' 分支: %s\n' "${CNB_BRANCH:-main}"
|
||
printf ' Commit: %s\n' "${CNB_COMMIT_SHORT:-unknown}"
|
||
printf ' ⏰ 时间: %s\n' "$(date +'%Y-%m-%d %H:%M:%S')"
|
||
printf ' 🚀 触发: %s\n\n' "$TRIGGER"
|
||
printf '📊 构建统计\n'
|
||
printf ' 文件总数: %s\n' "$(find public -type f 2>/dev/null | wc -l | tr -d ' ')"
|
||
printf ' 构建哈希: %s\n\n' "$(printf '%.12s' "$H")"
|
||
printf '📌 各线路状态\n'
|
||
printf ' ☁️ 又拍云源站(境内直传) %s\n' "$(st "$(stv upyun_sync)")"
|
||
printf ' 🔄 又拍云 CDN 刷新 %s\n' "$(st "$(stv upyun_purge)")"
|
||
printf ' 🔄 多吉云 CDN %s\n' "$(st "$(stv dogecloud)")"
|
||
printf ' 🌐 EdgeOne Pages(境外) %s\n\n' "$(st "$(stv edgeone)")"
|
||
printf '🔗 构建日志\n %s\n\n' "${CNB_BUILD_WEB_URL:-(无)}"
|
||
printf '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'
|
||
} > /tmp/mail_body.txt
|
||
|
||
MAIL_SUBJECT="🎉 部署成功 - ${CNB_REPO_SLUG:-blog} - $(date +'%Y-%m-%d %H:%M')" \
|
||
MAIL_BODY_FILE=/tmp/mail_body.txt \
|
||
node scripts/send_mail.js
|
||
|
||
# 失败通知:stages 里任一**阻断型** stage 失败时执行
|
||
.common-fail-stages: &common-fail-stages
|
||
- name: 发送邮件通知(失败)
|
||
allowFailure: true
|
||
script: |
|
||
set -eu
|
||
CI_STATUS="$PWD/.ci_status"
|
||
st() { case "${1:-}" in ok) printf '✅ 成功';; fail) printf '❌ 失败';; *) printf '⏭️ 未执行';; esac; }
|
||
stv() { sed -n "s/^$1=//p" "$CI_STATUS" 2>/dev/null | tail -1; }
|
||
if [ "${CNB_IS_CRONEVENT:-false}" = "true" ]; then TRIGGER=定时任务; else TRIGGER=代码推送; fi
|
||
|
||
{
|
||
printf '💥 部署失败\n'
|
||
printf '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n'
|
||
printf '📦 项目信息\n'
|
||
printf ' 项目: %s\n' "${CNB_REPO_SLUG:-zqlit/blog}"
|
||
printf ' 分支: %s\n' "${CNB_BRANCH:-main}"
|
||
printf ' Commit: %s\n' "${CNB_COMMIT_SHORT:-unknown}"
|
||
printf ' ⏰ 时间: %s\n' "$(date +'%Y-%m-%d %H:%M:%S')"
|
||
printf ' 🚀 触发: %s\n\n' "$TRIGGER"
|
||
printf '⚠️ 失败阶段\n %s\n\n' "${CNB_BUILD_FAILED_STAGE_NAME:-未知}"
|
||
printf '📋 失败原因\n'
|
||
printf '%s\n\n' "${CNB_BUILD_FAILED_MSG:-(未提供)}"
|
||
printf '📌 已执行到的步骤\n'
|
||
printf ' ☁️ 又拍云源站(境内直传) %s\n' "$(st "$(stv upyun_sync)")"
|
||
printf ' 🔄 又拍云 CDN 刷新 %s\n' "$(st "$(stv upyun_purge)")"
|
||
printf ' 🔄 多吉云 CDN %s\n' "$(st "$(stv dogecloud)")"
|
||
printf ' 🌐 EdgeOne Pages(境外) %s\n\n' "$(st "$(stv edgeone)")"
|
||
printf '🔗 构建日志\n %s\n\n' "${CNB_BUILD_WEB_URL:-(无)}"
|
||
printf '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'
|
||
} > /tmp/mail_body.txt
|
||
|
||
MAIL_SUBJECT="💥 部署失败 - ${CNB_REPO_SLUG:-blog} - $(date +'%Y-%m-%d %H:%M')" \
|
||
MAIL_BODY_FILE=/tmp/mail_body.txt \
|
||
node scripts/send_mail.js
|
||
|
||
# =====================================================================
|
||
# 触发 1:推送到 main
|
||
# =====================================================================
|
||
main:
|
||
push:
|
||
- name: 构建并发布(境内外双线路)
|
||
runner:
|
||
cpus: 4 # 4 核 8G;160 核时/月 → 约 240 次
|
||
# ★ CNB 的 docker build 上下文「只含 Dockerfile + by 列出的文件」,
|
||
# 未列入的一律视为不存在(官方原文)。Dockerfile 要 COPY bin/linux/hugo,
|
||
# 故必须写进 by。漏掉会报:
|
||
# failed to compute cache key ... "bin/linux/hugo": not found
|
||
docker:
|
||
build:
|
||
dockerfile: deploy/Dockerfile
|
||
by: [bin/linux/hugo]
|
||
versionBy: [bin/linux/hugo]
|
||
imports:
|
||
- https://cnb.cool/zqlit/blog-secrets/-/blob/main/secrets.yml
|
||
lock: # 对应原 concurrency.cancel-in-progress
|
||
key: deploy-main
|
||
cancel-in-progress: true
|
||
stages: *common-stages
|
||
failStages: *common-fail-stages
|
||
|
||
# =====================================================================
|
||
# 触发 2:定时构建(原 schedule '0 1 * * *' UTC = 北京 09:00;
|
||
# CNB 的 cron 时区就是 Asia/Shanghai,故直接写 09:00)
|
||
# =====================================================================
|
||
"crontab: 0 9 * * *":
|
||
- name: 定时构建(每天 09:00)
|
||
runner:
|
||
cpus: 4
|
||
# ★ CNB 的 docker build 上下文「只含 Dockerfile + by 列出的文件」,
|
||
# 未列入的一律视为不存在(官方原文)。Dockerfile 要 COPY bin/linux/hugo,
|
||
# 故必须写进 by。漏掉会报:
|
||
# failed to compute cache key ... "bin/linux/hugo": not found
|
||
docker:
|
||
build:
|
||
dockerfile: deploy/Dockerfile
|
||
by: [bin/linux/hugo]
|
||
versionBy: [bin/linux/hugo]
|
||
imports:
|
||
- https://cnb.cool/zqlit/blog-secrets/-/blob/main/secrets.yml
|
||
lock:
|
||
key: deploy-main
|
||
cancel-in-progress: true
|
||
stages: *common-stages
|
||
failStages: *common-fail-stages
|