prepare-release · git:20260827.4a5f580 · 2026-08-27 · sha256 a7c27559f40515b0
prepare-release git:20260827.4a5f580A
Immutable. This exact content is served forever at /api/v1/blob/a7c27559f40515b0.
---
name: prepare-release
description: Prepare a new release by collecting commits, generating bilingual release notes, updating version files, and creating a release branch. Use when asked to prepare/create a release, bump version, or run `/prepare-release`.
---
# Prepare Release
Automate the Cherry Studio release workflow: collect changes → generate bilingual release notes → update files → create release branch → trigger CI/CD.
## Arguments
Parse the version intent from the user's message. Accept any of these forms:
- Bump type keyword: `patch`, `minor`, `major`
- Exact version: strict `x.y.z` or `x.y.z-<prerelease>` without build metadata (e.g. `1.8.0`, `1.8.0-beta.1`, `1.8.0-rc.1`)
- Natural language: "prepare a beta release", "bump to 1.8.0-rc.2", etc.
Defaults to `patch` if no version is specified. Always echo the resolved target version back to the user before proceeding with any file edits.
- `--dry-run`: Preview only, do not create a release branch.
## Workflow
### Step 1: Determine Version
1. Fetch `origin/main` and all tags, then verify that the checkout is a clean `main` at exactly `origin/main`:
```bash
git fetch origin refs/heads/main:refs/remotes/origin/main --tags
test "$(git branch --show-current)" = main
test -z "$(git status --porcelain)"
test "$(git rev-parse HEAD)" = "$(git rev-parse origin/main)"
```
Stop before editing files if any check fails. This prevents a standalone run from creating a release branch from an arbitrary or stale checkout.
2. Read the current version from `package.json`. Post Release keeps this synchronized with the last published release.
3. Before editing, verify that there is no unpublished semantic-version `release/v<version>` branch and no draft semantic-version GitHub Release, including an orphan branch or draft whose counterpart is missing. Use `gh release list --limit 1000 --json isDraft,tagName` and `git ls-remote --heads origin 'refs/heads/release/v*'`, then compare every strict semantic-version tag and branch. Historical release branches are allowed only when their matching release is already published. Stop on any unfinished release state.
4. Resolve the baseline tag as `v{current-version}` and verify that it exists:
```bash
git rev-parse --verify refs/tags/v{current-version}
```
Stop if it is missing. Confirm that it is also the latest published, non-draft GitHub Release whose tag is strict `v<semver>`; non-semver preview releases are never a release baseline:
```bash
gh release list --limit 1000 --json isDraft,publishedAt,tagName --jq '[.[] | select(.isDraft == false and (.tagName | test("^v(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$")))] | sort_by(.publishedAt) | last | .tagName // empty'
```
Stop on a mismatch: the latest Post Release metadata PR must be merged into `main` before another release is prepared.
5. Compute the new version based on the argument:
- `patch` / `minor` / `major`: bump from the current version.
- An exact version must match `^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$` and pass `semver.valid`; build metadata such as `+build.1` is not accepted.
- In both cases, require the result to be strictly greater than the current version according to semver precedence. Reject equal versions and downgrades.
### Step 2: Collect Commits
1. Determine the release-note collection base:
- If the baseline tag is an ancestor of `HEAD`, use the tag.
- Otherwise, use the latest commit whose full message contains the exact marker `release-metadata-boundary: <baseline-tag>`. This machine marker is added to the Post Release pull request body and survives the required squash merge.
- For metadata pull requests created before the machine marker existed, accept a subject exactly equal to `chore(release): sync <baseline-tag> metadata` or that subject followed only by GitHub's squash suffix ` (#<PR-number>)`.
- Stop with an error if the tag is not an ancestor and its metadata sync commit is missing; otherwise already-released hotfixes could be included again.
2. List all commits since that base:
```bash
git log <collection-base>..HEAD --format="%H %s" --no-merges
```
3. For each commit, get the full body:
```bash
git log <hash> -1 --format="%B"
```
4. Extract the content inside `` ```release-note `` code blocks from each commit body.
5. Extract the conventional commit type from the title (`feat`, `fix`, `refactor`, `perf`, `docs`, etc.).
6. **Skip** these commits:
- Titles starting with `🤖 Daily Auto I18N`
- Titles starting with `Merge`
- Titles starting with `chore(deps)`
- Titles starting with `chore: release`
- Titles starting with `chore(release)`
- Commits where the release-note block says `NONE`
### Step 3: Generate Bilingual Release Notes
Using the collected commit information, generate release notes in **both English and Chinese**.
**Format** (must match exactly):
```
<!--LANG:en-->
Cherry Studio {version} - {Brief English Title}
✨ New Features
- [Component] Description
🐛 Bug Fixes
- [Component] Description
💄 Improvements
- [Component] Description
⚡ Performance
- [Component] Description
<!--LANG:zh-CN-->
Cherry Studio {version} - {简短中文标题}
✨ 新功能
- [组件] 描述
🐛 问题修复
- [组件] 描述
💄 改进
- [组件] 描述
⚡ 性能优化
- [组件] 描述
<!--LANG:END-->
```
**Rules:**
- Only include categories that have entries (omit empty categories).
- Each commit appears as exactly ONE line item in the appropriate category.
- Use the `release-note` field if present; otherwise summarize from the commit title.
- Component tags should be short: `[Chat]`, `[Models]`, `[Agent]`, `[MCP]`, `[Settings]`, `[Data]`, `[Build]`, etc.
- Chinese translations should be natural, not machine-literal.
- Do NOT include commit hashes or PR numbers.
- Read the **existing** release notes in `electron-builder.yml` as a style reference before writing.
**IMPORTANT: User-Focused Content Only**
Release notes are for **end users**, not developers. Exclude anything users don't care about:
- **EXCLUDE** internal refactoring, code cleanup, or architecture changes
- **EXCLUDE** CI/CD, build tooling, or test infrastructure changes
- **EXCLUDE** dependency updates (unless they add user-visible features)
- **EXCLUDE** documentation updates
- **EXCLUDE** developer experience improvements
- **EXCLUDE** technical debt fixes with no user-visible impact
- **EXCLUDE** overly technical descriptions (e.g., "fix race condition in Redux middleware")
**INCLUDE** only changes that users will notice:
- New features they can use
- Bug fixes that affected their workflow
- UI/UX improvements they can see
- Performance improvements they can feel
- Security fixes (simplified, without implementation details)
**Keep descriptions simple and non-technical:**
- ❌ "Fix streaming race condition causing partial tool response status in Redux state"
- ✅ "Fix tool status not stopping when aborting"
- ❌ "Auto-convert reasoning_effort to reasoningEffort for OpenAI-compatible providers"
- ✅ "Fix deep thinking mode not working with some providers"
### Step 4: Update Files
1. **`package.json`**: Update the `"version"` field to the new version.
2. **`electron-builder.yml`**: Replace the content under `releaseInfo.releaseNotes: |` with the generated notes. Preserve the 4-space YAML indentation for the block scalar content.
3. **`resources/cherry-studio/release-history.json`**: For a stable `x.y.z` release, add the version and its exact generated bilingual notes at the start of the array. Replace an existing entry for the same version instead of creating a duplicate. Leave this file unchanged for prereleases.
4. **Validate source metadata**: Before generating the product manifest, run `node scripts/release/validate-prepared-release.js --target-version {version}`. This validator is required for both standalone and GitHub Actions preparation; stop if it rejects the changed paths, version ordering, bilingual marker span, or stable history.
5. **Built-in knowledge**: For an interactive local run, run `pnpm build:builtin-knowledge` after validation. This refreshes `resources/builtin-agents/cherry-assistant/product-manifest.json` with the new package version. Never edit the generated manifest by hand. In GitHub Actions, do not run the generator: the workflow runs the same validator first, then runs the trusted generator itself.
### Step 5: Present for Review
Show the user:
- The new version number.
- The full generated release notes.
- A summary of which files were modified.
If `--dry-run` was specified, stop here.
Otherwise, ask the user to confirm before proceeding to Step 6.
### Step 6: Create Release Branch
1. For an interactive local run, repeat Step 1 items 1-4 immediately before creating the branch, including the all-version unfinished branch/draft comparison. Because Step 4 has intentionally prepared and validated release metadata, replace Step 1's clean-worktree assertion with `git status --short` and stop unless every listed path is one of the four allowed release metadata files. The target branch being absent is not a substitute for the complete remote-state recheck. Then create and push a signed, DCO-compliant release commit:
```bash
git fetch origin refs/heads/main:refs/remotes/origin/main --tags
test "$(git branch --show-current)" = main
test "$(git rev-parse HEAD)" = "$(git rev-parse origin/main)"
RELEASES_JSON="$(gh release list --limit 1000 --json isDraft,tagName)"
RELEASE_BRANCHES="$(git ls-remote --heads origin 'refs/heads/release/v*')"
UNFINISHED_RELEASES="$(RELEASES_JSON="$RELEASES_JSON" RELEASE_BRANCHES="$RELEASE_BRANCHES" node <<'NODE'
const strictTag = /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/
const releases = JSON.parse(process.env.RELEASES_JSON)
const branchTags = new Set(
process.env.RELEASE_BRANCHES.split('\n').filter(Boolean).map((line) => line.split(/\s+/)[1].replace('refs/heads/release/', ''))
)
const unfinished = new Set()
for (const tag of branchTags) {
if (!strictTag.test(tag)) continue
const release = releases.find((candidate) => candidate.tagName === tag)
if (!release || release.isDraft) unfinished.add(tag)
}
for (const release of releases) {
if (release.isDraft && strictTag.test(release.tagName) && !branchTags.has(release.tagName)) {
unfinished.add(`${release.tagName} (draft without branch)`)
}
}
process.stdout.write([...unfinished].join(', '))
NODE
)"
test -z "$UNFINISHED_RELEASES"
test "$(node -p "require('./package.json').version")" = "{version}"
BASELINE_VERSION="$(git show HEAD:package.json | jq -r .version)"
BASELINE_TAG="v$BASELINE_VERSION"
git rev-parse --verify "refs/tags/$BASELINE_TAG"
LATEST_PUBLISHED="$(gh release list --limit 1000 --json isDraft,publishedAt,tagName --jq '[.[] | select(.isDraft == false and (.tagName | test("^v(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$")))] | sort_by(.publishedAt) | last | .tagName // empty')"
test "$LATEST_PUBLISHED" = "$BASELINE_TAG"
test -z "$(git ls-remote --heads origin refs/heads/release/v{version})"
git status --short
UNEXPECTED_RELEASE_PATHS="$(git status --porcelain | cut -c4- | grep -Ev '^(package\.json|electron-builder\.yml|resources/cherry-studio/release-history\.json|resources/builtin-agents/cherry-assistant/product-manifest\.json)$' || true)"
test -z "$UNEXPECTED_RELEASE_PATHS"
git checkout -b release/v{version}
git add package.json electron-builder.yml resources/cherry-studio/release-history.json resources/builtin-agents/cherry-assistant/product-manifest.json
git commit -S --signoff -m "chore(release): prepare v{version}"
git cat-file commit HEAD | grep -q '^gpgsig '
git log -1 --format=%B | grep -q '^Signed-off-by: '
git push -u origin release/v{version}
```
2. In GitHub Actions, stop after updating `package.json` and `electron-builder.yml`, plus `resources/cherry-studio/release-history.json` only for a stable release, and leave them uncommitted. The `prepare-release.yml` workflow validates their contents, generates the product manifest, owns branch creation, and uses GitHub's API to create and verify the signed, DCO-compliant commit. Never run `git commit` or `git push` from the Claude step.
3. Report the release branch and next steps. Do not create a PR yet: the release must be built and published from this branch first.
## CI Trigger Chain
- Wait for the **CI** push run on the new `release/v{version}` commit to succeed, then run **`release.yml`** manually with that release branch selected. It validates the branch name against `package.json`, builds the exact branch commit on macOS, Windows, and Linux, and creates or updates a draft GitHub Release.
- While a single draft semantic-version release is active, **`backport-release-fixes.yml`** opens a backport PR for the first merged `hotfix: <description>` or `hotfix(<kebab-case-scope>): <description>` PR from `main`, applies any optional bilingual release note, then appends consecutive hotfixes and source markers to that same open topic branch. It manages every source PR's `hotfix` and backport-status labels and reports failures on the source PR; never merge `main` into the release branch.
- Review the backport PR, wait for its CI, and merge it. After the resulting release-branch push passes CI, run **`release.yml`** again from the release branch to rebuild the draft release.
- Publish only through the **`release.yml`** `publish` operation on the release branch. It shares the release-state lock with preparation, builds, and backports; verifies the exact successful all-platform build; then publishes the still-current draft. Publication triggers **`post-release.yml`**, which verifies that the tag still matches the release branch, applies only the release metadata delta to the latest `main`, and creates a `release-sync/v{version}` metadata-only PR.
- The metadata PR synchronizes only `package.json`, `electron-builder.yml`, release history, and the generated product manifest. It triggers **`ci.yml`**; merge it only after CI passes.
- When squash-merging the metadata PR, set the commit title to exactly `chore(release): sync v{version} metadata` with only GitHub's optional PR-number suffix, and keep `release-metadata-boundary: v{version}` on its own line in the squash commit body so the next release can find the boundary reliably.
## Constraints
- Always read `electron-builder.yml` before modifying it to understand the current format.
- Never modify files other than `package.json`, `electron-builder.yml`, `resources/cherry-studio/release-history.json`, and the generated `resources/builtin-agents/cherry-assistant/product-manifest.json`.
- Never push directly to `main`.
- Never create the release metadata PR before the GitHub Release is published; `post-release.yml` owns that step.
- Always show the generated release notes to the user before creating the release branch (unless running in CI with no interactive user).