794 lines
29 KiB
YAML
794 lines
29 KiB
YAML
name: Deploy to Production
|
||
|
||
on:
|
||
push:
|
||
branches:
|
||
- main
|
||
schedule:
|
||
- cron: '0 1 * * *' # UTC 01:00 = 北京时间 09:00
|
||
workflow_dispatch:
|
||
inputs:
|
||
skip_font_subset:
|
||
description: 'Skip font subset step'
|
||
type: boolean
|
||
default: true
|
||
|
||
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.repo-check.outputs.can_deploy }}
|
||
|
||
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
|
||
run: |
|
||
COMMIT_AUTHOR="${{ github.event.head_commit.author.name }}"
|
||
COMMIT_MESSAGE="${{ github.event.head_commit.message }}"
|
||
|
||
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: 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: Font Subset (字体子集化 - 可选并行任务)
|
||
# ============================================================
|
||
subset-fonts:
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 15
|
||
permissions:
|
||
contents: write
|
||
needs: pre-check
|
||
if: |
|
||
needs.pre-check.outputs.can_deploy == 'true' &&
|
||
inputs.skip_font_subset != 'true' &&
|
||
github.event_name == 'workflow_dispatch'
|
||
outputs:
|
||
font_updated: ${{ steps.update.outputs.font_updated }}
|
||
char_count: ${{ steps.stats.outputs.char_count }}
|
||
original_size: ${{ steps.stats.outputs.original_size }}
|
||
subset_size: ${{ steps.stats.outputs.subset_size }}
|
||
|
||
steps:
|
||
- name: Start timing
|
||
id: start-subset
|
||
run: echo "start_time=$(date +%s)" >> $GITHUB_OUTPUT
|
||
|
||
- name: Checkout code
|
||
uses: actions/checkout@v4
|
||
with:
|
||
fetch-depth: 0
|
||
|
||
- name: Setup Python
|
||
uses: actions/setup-python@v5
|
||
with:
|
||
python-version: '3.12'
|
||
|
||
- name: Install dependencies
|
||
run: pip install fonttools brotli zopfli
|
||
|
||
- name: Generate font subset
|
||
id: update
|
||
run: |
|
||
# 运行字体子集化脚本
|
||
# 会自动扫描:
|
||
# 1. 静态文件 (content, layouts, public)
|
||
# 2. 本地 JSON 数据 (友链、朋友圈)
|
||
# 3. 远程 API (api.usj.cc 的友链、订阅源、朋友圈)
|
||
python scripts/subset-font-safe.py
|
||
|
||
# 检查是否有变化
|
||
git config --global user.name "GitHub Actions"
|
||
git config --global user.email "actions@github.com"
|
||
|
||
git add themes/Ying/static/font/zql-v2-subset.woff2 themes/Ying/static/font/used_chars.txt
|
||
if ! git diff --staged --quiet; then
|
||
git commit -m "chore: update font subset [skip ci]"
|
||
git push origin HEAD:main
|
||
echo "font_updated=true" >> $GITHUB_OUTPUT
|
||
echo "✅ 字体子集已更新并推送"
|
||
else
|
||
echo "font_updated=false" >> $GITHUB_OUTPUT
|
||
echo "ℹ️ 字体子集无变化,跳过提交"
|
||
fi
|
||
|
||
- name: Collect stats
|
||
id: stats
|
||
if: always()
|
||
run: |
|
||
CHAR_COUNT=$(wc -m < themes/Ying/static/font/used_chars.txt | tr -d ' ')
|
||
ORIGINAL_SIZE=$(stat -c%s themes/Ying/static/font/zql-v2.woff2)
|
||
SUBSET_SIZE=$(stat -c%s themes/Ying/static/font/zql-v2-subset.woff2)
|
||
echo "char_count=$CHAR_COUNT" >> $GITHUB_OUTPUT
|
||
echo "original_size=$ORIGINAL_SIZE" >> $GITHUB_OUTPUT
|
||
echo "subset_size=$SUBSET_SIZE" >> $GITHUB_OUTPUT
|
||
|
||
- name: Create font subset summary
|
||
if: always()
|
||
run: |
|
||
ORIGINAL_SIZE_KB=$(echo "scale=1; ${{ steps.stats.outputs.original_size }} / 1024" | bc)
|
||
SUBSET_SIZE_KB=$(echo "scale=1; ${{ steps.stats.outputs.subset_size }} / 1024" | bc)
|
||
REDUCTION_KB=$(echo "scale=1; (${{ steps.stats.outputs.original_size }} - ${{ steps.stats.outputs.subset_size }}) / 1024" | bc)
|
||
PERCENTAGE=$(echo "scale=1; (${{ steps.stats.outputs.original_size }} - ${{ steps.stats.outputs.subset_size }}) * 100 / ${{ steps.stats.outputs.original_size }}" | bc)
|
||
|
||
echo "## 🔤 字体子集化" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
|
||
if [ "${{ steps.update.outputs.font_updated }}" = "true" ]; then
|
||
echo "✅ **字体子集已更新**" >> $GITHUB_STEP_SUMMARY
|
||
else
|
||
echo "ℹ️ **字体子集无变化**" >> $GITHUB_STEP_SUMMARY
|
||
fi
|
||
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "### 📊 统计信息" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **字符数:** ${{ steps.stats.outputs.char_count }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **原始大小:** ${ORIGINAL_SIZE_KB} KB" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **子集大小:** ${SUBSET_SIZE_KB} KB" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **节省空间:** ${REDUCTION_KB} KB (${PERCENTAGE}%)" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "### 🌐 扫描的数据源" >> $GITHUB_STEP_SUMMARY
|
||
echo "- 静态文件: content, layouts, public" >> $GITHUB_STEP_SUMMARY
|
||
echo "- 本地 JSON: link_lite.json, friend_circle_data.json" >> $GITHUB_STEP_SUMMARY
|
||
echo "- 远程 API: api.usj.cc (links, feeds, articles)" >> $GITHUB_STEP_SUMMARY
|
||
|
||
- name: Send font subset notification
|
||
if: steps.update.outputs.font_updated == 'true'
|
||
env:
|
||
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||
FEISHU_WEBHOOK_URL: ${{ secrets.FEISHU_WEBHOOK_URL }}
|
||
run: |
|
||
ORIGINAL_SIZE_KB=$(echo "scale=1; ${{ steps.stats.outputs.original_size }} / 1024" | bc)
|
||
SUBSET_SIZE_KB=$(echo "scale=1; ${{ steps.stats.outputs.subset_size }} / 1024" | bc)
|
||
REDUCTION_KB=$(echo "scale=1; (${{ steps.stats.outputs.original_size }} - ${{ steps.stats.outputs.subset_size }}) / 1024" | bc)
|
||
PERCENTAGE=$(echo "scale=1; (${{ steps.stats.outputs.original_size }} - ${{ steps.stats.outputs.subset_size }}) * 100 / ${{ steps.stats.outputs.original_size }}" | bc)
|
||
|
||
if [ -n "$TELEGRAM_BOT_TOKEN" ] && [ -n "$TELEGRAM_CHAT_ID" ]; then
|
||
MESSAGE="✅ **字体子集化完成**
|
||
|
||
📦 项目: ${{ github.repository }}
|
||
🌿 分支: ${{ github.ref_name }}
|
||
|
||
📊 优化统计:
|
||
- 字符数: ${{ steps.stats.outputs.char_count }}
|
||
- 原始大小: ${ORIGINAL_SIZE_KB} KB
|
||
- 子集大小: ${SUBSET_SIZE_KB} KB
|
||
- 节省空间: ${REDUCTION_KB} KB (${PERCENTAGE}%)
|
||
|
||
🌐 数据源:
|
||
- 静态文件 + 本地JSON + 远程API"
|
||
|
||
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"
|
||
fi
|
||
|
||
if [ -n "$FEISHU_WEBHOOK_URL" ]; then
|
||
curl -s -X POST "${FEISHU_WEBHOOK_URL}" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{
|
||
"msg_type": "text",
|
||
"content": {
|
||
"text": "✅ **字体子集化完成**\n\n📦 项目: ${{ github.repository }}\n🌿 分支: ${{ github.ref_name }}\n\n📊 优化统计:\n- 字符数: ${{ steps.stats.outputs.char_count }}\n- 原始大小: '"${ORIGINAL_SIZE_KB}"' KB\n- 子集大小: '"${SUBSET_SIZE_KB}"' KB\n- 节省空间: '"${REDUCTION_KB}"' KB ('"${PERCENTAGE}"'%)\n\n🌐 数据源: 静态文件 + 本地JSON + 远程API"
|
||
}
|
||
}'
|
||
fi
|
||
|
||
- name: End timing
|
||
id: end-subset
|
||
run: |
|
||
end_time=$(date +%s)
|
||
start_time=${{ steps.start-subset.outputs.start_time }}
|
||
duration=$((end_time - start_time))
|
||
echo "duration=${duration}" >> $GITHUB_OUTPUT
|
||
echo "## ⏱️ Subset-fonts 耗时: ${duration}秒" >> $GITHUB_STEP_SUMMARY
|
||
|
||
# ============================================================
|
||
# Job 2: Build (构建)
|
||
# ============================================================
|
||
build:
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 15
|
||
permissions:
|
||
contents: write
|
||
outputs:
|
||
build_hash: ${{ steps.hash.outputs.build_hash }}
|
||
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
|
||
|
||
- 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'
|
||
cache: 'npm'
|
||
|
||
- name: Install Node.js dependencies
|
||
run: npm ci
|
||
|
||
- name: Cache Hugo resources
|
||
uses: actions/cache@v4
|
||
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: pwsh
|
||
run: ./scripts/add_draft_to_hidden.ps1
|
||
|
||
- 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 && hugo --minify
|
||
|
||
- name: Generate build hash
|
||
id: hash
|
||
run: echo "build_hash=$(find public -type f -print0 | sort -z | xargs -0 sha256sum | sha256sum | awk '{print $1}')" >> $GITHUB_OUTPUT
|
||
|
||
- name: Upload build artifact
|
||
uses: actions/upload-artifact@v4
|
||
with:
|
||
name: hugo-public
|
||
path: public/
|
||
retention-days: 1
|
||
compression-level: 0
|
||
|
||
- 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
|
||
|
||
# ============================================================
|
||
# Job 3: Deploy to EdgeOne Pages (并行)
|
||
# ============================================================
|
||
deploy-edgeone:
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 10
|
||
permissions:
|
||
contents: read
|
||
needs: build
|
||
outputs:
|
||
deployed: ${{ steps.check.outputs.deployed }}
|
||
|
||
steps:
|
||
- name: Start timing
|
||
id: start-edgeone
|
||
run: echo "start_time=$(date +%s)" >> $GITHUB_OUTPUT
|
||
|
||
- name: Check build hash cache
|
||
id: cache-check
|
||
uses: actions/cache/restore@v4
|
||
with:
|
||
path: .last_deploy_hash
|
||
key: ${{ runner.os }}-deploy-edgeone-${{ needs.build.outputs.build_hash }}
|
||
restore-keys: |
|
||
${{ runner.os }}-deploy-edgeone-
|
||
|
||
- name: Check if deployment is needed
|
||
id: check
|
||
run: |
|
||
CURRENT_HASH="${{ needs.build.outputs.build_hash }}"
|
||
echo "当前 build_hash: $CURRENT_HASH"
|
||
|
||
if [ -f .last_deploy_hash ]; then
|
||
LAST_HASH=$(cat .last_deploy_hash)
|
||
echo "上次部署 hash: $LAST_HASH"
|
||
if [ "$CURRENT_HASH" = "$LAST_HASH" ]; then
|
||
echo "✅ 构建产物无变化,跳过 EdgeOne 部署"
|
||
echo "deployed=false" >> $GITHUB_OUTPUT
|
||
echo "skip_reason=hash_match" >> $GITHUB_OUTPUT
|
||
exit 0
|
||
fi
|
||
fi
|
||
|
||
echo "🔄 构建产物有变化,需要部署到 EdgeOne"
|
||
echo "deployed=true" >> $GITHUB_OUTPUT
|
||
|
||
- name: Checkout code (for scripts)
|
||
if: steps.check.outputs.deployed == 'true'
|
||
uses: actions/checkout@v4
|
||
with:
|
||
fetch-depth: 0
|
||
|
||
- name: Download build artifact
|
||
if: steps.check.outputs.deployed == 'true'
|
||
uses: actions/download-artifact@v4
|
||
with:
|
||
name: hugo-public
|
||
path: public/
|
||
|
||
- name: Deploy to EdgeOne Pages
|
||
if: steps.check.outputs.deployed == 'true'
|
||
env:
|
||
EDGEONE_API_TOKEN: ${{ secrets.EDGEONE_API_TOKEN }}
|
||
run: |
|
||
chmod +x scripts/deploy_edgeone.sh
|
||
./scripts/deploy_edgeone.sh hugo-blog ./public overseas
|
||
|
||
- name: Save deploy hash
|
||
if: steps.check.outputs.deployed == 'true'
|
||
run: echo "${{ needs.build.outputs.build_hash }}" > .last_deploy_hash
|
||
|
||
- name: Save build hash cache
|
||
if: steps.check.outputs.deployed == 'true'
|
||
uses: actions/cache/save@v4
|
||
with:
|
||
path: .last_deploy_hash
|
||
key: ${{ runner.os }}-deploy-edgeone-${{ needs.build.outputs.build_hash }}
|
||
|
||
- 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
|
||
else
|
||
echo "## EdgeOne 部署已跳过(构建产物无变化)" >> $GITHUB_STEP_SUMMARY
|
||
echo "总耗时: ${duration}秒" >> $GITHUB_STEP_SUMMARY
|
||
fi
|
||
|
||
# ============================================================
|
||
# Job 4: Deploy to UpYun (并行)
|
||
# ============================================================
|
||
deploy-upyun:
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 20
|
||
permissions:
|
||
contents: read
|
||
needs: build
|
||
outputs:
|
||
deployed: ${{ steps.check.outputs.deployed }}
|
||
|
||
steps:
|
||
- name: Start timing
|
||
id: start-upyun
|
||
run: echo "start_time=$(date +%s)" >> $GITHUB_OUTPUT
|
||
|
||
- name: Check build hash cache
|
||
id: cache-check
|
||
uses: actions/cache/restore@v4
|
||
with:
|
||
path: .last_deploy_hash
|
||
key: ${{ runner.os }}-deploy-upyun-${{ needs.build.outputs.build_hash }}
|
||
restore-keys: |
|
||
${{ runner.os }}-deploy-upyun-
|
||
|
||
- name: Check if deployment is needed
|
||
id: check
|
||
run: |
|
||
CURRENT_HASH="${{ needs.build.outputs.build_hash }}"
|
||
echo "当前 build_hash: $CURRENT_HASH"
|
||
|
||
if [ -f .last_deploy_hash ]; then
|
||
LAST_HASH=$(cat .last_deploy_hash)
|
||
echo "上次部署 hash: $LAST_HASH"
|
||
if [ "$CURRENT_HASH" = "$LAST_HASH" ]; then
|
||
echo "✅ 构建产物无变化,跳过 UpYun 部署"
|
||
echo "deployed=false" >> $GITHUB_OUTPUT
|
||
echo "skip_reason=hash_match" >> $GITHUB_OUTPUT
|
||
exit 0
|
||
fi
|
||
fi
|
||
|
||
echo "🔄 构建产物有变化,需要部署到 UpYun"
|
||
echo "deployed=true" >> $GITHUB_OUTPUT
|
||
|
||
- name: Start artifact download timing
|
||
if: steps.check.outputs.deployed == 'true'
|
||
id: start-upyun-download
|
||
run: echo "start_time=$(date +%s)" >> $GITHUB_OUTPUT
|
||
|
||
- name: Download build artifact
|
||
if: steps.check.outputs.deployed == 'true'
|
||
uses: actions/download-artifact@v4
|
||
with:
|
||
name: hugo-public
|
||
path: public/
|
||
|
||
- name: End artifact download timing
|
||
if: steps.check.outputs.deployed == 'true'
|
||
id: end-upyun-download
|
||
run: |
|
||
end_time=$(date +%s)
|
||
start_time=${{ steps.start-upyun-download.outputs.start_time }}
|
||
duration=$((end_time - start_time))
|
||
echo "duration=${duration}" >> $GITHUB_OUTPUT
|
||
echo "## UpYun artifact download duration: ${duration}s" >> $GITHUB_STEP_SUMMARY
|
||
|
||
- name: Start UpYun upload timing
|
||
if: steps.check.outputs.deployed == 'true'
|
||
id: start-upyun-upload
|
||
run: echo "start_time=$(date +%s)" >> $GITHUB_OUTPUT
|
||
|
||
- name: Checkout code (for scripts)
|
||
if: steps.check.outputs.deployed == 'true'
|
||
uses: actions/checkout@v4
|
||
with:
|
||
fetch-depth: 1
|
||
|
||
- name: Deploy to UpYun
|
||
if: steps.check.outputs.deployed == 'true'
|
||
env:
|
||
UPYUN_SERVICE: ${{ secrets.UPYUN_BUCKET }}
|
||
UPYUN_OPERATOR: ${{ secrets.UPYUN_OPERATOR }}
|
||
UPYUN_PASSWORD: ${{ secrets.UPYUN_PASSWORD }}
|
||
run: |
|
||
chmod +x scripts/deploy_upyun.sh
|
||
./scripts/deploy_upyun.sh ./public / 20
|
||
|
||
- name: Save deploy hash
|
||
if: steps.check.outputs.deployed == 'true'
|
||
run: echo "${{ needs.build.outputs.build_hash }}" > .last_deploy_hash
|
||
|
||
- name: Save build hash cache
|
||
if: steps.check.outputs.deployed == 'true'
|
||
uses: actions/cache/save@v4
|
||
with:
|
||
path: .last_deploy_hash
|
||
key: ${{ runner.os }}-deploy-upyun-${{ needs.build.outputs.build_hash }}
|
||
|
||
- name: End timing
|
||
id: end-upyun
|
||
run: |
|
||
end_time=$(date +%s)
|
||
total_start_time=${{ steps.start-upyun.outputs.start_time }}
|
||
total_duration=$((end_time - total_start_time))
|
||
echo "duration=${total_duration}" >> $GITHUB_OUTPUT
|
||
|
||
if [ "${{ steps.check.outputs.deployed }}" = "true" ]; then
|
||
upload_start_time=${{ steps.start-upyun-upload.outputs.start_time }}
|
||
upload_duration=$((end_time - upload_start_time))
|
||
echo "upload_duration=${upload_duration}" >> $GITHUB_OUTPUT
|
||
echo "## Deploy UpYun total duration: ${total_duration}s" >> $GITHUB_STEP_SUMMARY
|
||
echo "## UpYun upload duration: ${upload_duration}s" >> $GITHUB_STEP_SUMMARY
|
||
else
|
||
echo "## UpYun 部署已跳过(构建产物无变化)" >> $GITHUB_STEP_SUMMARY
|
||
echo "总耗时: ${total_duration}s" >> $GITHUB_STEP_SUMMARY
|
||
fi
|
||
|
||
# ============================================================
|
||
# Job 5: CDN Refresh & Notifications (after all deployments)
|
||
# ============================================================
|
||
finalize:
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 5
|
||
permissions:
|
||
contents: read
|
||
needs: [pre-check, subset-fonts, build, deploy-edgeone, deploy-upyun]
|
||
if: always() && needs.build.result == 'success' && needs.deploy-edgeone.result == 'success' && needs.deploy-upyun.result == 'success'
|
||
|
||
steps:
|
||
- name: Start timing
|
||
id: start-finalize
|
||
run: echo "start_time=$(date +%s)" >> $GITHUB_OUTPUT
|
||
|
||
- name: Checkout code
|
||
uses: actions/checkout@v4
|
||
with:
|
||
fetch-depth: 0
|
||
|
||
- name: Setup Node
|
||
uses: actions/setup-node@v4
|
||
with:
|
||
node-version: '22'
|
||
cache: 'npm'
|
||
|
||
- name: Install Node.js dependencies
|
||
run: npm ci
|
||
|
||
- name: Determine deployment status
|
||
id: deploy-status
|
||
run: |
|
||
EDGEONE_DEPLOYED="${{ needs.deploy-edgeone.outputs.deployed }}"
|
||
UPYUN_DEPLOYED="${{ needs.deploy-upyun.outputs.deployed }}"
|
||
|
||
echo "EdgeOne deployed: $EDGEONE_DEPLOYED"
|
||
echo "UpYun deployed: $UPYUN_DEPLOYED"
|
||
|
||
if [ "$EDGEONE_DEPLOYED" = "true" ] || [ "$UPYUN_DEPLOYED" = "true" ]; 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: node scripts/refresh_cdn.js
|
||
|
||
- name: Generate Notification Report
|
||
if: steps.deploy-status.outputs.any_deployed == 'true'
|
||
continue-on-error: true
|
||
run: node scripts/generate_notification_report.js
|
||
|
||
- name: Send Telegram 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
|
||
|
||
EDGEONE_STATUS="${{ needs.deploy-edgeone.outputs.deployed == 'true' && '✅' || '⏭️ 跳过' }}"
|
||
UPYUN_STATUS="${{ needs.deploy-upyun.outputs.deployed == 'true' && '✅' || '⏭️ 跳过' }}"
|
||
CDN_STATUS="${{ steps.deploy-status.outputs.any_deployed == 'true' && '✅' || '⏭️ 跳过' }}"
|
||
|
||
if [ "${{ steps.deploy-status.outputs.any_deployed }}" = "true" ]; then
|
||
TITLE="✅ **部署成功**"
|
||
else
|
||
TITLE="ℹ️ **无变化,跳过部署**"
|
||
fi
|
||
|
||
MESSAGE="${TITLE}
|
||
|
||
📦 项目: ${{ github.repository }}
|
||
🌿 分支: ${{ github.ref_name }}
|
||
🔗 Commit: ${{ github.sha }}
|
||
🕐 时间: $(date +'%Y-%m-%d %H:%M:%S')
|
||
|
||
📊 部署目标:
|
||
- EdgeOne Pages ${EDGEONE_STATUS}
|
||
- UpYun ${UPYUN_STATUS}
|
||
- CDN 刷新 ${CDN_STATUS}"
|
||
|
||
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"
|
||
|
||
- name: Send Feishu notification
|
||
env:
|
||
FEISHU_WEBHOOK_URL: ${{ secrets.FEISHU_WEBHOOK_URL }}
|
||
run: |
|
||
if [ -z "$FEISHU_WEBHOOK_URL" ]; then
|
||
echo "⚠️ 飞书 Webhook URL 未配置,跳过通知"
|
||
exit 0
|
||
fi
|
||
|
||
EDGEONE_STATUS="${{ needs.deploy-edgeone.outputs.deployed == 'true' && '✅' || '⏭️ 跳过' }}"
|
||
UPYUN_STATUS="${{ needs.deploy-upyun.outputs.deployed == 'true' && '✅' || '⏭️ 跳过' }}"
|
||
CDN_STATUS="${{ steps.deploy-status.outputs.any_deployed == 'true' && '✅' || '⏭️ 跳过' }}"
|
||
|
||
if [ "${{ steps.deploy-status.outputs.any_deployed }}" = "true" ]; then
|
||
TITLE="✅ **部署成功**"
|
||
else
|
||
TITLE="ℹ️ **无变化,跳过部署**"
|
||
fi
|
||
|
||
CURRENT_TIME=$(date +'%Y-%m-%d %H:%M:%S')
|
||
curl -s -X POST "${FEISHU_WEBHOOK_URL}" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{
|
||
"msg_type": "text",
|
||
"content": {
|
||
"text": "'"${TITLE}"'\n\n📦 项目: ${{ github.repository }}\n🌿 分支: ${{ github.ref_name }}\n🔗 Commit: ${{ github.sha }}\n🕐 时间: '"${CURRENT_TIME}"'\n\n📊 部署目标:\n- EdgeOne Pages '"${EDGEONE_STATUS}"'\n- UpYun '"${UPYUN_STATUS}"'\n- CDN 刷新 '"${CDN_STATUS}"'"
|
||
}
|
||
}'
|
||
|
||
- 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 deployment notification
|
||
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: 部署报告 - ${{ steps.date.outputs.date }}
|
||
to: ${{ secrets.MAIL_USERNAME }}
|
||
from: GitHub Actions <${{ github.event_name }}>
|
||
body: |
|
||
${{ env.DEPLOY_REPORT }}
|
||
|
||
- 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))
|
||
|
||
EDGEONE_DEPLOYED="${{ needs.deploy-edgeone.outputs.deployed }}"
|
||
UPYUN_DEPLOYED="${{ needs.deploy-upyun.outputs.deployed }}"
|
||
|
||
if [ "$EDGEONE_DEPLOYED" = "true" ]; then
|
||
EDGEONE_DISPLAY="✅ 已部署"
|
||
else
|
||
EDGEONE_DISPLAY="⏭️ 跳过(无变化)"
|
||
fi
|
||
|
||
if [ "$UPYUN_DEPLOYED" = "true" ]; then
|
||
UPYUN_DISPLAY="✅ 已部署"
|
||
else
|
||
UPYUN_DISPLAY="⏭️ 跳过(无变化)"
|
||
fi
|
||
|
||
echo "## ⏱️ 部署耗时统计" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "| 阶段 | 状态 |" >> $GITHUB_STEP_SUMMARY
|
||
echo "|------|------|" >> $GITHUB_STEP_SUMMARY
|
||
echo "| Pre-check | ${{ needs.pre-check.result == 'success' && '✅ 成功' || needs.pre-check.result == 'skipped' && '⏭️ 跳过' || '❌ 失败' }} |" >> $GITHUB_STEP_SUMMARY
|
||
echo "| Subset-fonts | ${{ needs.subset-fonts.result == 'success' && '✅ 成功' || needs.subset-fonts.result == 'skipped' && '⏭️ 跳过' || '❌ 失败' }} |" >> $GITHUB_STEP_SUMMARY
|
||
echo "| Build | ${{ needs.build.result == 'success' && '✅ 成功' || '❌ 失败' }} |" >> $GITHUB_STEP_SUMMARY
|
||
echo "| Deploy EdgeOne | ${EDGEONE_DISPLAY} |" >> $GITHUB_STEP_SUMMARY
|
||
echo "| Deploy UpYun | ${UPYUN_DISPLAY} |" >> $GITHUB_STEP_SUMMARY
|
||
echo "| Finalize | ✅ 成功 (${finalize_duration}秒) |" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "**Finalize 阶段耗时:** ${finalize_duration}秒" >> $GITHUB_STEP_SUMMARY
|
||
|
||
# ============================================================
|
||
# Job 6: Failure Notification (失败通知)
|
||
# ============================================================
|
||
notify-failure:
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 3
|
||
permissions:
|
||
contents: read
|
||
needs: [pre-check, subset-fonts, build, deploy-edgeone, deploy-upyun, finalize]
|
||
if: always() && (needs.pre-check.result == 'failure' || needs.build.result == 'failure' || needs.deploy-edgeone.result == 'failure' || needs.deploy-upyun.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
|
||
|
||
MESSAGE="❌ **部署失败**
|
||
|
||
📦 项目: ${{ github.repository }}
|
||
🌿 分支: ${{ github.ref_name }}
|
||
🔗 Commit: ${{ github.sha }}
|
||
🕐 时间: $(date +'%Y-%m-%d %H:%M:%S')
|
||
|
||
📝 工作流: ${{ github.workflow }}
|
||
🔍 查看详情: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||
|
||
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"
|
||
|
||
- 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
|
||
|
||
CURRENT_TIME=$(date +'%Y-%m-%d %H:%M:%S')
|
||
curl -s -X POST "${FEISHU_WEBHOOK_URL}" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{
|
||
"msg_type": "text",
|
||
"content": {
|
||
"text": "❌ **部署失败**\n\n📦 项目: ${{ github.repository }}\n🌿 分支: ${{ github.ref_name }}\n🔗 Commit: ${{ github.sha }}\n🕐 时间: '"${CURRENT_TIME}"'\n\n📝 工作流: ${{ github.workflow }}\n🔍 查看详情: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||
}
|
||
}' |