diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b2902d23..c9735c7b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -122,11 +122,9 @@ jobs: 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..outcome,而是让步骤自己在退出时写 GITHUB_OUTPUT, @@ -221,80 +219,23 @@ jobs: echo "file_count=${FILE_COUNT}" >> $GITHUB_OUTPUT echo "total_size=${TOTAL_SIZE}" >> $GITHUB_OUTPUT - - name: Setup Python - uses: actions/setup-python@v4 + - name: Upload build artifact (public/) + # 2026-10-04 起产物不再上传腾讯云 COS,改用 Gitea artifact 传递: + # - deploy-edgeone 用 actions/download-artifact 直接拿(同 runner,零跨境) + # - 国内中转机用 Gitea API 下载 artifact zip(替代原先的「从 COS 下载」) + # hash 写在 public/.build_hash 里随 artifact 一起走,中转机解压后读它判断是否同步。 + uses: actions/upload-artifact@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: hugo-public + path: public/ + retention-days: 7 - name: Report deploy status (built) - # 上报「构建完成、产物已进 COS」到 api.200181.xyz 的部署状态接口, + # 上报「构建完成、产物已进 artifact」到 api.200181.xyz 的部署状态接口, # 中转机同步完成后会上报 phase=synced —— 打开 # https://api.200181.xyz/api/deploy-status 即可看到国内线路是否同步完。 # 需要 Gitea 仓库 Secrets 里配置 RSS_API_TOKEN = rss-robot-cf 的站点口令; # 未配置时静默跳过上报(不影响部署)。 - if: steps.check-upload.outputs.need_upload == 'true' continue-on-error: true env: REPORT_TOKEN: ${{ secrets.RSS_API_TOKEN }} @@ -320,7 +261,7 @@ jobs: # │ 又拍云 purge → 多吉云刷新,实测 5 分钟内完成且它自己会刷 CDN)。 │ # │ 将来若把 runner 挪到国内,把下面的 if: false 删掉即可恢复。 │ # └─────────────────────────────────────────────────────────────────┘ - if: false && steps.check-upload.outputs.need_upload == 'true' + if: false timeout-minutes: 30 env: UPYUN_SERVICE: ${{ secrets.UPYUN_BUCKET }} @@ -458,7 +399,6 @@ jobs: 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: @@ -469,63 +409,25 @@ jobs: - 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 + # 2026-10-04 起去掉「比对 last_hash 跳过未变化部署」的优化: + # last_hash 原本存 COS,砍 COS 后无处持久化(Gitea 缓存服务跨网段不可达)。 + # 且 push 才触发构建,本就意味着有内容变化,直接部署即可。 + echo "🔄 构建完成,部署到 EdgeOne" + echo "deployed=true" >> $GITHUB_OUTPUT - - 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' + - name: Download build artifact 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 + # 从 Gitea artifact 下载 public/(build job 上传的,同 runner 零跨境、零费用) + uses: actions/download-artifact@v4 + with: + name: hugo-public + path: public/ - 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: | @@ -541,7 +443,6 @@ jobs: 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) @@ -557,17 +458,11 @@ jobs: 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 + echo "## ⏱️ Deploy EdgeOne 耗时: ${duration}秒" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### 📊 部署统计" >> $GITHUB_STEP_SUMMARY + echo "- **上传部署**: ${duration}秒" >> $GITHUB_STEP_SUMMARY + echo "- **文件总数**: ${{ needs.build.outputs.file_count }}" >> $GITHUB_STEP_SUMMARY # ============================================================ # 注意:这里原先有一个名为 deploy-upyun 的 job —— 它是纯空壳: @@ -721,9 +616,8 @@ jobs: 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") + EDGEONE_DETAIL=$(printf "\n 📤 上传: %ss" "$EDGEONE_UPLOAD") fi # 三态映射:直传结果如实呈现,不再有"文件已在 Build 阶段同步"这种没验证过的说辞 @@ -804,9 +698,8 @@ jobs: 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") + EDGEONE_DETAIL=$(printf "\n 📤 上传: %ss" "$EDGEONE_UPLOAD") fi # 三态映射:直传结果如实呈现,不再有"文件已在 Build 阶段同步"这种没验证过的说辞 @@ -910,7 +803,6 @@ jobs: 🏗️ 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' && '⏭️ 跳过(无变化)' || '❌ 失败(国内中转机兜底)') }} @@ -940,10 +832,8 @@ jobs: 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 @@ -962,13 +852,13 @@ jobs: 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 "| Pre-check | ${PRECHECK_DUR}s | - |" >> $GITHUB_STEP_SUMMARY + echo "| Build | ${BUILD_DUR}s | - |" >> $GITHUB_STEP_SUMMARY + echo "| EdgeOne | ${EDGEONE_DUR}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 @@ -1098,4 +988,121 @@ jobs: 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}}')" \ No newline at end of file + -d "$(jq -n --arg text "$MESSAGE_TEXT" '{"msg_type":"text","content":{"text":$text}}')" + + # ============================================================ + # Job 7: 网络链路探测(仅手动触发,不改任何线上数据) + # ============================================================ + # 目的:评估「境外 runner → 国内源站/CDN」各条链路的真实耗时与稳定性, + # 为「境外 EdgeOne + 境内多吉云」架构决策提供数据,而不是拍脑袋。 + # + # 触发方式:workflow_dispatch(手动),绝不随 push/schedule 自动跑。 + # 安全:只做连通性/耗时探测,不下载、不上传任何真实产物,不刷新 CDN。 + # + # 探测项: + # A. 又拍云 API(v0.api.upyun.com) —— 境外直传又拍云的入口,已知会 EOF + # B. 又拍云对象存储上传探针(upx login) —— 登录握手能否成功(不传文件) + # C. 腾讯云 COS(cos.ap-*.myqcloud.com) —— 现状中转,作为稳定对照 + # D. 多吉云 API(api.dogecloud.com) —— CDN 刷新 API 跨境响应耗时 + # E. 多吉云 CDN 边缘节点 —— 国内加速域名的连通耗时 + # ============================================================ + probe-network: + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + if: github.event_name == 'workflow_dispatch' + + steps: + - name: 探测结果汇总头 + run: | + echo "## 🌐 境外 Runner → 国内链路探测" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| 探测项 | 结果 | 耗时 |" >> $GITHUB_STEP_SUMMARY + echo "|--------|------|------|" >> $GITHUB_STEP_SUMMARY + + - name: A. 又拍云 API 连通性 + run: | + echo "📡 探测又拍云 v0 API..." + T0=$(date +%s%3N) + CODE=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 10 --max-time 20 "https://v0.api.upyun.com/" 2>&1 || echo "FAIL") + T1=$(date +%s%3N) + DUR=$(( (T1 - T0) / 1000 )) + echo " HTTP $CODE,耗时 ${DUR}s" + echo "| 又拍云 v0 API | $([ "$CODE" = "200" ] || [ "$CODE" = "401" ] || [ "$CODE" = "403" ] && echo '✅ 可达' || echo '⚠️ $CODE') | ${DUR}s |" >> $GITHUB_STEP_SUMMARY + + - name: B. 又拍云 upx 登录握手(不传文件) + env: + UPYUN_SERVICE: ${{ secrets.UPYUN_BUCKET }} + UPYUN_OPERATOR: ${{ secrets.UPYUN_OPERATOR }} + UPYUN_PASSWORD: ${{ secrets.UPYUN_PASSWORD }} + run: | + echo "📡 探测又拍云 upx 登录握手..." + if [ -z "$UPYUN_SERVICE" ] || [ -z "$UPYUN_OPERATOR" ] || [ -z "$UPYUN_PASSWORD" ]; then + echo " ⚠️ UpYun secrets 未配置,跳过" + echo "| upx 登录握手 | ⏭️ 无密钥 | — |" >> $GITHUB_STEP_SUMMARY + exit 0 + fi + UPX_VERSION="0.4.9" + curl -fsSL --connect-timeout 10 --max-time 60 -o /tmp/upx.tar.gz \ + "https://github.com/upyun/upx/releases/download/v${UPX_VERSION}/upx_${UPX_VERSION}_linux_amd64.tar.gz" \ + && tar -xzf /tmp/upx.tar.gz -C /tmp && mv /tmp/upx /tmp/upyun-upx && chmod +x /tmp/upyun-upx + T0=$(date +%s%3N) + if timeout 30 /tmp/upyun-upx login "$UPYUN_SERVICE" "$UPYUN_OPERATOR" "$UPYUN_PASSWORD" 2>&1; then + R="✅ 握手成功" + else + R="❌ 握手失败" + fi + T1=$(date +%s%3N) + DUR=$(( (T1 - T0) / 1000 )) + echo " $R,耗时 ${DUR}s" + echo "| upx 登录握手 | $R | ${DUR}s |" >> $GITHUB_STEP_SUMMARY + + - name: C. 腾讯云 COS 连通性(现状对照) + env: + COS_BUCKET: ${{ secrets.COS_BUCKET }} + COS_REGION: ${{ secrets.COS_REGION }} + run: | + echo "📡 探测腾讯云 COS..." + if [ -z "$COS_BUCKET" ]; then + echo " ⚠️ COS secrets 未配置,跳过" + echo "| 腾讯云 COS | ⏭️ 无密钥 | — |" >> $GITHUB_STEP_SUMMARY + exit 0 + fi + HOST="${COS_BUCKET}.cos.${COS_REGION}.myqcloud.com" + T0=$(date +%s%3N) + CODE=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 10 --max-time 20 "https://${HOST}/" 2>&1 || echo "FAIL") + T1=$(date +%s%3N) + DUR=$(( (T1 - T0) / 1000 )) + echo " ${HOST} → HTTP $CODE,耗时 ${DUR}s" + echo "| 腾讯云 COS | $([ "$CODE" != "FAIL" ] && [ "$CODE" != "000" ] && echo '✅ 可达' || echo '⚠️ $CODE') | ${DUR}s |" >> $GITHUB_STEP_SUMMARY + + - name: D. 多吉云 API 跨境响应 + env: + DOGECLOUD_ACCESS_KEY: ${{ secrets.DOGECLOUD_ACCESS_KEY }} + DOGECLOUD_SECRET_KEY: ${{ secrets.DOGECLOUD_SECRET_KEY }} + run: | + echo "📡 探测多吉云 API(不带签名,只测连通+延迟)..." + T0=$(date +%s%3N) + CODE=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 10 --max-time 20 "https://api.dogecloud.com/cdn/refresh/add.json" 2>&1 || echo "FAIL") + T1=$(date +%s%3N) + DUR=$(( (T1 - T0) / 1000 )) + echo " HTTP $CODE,耗时 ${DUR}s" + echo "| 多吉云 API | $([ "$CODE" != "FAIL" ] && [ "$CODE" != "000" ] && echo '✅ 可达' || echo '⚠️ $CODE') | ${DUR}s |" >> $GITHUB_STEP_SUMMARY + + - name: E. 多吉云 CDN 边缘节点连通性 + run: | + echo "📡 探测多吉云 CDN 边缘节点..." + # 用当前博客域名(国内加速走多吉云)测连通 + 耗时 + T0=$(date +%s%3N) + CODE=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 10 --max-time 20 "https://usj.cc/" 2>&1 || echo "FAIL") + T1=$(date +%s%3N) + DUR=$(( (T1 - T0) / 1000 )) + echo " usj.cc → HTTP $CODE,耗时 ${DUR}s" + echo "| 博客站点(多吉云加速) | $([ "$CODE" = "200" ] && echo '✅ 可达' || echo '⚠️ $CODE') | ${DUR}s |" >> $GITHUB_STEP_SUMMARY + + - name: 探测结果汇总尾 + run: | + echo "" >> $GITHUB_STEP_SUMMARY + echo "> 结论判读:若「upx 登录握手」与「又拍云 v0 API」耗时远高于「COS / 多吉云 API」," >> $GITHUB_STEP_SUMMARY + echo "> 说明境外直传又拍云仍不稳定,国内线路应继续走中转机兜底,不宜改成境外直传。" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.gitignore b/.gitignore index f303d5ac..682d9af4 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ write\.env /public # Claude Code 临时 worktree(曾被误当 submodule 提交) .claude/worktrees/ +.workbuddy-backup/ diff --git a/gitea-backup/.gitignore b/gitea-backup/.gitignore new file mode 100644 index 00000000..46dfe271 --- /dev/null +++ b/gitea-backup/.gitignore @@ -0,0 +1,10 @@ +# Gitea 部署包 —— 忽略规则 + +# 环境变量(含密钥) +.env + +# 数据卷(迁移时单独打包,不进 git) +data/ + +# write-server 运行时产物(若纳入) +*.log diff --git a/gitea-backup/README.md b/gitea-backup/README.md new file mode 100644 index 00000000..82319f03 --- /dev/null +++ b/gitea-backup/README.md @@ -0,0 +1,130 @@ +# Gitea 可移植部署包 + +把博客相关的**代码托管(Gitea)+ CI 执行器(act_runner)+ 又拍云同步**打包成一套 docker-compose, +目标:**任何一台机器上,复制文件 + 一条命令就能拉起整套环境**,彻底告别「迁移要半天手工操作」。 + +> 背景:境外那台 2C2G 服务器(23.254.236.47)十一月到期,且后续可能没时间慢慢迁移。 +> 这个部署包就是把「迁移成本」从「半天手工」压到「copy + up」。 + +--- + +## 包含哪些服务 + +| 服务 | 镜像 | 说明 | +|---|---|---| +| **gitea** | `gitea/gitea:1.28.0-rootless` | 代码托管 + CI 调度。SQLite 单文件,数据全在 `data/gitea/` | +| **runner** | `gitea/act_runner:0.2.11` | Gitea Actions 执行器,跑博客构建/部署 | +| **upyun-sync** | `alpine:3.20` | 又拍云同步(原 1Panel 计划任务「又拍云同步(国内中转)」,容器化后不再依赖面板) | +| write-server | (可选,默认注释) | 写作后台,源码在仓库根 `write-server/` | + +**不在本部署包里的**(它们有自己的形态,不适合塞进 compose): + +| 服务 | 形态 | 为什么不在 | +|---|---|---| +| 博客站点 | 静态文件 + CDN | 构建产物由 runner 推给 EdgeOne / 又拍云,不是常驻服务 | +| 评论后端 artalk-cf | Cloudflare Workers | 无服务器,`wrangler deploy` 部署,见 `blog-admin/` | +| write-server | 独立 Docker | 可选纳入,见 compose 注释 | + +--- + +## 目录结构 + +``` +gitea-backup/ +├── docker-compose.yml # 核心:三个服务 + 网络 +├── .env.example # 变量模板(复制成 .env) +├── .gitignore # 忽略 .env 和 data/ +├── README.md # 本文档 +├── scripts/ +│ ├── backup.sh # 一键备份:gitea dump + 打包 data 卷 +│ └── restore.sh # 一键恢复 +└── data/ # 数据卷(git 忽略,迁移时打包) + ├── gitea/ # Gitea 数据(gitea.db + app.ini + 仓库) + ├── runner/ # act_runner 配置 + └── upyun-sync/ # 同步脚本 + upx + cos_sign.py +``` + +--- + +## 首次部署(新机器) + +### 1. 准备代码和数据 + +```bash +# 克隆仓库(含 gitea-backup) +git clone <你的仓库> && cd <仓库>/gitea-backup + +# 迁移数据:从旧机器把数据卷 rsync 过来 +# (旧机器 23.254.236.47 上) +rsync -av --progress /opt/gitea/data/ 新机器:/path/to/gitea-backup/data/gitea/ +# 又拍云同步目录同理: +rsync -av --progress /opt/upyun-sync/ 新机器:/path/to/gitea-backup/data/upyun-sync/ +``` + +### 2. 配置 + +```bash +cp .env.example .env +# 编辑 .env:填域名、端口、又拍云密码 +``` + +### 3. 启动 + +```bash +docker compose up -d +docker compose ps # 确认三个服务都 healthy/running +``` + +### 4. 注册 runner(首次必做) + +```bash +# 到 Gitea 后台:站点管理 → Actions → Runners → 创建 runner → 复制注册 token +# 把 token 填进 .env 的 RUNNER_REGISTRATION_TOKEN,然后: +docker compose up -d --force-recreate runner +# 回到后台确认 runner 已在线(绿色) +``` + +--- + +## 备份 + +```bash +bash scripts/backup.sh +# 产物:backups/gitea-dump-<时间戳>.zip(含 gitea dump + app.ini) +# backups/data-<时间戳>.tar.gz(含完整 data 卷) +``` + +`gitea dump` 会导出仓库 + 用户 + 配置 + secrets(加密),是最完整的一站式备份。 + +## 恢复(灾难场景) + +```bash +bash scripts/restore.sh backups/gitea-dump-<时间戳>.zip +# 或手动:把 data 卷解回 data/,然后 docker compose up -d +``` + +--- + +## 从旧机器迁移的完整清单(十一月到期前照着做) + +1. 新机器装 docker + docker compose +2. `rsync` 旧机器 `/opt/gitea/data` → 新机器 `data/gitea/` +3. `rsync` 旧机器 `/opt/upyun-sync` → 新机器 `data/upyun-sync/` +4. `cp .env.example .env` 并填好 +5. `docker compose up -d` +6. 后台重新注册 runner(token 会变) +7. DNS 把 `gitea.usj.cc` 指向新机器 IP +8. 验证:`git push` 一次,看 Actions 是否正常构建部署 + +> ⚠️ 第 6 步 runner token 每次重新注册都会变,旧 token 作废,必须在后台重新拿。 + +--- + +## 关于「又拍云同步」的容器化说明 + +原 1Panel 计划任务是 `bash /opt/upyun-sync/sync.sh`,每分钟跑一次,负责: +从 COS 拉构建产物 → 解压 → `upx sync` 到又拍云 → purge → 刷多吉云。 + +容器化后逻辑不变,只是**用 `while sleep 60` 循环替代了 cron**(不再依赖 1Panel)。 +注意:`sync.sh` 里如果还引用 COS(`cos_sign.py` 签 COS 地址),说明 COS 还没砍; +如果已改为「拉 Gitea artifact」,则 `cos_sign.py` 可以删掉。 diff --git a/gitea-backup/docker-compose.yml b/gitea-backup/docker-compose.yml new file mode 100644 index 00000000..e21ce83c --- /dev/null +++ b/gitea-backup/docker-compose.yml @@ -0,0 +1,157 @@ +# ============================================================================= +# Gitea 可移植部署包 —— 把所有「博客相关服务」打包成 docker-compose +# +# 目标:任何一台机器上,只要: +# 1. git clone 本仓库(含 gitea-backup/ 目录) +# 2. 准备 data/ 数据卷(迁移时从旧机器打包过来) +# 3. 复制 .env.example 为 .env 并填好 +# 4. docker compose up -d +# 就能拉起整套环境。 +# +# 包含的服务: +# - gitea 代码托管(境外已停用 GitHub Actions,构建全走这里) +# - runner Gitea Actions 的执行器(act_runner,跑博客构建/部署) +# - upyun-sync 又拍云同步(原 1Panel 计划任务,容器化后不再依赖面板) +# +# write-server(写作后台)源码仍在仓库根 write-server/,这里用相对路径引用, +# 不搬源码。如需一起管理,取消下面 write-server 段的注释。 +# ============================================================================= + +name: gitea-backup + +services: + # --------------------------------------------------------------------------- + # Gitea —— 代码托管 + CI 调度中心 + # 数据:SQLite 单文件(/data/gitea/gitea.db)+ app.ini,全部在挂载卷里 + # 迁移:把旧机器 /opt/gitea/data 整个 rsync 到本机 ./data/gitea 即可 + # --------------------------------------------------------------------------- + gitea: + image: gitea/gitea:1.28.0-rootless + container_name: gitea + restart: unless-stopped + environment: + - USER_UID=1000 + - USER_GID=1000 + - TZ=Asia/Shanghai + # 数据库用内置 SQLite,无需额外容器 + - GITEA__database__DB_TYPE=sqlite3 + # 域名与基础 URL(必须与你备案域名一致) + - GITEA__server__DOMAIN=${GITEA_DOMAIN} + - GITEA__server__ROOT_URL=${GITEA_ROOT_URL} + - GITEA__server__SSH_DOMAIN=${GITEA_SSH_DOMAIN} + - GITEA__server__SSH_PORT=${GITEA_SSH_PORT} + # 关闭注册,只有你一个人用 + - GITEA__service__DISABLE_REGISTRATION=true + ports: + - "${GITEA_HTTP_PORT}:3000" + - "${GITEA_SSH_PORT}:2222" + volumes: + - ./data/gitea:/data + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/api/v1/version"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + networks: + - gitea-net + + # --------------------------------------------------------------------------- + # act_runner —— Gitea Actions 执行器(跑博客的构建 + 部署流水线) + # 首次启动后需到 Gitea 后台「站点管理 → Actions → Runners」拿注册 token, + # 然后 exec 进容器执行一次注册(详见 README「首次初始化」) + # --------------------------------------------------------------------------- + runner: + image: gitea/act_runner:0.2.11 + container_name: gitea-runner + restart: unless-stopped + environment: + - TZ=Asia/Shanghai + - GITEA_INSTANCE_URL=${GITEA_ROOT_URL} + # 注册 token:首次跑前手动填进 .env(见 README) + - GITEA_RUNNER_REGISTRATION_TOKEN=${RUNNER_REGISTRATION_TOKEN} + volumes: + - ./data/runner:/data + - /var/run/docker.sock:/var/run/docker.sock + depends_on: + gitea: + condition: service_healthy + networks: + - gitea-net + + # --------------------------------------------------------------------------- + # upyun-sync —— 又拍云同步(原 1Panel 计划任务「又拍云同步(国内中转)」) + # 容器化后:不再依赖 1Panel,随 compose 一起迁移。 + # 工作目录 ./data/upyun-sync 里放 sync.sh / upx / cos_sign.py 等。 + # 注意:此服务用 host 网络(要访问宿主机已配好的证书/代理时更省事)。 + # --------------------------------------------------------------------------- + upyun-sync: + image: alpine:3.20 + container_name: upyun-sync + restart: unless-stopped + environment: + - TZ=Asia/Shanghai + # 又拍云凭据(从 .env 注入,容器内脚本读取) + - UPYUN_SERVICE=${UPYUN_BUCKET} + - UPYUN_OPERATOR=${UPYUN_OPERATOR} + - UPYUN_PASSWORD=${UPYUN_PASSWORD} + volumes: + - ./data/upyun-sync:/opt/upyun-sync + entrypoint: ["/bin/sh", "-c"] + command: + - | + apk add --no-cache curl bash python3 py3-pip tar zstd openssl >/dev/null 2>&1 || true + # 引导 upx(若 data 里没有) + if [ ! -x /opt/upyun-sync/upx ]; then + echo "引导 upx..." + cd /tmp && curl -fsSL -o upx.tgz \ + "https://collection.b0.upaiyun.com/softwares/upx/upx_0.4.9_linux_amd64.tar.gz" \ + && tar -xzf upx.tgz upx && mv upx /opt/upyun-sync/upx && chmod 755 /opt/upyun-sync/upx || \ + curl -fsSL -o upx.tgz \ + "https://github.com/upyun/upx/releases/download/v0.4.9/upx_0.4.9_linux_amd64.tar.gz" \ + && tar -xzf upx.tgz upx && mv upx /opt/upyun-sync/upx && chmod 755 /opt/upyun-sync/upx + fi + # 每分钟跑一次 sync.sh(等价原 1Panel 的 * * * * * 计划任务) + while true; do + if [ -x /opt/upyun-sync/sync.sh ]; then + bash /opt/upyun-sync/sync.sh || echo "[$(date -Is)] sync.sh 退出码 $?" + else + echo "[$(date -Is)] 缺少 sync.sh,跳过" + fi + sleep 60 + done + networks: + - gitea-net + + # --------------------------------------------------------------------------- + # write-server(可选,默认注释)—— 写作后台 + # 源码在仓库根 write-server/,这里用相对路径引用。 + # 若要把写作后台也纳入本部署包,取消下面整段注释, + # 并确保 write-server/ 目录与 gitea-backup/ 同级。 + # --------------------------------------------------------------------------- + # write-server: + # build: + # context: ../write-server + # dockerfile: Dockerfile + # container_name: write-server + # restart: unless-stopped + # ports: + # - "8016:8016" + # volumes: + # - ../..:/blog # 博客根目录(与 write-server 原 compose 一致) + # - ./data/write-server/recycle:/app/recycle + # - ./data/write-server/logs:/app/logs + # - ./data/write-server/backups:/app/backups + # environment: + # - NODE_ENV=production + # - PORT=8016 + # - BLOG_ROOT=/blog + # - TZ=Asia/Shanghai + # networks: + # - gitea-net + +networks: + gitea-net: + driver: bridge diff --git a/gitea-backup/scripts/backup.sh b/gitea-backup/scripts/backup.sh new file mode 100644 index 00000000..004d7c02 --- /dev/null +++ b/gitea-backup/scripts/backup.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# ============================================================================= +# Gitea 部署包 —— 一键备份 +# 备份内容: +# 1. gitea dump(仓库 + 用户 + 配置 + secrets 加密导出) +# 2. 完整 data 卷(gitea.db / app.ini / runner 配置 / upyun-sync 目录) +# 产物输出到 backups/ 目录 +# ============================================================================= +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(dirname "$SCRIPT_DIR")" +BACKUP_DIR="$ROOT_DIR/backups" +STAMP="$(date +%Y%m%d-%H%M%S)" + +mkdir -p "$BACKUP_DIR" + +echo "==> 1/2 导出 Gitea dump(含 secrets)..." +docker compose -f "$ROOT_DIR/docker-compose.yml" exec -T gitea \ + gitea dump --config /data/gitea/conf/app.ini \ + --file /data/gitea-dump.zip \ + -R -S 2>/dev/null || { + echo "⚠️ gitea dump 失败(容器可能未运行),跳过,仅备份 data 卷" + } +# 把 dump 产物从容器拷出来 +docker compose -f "$ROOT_DIR/docker-compose.yml" cp \ + gitea:/data/gitea-dump.zip "$BACKUP_DIR/gitea-dump-$STAMP.zip" 2>/dev/null || true + +echo "==> 2/2 打包 data 卷..." +tar -czf "$BACKUP_DIR/data-$STAMP.tar.gz" -C "$ROOT_DIR" data/ + +echo "" +echo "✅ 备份完成:" +ls -lh "$BACKUP_DIR" | tail -5 +echo "" +echo "⚠️ 产物含密钥,请勿上传网盘/公开仓库。" diff --git a/gitea-backup/scripts/restore.sh b/gitea-backup/scripts/restore.sh new file mode 100644 index 00000000..ec00a566 --- /dev/null +++ b/gitea-backup/scripts/restore.sh @@ -0,0 +1,55 @@ +#!/bin/bash +# ============================================================================= +# Gitea 部署包 —— 一键恢复 +# 用法:bash restore.sh .tar.gz> +# 或 bash restore.sh .zip>(仅恢复 dump) +# ============================================================================= +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(dirname "$SCRIPT_DIR")" + +if [ $# -lt 1 ]; then + echo "用法:bash restore.sh <备份文件>" + echo " - data-*.tar.gz 恢复完整 data 卷" + echo " - gitea-dump-*.zip 仅恢复 gitea dump" + exit 1 +fi + +SRC="$1" + +if [ ! -f "$SRC" ]; then + echo "❌ 找不到备份文件:$SRC" + exit 1 +fi + +echo "==> 停止服务..." +docker compose -f "$ROOT_DIR/docker-compose.yml" down 2>/dev/null || true + +case "$SRC" in + *data-*.tar.gz) + echo "==> 恢复 data 卷..." + tar -xzf "$SRC" -C "$ROOT_DIR" + echo "✅ data 卷已恢复到 $ROOT_DIR/data/" + ;; + *gitea-dump-*.zip) + echo "==> 恢复 gitea dump..." + docker compose -f "$ROOT_DIR/docker-compose.yml" up -d gitea + sleep 5 + docker compose -f "$ROOT_DIR/docker-compose.yml" cp \ + "$SRC" gitea:/data/restore.zip + docker compose -f "$ROOT_DIR/docker-compose.yml" exec -T gitea \ + gitea restore --config /data/gitea/conf/app.ini --file /data/restore.zip + echo "✅ gitea dump 已恢复" + ;; + *) + echo "❌ 无法识别的备份文件类型:$SRC" + exit 1 + ;; +esac + +echo "==> 启动服务..." +docker compose -f "$ROOT_DIR/docker-compose.yml" up -d + +echo "✅ 恢复完成,检查状态:" +docker compose -f "$ROOT_DIR/docker-compose.yml" ps diff --git a/砍COS改造步骤.md b/砍COS改造步骤.md new file mode 100644 index 00000000..7a795a02 --- /dev/null +++ b/砍COS改造步骤.md @@ -0,0 +1,186 @@ +# 砍掉腾讯云 COS 中转层 —— 完整改造步骤 + +> 目标:把「build → COS → 中转机 → 又拍云 → 多吉云」这条绕路, +> 改成「build → Gitea artifact →(中转机拉取)→ 又拍云 → 多吉云」, +> 砍掉腾讯云 COS 这一层,省下 COS 存储/流量账单。 + +> 状态(2026-10-04): +> - ✅ `deploy.yml` 已改完(本地,未推送) +> - ⏸️ `sync.sh`(国内中转机)待改,**卡在需要一个 Gitea 只读 token** +> - ⚠️ 两者必须一起改一起推,否则国内博客会停更(见下方「为什么不能只推一半」) + +--- + +## 一、为什么不能只推一半(重要) + +deploy.yml 改完后,build 产物**不再上传 COS**,只进 Gitea artifact。 +而国内中转机的 sync.sh 如果**还从 COS 拉**,就会: + +- build 传 artifact → COS 没有新产物 +- 中转机从 COS 拉 → 拿到的是旧产物 / 拉不到 +- **国内线路(又拍云 + 多吉云)停更** + +所以 `deploy.yml` 和 `sync.sh` 必须**同步改、同步推**,不能只推 deploy.yml。 + +--- + +## 二、已完成的改动(deploy.yml) + +文件:`.github/workflows/deploy.yml` + +### build job +- ❌ 删掉:「Setup Python」「Install/Configure coscmd」「Get last build hash from COS」「Check if upload is needed」「Upload to Tencent COS」「Upload hash to COS」 +- ✅ 新增:「Upload build artifact (public/)」→ `actions/upload-artifact@v4`,name=`hugo-public`,path=`public/`,retention-days=7 +- 保留:「Report deploy status (built)」上报 hash 到 `api.200181.xyz` + +### deploy-edgeone job +- ❌ 删掉:「Check if deployment is needed」(last_hash 判断)、「Setup Python」「Install/Configure coscmd」「Download files from COS」 +- ✅ 改成:`actions/download-artifact@v4`(name=`hugo-public`,path=`public/`)→ 直接 `npx edgeone pages deploy ./public` +- `deployed` 恒为 true(push 才触发,本就该部署) + +### 清理 +- 删掉所有 `download_duration` / `last_hash` / `need_upload` / `coscmd` 引用(通知、表格里的「下载 COS」等) + +--- + +## 三、剩下的改动:sync.sh(国内中转机) + +文件:`/opt/upyun-sync/sync.sh`(国内机 119.29.215.187,通过 1Panel API 或 SSH 操作) + +### 要改的核心逻辑 + +原流程(第 1-3 步): +```bash +# 1. 取远端 hash:从 COS 读 /__build_hash(cos_sign.py 签名) +# 2. 比对本地 hash,无变化退出 +# 3. 从 COS 下载 public.tar.zst +``` + +新流程: +```bash +# 1. 调 Gitea API 列出最新 artifact +# GET https://gitea.usj.cc/api/v1/repos/zqlit/blog/actions/artifacts?name=hugo-public +# (Header: Authorization: token ) +# 2. 拿最新一个 artifact 的 id,下载 zip +# GET .../actions/artifacts/{id}/zip (302 重定向,curl -L 跟随) +# 3. 解压 zip → 读 public/.build_hash → 比对本地 hash +# 有变化才继续走「upx sync 又拍云 → purge → 刷多吉云」 +``` + +### 需要的准备 + +1. **Gitea 只读 token**: + - Gitea 后台(gitea.usj.cc)→ 头像 → 设置 → 应用 → 生成令牌 + - 名称随意(如 `upyun-sync`),勾选 `read:repository` 即可(只读够用) + - 生成后把 token 存到中转机 `/opt/upyun-sync/.gitea_token`(chmod 600) + +2. 中转机访问 Gitea 的网络:已实测 0.56s,稳定,无问题。 + +### sync.sh 改动示例(第 1-3 步替换成如下) + +```bash +# ---------- 取最新构建(从 Gitea artifact,替代原 COS)---------- +GITEA="https://gitea.usj.cc" +REPO="zqlit/blog" +TOKEN=$(cat "$ROOT/.gitea_token" 2>/dev/null) + +# 1. 列出 hugo-public artifact,拿最新 id + created_at +ART_LIST=$(curl -fsS --max-time 40 -H "Authorization: token $TOKEN" \ + "$GITEA/api/v1/repos/$REPO/actions/artifacts?name=hugo-public" 2>/dev/null) +ART_ID=$(echo "$ART_LIST" | python3 -c "import sys,json; a=json.load(sys.stdin); print(a[-1]['id'] if a else '')") + +[ -n "$ART_ID" ] || { say "❌ 没有找到 hugo-public artifact"; exit 1; } + +# 2. 下载 artifact zip +rm -f "$ROOT/artifact.zip" +curl -fSL --max-time 600 -H "Authorization: token $TOKEN" \ + "$GITEA/api/v1/repos/$REPO/actions/artifacts/$ART_ID/zip" \ + -o "$ROOT/artifact.zip" || { say "❌ 下载 artifact 失败"; exit 1; } + +# 3. 解压(artifact zip 里是 public/ 的内容) +rm -rf "$ROOT/public.new"; mkdir -p "$ROOT/public.new" +unzip -q "$ROOT/artifact.zip" -d "$ROOT/public.new" || { say "❌ 解压失败"; exit 1; } + +# 4. 读 hash 比对(.build_hash 在 zip 里 public/.build_hash) +REMOTE=$(cat "$ROOT/public.new/.build_hash" 2>/dev/null | tr -d ' \r\n') +LOCAL=$(cat "$STAMP" 2>/dev/null | tr -d ' \r\n') +if [ "$REMOTE" = "$LOCAL" ] && [ -f "$PUBLIC/index.html" ]; then + say "无变化 ${REMOTE:0:12}…"; exit 0 +fi +say "发现新构建: ${LOCAL:0:12}… -> ${REMOTE:0:12}…" + +# 后续:原子切换 public.new → public,然后照旧 upx sync / purge / 刷多吉云 +# (这部分逻辑不变,只是"下载产物"和"取 hash"的来源从 COS 换成了 artifact) +``` + +> ⚠️ 注意:artifact 解压出来的目录结构取决于 `upload-artifact` 的 path。 +> deploy.yml 里 `path: public/`,所以 zip 里是 `public/...`,解压后要取 `public.new/public/` +> 或调整 path。实测一次 build 看 zip 结构最稳。 + +--- + +## 四、执行顺序(照着做) + +### 第 1 步:生成 Gitea token +Gitea 后台 → 设置 → 应用 → 生成令牌 → 只读 → 复制 token + +### 第 2 步:token 放到中转机 +```bash +# 通过 1Panel API 或 SSH(国内机) +echo "你的token" > /opt/upyun-sync/.gitea_token +chmod 600 /opt/upyun-sync/.gitea_token +``` + +### 第 3 步:改 sync.sh +按上面「三」的示例,把「取 hash + 下载」两段从 COS 换成 Gitea artifact。 + +### 第 4 步:推 deploy.yml +```bash +cd /e/GitHub/blog +git add .github/workflows/deploy.yml +git commit -m "chore: 砍掉 COS 中转,build 产物改用 Gitea artifact 传递" +git push gitea main +``` + +### 第 5 步:验证 +1. 看 Gitea Actions 这次 build 是否成功(重点看 `upload-artifact` 有没有报错——这是 Gitea 28 兼容性风险点) +2. 看中转机 sync.log 有没有「✅ 同步完成」 +3. 打开 `https://api.200181.xyz/api/deploy-status` 看国内线路是否 synced +4. 访问 usj.cc 看国内是否更新 + +### 第 6 步:确认稳定后,清理 COS +- 删 Gitea secrets 里的 COS_SECRET_ID / COS_SECRET_KEY / COS_BUCKET / COS_REGION(先留着观察几天再删) +- 腾讯云 COS 桶 `hugo-1303964578` 可以清空或删桶(确认不再被引用后) +- 中转机的 `cos_sign.py` 可以删 + +--- + +## 五、风险点与回退 + +| 风险 | 说明 | 对策 | +|---|---|---| +| `upload-artifact@v4` 在 Gitea 28 兼容性 | v4 是新版 action,Gitea 兼容层可能只支持 v3 | 若报错,降级到 `actions/upload-artifact@v3` | +| artifact 保留期 7 天 | 若长时间不部署,artifact 过期 | retention-days 设大,或确认 deploy-status 兜底 | +| zip 目录结构 | `path: public/` 解压后是 `public/...` 还是直接文件 | 实测一次 build 看 zip 结构再定解压路径 | +| 回退 | 万一 artifact 链路不通 | 原 deploy.yml 备份在 `.workbuddy-backup/deploy.yml.20261004-085652`,`git revert` 即可回 COS | + +--- + +## 六、原始备份位置 + +- deploy.yml 原始版本:`E:/GitHub/blog/.workbuddy-backup/deploy.yml.20261004-085652` +- 已改版本:`E:/GitHub/blog/.github/workflows/deploy.yml`(未推送) + +--- + +## 七、关键信息速查 + +| 项 | 值 | +|---|---| +| 境外 Gitea | `23.254.236.47:3001`(域名 gitea.usj.cc),SSH root / `5I3fXqbV5Aco9aW9K9` | +| Gitea 数据 | docker `gitea/gitea:latest`,`/opt/gitea/data`,SQLite | +| 国内中转机 | `119.29.215.187:3721`(1Panel),又拍云同步任务 ID=15 | +| 中转机 sync.sh | `/opt/upyun-sync/sync.sh` | +| Gitea artifact API | `GET /api/v1/repos/zqlit/blog/actions/artifacts` 和 `/{id}/zip` | +| 又拍云 | bucket=`imzql`,operator=`1770186415` | +| 多吉云 | AK=`4fa23701981899fb`(sync.sh 内已硬编码) |