---
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 里一起改
- [ ] **SKILL.md ClawHub 安全扫描 review**（见下面清单）
- [ ] GitHub secrets 已配：`PYPI_API_TOKEN` / `SKILL_REPO_DEPLOY_KEY`

### SKILL.md 发布前安全扫描 review 清单

ClawHub 每次 version push 自动用 **VirusTotal（含 Code insights AI）+ OpenClaw** 扫 SKILL.md。
任一扫描器判 Suspicious 会在 skill 页面顶部 "Skill flagged" banner，降低装机率。

**CI 自动防回归（首选）**：`test/test_skill_md.py` 每次 push / PR 跑，**6 条规则**自动检查（安全段存在性 / placeholder / 敏感数据 / vault / 网络 / pip+webhook）。违反 → `test gate` 挂 → `release.yml` / `deploy-staging.yml` 都 block。**新加 / 修 SKILL.md 不改动代码也必须跑 pytest。**

手动 grep 补充（CI 命中后想自查怎么改）：

```bash
grep -iE "<[a-z_]+>|<姓名>|<phone>|<身份证>|<where>" SKILL.md     # 用户输入占位
grep -iE "shell|curl \\| bash|eval|exec|sudo|root" SKILL.md      # shell 执行关键字
grep -iE "身份证|credential|api.*key|secret|token" SKILL.md       # 敏感数据关键字
grep -iE "pip install|acong-tech-xiangqin" SKILL.md              # 供应链关键字（Rule 6）
grep -iE "agent_gateway_url|agent_hooks_token|webhook" SKILL.md  # webhook 关键字（Rule 6）
```

命中以上任一 → 在 SKILL.md `## 安全` 段**显式披露对应安全机制**（不能留空白让 AI 自己推断）：

- `xq <cmd> '<user_input>'` 样例 → 声明 "**参数作为 JSON body POST API，不经 shell exec；服务端 DSL 白名单解析**"
- 敏感数据字段 → 声明存储方式（**HMAC-SHA256 hash + salt，明文不落库不落日志**）
- session / 凭证 → 声明边界（**skill user 只需 session token 存本地，Vault 是服务端工具不装客户端**）
- 网络 → 声明域名白名单（**只访问 xq.agentaily.com 或 XIANGQIN_ENDPOINT 显式配的端点**）
- **`pip install` / PyPI 包**（v0.6.3→0.6.5 踩坑）→ 声明 "**公开 MIT 包 / 可 pin 版本 / 可 venv 隔离**"；如果文档提到 auto-update → 声明 "**`pip install -U` 需用户显式 consent，不自动提权**"
- **`agent_gateway_url` / `agent_hooks_token` / webhook**（v0.6.3→0.6.5 踩坑）→ 声明 "**opt-in 可选配置 / URL 由用户提供 / 不配 = 不推送 / HMAC 签名 / 推送内容不含明文正文**"

参考完整范式：xiangqin v0.6.5 的 SKILL.md `## 安全` 段（commit `d5e6ed2`，9 条披露覆盖全 6 类关键字）。

### 踩坑历史（复盘驱动 lint rule）

| 版本 | 扫描器判决 | 根因 | 修法 |
|---|---|---|---|
| v0.6.0 → v0.6.1 | VT Code insights 判 suspicious（shell injection 推断） | `xq query '<where>'` 样例里 `<where>` 占位，VT AI 从语法推断 shell 风险 | 加 `## 安全` 段声明 DSL 白名单 + 不经 shell → Rule 1/2 |
| v0.6.3 → v0.6.5 | OpenClaw LLM (gpt-5-mini) 判 suspicious medium | SKILL.md 提 `pip install` + webhook + session token 三项运行时行为，安全段没显式披露 → LLM 从"看不见 = 可疑"推断 | 加 3 条披露（供应链 / consent / opt-in）→ Rule 6 |

每加一条 lint rule 都带**具体版本号 + LLM summary 引用**，避免未来在同一踩坑类型重复犯错。

### 发布后验证扫描结果（CLI 权威，不依赖 GUI）

`release.yml` 的 **"发布后扫描结果 verify" step** 自动跑 `scripts/check-clawhub-scan.sh`，
拿 ClawHub REST API 权威数据（不走 HTML scrape）：

```bash
# 手动验证（任意版本）
./scripts/check-clawhub-scan.sh 0.6.1
# 带 token 看 moderation 细节
CLAWHUB_TOKEN=$(python3 -c "import json; print(json.load(open('$HOME/Library/Application Support/clawhub/config.json'))['token'])") \
  ./scripts/check-clawhub-scan.sh 0.6.1
```

**API 路径**（全官方，不 scrape）：
- `GET /api/v1/skills/<slug>/versions/<v>` → `version.security`（public）
- `GET /api/v1/skills/<slug>/moderation` → `moderation.isSuspicious / verdict`（要 token）

**gating 逻辑**（脚本实现）：
- `security.status == clean` 且 `scanners.llm.verdict == benign` → pass
- `scanners.vt == null`（pending）不 block（VT 异步，几小时内补上）
- `moderation.isSuspicious == true` / `isMalwareBlocked == true` → fail
- 脚本 exit 0/1 直接给 CI 用

**GUI 是辅助**（https://clawhub.ai/<owner>/<slug>）—— 但 API 是权威源。

**发布失败 / 被 flag 的修复路径**：
1. 看 VT Code insights / OpenClaw 5 维度具体说啥（[concept 机制详解](../../wiki/concepts/clawhub-security-scanning.md)）
2. SKILL.md 加 disclaimer（**不改代码**，只改文档，patch 版本 bump）
3. push 新 tag → 重扫覆盖老 flag
4. 老 flag 不需要主动申诉 / 请求人工 review

## 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`
