Files
blog/.cnb.yml
T
zqlit bcc2236bde ci: 迁移到 CNB 流水线(.cnb.yml)+ 零依赖邮件通知
- .cnb.yml:6 个 stage 顺序执行(Hugo 构建 → 又拍云 sync → 又拍云 purge →
  多吉云刷新 → EdgeOne 部署 → 上报状态);push 与每日 09:00 定时任务
  共用同一组 stage(YAML 锚点),失败通知走 failStages
- 又拍云保持「非阻断但如实上报」:allowFailure 沿用原版 continue-on-error
  语义,国内线路直传失败不拖垮境外线路
- scripts/send_mail.js:零依赖 SMTP 发信(node net/tls,465 隐式 TLS +
  587 STARTTLS),变量名沿用 MAIL_USERNAME / MAIL_PASSWORD
- 密钥经 imports 从密钥仓库 zqlit/blog-secrets 注入(共 10 项)
- .gitignore:补 cnb-secrets.yml 本地粘贴草稿(含真实令牌,不入库)

原 GitHub Actions 里的 artifact 传递 / COS 中转 / 广州机 sync.sh 轮询
整条链在此不再需要 —— 其唯一根因是 runner 在境外,CNB 节点在国内。
2026-10-04 12:56:05 +08:00

276 lines
13 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# =====================================================================
# 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
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 次
docker:
build:
dockerfile: deploy/Dockerfile
by: [package.json]
versionBy: [package-lock.json]
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
docker:
build:
dockerfile: deploy/Dockerfile
by: [package.json]
versionBy: [package-lock.json]
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