---
name: deploy
owner: team-lead
version: 3
last-updated: 2026-04-23
---

# SOP: 发布代码到 alpha / epsilon

两台机器：
- **alpha** 测试机（1C1G，issue #001 建的）—— 合 main 自动同步（`deploy-staging.yml`）
- **epsilon** 生产机（`112.124.27.213`）—— **打 tag 自动部署**（`release.yml` 的 deploy-production job）

整体链路见 [release-flow.md](../release-flow.md)。

## alpha（自动）

### 原理

合 main 后 GHA `deploy-staging.yml` 自动执行：
1. checkout 最新 main
2. ssh alpha：rsync 代码（排除 `.venv / data / .git / *.db*`）
3. ssh alpha：`uv sync`
4. ssh alpha：`systemctl restart xiangqin`
5. curl alpha `/health` smoke test

### 人肉验收

GHA 跑完后本地：

```bash
# 环境变量临时指向 alpha
xq --endpoint http://<alpha-IP> health
xq --endpoint http://<alpha-IP> register 13800001111   # 真跑注册流程（alpha 用测试号段）
```

完整 alpha 验收清单在 [../release-flow.md](../release-flow.md)。

### 失败处理

| 症状 | 看哪 |
|---|---|
| GHA deploy-staging.yml 红 | GHA 日志 → 常见：ssh key 过期 / 磁盘满 |
| curl alpha/health 不通 | `ssh alpha 'journalctl -u xiangqin -n 100'` |
| rsync 同步了但服务未重启 | `ssh alpha 'systemctl status xiangqin'` |

alpha 挂不阻塞生产（epsilon 独立），修 alpha 可以慢慢来。

## epsilon（tag 触发自动部署）

### 前置条件

- [ ] alpha 已跑通当前 main（上一节）
- [ ] 主仓 `pyproject.toml` 版本号 + `src/xiangqin/__init__.py.__version__` + `CHANGELOG.md` 三处对齐
- [ ] 如果涉及 schema 变更：备份已做（OSS `xiangqin.db` 最近 24h 内的快照存在）

### 触发：打 tag

```bash
cd /Users/yarnb/xiangqin
git tag v0.X.Y
git push --tags
```

push tag 自动触发 `release.yml`：
1. `test` job（gate）—— pytest + ruff 必须绿
2. `publish-pypi` `sync-skills-sh` `publish-clawhub` 并行发三端
3. `deploy-production` job（needs `publish-pypi`）—— rsync → epsilon → systemd restart → smoke test

全程无人工点按钮。GHA `environment: production` 标记了这是真生产部署。

### 本地兜底（GHA 挂时 / 紧急）

```bash
cd /Users/yarnb/xiangqin
bash scripts/deploy.sh epsilon
```

`deploy.sh` 做 4 步（参数化支持 `epsilon` / `alpha`）：
1. rsync 代码到 `root@epsilon:/opt/xiangqin/`（排除 `.venv / data / .git / *.db*`）
2. ssh root 跑 `uv sync`（用 `UV_PYTHON_INSTALL_DIR=/opt/uv-python` 共享路径）+ chown xiangqin
3. 复制 `systemd/xiangqin.service` → `/etc/systemd/system/` + `daemon-reload` + `systemctl restart`
4. 本机 `curl http://112.124.27.213/health` smoke test（内网 via ssh）

### 验收

- [ ] 最后一行 `curl http://112.124.27.213/health` 输出 `{"status":"ok","version":"<新版本>"}`
- [ ] 本地 `xq health`（装好的 PyPI 新版）过
- [ ] 如果是版本 bump：`xq --version` 显示新版

### 失败回滚

不要原地继续 debug 生产，直接按 [../emergency/rollback.md](../emergency/rollback.md) 回到上个 tag。

紧急停服（确认不影响用户再做）：

```bash
ssh root@112.124.27.213 'systemctl stop xiangqin'
```

数据损坏：按 [restore.md](./restore.md)。

## 配置变更（环境变量）

- `XIANGQIN_SMS_PROVIDER=mock|aliyun` —— 短信 provider
- `XIANGQIN_PAYMENT_PROVIDER=mock|wechat|alipay` —— 支付 provider（0.1 期都 mock）
- `XIANGQIN_PHONE_SALT=<随机盐>` —— 手机号 hash 盐（⚠️ 不要改，改了等于让所有老手机号匹配失败）

修 systemd unit 的 `Environment=` 行后：

```bash
scp systemd/xiangqin.service root@112.124.27.213:/etc/systemd/system/
ssh root@112.124.27.213 'systemctl daemon-reload && systemctl restart xiangqin'
```

## 历史事故

- （初版，待事故充填）
