# Release

> 发布 Usage4Claude 新版本时使用。当用户说“发布新版本 / 发版 / 出新版 / release / 打 tag 发布 / 准备发版材料”等，用本 skill 引导完成从收集变更、编写 CHANGELOG 与 RELEASE_NOTES、更新版本号、编译验证，到发版 commit、CI 自动发布的完整流程。

- Skill: `f-is-h/release` (Agent Skill)
- Install (CLI): `npx skillmds@latest add f-is-h/release`
- Raw SKILL.md: https://api.skillmd.com/api/skills/f-is-h/release/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: f-is-h (https://skillmd.com/u/f-is-h)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/f-is-h/release

---


# 发布新版本（Release）

Usage4Claude 采用 **CI 自动发布**：向 `main` push 一个满足条件的 commit 后，
GitHub Actions（`.github/workflows/release.yml`）自动完成构建、签名、发 Release、
更新 Sparkle 更新源。你（Claude）的职责是准备好发版材料并引导用户完成触发，
**不代替用户执行发版 commit 与 push**。

> 若根目录存在 `NEXT_RELEASE.md`，说明本次发版有一次性特殊情况，**先读它**再按本流程走。

## 必须先记住的架构事实

- **两份发布材料，各司其职**（详见下一节分工表）：
  - `CHANGELOG.md` — 完整技术档案 + **版本号权威源**，不进 Sparkle。
  - `docs/RELEASE_NOTES.md` — 面向用户的发布说明，CI 提取当前版本段落后**同时**注入
    Sparkle 弹窗（`appcast.xml` 的 `<description>`）和 GitHub Release 正文。
- **触发条件**：commit message 含 `[release]`/`[RELEASE]`，push 到 `main`，且本次改动
  包含 `CHANGELOG.md` 或 `docs/RELEASE_NOTES.md`（发版通常两者都改）。
- **Sparkle 弹窗内容来自 docs/RELEASE_NOTES.md**（不是 CHANGELOG）。用户在应用内“检查更新”
  看到的就是 RELEASE_NOTES 当前版本段落。
- **docs/RELEASE_NOTES.md 必须有当前版本段落**：否则 Sparkle/Release 正文会为空。CI 的
  validate 阶段会 `grep "^## [X.Y.Z]"` fail-fast，但应发版前就写好。
- **Build 号自动跟随**：`CURRENT_PROJECT_VERSION = $(MARKETING_VERSION)`，Build 恒等于
  Version。**只改 Version，绝不手动固定 Build**，否则 Sparkle 认不出新版本。
- **appcast.xml 由 CI 维护**，绝不手改。
- 详细背景见 `docs/DAILY_RELEASE_WORKFLOW.md`、`docs/SPARKLE_SETUP.md`。

## 两份材料的分工（关键）

| | CHANGELOG.md | docs/RELEASE_NOTES.md |
|---|---|---|
| 定位 | 完整技术档案 + 版本号权威源 | 面向用户的发布说明 |
| 收录范围 | **所有改动**，含内部重构、CI、安全加固 | **只留用户可感知的现象** |
| 措辞 | 可保留技术细节（JWT、actor、base64url 等） | 口语化，去技术词 |
| 致谢 | 不加 | 在相关条目末尾加 `(thanks @author, #N)` |
| CI 喂给 | 无（纯档案；validate 从它提版本号） | Sparkle 弹窗 + GitHub Release 正文 |
| 何时写 | 发版前 | 发版前（不是发布后精修） |

> 规则细节见 `docs/CHANGELOG_AND_RELEASE_NOTES_GUIDELINES.md`。

## 流程

### 1. 收集自上个 tag 以来的变更

```bash
git fetch origin                                     # 先核对远程，避免发版已在别处完成
LAST_TAG=$(git describe --tags --abbrev=0)           # 上一个发布 tag，如 v3.3.0
git log "$LAST_TAG"..HEAD --oneline                  # 变更概览
git log "$LAST_TAG"..HEAD --format='=== %h ===%n%B'  # 完整 message（判断影响面必读）
git log "$LAST_TAG"..HEAD --merges --format='%h %s'  # 合并的 PR（用于致谢）
```

逐条阅读完整 message，区分：用户可感知的现象 vs 纯内部改动。两类都进 CHANGELOG，
但只有前者进 RELEASE_NOTES。

### 2. 决定版本号

读 CHANGELOG.md 顶部当前版本，按语义化递增：

| 改动类型 | 递增 | 例 |
|---|---|---|
| 仅 Bug 修复 | patch | 3.3.0 → 3.3.1 |
| 含新功能 | minor | 3.3.0 → 3.4.0 |
| 破坏性变更 | major | 3.3.0 → 4.0.0 |

版本号不确定时用 AskUserQuestion 让用户确认。

### 3. 编写两份发布材料

**3a. CHANGELOG.md（完整技术档案）**
- 在文件顶部（`# Changelog` 与首个 `## [` 之间）插入新版本段落，日期用当天。
- 收录**所有**改动，按 `Added` / `Changed` / `Fixed` / `Security` 分类。
- **新功能的后续修改/优化/bug 修复并入该功能条目**，不在 Fixed 里重复列出。
- 每个变更点一条，不同变更点只出现一次，简洁不赘述。
- **更新文件底部版本链接**：新增
  `[X.Y.Z]: https://github.com/f-is-h/Usage4Claude/releases/tag/vX.Y.Z`

**3b. docs/RELEASE_NOTES.md（面向用户 + 致谢）**
- 在文件顶部插入 `## [X.Y.Z] - 当天日期` 段落（结构同 CHANGELOG）。
- **只保留用户可感知的现象**，去技术词、口语化。
- 收集本版本对应的 **已合并 PR** 与 **已解决 Issue** 及作者，条目末尾致谢
  `(thanks @author, #N)`：
  ```bash
  gh pr view <n> --repo f-is-h/Usage4Claude --json number,title,author,state
  gh issue view <n> --repo f-is-h/Usage4Claude --json number,title,author,state
  ```
  **只对确已合并的 PR / 确已解决的 Issue 致谢**。未合并的 PR、仍 Open 且本次并未真正
  修复的 Issue **不致谢**，避免误导用户（硬规则）。

两份写入文件的都是**英文**；同时在对话里给用户中文对照（不写进文件）。写好后用
AskUserQuestion 让用户确认草稿再继续。

### 4. 更新 Xcode 版本号

两处 `MARKETING_VERSION`（Debug/Release）都要改：

```bash
sed -i '' 's/MARKETING_VERSION = <旧版本>;/MARKETING_VERSION = <新版本>;/g' \
  Usage4Claude.xcodeproj/project.pbxproj
grep -n "MARKETING_VERSION" Usage4Claude.xcodeproj/project.pbxproj  # 确认两处都改了
```

CHANGELOG 版本与 Xcode 版本**必须完全一致**，否则 CI 的 `verify_version.sh` 会失败。
RELEASE_NOTES 也必须有同一版本段落（CI validate 会 fail-fast）。

### 5. 编译验证

```bash
xcodebuild -project Usage4Claude.xcodeproj -scheme Usage4Claude -configuration Release build 2>&1 | tail -5
```

看到 `** BUILD SUCCEEDED **` 后，核对产物版本号：

```bash
APP=$(find ~/Library/Developer/Xcode/DerivedData -name Usage4Claude.app -path '*/Release/*' | head -1)
/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "$APP/Contents/Info.plist"
/usr/libexec/PlistBuddy -c "Print :CFBundleVersion" "$APP/Contents/Info.plist"  # 应与 Version 相同
```

### 6. 发版 commit + push（**commit 由用户手写**）

发版 commit message 不走日常 `COMMIT_MESSAGE_GUIDELINES` 那套，由用户手工编写。
你只提供草稿供参考，**不擅自 commit/push**——`git add`/`commit`/`push` 需用户确认后执行。

- **只写一行标题，不要正文。** CI 只取第一行（去掉 `[release]` 前缀）作为 GitHub
  Release 标题；正文不被使用（Release 正文来自 RELEASE_NOTES），写了也是浪费。
- 格式：

```
[release] vX.Y.Z - 简短标题
```

- 触发前自检：commit 含 `[release]`、本次改动含 `CHANGELOG.md`/`docs/RELEASE_NOTES.md`、目标分支 `main`。

推送后 CI 触发。

### 7. 监控 CI

```bash
gh run list --workflow=release.yml --limit 3
gh run watch                     # 或看 https://github.com/f-is-h/Usage4Claude/actions
```

CI 三段：validate（版本校验 + RELEASE_NOTES 段落校验）→ build（构建签名，约 8 分钟）→
release（发 Release + 推 appcast.xml 回 main）。失败常见原因：版本号不一致、RELEASE_NOTES
缺当前版本段落、CHANGELOG 版本已发布过。

### 8.（可选）发布后装饰 GitHub Release 页面

CI 已用 RELEASE_NOTES 自动发布了面向用户的 Release，且 Sparkle 弹窗同源——**通常无需再做**。
若想让 GitHub Release 页面更精致（大标题、总览段落、emoji），可发布后手工编辑网页。

- 这属于 **GitHub 页面装饰**，**不回流 Sparkle**（Sparkle 已在发布时拿到 RELEASE_NOTES 段落）。
- 改的是对外公开页面，**先与用户确认再执行**。
- 页面正文结构 = 「RELEASE_NOTES 段落」+ `---` + Installation + `---` + Full Changelog，
  只改第一个 `---` 上方那段，下方模板不要动。用 `gh release edit` 会整体替换正文，需先
  `gh release view vX.Y.Z --json body -q .body` 取回完整正文改上半段后整体回填。

## 发版前的安全测试（不真正发布）

按影响面从小到大三档，用于在正式发版前验证：

**① 本地预览（零风险，什么都不发）**
```bash
swift test                                                          # 单元测试
xcodebuild -project Usage4Claude.xcodeproj -scheme Usage4Claude \
  -configuration Release build                                      # 能否编译
.github/scripts/verify_version.sh verify CHANGELOG.md Usage4Claude.xcodeproj  # 版本号一致性
.github/scripts/generate_release_notes.sh \
  .github/RELEASE_TEMPLATE.md <版本> /tmp/rn_preview.md docs/RELEASE_NOTES.md      # 预览 Release 正文
./scripts/build.sh --config Release                                # 本地打 DMG，验证打包脚本
```
`generate_release_notes.sh` 输出里第一个 `---` **上方**那段，同时也是 **Sparkle 更新弹窗**
会显示的内容（都来自 RELEASE_NOTES）——发版前务必看一眼措辞是否面向用户。

**② `test-release` 分支（CI 构建冒烟，不发布）**
- 把改动 push 到 `test-release` 分支（commit 仍含 `[release]`）。
- CI 跑 validate + build：在 CI 环境编译、导入证书、Sparkle 签名、打 DMG。
- **跳过 release job**（`is_test=true`）→ 不打 tag、不发 Release、不碰 appcast、不 push main。
- 用途：验证 CI 能否构建出签名 DMG（本地过 ≠ CI 过，证书/Sparkle 私钥只在 CI secrets）。
  DMG 作为 artifact 可下载。几乎无需清理。

**③ `workflow_dispatch` dry_run（端到端演练，产草稿）**
- 在 Actions 页面对 **main** 手动运行 workflow，勾选 `dry_run`。
- 跑完整三段，但：tag=`test-v<版本>`、Release 走 `--draft`（公众不可见）、appcast 只打印不 push。
- 用途：演练整条发布链路（含 release job 的 tag/release/appcast 逻辑）而不影响用户。
- **需清理**：`gh release delete test-v<版本> --yes` 且 `git push origin :refs/tags/test-v<版本>`。
- 注意：dry_run 必须在 main 触发才有端到端效果；在 test-release 触发会退化成 ②。

选择：只验发布材料 → ①；验 CI 构建 → ②；验整条链路 → ③。

## 红线清单

- 发版 commit 与 push **由用户执行**，你只准备材料 + 提供草稿。
- 发版前 `CHANGELOG.md` 与 `docs/RELEASE_NOTES.md` 都要有当前版本段落。
- CHANGELOG 版本号与 Xcode `MARKETING_VERSION` 必须一致。
- 只改 Version，**不碰 Build 号**（自动跟随 MARKETING_VERSION）。
- **不手改 appcast.xml**（CI 维护）。
- 致谢只给**确已合并/解决**的 PR/Issue。
- **已发布版本的 CHANGELOG / RELEASE_NOTES 段落不回改**（否则与已发内容、Sparkle 说明不一致）。
- 冒烟测试若需杀进程，用 `kill <PID>` 而非 `killall`（避免误伤用户在跑的正式版实例）。

## 相关文档

- `docs/DAILY_RELEASE_WORKFLOW.md` — 日常发版流程全文
- `docs/CHANGELOG_AND_RELEASE_NOTES_GUIDELINES.md` — CHANGELOG 与 RELEASE_NOTES 编写规范
- `docs/COMMIT_MESSAGE_GUIDELINES.md` — 日常 commit 规范（发版 commit 不适用）
- `docs/SPARKLE_SETUP.md` — Sparkle 自动更新机制
- `.github/workflows/release.yml` — CI 发布流水线
- `.github/RELEASE_TEMPLATE.md` — Release 正文固定模板（Installation 等）

