name: domain-server-rollback # ════════════════════════════════════════════════════════════════ # 国内域名机 · 一键回滚 (单独红按钮) # Sovereign: TCS-0002∞ · 国作登字-2026-A-00037559 # 守护: 铸渊 · ICE-GL-ZY001 # # 跟 deploy-domain-server.yml 区分: 这一份给 Awen 一个**显眼的红按钮**, # 不会跟"重装"在同一界面被误点. 误触锁同样要求输入「重装广州」. # # 用法: Actions → domain-server-rollback → Run workflow # mode: list-only (默认, 只列快照) / rollback # snapshot_ts: 留空 = 回到最近一个快照 / 填具体 YYYYMMDD-HHMMSS # confirm_phrase: rollback 模式必填「重装广州」, 否则降级 list-only # # 必需 Secrets (与 deploy-domain-server.yml 相同): # ZY_CN_SERVER_HOST / ZY_CN_SERVER_USER / ZY_CN_SERVER_KEY # ZY_CN_SERVER_PATH / ZY_CN_SSH_PORT # ════════════════════════════════════════════════════════════════ on: workflow_dispatch: inputs: mode: description: '操作模式' required: true default: 'list-only' type: choice options: - list-only - rollback snapshot_ts: description: '快照时间戳 (留空 = 回到最近一个; 用 list-only 模式查可用快照)' required: false default: '' type: string confirm_phrase: description: 'rollback 模式必填 · 输入「重装广州」才会真跑' required: false default: '' type: string permissions: contents: read concurrency: group: domain-server-rollback cancel-in-progress: false jobs: preflight: name: 启动前预检 runs-on: ubuntu-latest timeout-minutes: 5 outputs: effective_mode: ${{ steps.gate.outputs.effective_mode }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: '20' } - name: 密钥预检 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 }} run: | node scripts/preflight/check-secrets.js \ --workflow cn-domain-deploy \ --stage rollback \ --target main - name: 误触锁 id: gate env: MODE: ${{ inputs.mode }} PHRASE: ${{ inputs.confirm_phrase }} run: | set -e EFFECTIVE="$MODE" if [ "$MODE" = "rollback" ] && [ "$PHRASE" != "重装广州" ]; then echo "═══════════════════════════════════════════════" echo " ⚠️ 误触保护: 选了 rollback 但 confirm_phrase != 「重装广州」" echo " 自动降级 list-only (只列快照, 不真回滚)." echo " 要真跑回滚, 在 confirm_phrase 字段精确输入: 重装广州" echo "═══════════════════════════════════════════════" EFFECTIVE="list-only" fi echo "effective_mode=$EFFECTIVE" >> "$GITHUB_OUTPUT" rollback: name: domain-cn · ${{ needs.preflight.outputs.effective_mode }} needs: preflight runs-on: ubuntu-latest timeout-minutes: 15 steps: - uses: actions/checkout@v4 - name: 装载 SSH 凭据 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/domain_key chmod 600 ~/.ssh/domain_key ssh-keyscan -p "$PORT" -H "$HOST" >> ~/.ssh/known_hosts 2>/dev/null || true printf '%s' "$HOST" > ~/.ssh/domain_host chmod 600 ~/.ssh/domain_host - name: 列出可用快照 env: USER: ${{ steps.target.outputs.user }} PORT: ${{ steps.target.outputs.port }} DEPLOY_PATH: ${{ steps.target.outputs.deploy_path }} run: | set -e HOST="$(cat ~/.ssh/domain_host)" ssh -i ~/.ssh/domain_key -p "$PORT" \ -o StrictHostKeyChecking=accept-new \ "$USER@$HOST" \ "sudo env DATA_ROOT='$DEPLOY_PATH' bash /opt/guanghulab/domain-cn/rollback.sh --list" \ 2>&1 | tee snapshot-list.log - name: 校验 snapshot_ts 格式 (防注入) if: needs.preflight.outputs.effective_mode == 'rollback' env: TS: ${{ inputs.snapshot_ts }} run: | set -e # snapshot_ts 走用户输入, 后面进 ssh 命令字符串. 严格白名单. # 合法形式 (来自 bootstrap.sh `$PRE_TS-pre-$STAGE`, 见 bootstrap.sh L161): # YYYYMMDD-HHMMSS-pre-bootstrap # YYYYMMDD-HHMMSS-pre-update # YYYYMMDD-HHMMSS-pre-rollback # 空 = 最近一个 # 注: bootstrap 总是带 -pre-{stage} 后缀, 但兼容裸时间戳 (理论上不会出现) # 也不放过, 避免万一手动建快照只用了时间戳的情况误闯. if [ -n "$TS" ] && ! printf '%s' "$TS" | grep -Eq '^[0-9]{8}-[0-9]{6}-pre-(bootstrap|update|rollback)$'; then echo "❌ snapshot_ts 格式不合法: 「$TS」" >&2 echo " 合法形式示例: 20260509-163700-pre-bootstrap" >&2 echo " 或留空 (= 回到最近一次快照)" >&2 echo " 提示: 用 list-only 模式查实际可用快照名" >&2 exit 1 fi echo "✅ snapshot_ts 格式校验通过${TS:+: $TS}" - name: 执行回滚 if: needs.preflight.outputs.effective_mode == 'rollback' env: USER: ${{ steps.target.outputs.user }} PORT: ${{ steps.target.outputs.port }} DEPLOY_PATH: ${{ steps.target.outputs.deploy_path }} TS: ${{ inputs.snapshot_ts }} run: | set -e HOST="$(cat ~/.ssh/domain_host)" # 双层防御: TS 已在上一步白名单校验, 这里走 sh -c argv 传入而非 # 命令字符串拼接, 即使 TS 含特殊字符也不会被本地/远程 shell 二次解释. if [ -n "$TS" ]; then ssh -i ~/.ssh/domain_key -p "$PORT" \ -o StrictHostKeyChecking=accept-new \ "$USER@$HOST" \ "sudo env DATA_ROOT='$DEPLOY_PATH' sh -c 'bash /opt/guanghulab/domain-cn/rollback.sh --to \"\$1\"' _ \"$TS\"" \ 2>&1 | tee rollback-output.log else ssh -i ~/.ssh/domain_key -p "$PORT" \ -o StrictHostKeyChecking=accept-new \ "$USER@$HOST" \ "sudo env DATA_ROOT='$DEPLOY_PATH' bash /opt/guanghulab/domain-cn/rollback.sh" \ 2>&1 | tee rollback-output.log fi - name: 拉回中文回执 if: always() env: USER: ${{ steps.target.outputs.user }} PORT: ${{ steps.target.outputs.port }} run: | set -uo pipefail HOST="$(cat ~/.ssh/domain_host)" mkdir -p artifacts scp -i ~/.ssh/domain_key -P "$PORT" \ -o StrictHostKeyChecking=accept-new \ "$USER@$HOST:/opt/guanghulab/_logs/rollback-*.json" \ artifacts/ 2>/dev/null || echo "(no rollback receipt — 可能 list-only 模式 / 无快照)" scp -i ~/.ssh/domain_key -P "$PORT" \ -o StrictHostKeyChecking=accept-new \ "$USER@$HOST:/opt/guanghulab/_logs/deploy-report.md" \ artifacts/deploy-report.md 2>/dev/null || true ls -la artifacts/ || true - name: 把回执贴到 GitHub Actions Summary if: always() run: | set -uo pipefail { echo "# 国内域名机回滚" echo "" echo "模式: \`${{ needs.preflight.outputs.effective_mode }}\`" echo "" echo "## 可用快照" echo "" echo '```' cat snapshot-list.log 2>/dev/null || echo "(no snapshot list)" echo '```' if [ -f rollback-output.log ]; then echo "" echo "## 回滚执行日志" echo "" echo '```' tail -40 rollback-output.log echo '```' fi if [ -f artifacts/deploy-report.md ]; then echo "" echo "## 中文回执 (deploy-report.md)" echo "" cat artifacts/deploy-report.md fi } >> "$GITHUB_STEP_SUMMARY" - name: 上传 artifact if: always() uses: actions/upload-artifact@v4 with: name: domain-rollback-${{ github.run_id }} path: | artifacts/ snapshot-list.log rollback-output.log retention-days: 30 if-no-files-found: warn - name: 清理 SSH 私钥 if: always() run: rm -f ~/.ssh/domain_key ~/.ssh/domain_host