name: lighthouse-cn-deploy # ════════════════════════════════════════════════════════════════ # 光湖灯塔 · 国内主服务器手动部署 # Sovereign: TCS-0002∞ · 国作登字-2026-A-00037559 # 守护: 铸渊 · ICE-GL-ZY001 # # 仅手动触发. 在 Actions 页面 Run workflow: # server_target: main / backup # stage: bootstrap / update / rollback # # 必需 Secrets (主服务器): # ZY_LIGHTHOUSE_HOST / ZY_LIGHTHOUSE_USER / ZY_LIGHTHOUSE_KEY / ZY_LIGHTHOUSE_PORT # ZY_LIGHTHOUSE_DOMAIN # 可选 (备用机): # ZY_LIGHTHOUSE_BACKUP_HOST / ZY_LIGHTHOUSE_BACKUP_USER / ZY_LIGHTHOUSE_BACKUP_KEY # Bootstrap 阶段需要: # ZY_GITEA_ADMIN_USER / ZY_GITEA_ADMIN_PASS / ZY_GITEA_ADMIN_EMAIL / ZY_GITEA_DB_PASS # Update 阶段 (注册 runner) 还需要: # ZY_GITEA_RUNNER_TOKEN # ════════════════════════════════════════════════════════════════ on: workflow_dispatch: inputs: server_target: description: '部署目标' required: true default: 'main' type: choice options: - main - backup stage: description: '部署阶段' required: true default: 'bootstrap' type: choice options: - bootstrap - update - rollback confirm_phrase: description: 'bootstrap 必填『我确认部署』 / rollback 必填『我确认回滚』(也可填『我确认部署』向下兼容); 空 = 自动 dry-run' required: false default: '' type: string dry_run: description: '只打印计划, 不真的执行 (true/false)' required: false default: 'false' type: choice options: - 'false' - 'true' permissions: contents: read concurrency: group: lighthouse-cn-${{ inputs.server_target }} 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: - name: Checkout uses: actions/checkout@v4 - name: Setup Node uses: actions/setup-node@v4 with: node-version: '20' - name: 启动前最小密钥校验 (中文报告) env: ZY_LIGHTHOUSE_HOST: ${{ secrets.ZY_LIGHTHOUSE_HOST }} ZY_LIGHTHOUSE_USER: ${{ secrets.ZY_LIGHTHOUSE_USER }} ZY_LIGHTHOUSE_KEY: ${{ secrets.ZY_LIGHTHOUSE_KEY }} ZY_LIGHTHOUSE_PORT: ${{ secrets.ZY_LIGHTHOUSE_PORT }} ZY_LIGHTHOUSE_DOMAIN: ${{ secrets.ZY_LIGHTHOUSE_DOMAIN }} ZY_GITEA_ADMIN_USER: ${{ secrets.ZY_GITEA_ADMIN_USER }} ZY_GITEA_ADMIN_PASS: ${{ secrets.ZY_GITEA_ADMIN_PASS }} ZY_GITEA_ADMIN_EMAIL: ${{ secrets.ZY_GITEA_ADMIN_EMAIL }} ZY_GITEA_DB_PASS: ${{ secrets.ZY_GITEA_DB_PASS }} ZY_GITEA_RUNNER_TOKEN: ${{ secrets.ZY_GITEA_RUNNER_TOKEN }} ZY_LIGHTHOUSE_BACKUP_HOST: ${{ secrets.ZY_LIGHTHOUSE_BACKUP_HOST }} ZY_LIGHTHOUSE_BACKUP_USER: ${{ secrets.ZY_LIGHTHOUSE_BACKUP_USER }} ZY_LIGHTHOUSE_BACKUP_KEY: ${{ secrets.ZY_LIGHTHOUSE_BACKUP_KEY }} ZY_COS_SECRET_ID: ${{ secrets.ZY_COS_SECRET_ID }} ZY_COS_SECRET_KEY: ${{ secrets.ZY_COS_SECRET_KEY }} ZY_COS_BUCKET: ${{ secrets.ZY_COS_BUCKET }} ZY_COS_REGION: ${{ secrets.ZY_COS_REGION }} ZY_DISPATCH_TOKEN: ${{ secrets.ZY_DISPATCH_TOKEN }} ZY_FEISHU_WEBHOOK: ${{ secrets.ZY_FEISHU_WEBHOOK }} run: | node scripts/preflight/check-secrets.js \ --workflow lighthouse-cn-deploy \ --stage "${{ inputs.stage }}" \ --target "${{ inputs.server_target }}" - name: 误触锁 · bootstrap/rollback 必须显式确认 id: misclick env: STAGE: ${{ inputs.stage }} PHRASE: ${{ inputs.confirm_phrase }} DRY: ${{ inputs.dry_run }} run: | set -e EFFECTIVE_DRY="$DRY" # 口令兼容: bootstrap 收『我确认部署』; rollback 同时接受 # 『我确认部署』(向下兼容旧用户记忆) 和『我确认回滚』 # (与独立的 lighthouse-cn-rollback workflow 对齐). REQUIRED="" OK="false" if [ "$STAGE" = "bootstrap" ]; then REQUIRED="我确认部署" [ "$PHRASE" = "我确认部署" ] && OK="true" elif [ "$STAGE" = "rollback" ]; then REQUIRED="我确认回滚 (或我确认部署)" { [ "$PHRASE" = "我确认回滚" ] || [ "$PHRASE" = "我确认部署" ]; } && OK="true" else OK="skip" fi if [ "$OK" = "false" ]; then echo "═══════════════════════════════════════════════" echo " ⚠️ 误触保护: stage=$STAGE 必须在 confirm_phrase" echo " 字段输入『$REQUIRED』才会真跑." echo " 当前值=「$PHRASE」 → 自动切到 dry-run 模式." echo " (这一层是为了防止 Awen / 冰朔不小心点错按钮)" echo "═══════════════════════════════════════════════" EFFECTIVE_DRY="true" elif [ "$OK" = "true" ]; then echo "✅ 已显式确认 (stage=$STAGE, phrase=「$PHRASE」)" fi echo "effective_dry_run=$EFFECTIVE_DRY" >> "$GITHUB_OUTPUT" deploy: name: ${{ inputs.server_target }} · ${{ inputs.stage }} needs: preflight runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout uses: actions/checkout@v4 - name: Resolve target server id: target env: MAIN_HOST: ${{ secrets.ZY_LIGHTHOUSE_HOST }} MAIN_USER: ${{ secrets.ZY_LIGHTHOUSE_USER }} MAIN_KEY: ${{ secrets.ZY_LIGHTHOUSE_KEY }} MAIN_PORT: ${{ secrets.ZY_LIGHTHOUSE_PORT }} BACKUP_HOST: ${{ secrets.ZY_LIGHTHOUSE_BACKUP_HOST }} BACKUP_USER: ${{ secrets.ZY_LIGHTHOUSE_BACKUP_USER }} BACKUP_KEY: ${{ secrets.ZY_LIGHTHOUSE_BACKUP_KEY }} TARGET: ${{ inputs.server_target }} run: | set -euo pipefail if [ "$TARGET" = "main" ]; then HOST="$MAIN_HOST" USER="$MAIN_USER" KEY="$MAIN_KEY" PORT="${MAIN_PORT:-22}" SERVER_ID="GH-CVM-MAIN-PROD-01" else HOST="$BACKUP_HOST" USER="$BACKUP_USER" KEY="$BACKUP_KEY" PORT="22" SERVER_ID="GH-CVM-BACKUP-01" fi if [ -z "$HOST" ]; then echo "❌ 目标服务器 ($TARGET) 的 HOST secret 未配置, 终止" >&2 exit 1 fi if [ -z "$KEY" ]; then echo "❌ 目标服务器 ($TARGET) 的 KEY secret 未配置, 终止" >&2 exit 1 fi # 不输出 HOST 到 stdout 以避免被截图泄露 echo "host=$HOST" >> "$GITHUB_OUTPUT" echo "user=$USER" >> "$GITHUB_OUTPUT" echo "port=$PORT" >> "$GITHUB_OUTPUT" echo "server_id=$SERVER_ID" >> "$GITHUB_OUTPUT" # KEY 单独写入文件 umask 077 mkdir -p ~/.ssh printf '%s\n' "$KEY" > ~/.ssh/lighthouse_key chmod 600 ~/.ssh/lighthouse_key ssh-keyscan -p "$PORT" -H "$HOST" >> ~/.ssh/known_hosts 2>/dev/null || true echo "✅ SSH 凭据就绪 ($SERVER_ID)" - name: Sanity check (dry-run) if: needs.preflight.outputs.effective_dry_run == 'true' env: HOST: ${{ steps.target.outputs.host }} USER: ${{ steps.target.outputs.user }} PORT: ${{ steps.target.outputs.port }} run: | echo "=== DRY RUN ===" echo "would rsync server/setup/lighthouse-cn/ → $USER@:/opt/guanghu/lighthouse-cn/" echo "would ssh $USER@ bash /opt/guanghu/lighthouse-cn/bootstrap.sh STAGE=${{ inputs.stage }}" echo "(real host masked)" - name: Rsync template to server if: needs.preflight.outputs.effective_dry_run != 'true' env: HOST: ${{ steps.target.outputs.host }} USER: ${{ steps.target.outputs.user }} PORT: ${{ steps.target.outputs.port }} run: | set -euo pipefail ssh -i ~/.ssh/lighthouse_key -p "$PORT" \ -o StrictHostKeyChecking=accept-new \ "$USER@$HOST" \ "mkdir -p /opt/guanghu/lighthouse-cn" rsync -avz --delete \ -e "ssh -i ~/.ssh/lighthouse_key -p $PORT -o StrictHostKeyChecking=accept-new" \ server/setup/lighthouse-cn/ \ "$USER@$HOST:/opt/guanghu/lighthouse-cn/" # 顺带把 restore 脚本 + workflow 转换器也推上去, 后续阶段要用 rsync -avz \ -e "ssh -i ~/.ssh/lighthouse_key -p $PORT -o StrictHostKeyChecking=accept-new" \ scripts/migration/ \ "$USER@$HOST:/opt/guanghu/lighthouse-cn/migration/" - name: Run bootstrap / update / rollback if: needs.preflight.outputs.effective_dry_run != 'true' env: HOST: ${{ steps.target.outputs.host }} USER: ${{ steps.target.outputs.user }} PORT: ${{ steps.target.outputs.port }} SERVER_ID: ${{ steps.target.outputs.server_id }} STAGE: ${{ inputs.stage }} GITEA_ADMIN_USER: ${{ secrets.ZY_GITEA_ADMIN_USER }} GITEA_ADMIN_PASS: ${{ secrets.ZY_GITEA_ADMIN_PASS }} GITEA_ADMIN_EMAIL: ${{ secrets.ZY_GITEA_ADMIN_EMAIL }} GITEA_DB_PASS: ${{ secrets.ZY_GITEA_DB_PASS }} GITEA_RUNNER_TOKEN: ${{ secrets.ZY_GITEA_RUNNER_TOKEN }} run: | set -euo pipefail # 把 secrets 写到一个临时 envfile, scp 上去 source, 不进进程参数 ENVFILE="$(mktemp)" chmod 600 "$ENVFILE" { printf 'export CN_LIGHTHOUSE_SERVER_ID=%q\n' "$SERVER_ID" printf 'export STAGE=%q\n' "$STAGE" printf 'export GITEA_ADMIN_USER=%q\n' "$GITEA_ADMIN_USER" printf 'export GITEA_ADMIN_PASS=%q\n' "$GITEA_ADMIN_PASS" printf 'export GITEA_ADMIN_EMAIL=%q\n' "$GITEA_ADMIN_EMAIL" printf 'export GITEA_DB_PASS=%q\n' "$GITEA_DB_PASS" printf 'export GITEA_RUNNER_TOKEN=%q\n' "$GITEA_RUNNER_TOKEN" } > "$ENVFILE" REMOTE_ENV="/tmp/lighthouse-bootstrap.env.$$" scp -i ~/.ssh/lighthouse_key -P "$PORT" \ -o StrictHostKeyChecking=accept-new \ "$ENVFILE" "$USER@$HOST:$REMOTE_ENV" rm -f "$ENVFILE" # 远程执行: source envfile → 跑 bootstrap → 立刻删 envfile # GitHub Actions 自动屏蔽 secrets 在 stdout/stderr 中的明文出现 (***) ssh -i ~/.ssh/lighthouse_key -p "$PORT" \ -o StrictHostKeyChecking=accept-new \ "$USER@$HOST" \ "set -e; chmod 600 '$REMOTE_ENV'; \ . '$REMOTE_ENV'; \ rm -f '$REMOTE_ENV'; \ set +x; \ bash /opt/guanghu/lighthouse-cn/bootstrap.sh" \ 2>&1 | tee bootstrap-output.log - name: Pull back latest receipt if: needs.preflight.outputs.effective_dry_run != 'true' && always() env: HOST: ${{ steps.target.outputs.host }} USER: ${{ steps.target.outputs.user }} PORT: ${{ steps.target.outputs.port }} run: | set -uo pipefail mkdir -p artifacts scp -i ~/.ssh/lighthouse_key -P "$PORT" \ -o StrictHostKeyChecking=accept-new \ "$USER@$HOST:/opt/guanghu/_logs/lighthouse-bootstrap-*.json" \ artifacts/ 2>/dev/null || echo "(no receipt yet)" ls -la artifacts/ || true - name: Upload receipts if: needs.preflight.outputs.effective_dry_run != 'true' && always() uses: actions/upload-artifact@v4 with: name: lighthouse-bootstrap-receipt-${{ inputs.server_target }}-${{ inputs.stage }} path: | artifacts/ bootstrap-output.log retention-days: 30 if-no-files-found: warn - name: Cleanup SSH key if: always() run: | rm -f ~/.ssh/lighthouse_key