310 lines
13 KiB
YAML
310 lines
13 KiB
YAML
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@<host>:/opt/guanghu/lighthouse-cn/"
|
|
echo "would ssh $USER@<host> 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
|