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

# SOP: 发布新版本到 PyPI + ClawHub Skill

一个 tag 同时发两边。决策见 [release-deploy-sop ADR](../../decisions/2026-04-23-release-deploy-sop.md) C/D 条。

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

## 前置

- [ ] alpha 已经跑过当前 main（见 [deploy.md](./deploy.md) alpha 段）
- [ ] 本地 `uv run pytest test/ -q` 全绿
- [ ] `pyproject.toml` 版本号和 `src/xiangqin/__init__.py.__version__` 对齐
- [ ] `CHANGELOG.md` 把 `[Unreleased]` 段提到 `[X.Y.Z]` 新段（老段保留不改）
- [ ] `SKILL.md` 内容如果变更，本次 commit 里一起改
- [ ] GitHub secrets 已配：`PYPI_API_TOKEN` / `SKILL_REPO_DEPLOY_KEY`

## live（推荐，走 GHA）

```bash
cd /Users/yarnb/xiangqin

# 1. bump 版本号（3 处同时）
#    pyproject.toml       version = "0.4.0"
#    src/xiangqin/__init__.py   __version__ = "0.4.0"
#    CHANGELOG.md         [Unreleased] → [0.4.0] - 2026-04-23

# 2. commit
git add pyproject.toml src/xiangqin/__init__.py CHANGELOG.md
git commit -m "release: 0.4.0"
git push

# 3. 等 ci.yml 绿（test / lint / mkdocs --strict）
#    合 main 后 deploy-staging.yml 会自动同步到 alpha，先在 alpha 跑一圈

# 4. 打 tag + push → 触发 release.yml
git tag v0.4.0
git push --tags
```

GHA `release.yml`（on tag push）做两件事：

1. **发 PyPI**
   ```
   uv build
   uv publish --token $PYPI_API_TOKEN
   ```

2. **同步 SKILL.md 到 xiangqin skill 仓**
   ```
   git clone git@github.com:acong-tech-skills/xiangqin.git /tmp/skill
   cp SKILL.md /tmp/skill/
   cd /tmp/skill
   git commit -am "sync xiangqin SKILL.md v0.4.0"
   git tag v0.4.0
   git push --follow-tags
   ```

## canary（开发中验证包能装）

真发布前想让自己装一下包测测，用 dev 后缀不占 live 版本号：

```bash
bash scripts/publish-pypi.sh --canary

# 干净环境装测
python3 -m venv /tmp/xq-verify
/tmp/xq-verify/bin/pip install --pre acong-tech-xiangqin
/tmp/xq-verify/bin/xq health
```

canary 只发 PyPI，不碰 skill 仓。canary 版本号形如 `0.4.0.dev202604231930`。

## 本地兜底（GHA 挂时）

如果 release.yml 红了不能等，本地直接发：

```bash
# PyPI
cd /Users/yarnb/xiangqin
rm -rf dist/
uv build
uv publish --token $(vault get pypi api_tokens --unmask --json | jq -r '.[] | select(.name=="acong-tech-publish") | .token')

# Skill 仓
git clone git@github.com:acong-tech-skills/xiangqin.git /tmp/skill  # 首次
cd /tmp/skill && git pull
cp /Users/yarnb/xiangqin/SKILL.md .
git commit -am "sync xiangqin SKILL.md v0.4.0"
git tag v0.4.0
git push --follow-tags
```

## 验收

- [ ] `curl -s https://pypi.org/pypi/acong-tech-xiangqin/json | python3 -c 'import json,sys; d=json.load(sys.stdin); print(sorted(d["releases"].keys())[-3:])'` 看到新版本
- [ ] `pip install acong-tech-xiangqin==X.Y.Z` 在干净环境能装
- [ ] `github.com/acong-tech-skills/xiangqin` 有新 tag `vX.Y.Z`
- [ ] ClawHub 同步延迟几分钟到半小时（观察）
- [ ] deploy-production job 自动跑（release.yml 的 chain 里）；ssh epsilon curl /health 看 version 是否对

## 版本号 bump 规则

SemVer：
- **patch**：bugfix（`0.3.1 → 0.3.2`）
- **minor**：新功能（`0.3.1 → 0.4.0`）；0.x 期允许 BREAKING，`CHANGELOG.md` 标 `### Changed (BREAKING)`
- **major**：生产就绪 + API 稳定承诺（`0.x → 1.0.0`）

**版本号唯一源 = `pyproject.toml` 的 `version`**。改这个字段时必须同步改：
1. `src/xiangqin/__init__.py.__version__`
2. `CHANGELOG.md` 新版本段

## 回滚

见 [../emergency/rollback.md](../emergency/rollback.md)。核心：PyPI 不能删版本，发 patch 修正版；skill 仓 revert；epsilon 重跑老 tag deploy。

## 坑

- `uv tool install --prerelease allow` 刚发完立即装有 simple index cache 问题，用 `pip install --pre` 替代
- `httpx[socks]` extra 忘了会在有 SOCKS 代理的环境崩（pyproject 要写 `httpx[socks]>=0.27,<1.0`）
- 版本改了但 `__init__.py.__version__` 忘了跟进：`/health` 显示旧版本，用户迷惑
- **skill 仓首次建需要手动**：`github.com/acong-tech-skills/xiangqin` 仓必须先存在，`release.yml` 不会替你建空仓
- CHANGELOG 老版本段**不改**（只 append `[Unreleased]` → 新版本段）—— 见全局 `feedback_no_rewrite_history_docs`
