342 lines
15 KiB
YAML
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
|