chore: 砍COS改造(deploy.yml改artifact传递+gitea-backup可移植部署包+改造步骤文档)

This commit is contained in:
zqlit committed 2026-10-04 09:52:50 +08:00
1 parent a74b3c7127
commit 2388694e8a
8 files changed
+728 -146

No files matched your search

+153 -146
View File
@@ -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.<id>.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 }}"
# 2026-10-04 起去掉「比对 last_hash 跳过未变化部署」的优化:
# last_hash 原本存 COS,砍 COS 后无处持久化(Gitea 缓存服务跨网段不可达)。
# 且 push 才触发构建,本就意味着有内容变化,直接部署即可。
echo "🔄 构建完成,部署到 EdgeOne"
echo "deployed=true" >> $GITHUB_OUTPUT
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'
- 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
@@ -1099,3 +989,120 @@ 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}}')"
# ============================================================
# 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
+1
View File
@@ -10,3 +10,4 @@ write\.env
/public
# Claude Code 临时 worktree(曾被误当 submodule 提交)
.claude/worktrees/
.workbuddy-backup/
+10
View File
@@ -0,0 +1,10 @@
# Gitea 部署包 —— 忽略规则
# 环境变量(含密钥)
.env
# 数据卷(迁移时单独打包,不进 git)
data/
# write-server 运行时产物(若纳入)
*.log
+130
View File
@@ -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` 可以删掉。
+157
View File
@@ -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
+36
View File
@@ -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 "⚠️ 产物含密钥,请勿上传网盘/公开仓库。"
+55
View File
@@ -0,0 +1,55 @@
#!/bin/bash
# =============================================================================
# Gitea 部署包 —— 一键恢复
# 用法:bash restore.sh <backups/data-<时间戳>.tar.gz>
# 或 bash restore.sh <backups/gitea-dump-<时间戳>.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
+186
View File
@@ -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 <GITEA_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 内已硬编码) |