name: Deploy to Production on: push: branches: - main schedule: - cron: '0 1 * * *' # UTC 01:00 = 北京时间 09:00 workflow_dispatch: inputs: force_font_subset: description: 'Force font subset regeneration' type: boolean default: false skip_font_subset: description: 'Skip font subset step' type: boolean default: false 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.check.outputs.can_deploy }} steps: - 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 environment variables id: env-check run: | REQUIRED_SECRETS=( "EDGEONE_API_TOKEN" "UPYUN_BUCKET" "UPYUN_OPERATOR" "UPYUN_PASSWORD" "DOGECLOUD_ACCESS_KEY" "DOGECLOUD_SECRET_KEY" "MAIL_USERNAME" "MAIL_PASSWORD" ) MISSING="" for SECRET in "${REQUIRED_SECRETS[@]}"; do if [ -z "${{ secrets[SECRET] }}" ]; then MISSING="$MISSING $SECRET" fi done if [ -n "$MISSING" ]; then echo "❌ 缺少必要的环境变量: $MISSING" exit 1 fi echo "✅ 所有必要环境变量已配置" - 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 # ============================================================ # 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' 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: 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 # ============================================================ # 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: 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 - 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 # ============================================================ # Job 3: Deploy to EdgeOne Pages (并行) # ============================================================ deploy-edgeone: runs-on: ubuntu-latest timeout-minutes: 10 permissions: contents: read needs: build steps: - name: Checkout code (for scripts) uses: actions/checkout@v4 with: fetch-depth: 0 - name: Download build artifact uses: actions/download-artifact@v4 with: name: hugo-public path: public/ - name: Deploy to EdgeOne Pages env: EDGEONE_API_TOKEN: ${{ secrets.EDGEONE_API_TOKEN }} run: | chmod +x scripts/deploy_edgeone.sh ./scripts/deploy_edgeone.sh hugo-blog ./public overseas # ============================================================ # Job 4: Deploy to UpYun (并行) # ============================================================ deploy-upyun: runs-on: ubuntu-latest timeout-minutes: 10 permissions: contents: read needs: build steps: - name: Download build artifact uses: actions/download-artifact@v4 with: name: hugo-public path: public/ - name: Deploy to UpYun uses: her-cat/upyun-deployer@v1.0.3 with: bucket: ${{ secrets.UPYUN_BUCKET }} operator: ${{ secrets.UPYUN_OPERATOR }} password: ${{ secrets.UPYUN_PASSWORD }} dir: 'public' publish_dir: '/' # ============================================================ # Job 5: CDN Refresh & Notifications (在所有部署完成后) # ============================================================ finalize: runs-on: ubuntu-latest timeout-minutes: 5 permissions: contents: read needs: [deploy-edgeone, deploy-upyun] steps: - 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: Refresh DogeCloud CDN 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 continue-on-error: true run: node scripts/generate_notification_report.js - name: Send Telegram notification if: ${{ secrets.TELEGRAM_BOT_TOKEN != '' && secrets.TELEGRAM_CHAT_ID != '' }} env: TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }} run: | MESSAGE="✅ **部署成功** 📦 项目: ${{ github.repository }} 🌿 分支: ${{ github.ref_name }} 🔗 Commit: ${{ github.sha }} 🕐 时间: $(date +'%Y-%m-%d %H:%M:%S') 📊 部署目标: - EdgeOne Pages ✓ - UpYun ✓ - CDN 刷新 ✓" 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 if: ${{ secrets.FEISHU_WEBHOOK_URL != '' }} env: FEISHU_WEBHOOK_URL: ${{ secrets.FEISHU_WEBHOOK_URL }} run: | 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📊 部署目标:\n- EdgeOne Pages ✓\n- UpYun ✓\n- CDN 刷新 ✓" } }' - name: Get current date (CN) id: date run: echo "date=$(date +'%Y-%m-%d %H:%M:%S')" >> $GITHUB_OUTPUT - name: Send deployment notification 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 }} # ============================================================ # Job 6: Failure Notification (失败通知) # ============================================================ notify-failure: runs-on: ubuntu-latest timeout-minutes: 3 permissions: contents: read needs: [build, deploy-edgeone, deploy-upyun, finalize] if: failure() steps: - name: Send Telegram failure notification if: ${{ secrets.TELEGRAM_BOT_TOKEN != '' && secrets.TELEGRAM_CHAT_ID != '' }} env: TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }} run: | 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 if: ${{ secrets.FEISHU_WEBHOOK_URL != '' }} env: FEISHU_WEBHOOK_URL: ${{ secrets.FEISHU_WEBHOOK_URL }} run: | 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 }}" } }'