chore: 砍COS改造(deploy.yml改artifact传递+gitea-backup可移植部署包+改造步骤文档)
This commit is contained in:
1 parent
a74b3c7127
commit
2388694e8a
8 files changed
+728
-146
No files matched your search
+153
-146
@@ -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
|
||||
@@ -10,3 +10,4 @@ write\.env
|
||||
/public
|
||||
# Claude Code 临时 worktree(曾被误当 submodule 提交)
|
||||
.claude/worktrees/
|
||||
.workbuddy-backup/
|
||||
@@ -0,0 +1,10 @@
|
||||
# Gitea 部署包 —— 忽略规则
|
||||
|
||||
# 环境变量(含密钥)
|
||||
.env
|
||||
|
||||
# 数据卷(迁移时单独打包,不进 git)
|
||||
data/
|
||||
|
||||
# write-server 运行时产物(若纳入)
|
||||
*.log
|
||||
@@ -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` 可以删掉。
|
||||
@@ -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
|
||||
@@ -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 "⚠️ 产物含密钥,请勿上传网盘/公开仓库。"
|
||||
@@ -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
@@ -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 内已硬编码) |
|
||||
Reference in new issue
Block a user