guanghulab/.github/workflows/migrate-to-cn-restore.yml
2026-05-10 13:12:44 +08:00

342 lines
15 KiB
YAML

name: '📥 国内搬家·从COS恢复'
# ════════════════════════════════════════════════════════════════
# 国内搬家·从 COS 恢复 · migrate-to-cn-restore
# 编号: ZY-WF-MIGRATE-TO-CN-RESTORE
# Sovereign: TCS-0002∞ · 国作登字-2026-A-00037559
# 守护: 铸渊 · ICE-GL-ZY001
#
# 前提:
# 1. 冰朔已经跑过 migrate-to-cn-build.yml, 下载了 Artifact
# 2. 冰朔已经把 lighthouse-migration-*.tar.gz + .sha256 上传到
# cos://sy-finetune-corpus-1317346199/lighthouse-migration/
# 3. ZY_CN_SERVER_HOST/USER/KEY 已配 (PR-2 之前已配)
# 4. ZY_COS_SECRET_ID/KEY 已配 (训练时已配)
#
# 流程:
# ① 误触锁校验「从COS恢复」
# ② check-secrets (cn-domain-deploy + COS) + 选搬家包
# ③ SSH 装载凭据 + rsync setup-forgejo.sh + 远端脚本
# ④ scp COS 凭据到 /tmp/.cos-creds (chmod 600, 用完即删)
# ⑤ ssh 跑 /tmp/migrate-restore.sh (script 走 stdin pipe 上传, 不入 ps)
# ⑥ 拉回 _logs/migrate-restore-report.md → GH Step Summary
# ⑦ 清 /tmp/.cos-creds + /tmp/migrate-restore.sh + 本地 SSH 私钥
#
# 误触锁: confirm_phrase = 「从COS恢复」
# 因果链: cc-001 涌现洁净 + cc-003 动态适配 + cc-004 中文一次性
# ════════════════════════════════════════════════════════════════
on:
workflow_dispatch:
inputs:
confirm_phrase:
description: '误触锁 · 必须输入「从COS恢复」才会真跑, 其他值降级 dry-run'
required: false
default: ''
type: string
tag:
description: '指定的搬家包 tag (例: 20260509-1817). 留空 = 用 cos://lighthouse-migration/ 下最新的.'
required: false
default: ''
type: string
cos_bucket:
description: 'COS 桶名 (默认 sy-finetune-corpus-1317346199)'
required: false
default: 'sy-finetune-corpus-1317346199'
type: string
cos_region:
description: 'COS 区域 (默认 ap-guangzhou)'
required: false
default: 'ap-guangzhou'
type: string
dry_run:
description: '只打印计划不真跑 (true/false)'
required: false
default: 'false'
type: choice
options:
- 'false'
- 'true'
permissions:
contents: read
concurrency:
group: migrate-to-cn-restore
cancel-in-progress: false
jobs:
preflight:
name: 启动前预检 · 密钥 + 误触锁
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
effective_dry_run: ${{ steps.misclick.outputs.effective_dry_run }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: 启动前最小密钥校验 (cn-domain-deploy / restore stage)
env:
ZY_CN_SERVER_HOST: ${{ secrets.ZY_CN_SERVER_HOST }}
ZY_CN_SERVER_USER: ${{ secrets.ZY_CN_SERVER_USER }}
ZY_CN_SERVER_KEY: ${{ secrets.ZY_CN_SERVER_KEY }}
ZY_CN_SERVER_PATH: ${{ secrets.ZY_CN_SERVER_PATH }}
ZY_CN_SSH_PORT: ${{ secrets.ZY_CN_SSH_PORT }}
ZY_COS_SECRET_ID: ${{ secrets.ZY_COS_SECRET_ID }}
ZY_COS_SECRET_KEY: ${{ secrets.ZY_COS_SECRET_KEY }}
run: |
node scripts/preflight/check-secrets.js \
--workflow cn-domain-deploy \
--stage restore \
--target main
- name: 误触锁 · 必须输入「从COS恢复」
id: misclick
env:
PHRASE: ${{ inputs.confirm_phrase }}
DRY: ${{ inputs.dry_run }}
run: |
set -e
EFFECTIVE_DRY="$DRY"
REQUIRED="从COS恢复"
if [ "$PHRASE" != "$REQUIRED" ]; then
echo "═══════════════════════════════════════════════"
echo " ⚠️ 误触保护"
echo " 要真在 2C2G 域名机上装 Forgejo + push 搬家包,"
echo " 必须在 confirm_phrase 字段精确输入: 「$REQUIRED」"
echo " 当前值=「$PHRASE」 → 自动切到 dry-run."
echo "═══════════════════════════════════════════════"
EFFECTIVE_DRY="true"
else
echo "✅ 已显式确认 (phrase=「$PHRASE」), 即将真跑 restore"
fi
echo "effective_dry_run=$EFFECTIVE_DRY" >> "$GITHUB_OUTPUT"
restore:
name: 拉包 + 装 Forgejo + push 历史
needs: preflight
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: 装载 SSH 凭据 (写到 2C2G 域名机)
id: target
env:
HOST: ${{ secrets.ZY_CN_SERVER_HOST }}
USER: ${{ secrets.ZY_CN_SERVER_USER }}
KEY: ${{ secrets.ZY_CN_SERVER_KEY }}
PORT: ${{ secrets.ZY_CN_SSH_PORT }}
DEPLOY_PATH: ${{ secrets.ZY_CN_SERVER_PATH }}
run: |
set -euo pipefail
if [ -z "$HOST" ] || [ -z "$KEY" ] || [ -z "$USER" ]; then
echo "❌ ZY_CN_SERVER_{HOST,USER,KEY} 任一为空" >&2; exit 1
fi
PORT="${PORT:-22}"
DEPLOY_PATH="${DEPLOY_PATH:-/data/guanghulab}"
echo "user=$USER" >> "$GITHUB_OUTPUT"
echo "port=$PORT" >> "$GITHUB_OUTPUT"
echo "deploy_path=$DEPLOY_PATH" >> "$GITHUB_OUTPUT"
umask 077
mkdir -p ~/.ssh
printf '%s\n' "$KEY" > ~/.ssh/restore_key
chmod 600 ~/.ssh/restore_key
ssh-keyscan -p "$PORT" -H "$HOST" >> ~/.ssh/known_hosts 2>/dev/null || true
printf '%s' "$HOST" > ~/.ssh/restore_host
chmod 600 ~/.ssh/restore_host
echo "✅ SSH 凭据就绪"
- name: Dry-run · 只打印计划
if: needs.preflight.outputs.effective_dry_run == 'true'
env:
USER: ${{ steps.target.outputs.user }}
DEPLOY_PATH: ${{ steps.target.outputs.deploy_path }}
BUCKET: ${{ inputs.cos_bucket }}
REGION: ${{ inputs.cos_region }}
TAG: ${{ inputs.tag }}
run: |
echo "═══════════════ DRY RUN ═══════════════"
echo "would ssh $USER@<host> :"
echo " 1. rsync server/setup/domain-cn/forgejo/ → /opt/guanghulab/domain-cn/forgejo/"
echo " 2. scp scripts/migration/remote/migrate-restore-remote.sh → /tmp/migrate-restore.sh"
echo " 3. scp COS 凭据 (走 stdin pipe) → /tmp/.cos-creds (chmod 600)"
echo " 4. sudo bash /tmp/migrate-restore.sh"
echo " BUCKET=$BUCKET REGION=$REGION TAG=$TAG DEPLOY_PATH=$DEPLOY_PATH"
echo " 5. 远端流程 10 步 (装 coscli → 拉包 → 校验 → 解 → forgejo → nginx → push)"
echo " 6. 拉回 /opt/guanghulab/_logs/migrate-restore-report.md"
echo " 7. 清 /tmp/.cos-creds + /tmp/migrate-restore.sh"
- name: Rsync setup-forgejo.sh + remote 脚本上去
if: needs.preflight.outputs.effective_dry_run != 'true'
env:
USER: ${{ steps.target.outputs.user }}
PORT: ${{ steps.target.outputs.port }}
run: |
set -euo pipefail
HOST="$(cat ~/.ssh/restore_host)"
ssh -i ~/.ssh/restore_key -p "$PORT" \
-o StrictHostKeyChecking=accept-new \
"$USER@$HOST" \
"sudo mkdir -p /opt/guanghulab/domain-cn/forgejo && sudo chown -R $USER:$USER /opt/guanghulab"
rsync -avz --delete \
-e "ssh -i ~/.ssh/restore_key -p $PORT -o StrictHostKeyChecking=accept-new" \
server/setup/domain-cn/forgejo/ \
"$USER@$HOST:/opt/guanghulab/domain-cn/forgejo/"
# 把远端脚本传上去 (走 scp), 不通过命令行参数
scp -i ~/.ssh/restore_key -P "$PORT" \
-o StrictHostKeyChecking=accept-new \
scripts/migration/remote/migrate-restore-remote.sh \
"$USER@$HOST:/tmp/migrate-restore.sh"
ssh -i ~/.ssh/restore_key -p "$PORT" \
-o StrictHostKeyChecking=accept-new \
"$USER@$HOST" "chmod 700 /tmp/migrate-restore.sh"
- name: 上传 COS 凭据 (走 SSH stdin pipe, 不入 ps)
if: needs.preflight.outputs.effective_dry_run != 'true'
env:
USER: ${{ steps.target.outputs.user }}
PORT: ${{ steps.target.outputs.port }}
COS_SECRET_ID: ${{ secrets.ZY_COS_SECRET_ID }}
COS_SECRET_KEY: ${{ secrets.ZY_COS_SECRET_KEY }}
run: |
set -euo pipefail
HOST="$(cat ~/.ssh/restore_host)"
# secrets 通过 stdin → cat > 文件, 不出现在 ssh 命令行里 (避免 ps 暴露)
# 用 printf 而非 here-doc, 避免 YAML 块标量缩进与 here-doc 终结符冲突
printf 'COS_SECRET_ID=%s\nCOS_SECRET_KEY=%s\n' "$COS_SECRET_ID" "$COS_SECRET_KEY" \
| ssh -i ~/.ssh/restore_key -p "$PORT" \
-o StrictHostKeyChecking=accept-new \
"$USER@$HOST" \
"umask 077 && cat > /tmp/.cos-creds && chmod 600 /tmp/.cos-creds"
# 移到 root 名下 (脚本 sudo 跑, 读 /tmp/.cos-creds 需要可读)
ssh -i ~/.ssh/restore_key -p "$PORT" \
-o StrictHostKeyChecking=accept-new \
"$USER@$HOST" \
"sudo chown root:root /tmp/.cos-creds && sudo chmod 600 /tmp/.cos-creds"
- name: 真跑远端 restore
if: needs.preflight.outputs.effective_dry_run != 'true'
env:
USER: ${{ steps.target.outputs.user }}
PORT: ${{ steps.target.outputs.port }}
DEPLOY_PATH: ${{ steps.target.outputs.deploy_path }}
BUCKET: ${{ inputs.cos_bucket }}
REGION: ${{ inputs.cos_region }}
TAG: ${{ inputs.tag }}
run: |
set -euo pipefail
# 校验所有上游 input · sudo env 之前必须 whitelist · 防止注入
# TAG 允许空 (= 用最新), 或 alphanumeric + - + _ + . + 中间 *
if ! printf '%s' "$TAG" | grep -qE '^[0-9A-Za-z._*-]*$'; then
echo "❌ TAG 含非法字符: $TAG (只允许 [0-9A-Za-z._*-])" >&2; exit 1
fi
if ! printf '%s' "$BUCKET" | grep -qE '^[0-9a-z][0-9a-z.-]{2,62}$'; then
echo "❌ BUCKET 不符合 COS 桶名规范: $BUCKET" >&2; exit 1
fi
if ! printf '%s' "$REGION" | grep -qE '^ap-[a-z]+(-[0-9]+)?$'; then
echo "❌ REGION 不符合腾讯云 region 规范: $REGION" >&2; exit 1
fi
if ! printf '%s' "$DEPLOY_PATH" | grep -qE '^/[A-Za-z0-9_/-]+$'; then
echo "❌ DEPLOY_PATH 不是绝对路径或含非法字符: $DEPLOY_PATH" >&2; exit 1
fi
HOST="$(cat ~/.ssh/restore_host)"
ssh -i ~/.ssh/restore_key -p "$PORT" \
-o StrictHostKeyChecking=accept-new \
"$USER@$HOST" \
"sudo env BUCKET='$BUCKET' REGION='$REGION' TAG='$TAG' DEPLOY_PATH='$DEPLOY_PATH' bash /tmp/migrate-restore.sh" \
2>&1 | tee restore-output.log
- name: 远端清理 (无论成败都跑)
if: needs.preflight.outputs.effective_dry_run != 'true' && always()
env:
USER: ${{ steps.target.outputs.user }}
PORT: ${{ steps.target.outputs.port }}
run: |
set -uo pipefail
HOST="$(cat ~/.ssh/restore_host)"
ssh -i ~/.ssh/restore_key -p "$PORT" \
-o StrictHostKeyChecking=accept-new \
"$USER@$HOST" \
"sudo rm -f /tmp/.cos-creds /tmp/migrate-restore.sh" || true
- name: 拉回中文回执
if: needs.preflight.outputs.effective_dry_run != 'true' && always()
env:
USER: ${{ steps.target.outputs.user }}
PORT: ${{ steps.target.outputs.port }}
run: |
set -uo pipefail
HOST="$(cat ~/.ssh/restore_host)"
mkdir -p artifacts
scp -i ~/.ssh/restore_key -P "$PORT" \
-o StrictHostKeyChecking=accept-new \
"$USER@$HOST:/opt/guanghulab/_logs/migrate-restore-report.md" \
artifacts/ 2>/dev/null || echo "(no migrate-restore-report.md)"
scp -i ~/.ssh/restore_key -P "$PORT" \
-o StrictHostKeyChecking=accept-new \
"$USER@$HOST:/opt/guanghulab/_logs/migrate-restore-*.json" \
artifacts/ 2>/dev/null || true
scp -i ~/.ssh/restore_key -P "$PORT" \
-o StrictHostKeyChecking=accept-new \
"$USER@$HOST:/opt/guanghulab/_logs/forgejo-setup-*.json" \
artifacts/ 2>/dev/null || true
ls -la artifacts/ || true
- name: 把中文回执贴到 GitHub Actions Summary
if: always()
run: |
set -uo pipefail
{
echo "# 📥 国内搬家·从 COS 恢复 · 回执"
echo ""
echo "> 给冰朔看的中文回执 · 守护: 铸渊 · ICE-GL-ZY001"
echo ""
if [ "${{ needs.preflight.outputs.effective_dry_run }}" = "true" ]; then
echo "## ⚠️ DRY RUN (没真在域名机上动手)"
echo ""
echo "误触锁口令没填对, 工作流只打印了计划. 要真跑 confirm_phrase 必须填: \`从COS恢复\`"
echo ""
else
if [ -f artifacts/migrate-restore-report.md ]; then
cat artifacts/migrate-restore-report.md
else
echo "## ⚠️ 没找到远端回执 \`migrate-restore-report.md\`"
echo ""
echo "可能远端 /tmp/migrate-restore.sh 半路死了. 看下面的 restore-output.log artifact."
fi
fi
echo ""
echo "---"
echo ""
echo "## 🎯 接下来 (恢复成功后)"
echo ""
echo "1. 浏览器开 \`https://guanghulab.com/git\`"
echo "2. SSH 到 2C2G 看 \`/data/guanghulab/_logs/forgejo-credentials-FIRST-BOOT.txt\` 抄密码"
echo "3. **抄完后 \`sudo rm\` 删那个凭据文件** — 安全要求 (cc-001 涌现洁净)"
} >> "$GITHUB_STEP_SUMMARY"
- name: 上传 artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: migrate-to-cn-restore-${{ github.run_id }}
path: |
artifacts/
restore-output.log
retention-days: 30
if-no-files-found: warn
- name: 清理本地 SSH 私钥
if: always()
run: |
rm -f ~/.ssh/restore_key ~/.ssh/restore_host