Deploy to Production / pre-check (push) Successful in 2m6s
Deploy to Production / build (push) Successful in 4m18s
Deploy to Production / deploy-edgeone (push) Successful in 7s
Deploy to Production / finalize (push) Successful in 16s
Deploy to Production / notify-failure (push) Skipped
deploy-upyun 是零上传动作的空壳 job,却无条件打印「UpYun 已同步最新内容」,其输出被 Telegram / 飞书 / 邮件通知当成 UpYun 部署成功的依据;真正的上传步骤标了 continue-on-error,失败也不影响 job 颜色。三层叠加,导致 7/25 起国内线路停更两个多月而通知始终显示成功。 改动:删除 deploy-upyun(finalize / notify-failure 的 needs 同步清理);Upload to UpYun 在退出时写 GITHUB_OUTPUT,如实上报 success / failure / skipped 三态与真实耗时(不用 steps.outcome,只依赖最通用的 output 传递);通知与 Step Summary 改为展示三态真实结果,移除「文件已在 Build 阶段同步」这类未经验证的说法。 又拍云直传失败仍不阻断流水线 —— 国内线路由腾讯云广州的 1Panel 计划任务「又拍云同步(国内中转)」每分钟兜底增量推送。
1040 lines
48 KiB
YAML
1040 lines
48 KiB
YAML
name: Deploy to Production
|
||
|
||
on:
|
||
push:
|
||
branches:
|
||
- main
|
||
schedule:
|
||
- cron: '0 1 * * *' # UTC 01:00 = 北京时间 09:00
|
||
workflow_dispatch:
|
||
|
||
concurrency:
|
||
group: ${{ github.workflow }}-${{ github.ref }}
|
||
cancel-in-progress: true
|
||
|
||
env:
|
||
TZ: Asia/Shanghai
|
||
|
||
jobs:
|
||
# ============================================================
|
||
# Job 0: Pre-Flight Checks (前置检查)
|
||
# ============================================================
|
||
pre-check:
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 2
|
||
outputs:
|
||
can_deploy: ${{ steps.decide.outputs.can_deploy }}
|
||
duration: ${{ steps.end-pre-check.outputs.duration }}
|
||
|
||
steps:
|
||
- name: Start timing
|
||
id: start-pre-check
|
||
run: echo "start_time=$(date +%s)" >> $GITHUB_OUTPUT
|
||
|
||
- name: Checkout code
|
||
uses: actions/checkout@v4
|
||
with:
|
||
fetch-depth: 1
|
||
|
||
- name: Validate repository
|
||
id: repo-check
|
||
run: |
|
||
if [ "${{ github.repository }}" != "zqlit/blog" ]; then
|
||
echo "❌ 只能在主仓库执行部署"
|
||
echo "can_deploy=false" >> $GITHUB_OUTPUT
|
||
exit 1
|
||
fi
|
||
echo "✅ 仓库验证通过"
|
||
echo "can_deploy=true" >> $GITHUB_OUTPUT
|
||
|
||
- name: Validate commit (only for push events)
|
||
if: github.event_name == 'push'
|
||
id: commit-check
|
||
# 注意:commit message 是用户可控的多行文本,必须经 env 传入。
|
||
# 直接写 ${{ github.event.head_commit.message }} 会把内容展开进脚本,
|
||
# 多行信息或含 $ / ` 的字符会导致 "bad substitution" 甚至命令注入。
|
||
env:
|
||
COMMIT_AUTHOR: ${{ github.event.head_commit.author.name }}
|
||
COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
|
||
run: |
|
||
echo "📝 提交作者: $COMMIT_AUTHOR"
|
||
echo "📄 提交信息: $COMMIT_MESSAGE"
|
||
|
||
if echo "$COMMIT_MESSAGE" | grep -qE "^(feat|fix|chore|docs|style|refactor|test): "; then
|
||
echo "✅ 提交信息符合规范"
|
||
else
|
||
echo "⚠️ 提交信息格式建议: feat/fix/chore/docs/style/refactor/test: description"
|
||
fi
|
||
|
||
- name: Decide whether to deploy
|
||
id: decide
|
||
run: |
|
||
# 基础决策来自仓库校验
|
||
CAN_DEPLOY="${{ steps.repo-check.outputs.can_deploy }}"
|
||
echo "📌 仓库校验结果: can_deploy=${CAN_DEPLOY}"
|
||
|
||
# 定时触发:若 1 小时内无新提交则跳过,避免重复构建
|
||
if [ "${{ github.event_name }}" = "schedule" ]; then
|
||
LAST_COMMIT_TIME=$(git log -1 --format=%ct)
|
||
CURRENT_TIME=$(date +%s)
|
||
DIFF_HOURS=$(( (CURRENT_TIME - LAST_COMMIT_TIME) / 3600 ))
|
||
|
||
echo "⏰ 最后一次提交时间: $(date -d @$LAST_COMMIT_TIME)"
|
||
echo "⏰ 当前时间: $(date)"
|
||
echo "⏰ 距离上次提交: ${DIFF_HOURS} 小时"
|
||
|
||
if [ "$DIFF_HOURS" -ge 1 ]; then
|
||
echo "⏭️ 超过 1 小时没有新提交,跳过定时构建"
|
||
CAN_DEPLOY="false"
|
||
else
|
||
echo "✅ 最近有提交,继续构建"
|
||
fi
|
||
fi
|
||
|
||
echo "can_deploy=${CAN_DEPLOY}" >> $GITHUB_OUTPUT
|
||
echo "## 🚦 部署决策: can_deploy=${CAN_DEPLOY}" >> $GITHUB_STEP_SUMMARY
|
||
|
||
- name: Summary
|
||
run: |
|
||
echo "## 🔍 前置检查完成" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **仓库**: ${{ github.repository }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **分支**: ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **触发方式**: ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Commit SHA**: ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY
|
||
|
||
- name: End timing
|
||
id: end-pre-check
|
||
run: |
|
||
end_time=$(date +%s)
|
||
start_time=${{ steps.start-pre-check.outputs.start_time }}
|
||
duration=$((end_time - start_time))
|
||
echo "duration=${duration}" >> $GITHUB_OUTPUT
|
||
echo "## ⏱️ Pre-check 耗时: ${duration}秒" >> $GITHUB_STEP_SUMMARY
|
||
|
||
# ============================================================
|
||
# Job 1: Build (构建)
|
||
# ============================================================
|
||
build:
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 15
|
||
permissions:
|
||
contents: write
|
||
outputs:
|
||
build_hash: ${{ steps.hash.outputs.build_hash }}
|
||
last_hash: ${{ steps.last-hash.outputs.last_hash }}
|
||
duration: ${{ steps.end-build.outputs.duration }}
|
||
file_count: ${{ steps.stats.outputs.file_count }}
|
||
total_size: ${{ steps.stats.outputs.total_size }}
|
||
need_upload: ${{ steps.check-upload.outputs.need_upload }}
|
||
# Upload to UpYun 的真实三态:success / failure / skipped(未执行)。
|
||
# 该步骤标了 continue-on-error,conclusion 恒为 success,不能用来判断成败。
|
||
# 这里刻意不用 steps.<id>.outcome,而是让步骤自己在退出时写 GITHUB_OUTPUT,
|
||
# 只依赖最通用的 output 传递机制,GitHub / Gitea 两边都稳。
|
||
upyun_status: ${{ steps.upload.outputs.upyun_status || 'skipped' }}
|
||
upyun_duration: ${{ steps.upload.outputs.duration }}
|
||
needs: pre-check
|
||
if: needs.pre-check.outputs.can_deploy == 'true'
|
||
|
||
steps:
|
||
- name: Start timing
|
||
id: start-build
|
||
run: echo "start_time=$(date +%s)" >> $GITHUB_OUTPUT
|
||
|
||
- name: Checkout code
|
||
uses: actions/checkout@v4
|
||
with:
|
||
fetch-depth: 0
|
||
# Hugo 的 lastmod 用了 :git,必须保留完整提交历史(commit/tree),
|
||
# 但历史版本的文件内容(blob,多为图片)不需要。
|
||
# partial clone 只按需拉取当前工作区的 blob,实测本仓库可省约 200MB 传输。
|
||
filter: blob:none
|
||
|
||
- name: Setup Hugo
|
||
uses: peaceiris/actions-hugo@v2
|
||
with:
|
||
hugo-version: '0.128.2'
|
||
extended: true
|
||
|
||
- name: Setup Node
|
||
uses: actions/setup-node@v4
|
||
with:
|
||
node-version: '22'
|
||
# Gitea 的 runner 缓存服务与 job 容器跨 bridge 网络不可达,
|
||
# 每次 restore/save 各死等约 4 分半后 ETIMEDOUT(纯浪费约 9 分钟),
|
||
# 因此仅在 GitHub 上启用缓存;Gitea 上传空值让 setup-node 跳过缓存。
|
||
cache: ${{ github.server_url == 'https://github.com' && 'npm' || '' }}
|
||
|
||
- name: Install Node.js dependencies
|
||
run: npm ci --no-audit --no-fund
|
||
|
||
- name: Cache Hugo resources
|
||
# 同 Setup Node:Gitea 环境缓存服务不可达,跳过以免每次白等
|
||
if: github.server_url == 'https://github.com'
|
||
uses: actions/cache@v4
|
||
id: hugo-cache
|
||
with:
|
||
path: |
|
||
resources/_gen
|
||
/home/runner/.cache/hugo_cache
|
||
key: ${{ runner.os }}-hugo-${{ hashFiles('go.sum', 'package-lock.json', 'hugo.toml', 'config/_default/*.toml') }}
|
||
restore-keys: |
|
||
${{ runner.os }}-hugo-
|
||
|
||
- name: Run Pre-Build Scripts
|
||
shell: bash
|
||
run: python3 scripts/add_draft_to_hidden.py
|
||
|
||
- name: Optimize Images
|
||
continue-on-error: true
|
||
timeout-minutes: 5
|
||
run: node scripts/optimize_images.js
|
||
|
||
- name: Commit Optimized Images
|
||
continue-on-error: true
|
||
run: |
|
||
git config --global user.name "GitHub Actions"
|
||
git config --global user.email "actions@github.com"
|
||
git add content/posts static
|
||
if ! git diff --staged --quiet; then
|
||
git commit -m "chore: auto-optimize images [skip ci]"
|
||
else
|
||
echo "No changes to commit"
|
||
fi
|
||
|
||
- name: Build Hugo site
|
||
timeout-minutes: 10
|
||
run: rm -rf public resources/_gen && hugo --minify --gc
|
||
|
||
- name: Generate build hash
|
||
id: hash
|
||
run: |
|
||
BUILD_HASH=$(find public -type f -print0 | sort -z | xargs -0 sha256sum | sha256sum | awk '{print $1}')
|
||
echo "build_hash=${BUILD_HASH}" >> $GITHUB_OUTPUT
|
||
echo "$BUILD_HASH" > public/.build_hash
|
||
|
||
- name: Build statistics
|
||
id: stats
|
||
run: |
|
||
FILE_COUNT=$(find public -type f | wc -l)
|
||
TOTAL_SIZE=$(du -sh public | awk '{print $1}')
|
||
echo "file_count=${FILE_COUNT}" >> $GITHUB_OUTPUT
|
||
echo "total_size=${TOTAL_SIZE}" >> $GITHUB_OUTPUT
|
||
|
||
- name: Setup Python
|
||
uses: actions/setup-python@v4
|
||
with:
|
||
python-version: '3.x'
|
||
|
||
- name: Install coscmd
|
||
run: pip install -U coscmd
|
||
|
||
- name: Configure coscmd
|
||
env:
|
||
COS_SECRET_ID: ${{ secrets.COS_SECRET_ID }}
|
||
COS_SECRET_KEY: ${{ secrets.COS_SECRET_KEY }}
|
||
COS_BUCKET: ${{ secrets.COS_BUCKET }}
|
||
COS_REGION: ${{ secrets.COS_REGION }}
|
||
run: coscmd config -a $COS_SECRET_ID -s $COS_SECRET_KEY -b $COS_BUCKET -r $COS_REGION -m 30
|
||
|
||
- name: Get last build hash from COS
|
||
id: last-hash
|
||
continue-on-error: true
|
||
run: |
|
||
echo "🔍 从腾讯云 COS 获取上次部署的 hash..."
|
||
coscmd download /__build_hash /tmp/last_hash 2>/dev/null || echo ""
|
||
LAST_HASH=$(cat /tmp/last_hash 2>/dev/null || echo "")
|
||
if [ -z "$LAST_HASH" ]; then
|
||
echo "⚠️ 没有找到上次部署的 hash(首次部署)"
|
||
else
|
||
echo "上次部署 hash: $LAST_HASH"
|
||
fi
|
||
echo "last_hash=${LAST_HASH}" >> $GITHUB_OUTPUT
|
||
|
||
- name: Check if upload is needed
|
||
id: check-upload
|
||
run: |
|
||
LAST_HASH="${{ steps.last-hash.outputs.last_hash }}"
|
||
CURRENT_HASH="${{ steps.hash.outputs.build_hash }}"
|
||
|
||
echo "上次部署 hash: $LAST_HASH"
|
||
echo "当前构建 hash: $CURRENT_HASH"
|
||
|
||
if [ -z "$LAST_HASH" ] || [ "$CURRENT_HASH" != "$LAST_HASH" ]; then
|
||
echo "🔄 构建产物有变化,需要上传"
|
||
echo "need_upload=true" >> $GITHUB_OUTPUT
|
||
else
|
||
echo "✅ 构建产物无变化,跳过上传"
|
||
echo "need_upload=false" >> $GITHUB_OUTPUT
|
||
fi
|
||
|
||
- name: Upload to Tencent COS
|
||
if: steps.check-upload.outputs.need_upload == 'true'
|
||
continue-on-error: true
|
||
run: |
|
||
echo "📤 打包并上传到腾讯云 COS..."
|
||
if command -v zstd; then
|
||
tar -cf - public/ | zstd -1 -T0 -o public.tar.zst
|
||
coscmd upload public.tar.zst /public.tar.zst
|
||
else
|
||
tar -czf public.tar.gz public/
|
||
coscmd upload public.tar.gz /public.tar.gz
|
||
fi
|
||
coscmd upload public/.build_hash /__build_hash
|
||
|
||
- name: Upload hash to COS (even if skipped)
|
||
if: steps.check-upload.outputs.need_upload == 'false'
|
||
run: |
|
||
echo "📤 更新 hash 文件到腾讯云 COS..."
|
||
coscmd upload public/.build_hash /__build_hash
|
||
|
||
- name: Upload to UpYun
|
||
if: steps.check-upload.outputs.need_upload == 'true'
|
||
timeout-minutes: 10
|
||
env:
|
||
UPYUN_SERVICE: ${{ secrets.UPYUN_BUCKET }}
|
||
UPYUN_OPERATOR: ${{ secrets.UPYUN_OPERATOR }}
|
||
UPYUN_PASSWORD: ${{ secrets.UPYUN_PASSWORD }}
|
||
id: upload
|
||
# 直传失败不阻断整条流水线 —— 国内线路已由腾讯云广州的中转机兜底
|
||
# (1Panel 计划任务「又拍云同步(国内中转)」每分钟比对 COS 上的 build hash,
|
||
# 有变化才增量推送)。但失败必须被如实上报,绝不能再伪装成成功。
|
||
continue-on-error: true
|
||
run: |
|
||
echo "📤 上传构建产物到 UpYun..."
|
||
START_TS=$(date +%s)
|
||
# 无论成功、失败还是中途退出,都在退出时如实上报结果与耗时。
|
||
# 这是"不再假绿"的核心:过去无论传没传上去,通知都显示成功。
|
||
on_exit() {
|
||
rc=$?
|
||
if [ "$rc" -eq 0 ]; then
|
||
echo "upyun_status=success" >> $GITHUB_OUTPUT
|
||
else
|
||
echo "upyun_status=failure" >> $GITHUB_OUTPUT
|
||
fi
|
||
echo "duration=$(($(date +%s) - START_TS))" >> $GITHUB_OUTPUT
|
||
}
|
||
trap on_exit EXIT
|
||
|
||
UPX_VERSION="0.4.9"
|
||
UPX_BIN="/tmp/upyun-upx"
|
||
|
||
# 主源用 GitHub Releases(实测 0.5s 稳定),
|
||
# 官方 collection.b0.upaiyun.com 实测 2/3 概率整站无响应,仅作兜底
|
||
UPX_URLS=(
|
||
"https://github.com/upyun/upx/releases/download/v${UPX_VERSION}/upx_${UPX_VERSION}_linux_amd64.tar.gz"
|
||
"https://collection.b0.upaiyun.com/softwares/upx/upx_${UPX_VERSION}_linux_amd64.tar.gz"
|
||
)
|
||
|
||
if [ ! -x "$UPX_BIN" ]; then
|
||
ok=false
|
||
for U in "${UPX_URLS[@]}"; do
|
||
echo "⬇️ 下载 upx: $U"
|
||
if curl -fsSL --connect-timeout 10 --max-time 60 --retry 2 --retry-delay 2 \
|
||
-o /tmp/upx.tar.gz "$U"; then
|
||
if tar -xzf /tmp/upx.tar.gz -C /tmp && [ -f /tmp/upx ]; then
|
||
mv /tmp/upx "$UPX_BIN" && chmod +x "$UPX_BIN" && ok=true
|
||
echo "✅ upx 就绪: $("$UPX_BIN" --version 2>&1 | head -1)"
|
||
break
|
||
fi
|
||
fi
|
||
echo "⚠️ 该源失败,尝试下一个..."
|
||
done
|
||
if [ "$ok" != "true" ]; then
|
||
echo "❌ 所有 upx 下载源均失败,跳过 UpYun 上传"
|
||
exit 1
|
||
fi
|
||
fi
|
||
|
||
for i in 1 2 3 4 5; do
|
||
echo "🔐 UpYun 登录 (尝试 $i/5)..."
|
||
if timeout 30 "$UPX_BIN" login "$UPYUN_SERVICE" "$UPYUN_OPERATOR" "$UPYUN_PASSWORD" 2>&1; then
|
||
echo "✅ 登录成功"
|
||
break
|
||
else
|
||
echo "⚠️ 登录失败,等待 10 秒后重试..."
|
||
sleep 10
|
||
fi
|
||
done
|
||
|
||
echo "🔄 开始上传到 UpYun..."
|
||
timeout 480 "$UPX_BIN" sync public/ / -w 10 --delete --strong
|
||
timeout 30 "$UPX_BIN" put public/.build_hash /__build_hash
|
||
echo "✅ UpYun 上传完成"
|
||
|
||
- name: Push Image Optimizations
|
||
continue-on-error: true
|
||
run: |
|
||
git fetch origin main
|
||
git stash push -m "temp stash for rebase" || true
|
||
git rebase origin/main || true
|
||
git stash pop || true
|
||
if git log --oneline -1 | grep -q "\[skip ci\]"; then
|
||
git push origin HEAD:main
|
||
else
|
||
echo "No auto-commits to push"
|
||
fi
|
||
|
||
- name: End timing
|
||
id: end-build
|
||
run: |
|
||
end_time=$(date +%s)
|
||
start_time=${{ steps.start-build.outputs.start_time }}
|
||
duration=$((end_time - start_time))
|
||
echo "duration=${duration}" >> $GITHUB_OUTPUT
|
||
|
||
echo "## ⏱️ Build 耗时: ${duration}秒" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "### 📊 构建统计" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **文件总数**: ${{ steps.stats.outputs.file_count }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **总大小**: ${{ steps.stats.outputs.total_size }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **构建哈希**: ${{ steps.hash.outputs.build_hash }}" >> $GITHUB_STEP_SUMMARY
|
||
if [ "${{ github.server_url }}" = "https://github.com" ]; then
|
||
echo "- **Hugo 缓存**: ${{ steps.hugo-cache.outputs.cache-hit == 'true' && '✅ 命中' || '🔄 未命中' }}" >> $GITHUB_STEP_SUMMARY
|
||
else
|
||
echo "- **Hugo 缓存**: ⏭️ 已跳过 (Gitea 缓存服务跨网段不可达)" >> $GITHUB_STEP_SUMMARY
|
||
fi
|
||
|
||
# ============================================================
|
||
# Job 3: Deploy to EdgeOne Pages (并行)
|
||
# ============================================================
|
||
deploy-edgeone:
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 20
|
||
permissions:
|
||
contents: read
|
||
needs: build
|
||
outputs:
|
||
deployed: ${{ steps.check.outputs.deployed }}
|
||
duration: ${{ steps.end-edgeone.outputs.duration }}
|
||
download_duration: ${{ steps.download-files.outputs.download_duration }}
|
||
upload_duration: ${{ steps.end-edgeone-upload.outputs.duration }}
|
||
|
||
steps:
|
||
- name: Start timing
|
||
id: start-edgeone
|
||
run: echo "start_time=$(date +%s)" >> $GITHUB_OUTPUT
|
||
|
||
- name: Check if deployment is needed
|
||
id: check
|
||
run: |
|
||
CURRENT_HASH="${{ needs.build.outputs.build_hash }}"
|
||
LAST_HASH="${{ needs.build.outputs.last_hash }}"
|
||
|
||
echo "当前 build_hash: $CURRENT_HASH"
|
||
echo "上次部署 hash: $LAST_HASH"
|
||
|
||
if [ -z "$LAST_HASH" ] || [ "$CURRENT_HASH" != "$LAST_HASH" ]; then
|
||
echo "🔄 构建产物有变化,需要部署到 EdgeOne"
|
||
echo "deployed=true" >> $GITHUB_OUTPUT
|
||
else
|
||
echo "✅ 构建产物无变化,跳过 EdgeOne 部署"
|
||
echo "deployed=false" >> $GITHUB_OUTPUT
|
||
fi
|
||
|
||
- name: Setup Python
|
||
if: steps.check.outputs.deployed == 'true'
|
||
uses: actions/setup-python@v4
|
||
with:
|
||
python-version: '3.x'
|
||
|
||
- name: Install coscmd
|
||
if: steps.check.outputs.deployed == 'true'
|
||
run: pip install -U coscmd
|
||
|
||
- name: Configure coscmd
|
||
if: steps.check.outputs.deployed == 'true'
|
||
env:
|
||
COS_SECRET_ID: ${{ secrets.COS_SECRET_ID }}
|
||
COS_SECRET_KEY: ${{ secrets.COS_SECRET_KEY }}
|
||
COS_BUCKET: ${{ secrets.COS_BUCKET }}
|
||
COS_REGION: ${{ secrets.COS_REGION }}
|
||
run: coscmd config -a $COS_SECRET_ID -s $COS_SECRET_KEY -b $COS_BUCKET -r $COS_REGION
|
||
|
||
- name: Download files from COS
|
||
if: steps.check.outputs.deployed == 'true'
|
||
id: download-files
|
||
run: |
|
||
echo "📥 从腾讯云 COS 下载构建产物..."
|
||
DOWNLOAD_START=$(date +%s)
|
||
if command -v zstd; then
|
||
coscmd download /public.tar.zst /tmp/public.tar.zst
|
||
tar -I zstd -xf /tmp/public.tar.zst
|
||
else
|
||
coscmd download /public.tar.gz /tmp/public.tar.gz
|
||
tar -xzf /tmp/public.tar.gz
|
||
fi
|
||
DOWNLOAD_END=$(date +%s)
|
||
DOWNLOAD_DURATION=$((DOWNLOAD_END - DOWNLOAD_START))
|
||
echo "download_duration=${DOWNLOAD_DURATION}" >> $GITHUB_OUTPUT
|
||
|
||
- name: Start upload timing
|
||
if: steps.check.outputs.deployed == 'true'
|
||
id: start-edgeone-upload
|
||
run: echo "start_time=$(date +%s)" >> $GITHUB_OUTPUT
|
||
|
||
- name: Deploy to EdgeOne Pages
|
||
if: steps.check.outputs.deployed == 'true'
|
||
env:
|
||
EDGEONE_API_TOKEN: ${{ secrets.EDGEONE_API_TOKEN }}
|
||
run: |
|
||
# 内联 scripts/deploy_edgeone.sh —— 本步骤只需一个部署命令,
|
||
# 原先为此 checkout 整个仓库要花约 81 秒,不值得。
|
||
if [ -z "$EDGEONE_API_TOKEN" ]; then
|
||
echo "Error: EDGEONE_API_TOKEN is not set."
|
||
exit 1
|
||
fi
|
||
echo "Deploying to EdgeOne Pages..."
|
||
echo "Project: hugo-blog | Directory: ./public | Area: overseas"
|
||
# 国际站必须加 --area overseas
|
||
npx edgeone pages deploy ./public -n hugo-blog -t "$EDGEONE_API_TOKEN" --area overseas
|
||
|
||
- name: End upload timing
|
||
if: steps.check.outputs.deployed == 'true'
|
||
id: end-edgeone-upload
|
||
run: |
|
||
end_time=$(date +%s)
|
||
start_time=${{ steps.start-edgeone-upload.outputs.start_time }}
|
||
duration=$((end_time - start_time))
|
||
echo "duration=${duration}" >> $GITHUB_OUTPUT
|
||
|
||
- name: End timing
|
||
id: end-edgeone
|
||
run: |
|
||
end_time=$(date +%s)
|
||
start_time=${{ steps.start-edgeone.outputs.start_time }}
|
||
duration=$((end_time - start_time))
|
||
echo "duration=${duration}" >> $GITHUB_OUTPUT
|
||
|
||
if [ "${{ steps.check.outputs.deployed }}" = "true" ]; then
|
||
echo "## ⏱️ Deploy EdgeOne 耗时: ${duration}秒" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "### 📊 部署统计" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **下载: ${{ steps.download-files.outputs.download_duration }}s**" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **上传部署**: ${duration}秒" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **文件总数**: ${{ needs.build.outputs.file_count }}" >> $GITHUB_STEP_SUMMARY
|
||
else
|
||
echo "## EdgeOne 部署已跳过(构建产物无变化)" >> $GITHUB_STEP_SUMMARY
|
||
echo "总耗时: ${duration}秒" >> $GITHUB_STEP_SUMMARY
|
||
fi
|
||
|
||
# ============================================================
|
||
# 注意:这里原先有一个名为 deploy-upyun 的 job —— 它是纯空壳:
|
||
# 零上传动作,却无条件打印「UpYun 已同步最新内容」,且它的输出被
|
||
# Telegram / 飞书 / 邮件通知当成"UpYun 部署成功"的依据。
|
||
# 因它持续掩盖真实故障(7/25 起国内线路停更两个多月),已于 2026-10-01 删除。
|
||
# 又拍云的真实状态现在来自 build job 的 upyun_status 输出。
|
||
# ============================================================
|
||
|
||
# ============================================================
|
||
# Job 5: CDN Refresh & Notifications (after all deployments)
|
||
# ============================================================
|
||
finalize:
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 5
|
||
permissions:
|
||
contents: write
|
||
needs: [pre-check, build, deploy-edgeone]
|
||
if: always() && needs.build.result == 'success' && needs.deploy-edgeone.result == 'success'
|
||
|
||
steps:
|
||
- name: Start timing
|
||
id: start-finalize
|
||
run: echo "start_time=$(date +%s)" >> $GITHUB_OUTPUT
|
||
|
||
- name: Setup Node
|
||
uses: actions/setup-node@v4
|
||
with:
|
||
node-version: '22'
|
||
# 本 job 不再安装 npm 依赖(CDN 刷新脚本只用 node 内置模块),
|
||
# 因此不启用 npm 缓存 —— 也免得在没有 package-lock.json 时算缓存 key 出错。
|
||
|
||
- name: Determine deployment status
|
||
id: deploy-status
|
||
run: |
|
||
EDGEONE_DEPLOYED="${{ needs.deploy-edgeone.outputs.deployed }}"
|
||
UPYUN_STATUS="${{ needs.build.outputs.upyun_status }}"
|
||
|
||
echo "EdgeOne deployed: $EDGEONE_DEPLOYED"
|
||
echo "UpYun status: $UPYUN_STATUS"
|
||
|
||
if [ "$EDGEONE_DEPLOYED" = "true" ] || [ "$UPYUN_STATUS" = "success" ]; then
|
||
echo "any_deployed=true" >> $GITHUB_OUTPUT
|
||
else
|
||
echo "any_deployed=false" >> $GITHUB_OUTPUT
|
||
fi
|
||
|
||
- name: Refresh DogeCloud CDN
|
||
if: steps.deploy-status.outputs.any_deployed == 'true'
|
||
env:
|
||
DOGECLOUD_ACCESS_KEY: ${{ secrets.DOGECLOUD_ACCESS_KEY }}
|
||
DOGECLOUD_SECRET_KEY: ${{ secrets.DOGECLOUD_SECRET_KEY }}
|
||
CDN_URL_LIST: ${{ secrets.CDN_URL_LIST }}
|
||
run: |
|
||
# 内联 scripts/refresh_cdn.js(修改该文件时需同步这里)。
|
||
# 该脚本零外部依赖 —— 仅用 node 内置的 crypto 与 fetch ——
|
||
# 因此不必 checkout 整个仓库、也不必 npm ci(原先这两步合计约 211 秒)。
|
||
cat > /tmp/refresh_cdn.js <<'REFRESH_CDN_EOF'
|
||
const crypto = require("crypto");
|
||
|
||
async function dogecloudApi(apiPath, data = {}) {
|
||
const accessKey = process.env.DOGECLOUD_ACCESS_KEY;
|
||
const secretKey = process.env.DOGECLOUD_SECRET_KEY;
|
||
|
||
if (!accessKey || !secretKey) {
|
||
console.error("Error: DOGECLOUD_ACCESS_KEY or DOGECLOUD_SECRET_KEY not set.");
|
||
process.exit(1);
|
||
}
|
||
|
||
const body = JSON.stringify(data);
|
||
const signStr = apiPath + "\n" + body;
|
||
const sign = crypto.createHmac("sha1", secretKey).update(signStr).digest("hex");
|
||
const authorization = `TOKEN ${accessKey}:${sign}`;
|
||
|
||
const res = await fetch(`https://api.dogecloud.com${apiPath}`, {
|
||
method: "POST",
|
||
headers: {
|
||
Authorization: authorization,
|
||
"Content-Type": "application/json",
|
||
},
|
||
body,
|
||
});
|
||
return res.json();
|
||
}
|
||
|
||
async function main() {
|
||
const cdnUrlListStr = process.env.CDN_URL_LIST;
|
||
if (!cdnUrlListStr) {
|
||
console.log("Warning: CDN_URL_LIST not found or empty.");
|
||
return;
|
||
}
|
||
|
||
const urlList = cdnUrlListStr
|
||
.replace(/\n/g, ",")
|
||
.split(",")
|
||
.map((u) => u.trim())
|
||
.filter(Boolean);
|
||
|
||
if (urlList.length === 0) {
|
||
console.log("No URLs to refresh.");
|
||
return;
|
||
}
|
||
|
||
console.log(`Refreshing ${urlList.length} URLs...`);
|
||
|
||
const result = await dogecloudApi("/cdn/refresh/add.json", {
|
||
rtype: "path",
|
||
urls: JSON.stringify(urlList),
|
||
});
|
||
|
||
if (result && result.code === 200) {
|
||
console.log(`Refresh task submitted. Task ID: ${result.data.task_id}`);
|
||
} else {
|
||
console.error(`Failed to submit refresh task. Response: ${JSON.stringify(result)}`);
|
||
process.exit(1);
|
||
}
|
||
}
|
||
|
||
main();
|
||
REFRESH_CDN_EOF
|
||
node /tmp/refresh_cdn.js
|
||
|
||
- name: Send Telegram notification with detailed timing
|
||
env:
|
||
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||
run: |
|
||
if [ -z "$TELEGRAM_BOT_TOKEN" ] || [ -z "$TELEGRAM_CHAT_ID" ]; then
|
||
echo "⚠️ Telegram secrets 未配置,跳过通知"
|
||
exit 0
|
||
fi
|
||
|
||
PRECHECK_DUR="${{ needs.pre-check.outputs.duration }}"
|
||
BUILD_DUR="${{ needs.build.outputs.duration }}"
|
||
EDGEONE_DUR="${{ needs.deploy-edgeone.outputs.duration }}"
|
||
EDGEONE_DEPLOYED="${{ needs.deploy-edgeone.outputs.deployed }}"
|
||
UPYUN_DUR="${{ needs.build.outputs.upyun_duration }}"
|
||
UPYUN_STATUS_RAW="${{ needs.build.outputs.upyun_status }}"
|
||
FILE_COUNT="${{ needs.build.outputs.file_count }}"
|
||
TOTAL_SIZE="${{ needs.build.outputs.total_size }}"
|
||
|
||
EDGEONE_DETAIL=""
|
||
if [ "$EDGEONE_DEPLOYED" = "true" ]; then
|
||
EDGEONE_DOWNLOAD="${{ needs.deploy-edgeone.outputs.download_duration }}"
|
||
EDGEONE_UPLOAD="${{ needs.deploy-edgeone.outputs.upload_duration }}"
|
||
EDGEONE_DETAIL=$(printf "\n 📥 下载: %ss\n 📤 上传: %ss" "$EDGEONE_DOWNLOAD" "$EDGEONE_UPLOAD")
|
||
fi
|
||
|
||
# 三态映射:直传结果如实呈现,不再有"文件已在 Build 阶段同步"这种没验证过的说辞
|
||
case "$UPYUN_STATUS_RAW" in
|
||
success) UPYUN_TEXT="✅ 已直传";;
|
||
failure) UPYUN_TEXT="❌ 直传失败(国内中转机兜底)";;
|
||
skipped) UPYUN_TEXT="⏭️ 跳过(无变化)";;
|
||
*) UPYUN_TEXT="❔ 未执行";;
|
||
esac
|
||
if [ -n "$UPYUN_DUR" ]; then UPYUN_DUR_TEXT="${UPYUN_DUR}s"; else UPYUN_DUR_TEXT="—"; fi
|
||
|
||
if [ "${{ steps.deploy-status.outputs.any_deployed }}" = "true" ]; then
|
||
STATUS_ICON="🎉"
|
||
STATUS_TEXT="部署成功"
|
||
else
|
||
STATUS_ICON="⏭️"
|
||
STATUS_TEXT="无变化,跳过部署"
|
||
fi
|
||
|
||
EDGEONE_STATUS=$([ "$EDGEONE_DEPLOYED" = "true" ] && echo "✅ 已部署" || echo "⏭️ 跳过")
|
||
|
||
CURRENT_TIME=$(date +'%Y-%m-%d %H:%M:%S')
|
||
|
||
MESSAGE="$STATUS_ICON *$STATUS_TEXT*"
|
||
MESSAGE="$MESSAGE"$'\n\n'
|
||
MESSAGE="$MESSAGE""📦 *项目*: \`${{ github.repository }}\`"$'\n'
|
||
MESSAGE="$MESSAGE""🌿 *分支*: \`${{ github.ref_name }}\`"$'\n'
|
||
MESSAGE="$MESSAGE""🔗 *Commit*: \`${{ github.sha }}\`"$'\n'
|
||
MESSAGE="$MESSAGE""⏰ *时间*: $CURRENT_TIME"$'\n'
|
||
MESSAGE="$MESSAGE""🚀 *触发方式*: ${{ github.event_name }}"$'\n\n'
|
||
MESSAGE="$MESSAGE""━━━━━━━━━━━━━━━━━━━━━━"$'\n'
|
||
MESSAGE="$MESSAGE""📊 *构建统计*"$'\n'
|
||
MESSAGE="$MESSAGE""━━━━━━━━━━━━━━━━━━━━━━"$'\n'
|
||
MESSAGE="$MESSAGE""📁 文件总数: $FILE_COUNT"$'\n'
|
||
MESSAGE="$MESSAGE""💾 总大小: $TOTAL_SIZE"$'\n\n'
|
||
MESSAGE="$MESSAGE""━━━━━━━━━━━━━━━━━━━━━━"$'\n'
|
||
MESSAGE="$MESSAGE""⏱️ *耗时统计*"$'\n'
|
||
MESSAGE="$MESSAGE""━━━━━━━━━━━━━━━━━━━━━━"$'\n'
|
||
MESSAGE="$MESSAGE""🔍 Pre-check: ${PRECHECK_DUR}s"$'\n'
|
||
MESSAGE="$MESSAGE""🏗️ Build: ${BUILD_DUR}s"$'\n'
|
||
MESSAGE="$MESSAGE""🌐 EdgeOne: ${EDGEONE_DUR}s$EDGEONE_DETAIL"$'\n'
|
||
MESSAGE="$MESSAGE""☁️ UpYun 直传: ${UPYUN_DUR_TEXT}"$'\n\n'
|
||
MESSAGE="$MESSAGE""━━━━━━━━━━━━━━━━━━━━━━"$'\n'
|
||
MESSAGE="$MESSAGE""📌 *部署状态*"$'\n'
|
||
MESSAGE="$MESSAGE""━━━━━━━━━━━━━━━━━━━━━━"$'\n'
|
||
MESSAGE="$MESSAGE""🌐 EdgeOne Pages: $EDGEONE_STATUS"$'\n'
|
||
MESSAGE="$MESSAGE""☁️ UpYun: $UPYUN_TEXT"$'\n\n'
|
||
if [ "${{ github.server_url }}" = "https://github.com" ]; then
|
||
RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||
else
|
||
RUN_URL="http://23.254.236.47:3001/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||
fi
|
||
MESSAGE="$MESSAGE""🔗 [查看详情]($RUN_URL)"
|
||
|
||
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
|
||
-d chat_id="${TELEGRAM_CHAT_ID}" \
|
||
-d text="$MESSAGE" \
|
||
-d parse_mode="Markdown" \
|
||
-d disable_web_page_preview=true
|
||
|
||
- name: Send Feishu notification with detailed timing
|
||
env:
|
||
FEISHU_WEBHOOK_URL: ${{ secrets.FEISHU_WEBHOOK_URL }}
|
||
run: |
|
||
if [ -z "$FEISHU_WEBHOOK_URL" ]; then
|
||
echo "⚠️ 飞书 Webhook URL 未配置,跳过通知"
|
||
exit 0
|
||
fi
|
||
|
||
PRECHECK_DUR="${{ needs.pre-check.outputs.duration }}"
|
||
BUILD_DUR="${{ needs.build.outputs.duration }}"
|
||
EDGEONE_DUR="${{ needs.deploy-edgeone.outputs.duration }}"
|
||
EDGEONE_DEPLOYED="${{ needs.deploy-edgeone.outputs.deployed }}"
|
||
UPYUN_DUR="${{ needs.build.outputs.upyun_duration }}"
|
||
UPYUN_STATUS_RAW="${{ needs.build.outputs.upyun_status }}"
|
||
FILE_COUNT="${{ needs.build.outputs.file_count }}"
|
||
TOTAL_SIZE="${{ needs.build.outputs.total_size }}"
|
||
|
||
EDGEONE_DETAIL=""
|
||
if [ "$EDGEONE_DEPLOYED" = "true" ]; then
|
||
EDGEONE_DOWNLOAD="${{ needs.deploy-edgeone.outputs.download_duration }}"
|
||
EDGEONE_UPLOAD="${{ needs.deploy-edgeone.outputs.upload_duration }}"
|
||
EDGEONE_DETAIL=$(printf "\n 📥 下载: %ss\n 📤 上传: %ss" "$EDGEONE_DOWNLOAD" "$EDGEONE_UPLOAD")
|
||
fi
|
||
|
||
# 三态映射:直传结果如实呈现,不再有"文件已在 Build 阶段同步"这种没验证过的说辞
|
||
case "$UPYUN_STATUS_RAW" in
|
||
success) UPYUN_TEXT="✅ 已直传";;
|
||
failure) UPYUN_TEXT="❌ 直传失败(国内中转机兜底)";;
|
||
skipped) UPYUN_TEXT="⏭️ 跳过(无变化)";;
|
||
*) UPYUN_TEXT="❔ 未执行";;
|
||
esac
|
||
if [ -n "$UPYUN_DUR" ]; then UPYUN_DUR_TEXT="${UPYUN_DUR}s"; else UPYUN_DUR_TEXT="—"; fi
|
||
|
||
if [ "${{ steps.deploy-status.outputs.any_deployed }}" = "true" ]; then
|
||
STATUS_ICON="🎉"
|
||
STATUS_TEXT="部署成功"
|
||
else
|
||
STATUS_ICON="⏭️"
|
||
STATUS_TEXT="无变化,跳过部署"
|
||
fi
|
||
|
||
EDGEONE_STATUS=$([ "$EDGEONE_DEPLOYED" = "true" ] && echo "✅ 已部署" || echo "⏭️ 跳过")
|
||
|
||
CURRENT_TIME=$(date +'%Y-%m-%d %H:%M:%S')
|
||
|
||
MESSAGE_TEXT="$STATUS_ICON $STATUS_TEXT"
|
||
MESSAGE_TEXT="$MESSAGE_TEXT"$'\n\n'
|
||
MESSAGE_TEXT="$MESSAGE_TEXT""📦 项目: ${{ github.repository }}"$'\n'
|
||
MESSAGE_TEXT="$MESSAGE_TEXT""🌿 分支: ${{ github.ref_name }}"$'\n'
|
||
MESSAGE_TEXT="$MESSAGE_TEXT""🔗 Commit: ${{ github.sha }}"$'\n'
|
||
MESSAGE_TEXT="$MESSAGE_TEXT""⏰ 时间: $CURRENT_TIME"$'\n'
|
||
MESSAGE_TEXT="$MESSAGE_TEXT""🚀 触发方式: ${{ github.event_name }}"$'\n\n'
|
||
MESSAGE_TEXT="$MESSAGE_TEXT""━━━━━━━━━━━━━━━━━━━━━━"$'\n'
|
||
MESSAGE_TEXT="$MESSAGE_TEXT""📊 构建统计"$'\n'
|
||
MESSAGE_TEXT="$MESSAGE_TEXT""━━━━━━━━━━━━━━━━━━━━━━"$'\n'
|
||
MESSAGE_TEXT="$MESSAGE_TEXT""📁 文件总数: $FILE_COUNT"$'\n'
|
||
MESSAGE_TEXT="$MESSAGE_TEXT""💾 总大小: $TOTAL_SIZE"$'\n\n'
|
||
MESSAGE_TEXT="$MESSAGE_TEXT""━━━━━━━━━━━━━━━━━━━━━━"$'\n'
|
||
MESSAGE_TEXT="$MESSAGE_TEXT""⏱️ 耗时统计"$'\n'
|
||
MESSAGE_TEXT="$MESSAGE_TEXT""━━━━━━━━━━━━━━━━━━━━━━"$'\n'
|
||
MESSAGE_TEXT="$MESSAGE_TEXT""🔍 Pre-check: ${PRECHECK_DUR}s"$'\n'
|
||
MESSAGE_TEXT="$MESSAGE_TEXT""🏗️ Build: ${BUILD_DUR}s"$'\n'
|
||
MESSAGE_TEXT="$MESSAGE_TEXT""🌐 EdgeOne: ${EDGEONE_DUR}s$EDGEONE_DETAIL"$'\n'
|
||
MESSAGE_TEXT="$MESSAGE_TEXT""☁️ UpYun 直传: ${UPYUN_DUR_TEXT}"$'\n\n'
|
||
MESSAGE_TEXT="$MESSAGE_TEXT""━━━━━━━━━━━━━━━━━━━━━━"$'\n'
|
||
MESSAGE_TEXT="$MESSAGE_TEXT""📌 部署状态"$'\n'
|
||
MESSAGE_TEXT="$MESSAGE_TEXT""━━━━━━━━━━━━━━━━━━━━━━"$'\n'
|
||
MESSAGE_TEXT="$MESSAGE_TEXT""🌐 EdgeOne Pages: $EDGEONE_STATUS"$'\n'
|
||
MESSAGE_TEXT="$MESSAGE_TEXT""☁️ UpYun: $UPYUN_TEXT"$'\n\n'
|
||
if [ "${{ github.server_url }}" = "https://github.com" ]; then
|
||
RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||
else
|
||
RUN_URL="http://23.254.236.47:3001/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||
fi
|
||
MESSAGE_TEXT="$MESSAGE_TEXT""🔗 查看详情: $RUN_URL"
|
||
|
||
echo "📤 发送飞书通知..."
|
||
echo "Message length: ${#MESSAGE_TEXT}"
|
||
|
||
RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" -X POST "$FEISHU_WEBHOOK_URL" \
|
||
-H "Content-Type: application/json" \
|
||
-d "$(jq -n --arg text "$MESSAGE_TEXT" '{"msg_type":"text","content":{"text":$text}}')")
|
||
|
||
echo "飞书响应: $RESPONSE"
|
||
|
||
- name: Get current date (CN)
|
||
if: steps.deploy-status.outputs.any_deployed == 'true'
|
||
id: date
|
||
run: echo "date=$(date +'%Y-%m-%d %H:%M:%S')" >> $GITHUB_OUTPUT
|
||
|
||
- name: Send email notification with detailed timing
|
||
if: steps.deploy-status.outputs.any_deployed == 'true'
|
||
uses: dawidd6/action-send-mail@v3
|
||
with:
|
||
server_address: smtp.qq.com
|
||
server_port: 465
|
||
secure: true
|
||
username: ${{ secrets.MAIL_USERNAME }}
|
||
password: ${{ secrets.MAIL_PASSWORD }}
|
||
subject: "🎉 部署成功 - ${{ github.repository }} - ${{ steps.date.outputs.date }}"
|
||
to: ${{ secrets.MAIL_USERNAME }}
|
||
from: GitHub Actions <${{ github.event_name }}>
|
||
body: |
|
||
🎉 部署成功 🎉
|
||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
|
||
📦 项目信息
|
||
──────────────────────────────────
|
||
项目: ${{ github.repository }}
|
||
分支: ${{ github.ref_name }}
|
||
Commit: ${{ github.sha }}
|
||
⏰ 时间: ${{ steps.date.outputs.date }}
|
||
🚀 触发方式: ${{ github.event_name }}
|
||
|
||
📊 构建统计
|
||
──────────────────────────────────
|
||
📁 文件总数: ${{ needs.build.outputs.file_count }}
|
||
💾 总大小: ${{ needs.build.outputs.total_size }}
|
||
|
||
⏱️ 耗时统计
|
||
──────────────────────────────────
|
||
🔍 Pre-check: ${{ needs.pre-check.outputs.duration }}s
|
||
🏗️ Build: ${{ needs.build.outputs.duration }}s
|
||
|
||
🌐 EdgeOne Pages: ${{ needs.deploy-edgeone.outputs.duration }}s
|
||
📥 下载 COS: ${{ needs.deploy-edgeone.outputs.download_duration }}s
|
||
📤 上传部署: ${{ needs.deploy-edgeone.outputs.upload_duration }}s
|
||
|
||
☁️ UpYun 直传: ${{ needs.build.outputs.upyun_status == 'success' && '✅ 成功' || (needs.build.outputs.upyun_status == 'skipped' && '⏭️ 跳过(无变化)' || '❌ 失败(国内中转机兜底)') }}
|
||
|
||
📌 部署状态
|
||
──────────────────────────────────
|
||
🌐 EdgeOne Pages: ${{ needs.deploy-edgeone.outputs.deployed == 'true' && '✅ 已部署' || '⏭️ 跳过(无变化)' }}
|
||
☁️ UpYun: ${{ needs.build.outputs.upyun_status == 'success' && '✅ 已直传' || (needs.build.outputs.upyun_status == 'skipped' && '⏭️ 跳过(无变化)' || '❌ 直传失败(中转机兜底)') }}
|
||
|
||
🔗 查看详情
|
||
──────────────────────────────────
|
||
${{ github.server_url == 'https://github.com' && 'https://github.com/zqlit/blog' || 'http://23.254.236.47:3001/zqlit/blog' }}/actions/runs/${{ github.run_id }}
|
||
|
||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
|
||
- name: Generate timing summary
|
||
id: timing-summary
|
||
run: |
|
||
end_time=$(date +%s)
|
||
start_time=${{ steps.start-finalize.outputs.start_time }}
|
||
finalize_duration=$((end_time - start_time))
|
||
|
||
PRECHECK_DUR="${{ needs.pre-check.outputs.duration }}"
|
||
BUILD_DUR="${{ needs.build.outputs.duration }}"
|
||
EDGEONE_DUR="${{ needs.deploy-edgeone.outputs.duration }}"
|
||
EDGEONE_DEPLOYED="${{ needs.deploy-edgeone.outputs.deployed }}"
|
||
UPYUN_DUR="${{ needs.build.outputs.upyun_duration }}"
|
||
|
||
if [ "$EDGEONE_DEPLOYED" = "true" ]; then
|
||
EDGEONE_DOWNLOAD="${{ needs.deploy-edgeone.outputs.download_duration }}"
|
||
EDGEONE_UPLOAD="${{ needs.deploy-edgeone.outputs.upload_duration }}"
|
||
else
|
||
EDGEONE_DOWNLOAD="0"
|
||
EDGEONE_UPLOAD="0"
|
||
fi
|
||
|
||
UPYUN_DUR_CELL="${UPYUN_DUR}s"; [ -z "$UPYUN_DUR" ] && UPYUN_DUR_CELL="—"
|
||
|
||
echo "## 📊 部署报告" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "### 基本信息" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **项目**: ${{ github.repository }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **分支**: ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Commit**: ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **触发方式**: ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **文件总数**: ${{ needs.build.outputs.file_count }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **总大小**: ${{ needs.build.outputs.total_size }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
|
||
echo "### ⏱️ 耗时统计" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "| 阶段 | 总耗时 | 下载 | 上传 |" >> $GITHUB_STEP_SUMMARY
|
||
echo "|------|--------|------|------|" >> $GITHUB_STEP_SUMMARY
|
||
echo "| Pre-check | ${PRECHECK_DUR}s | - | - |" >> $GITHUB_STEP_SUMMARY
|
||
echo "| Build | ${BUILD_DUR}s | - | - |" >> $GITHUB_STEP_SUMMARY
|
||
echo "| EdgeOne | ${EDGEONE_DUR}s | ${EDGEONE_DOWNLOAD}s | ${EDGEONE_UPLOAD}s |" >> $GITHUB_STEP_SUMMARY
|
||
echo "| UpYun 直传 | ${UPYUN_DUR_CELL} | - | ${UPYUN_DUR_CELL} |" >> $GITHUB_STEP_SUMMARY
|
||
echo "| Finalize | ${finalize_duration}s | - | - |" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
|
||
echo "### 🚀 部署状态" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "| 目标 | 状态 |" >> $GITHUB_STEP_SUMMARY
|
||
echo "|------|------|" >> $GITHUB_STEP_SUMMARY
|
||
echo "| EdgeOne Pages | ${{ needs.deploy-edgeone.outputs.deployed == 'true' && '✅ 已部署' || '⏭️ 跳过(无变化)' }} |" >> $GITHUB_STEP_SUMMARY
|
||
echo "| UpYun 直传 | ${{ needs.build.outputs.upyun_status == 'success' && '✅ 成功' || (needs.build.outputs.upyun_status == 'skipped' && '⏭️ 跳过(无变化)' || '❌ 失败(中转机兜底)') }} |" >> $GITHUB_STEP_SUMMARY
|
||
echo "| CDN 刷新 | ${{ steps.deploy-status.outputs.any_deployed == 'true' && '✅ 已刷新' || '⏭️ 跳过' }} |" >> $GITHUB_STEP_SUMMARY
|
||
|
||
# ============================================================
|
||
# Job 6: Failure Notification (失败通知)
|
||
# ============================================================
|
||
notify-failure:
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 3
|
||
permissions:
|
||
contents: read
|
||
needs: [pre-check, build, deploy-edgeone, finalize]
|
||
if: always() && (needs.pre-check.result == 'failure' || needs.build.result == 'failure' || needs.deploy-edgeone.result == 'failure' || needs.finalize.result == 'failure')
|
||
|
||
steps:
|
||
- name: Send Telegram failure notification
|
||
env:
|
||
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||
run: |
|
||
if [ -z "$TELEGRAM_BOT_TOKEN" ] || [ -z "$TELEGRAM_CHAT_ID" ]; then
|
||
echo "⚠️ Telegram secrets 未配置,跳过失败通知"
|
||
exit 0
|
||
fi
|
||
|
||
FAILURES=""
|
||
if [ "${{ needs.pre-check.result }}" = "failure" ]; then FAILURES=$(printf "%s\n 🔴 Pre-check" "$FAILURES"); fi
|
||
if [ "${{ needs.build.result }}" = "failure" ]; then FAILURES=$(printf "%s\n 🔴 Build" "$FAILURES"); fi
|
||
if [ "${{ needs.deploy-edgeone.result }}" = "failure" ]; then FAILURES=$(printf "%s\n 🔴 Deploy EdgeOne" "$FAILURES"); fi
|
||
if [ "${{ needs.build.outputs.upyun_status }}" = "failure" ]; then FAILURES=$(printf "%s\n ⚠️ UpYun 直传失败(不阻断,国内中转机兜底)" "$FAILURES"); fi
|
||
if [ "${{ needs.finalize.result }}" = "failure" ]; then FAILURES=$(printf "%s\n 🔴 Finalize" "$FAILURES"); fi
|
||
|
||
PRECHECK_STATUS=$([ "${{ needs.pre-check.result }}" = "success" ] && echo "✅ 成功" || ([ "${{ needs.pre-check.result }}" = "failure" ] && echo "❌ 失败" || echo "⏭️ 跳过"))
|
||
BUILD_STATUS=$([ "${{ needs.build.result }}" = "success" ] && echo "✅ 成功" || ([ "${{ needs.build.result }}" = "failure" ] && echo "❌ 失败" || echo "⏭️ 跳过"))
|
||
EDGEONE_STATUS=$([ "${{ needs.deploy-edgeone.result }}" = "success" ] && echo "✅ 成功" || ([ "${{ needs.deploy-edgeone.result }}" = "failure" ] && echo "❌ 失败" || echo "⏭️ 跳过"))
|
||
UPYUN_STATUS=$([ "${{ needs.build.outputs.upyun_status }}" = "success" ] && echo "✅ 直传成功" || ([ "${{ needs.build.outputs.upyun_status }}" = "failure" ] && echo "❌ 直传失败(中转兜底)" || echo "⏭️ 跳过(无变化)"))
|
||
FINALIZE_STATUS=$([ "${{ needs.finalize.result }}" = "success" ] && echo "✅ 成功" || ([ "${{ needs.finalize.result }}" = "failure" ] && echo "❌ 失败" || echo "⏭️ 跳过"))
|
||
|
||
CURRENT_TIME=$(date +'%Y-%m-%d %H:%M:%S')
|
||
|
||
MESSAGE="💥 *部署失败*"
|
||
MESSAGE="$MESSAGE"$'\n\n'
|
||
MESSAGE="$MESSAGE""📦 *项目*: \`${{ github.repository }}\`"$'\n'
|
||
MESSAGE="$MESSAGE""🌿 *分支*: \`${{ github.ref_name }}\`"$'\n'
|
||
MESSAGE="$MESSAGE""🔗 *Commit*: \`${{ github.sha }}\`"$'\n'
|
||
MESSAGE="$MESSAGE""⏰ *时间*: $CURRENT_TIME"$'\n'
|
||
MESSAGE="$MESSAGE""🚀 *触发方式*: ${{ github.event_name }}"$'\n\n'
|
||
MESSAGE="$MESSAGE""━━━━━━━━━━━━━━━━━━━━━━"$'\n'
|
||
MESSAGE="$MESSAGE""⚠️ *失败阶段*"$'\n'
|
||
MESSAGE="$MESSAGE""━━━━━━━━━━━━━━━━━━━━━━$FAILURES"$'\n\n'
|
||
MESSAGE="$MESSAGE""━━━━━━━━━━━━━━━━━━━━━━"$'\n'
|
||
MESSAGE="$MESSAGE""📊 *各阶段状态*"$'\n'
|
||
MESSAGE="$MESSAGE""━━━━━━━━━━━━━━━━━━━━━━"$'\n'
|
||
MESSAGE="$MESSAGE""🔍 Pre-check: $PRECHECK_STATUS"$'\n'
|
||
MESSAGE="$MESSAGE""🏗️ Build: $BUILD_STATUS"$'\n'
|
||
MESSAGE="$MESSAGE""🌐 Deploy EdgeOne: $EDGEONE_STATUS"$'\n'
|
||
MESSAGE="$MESSAGE""☁️ UpYun 直传: $UPYUN_STATUS"$'\n'
|
||
MESSAGE="$MESSAGE""📤 Finalize: $FINALIZE_STATUS"$'\n\n'
|
||
if [ "${{ github.server_url }}" = "https://github.com" ]; then
|
||
RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||
else
|
||
RUN_URL="http://23.254.236.47:3001/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||
fi
|
||
MESSAGE="$MESSAGE""🔗 [查看详情]($RUN_URL)"
|
||
|
||
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
|
||
-d chat_id="${TELEGRAM_CHAT_ID}" \
|
||
-d text="$MESSAGE" \
|
||
-d parse_mode="Markdown" \
|
||
-d disable_web_page_preview=true
|
||
|
||
- name: Send Feishu failure notification
|
||
env:
|
||
FEISHU_WEBHOOK_URL: ${{ secrets.FEISHU_WEBHOOK_URL }}
|
||
run: |
|
||
if [ -z "$FEISHU_WEBHOOK_URL" ]; then
|
||
echo "⚠️ 飞书 Webhook URL 未配置,跳过失败通知"
|
||
exit 0
|
||
fi
|
||
|
||
FAILURES=""
|
||
if [ "${{ needs.pre-check.result }}" = "failure" ]; then FAILURES=$(printf "%s\n 🔴 Pre-check" "$FAILURES"); fi
|
||
if [ "${{ needs.build.result }}" = "failure" ]; then FAILURES=$(printf "%s\n 🔴 Build" "$FAILURES"); fi
|
||
if [ "${{ needs.deploy-edgeone.result }}" = "failure" ]; then FAILURES=$(printf "%s\n 🔴 Deploy EdgeOne" "$FAILURES"); fi
|
||
if [ "${{ needs.build.outputs.upyun_status }}" = "failure" ]; then FAILURES=$(printf "%s\n ⚠️ UpYun 直传失败(不阻断,国内中转机兜底)" "$FAILURES"); fi
|
||
if [ "${{ needs.finalize.result }}" = "failure" ]; then FAILURES=$(printf "%s\n 🔴 Finalize" "$FAILURES"); fi
|
||
|
||
PRECHECK_STATUS=$([ "${{ needs.pre-check.result }}" = "success" ] && echo "✅ 成功" || ([ "${{ needs.pre-check.result }}" = "failure" ] && echo "❌ 失败" || echo "⏭️ 跳过"))
|
||
BUILD_STATUS=$([ "${{ needs.build.result }}" = "success" ] && echo "✅ 成功" || ([ "${{ needs.build.result }}" = "failure" ] && echo "❌ 失败" || echo "⏭️ 跳过"))
|
||
EDGEONE_STATUS=$([ "${{ needs.deploy-edgeone.result }}" = "success" ] && echo "✅ 成功" || ([ "${{ needs.deploy-edgeone.result }}" = "failure" ] && echo "❌ 失败" || echo "⏭️ 跳过"))
|
||
UPYUN_STATUS=$([ "${{ needs.build.outputs.upyun_status }}" = "success" ] && echo "✅ 直传成功" || ([ "${{ needs.build.outputs.upyun_status }}" = "failure" ] && echo "❌ 直传失败(中转兜底)" || echo "⏭️ 跳过(无变化)"))
|
||
FINALIZE_STATUS=$([ "${{ needs.finalize.result }}" = "success" ] && echo "✅ 成功" || ([ "${{ needs.finalize.result }}" = "failure" ] && echo "❌ 失败" || echo "⏭️ 跳过"))
|
||
|
||
CURRENT_TIME=$(date +'%Y-%m-%d %H:%M:%S')
|
||
|
||
MESSAGE_TEXT="💥 部署失败"
|
||
MESSAGE_TEXT="$MESSAGE_TEXT"$'\n\n'
|
||
MESSAGE_TEXT="$MESSAGE_TEXT""📦 项目: ${{ github.repository }}"$'\n'
|
||
MESSAGE_TEXT="$MESSAGE_TEXT""🌿 分支: ${{ github.ref_name }}"$'\n'
|
||
MESSAGE_TEXT="$MESSAGE_TEXT""🔗 Commit: ${{ github.sha }}"$'\n'
|
||
MESSAGE_TEXT="$MESSAGE_TEXT""⏰ 时间: $CURRENT_TIME"$'\n'
|
||
MESSAGE_TEXT="$MESSAGE_TEXT""🚀 触发方式: ${{ github.event_name }}"$'\n\n'
|
||
MESSAGE_TEXT="$MESSAGE_TEXT""━━━━━━━━━━━━━━━━━━━━━━"$'\n'
|
||
MESSAGE_TEXT="$MESSAGE_TEXT""⚠️ 失败阶段"$'\n'
|
||
MESSAGE_TEXT="$MESSAGE_TEXT""━━━━━━━━━━━━━━━━━━━━━━$FAILURES"$'\n\n'
|
||
MESSAGE_TEXT="$MESSAGE_TEXT""━━━━━━━━━━━━━━━━━━━━━━"$'\n'
|
||
MESSAGE_TEXT="$MESSAGE_TEXT""📊 各阶段状态"$'\n'
|
||
MESSAGE_TEXT="$MESSAGE_TEXT""━━━━━━━━━━━━━━━━━━━━━━"$'\n'
|
||
MESSAGE_TEXT="$MESSAGE_TEXT""🔍 Pre-check: $PRECHECK_STATUS"$'\n'
|
||
MESSAGE_TEXT="$MESSAGE_TEXT""🏗️ Build: $BUILD_STATUS"$'\n'
|
||
MESSAGE_TEXT="$MESSAGE_TEXT""🌐 Deploy EdgeOne: $EDGEONE_STATUS"$'\n'
|
||
MESSAGE_TEXT="$MESSAGE_TEXT""☁️ UpYun 直传: $UPYUN_STATUS"$'\n'
|
||
MESSAGE_TEXT="$MESSAGE_TEXT""📤 Finalize: $FINALIZE_STATUS"$'\n\n'
|
||
if [ "${{ github.server_url }}" = "https://github.com" ]; then
|
||
RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||
else
|
||
RUN_URL="http://23.254.236.47:3001/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||
fi
|
||
MESSAGE_TEXT="$MESSAGE_TEXT""🔗 查看详情: $RUN_URL"
|
||
|
||
curl -s -X POST "$FEISHU_WEBHOOK_URL" \
|
||
-H "Content-Type: application/json" \
|
||
-d "$(jq -n --arg text "$MESSAGE_TEXT" '{"msg_type":"text","content":{"text":$text}}')" |