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
- For an interactive local run, fetch
origin/main and all tags, then verify that the checkout is a clean main at exactly origin/main: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.
In GitHub Actions, use the workflow's frozen dispatch SHA and leave checkout validation to the workflow. Do not fetch or compare the later origin/main head.
- Read the current version from
package.json. Post Release keeps this synchronized with the last published release.
- Resolve the baseline tag as
v{current-version} and verify that it exists: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: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.
- 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
- 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.
- List all commits since that base:
git log <collection-base>..HEAD --format="%H %s" --no-merges
- For each commit, get the full body:
git log <hash> -1 --format="%B"
- Extract the content inside
```release-note code blocks from each commit body.
- Extract the conventional commit type from the title (
feat, fix, refactor, perf, docs, etc.).
- 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.
Recommended format:
<!--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-->
The language markers are the machine-readable contract: include each marker once, keep them in order, and provide non-empty English and Chinese sections. Titles and surrounding explanatory text are presentation choices, not validation requirements.
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
package.json: Update the "version" field to the new version.
electron-builder.yml: Replace the content under releaseInfo.releaseNotes: | with the generated notes. Preserve the 4-space YAML indentation for the block scalar content.
resources/cherry-studio/release-history.json: Never edit by hand; the notes must match electron-builder.yml byte for byte. Run node scripts/release/sync-release-history.js --target-version {version}, which prepends (or replaces) the entry for a stable release and leaves the file untouched for a prerelease. In GitHub Actions, the workflow runs this itself after the Claude step.
- Validate source metadata: For an interactive local run, run
node scripts/release/validate-prepared-release.js --target-version {version} before generating the product manifest, and stop if it rejects the changed paths, version ordering, bilingual sections, or stable history. In GitHub Actions, leave validation to the workflow step that runs after Claude.
- 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
- For an interactive local run, repeat Step 1 items 1-3 immediately before creating the branch. 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. Then create and push a signed, DCO-compliant release commit: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)"
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"
REPO="$(gh repo view --json nameWithOwner --jq .nameWithOwner)"
gh api --paginate --slurp "repos/$REPO/releases?per_page=100" | TAG="v{version}" node scripts/release/validate-release-state.js prepare
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}
- In GitHub Actions, stop after updating
package.json and electron-builder.yml. Temporary helper files and local Git operations are allowed; the workflow extracts those two file changes, restores the frozen source SHA, and discards everything else. It then derives the release history, validates, generates the product manifest, creates the branch, and uses GitHub's API to create and verify the signed, DCO-compliant commit. Never push from the Claude step.
- 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. auto-release-build.yml revalidates that exact live branch head and dispatches release.yml with all; it builds macOS, Windows, and Linux and creates or updates the draft GitHub Release. Use release.yml manually only to retry a failed all-platform build or one platform for the unchanged tagged commit.
- 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, the exact-head all-platform draft rebuild starts automatically.
- A successful exact-head all-platform build starts
publish-release.yml. Approve the release Environment deployment after inspecting the draft. Publication then acquires the release-state lock, revalidates the approved run, release branch, tag, draft, artifacts, open PRs, and pending hotfixes, and publishes only if they still agree. The draft body contains the bilingual electron-builder.yml notes followed by GitHub's generated changes. The final fetched main SHA is the hotfix cutoff; a hotfix merged after that snapshot belongs to the next release. Publication triggers post-release.yml, which uses the published tag as its source, 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 retain changes outside
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).
1---2name: prepare-release3description: 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`.4---5
6# Prepare Release
7
8Automate the Cherry Studio release workflow: collect changes → generate bilingual release notes → update files → create release branch → trigger CI/CD.
9
10## Arguments
11
12Parse the version intent from the user's message. Accept any of these forms:
13- Bump type keyword: `patch`, `minor`, `major`
14- 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`)
15- Natural language: "prepare a beta release", "bump to 1.8.0-rc.2", etc.
16
17Defaults to `patch` if no version is specified. Always echo the resolved target version back to the user before proceeding with any file edits.
18
19- `--dry-run`: Preview only, do not create a release branch.
20
21## Workflow
22
23### Step 1: Determine Version
24
251. For an interactive local run, fetch `origin/main` and all tags, then verify that the checkout is a clean `main` at exactly `origin/main`:
26 ```bash
27 git fetch origin refs/heads/main:refs/remotes/origin/main --tags
28 test "$(git branch --show-current)" = main
29 test -z "$(git status --porcelain)"
30 test "$(git rev-parse HEAD)" = "$(git rev-parse origin/main)"
31 ```
32 Stop before editing files if any check fails. This prevents a standalone run from creating a release branch from an arbitrary or stale checkout.
33 In GitHub Actions, use the workflow's frozen dispatch SHA and leave checkout validation to the workflow. Do not fetch or compare the later `origin/main` head.
342. Read the current version from `package.json`. Post Release keeps this synchronized with the last published release.
353. Resolve the baseline tag as `v{current-version}` and verify that it exists:
36 ```bash
37 git rev-parse --verify refs/tags/v{current-version}
38 ```
39 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:
40 ```bash
41 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'
42 ```
43 Stop on a mismatch: the latest Post Release metadata PR must be merged into `main` before another release is prepared.
444. Compute the new version based on the argument:
45 - `patch` / `minor` / `major`: bump from the current version.
46 - 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.
47 - In both cases, require the result to be strictly greater than the current version according to semver precedence. Reject equal versions and downgrades.
48
49### Step 2: Collect Commits
50
511. Determine the release-note collection base:
52 - If the baseline tag is an ancestor of `HEAD`, use the tag.
53 - 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.
54 - 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>)`.
55 - 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.
562. List all commits since that base:
57 ```bash
58 git log <collection-base>..HEAD --format="%H %s" --no-merges
59 ```
603. For each commit, get the full body:
61 ```bash
62 git log <hash> -1 --format="%B"
63 ```
644. Extract the content inside `` ```release-note `` code blocks from each commit body.
655. Extract the conventional commit type from the title (`feat`, `fix`, `refactor`, `perf`, `docs`, etc.).
666. **Skip** these commits:
67 - Titles starting with `🤖 Daily Auto I18N`
68 - Titles starting with `Merge`
69 - Titles starting with `chore(deps)`
70 - Titles starting with `chore: release`
71 - Titles starting with `chore(release)`
72 - Commits where the release-note block says `NONE`
73
74### Step 3: Generate Bilingual Release Notes
75
76Using the collected commit information, generate release notes in **both English and Chinese**.
77
78**Recommended format:**
79
80```
81<!--LANG:en-->
82Cherry Studio {version} - {Brief English Title}
83
84✨ New Features
85- [Component] Description
86
87🐛 Bug Fixes
88- [Component] Description
89
90💄 Improvements
91- [Component] Description
92
93⚡ Performance
94- [Component] Description
95
96<!--LANG:zh-CN-->
97Cherry Studio {version} - {简短中文标题}
98
99✨ 新功能
100- [组件] 描述
101
102🐛 问题修复
103- [组件] 描述
104
105💄 改进
106- [组件] 描述
107
108⚡ 性能优化
109- [组件] 描述
110<!--LANG:END-->
111```
112
113The language markers are the machine-readable contract: include each marker once, keep them in order, and provide non-empty English and Chinese sections. Titles and surrounding explanatory text are presentation choices, not validation requirements.
114
115**Rules:**
116- Only include categories that have entries (omit empty categories).
117- Each commit appears as exactly ONE line item in the appropriate category.
118- Use the `release-note` field if present; otherwise summarize from the commit title.
119- Component tags should be short: `[Chat]`, `[Models]`, `[Agent]`, `[MCP]`, `[Settings]`, `[Data]`, `[Build]`, etc.
120- Chinese translations should be natural, not machine-literal.
121- Do NOT include commit hashes or PR numbers.
122- Read the **existing** release notes in `electron-builder.yml` as a style reference before writing.
123
124**IMPORTANT: User-Focused Content Only**
125
126Release notes are for **end users**, not developers. Exclude anything users don't care about:
127
128- **EXCLUDE** internal refactoring, code cleanup, or architecture changes
129- **EXCLUDE** CI/CD, build tooling, or test infrastructure changes
130- **EXCLUDE** dependency updates (unless they add user-visible features)
131- **EXCLUDE** documentation updates
132- **EXCLUDE** developer experience improvements
133- **EXCLUDE** technical debt fixes with no user-visible impact
134- **EXCLUDE** overly technical descriptions (e.g., "fix race condition in Redux middleware")
135
136**INCLUDE** only changes that users will notice:
137- New features they can use
138- Bug fixes that affected their workflow
139- UI/UX improvements they can see
140- Performance improvements they can feel
141- Security fixes (simplified, without implementation details)
142
143**Keep descriptions simple and non-technical:**
144- ❌ "Fix streaming race condition causing partial tool response status in Redux state"
145- ✅ "Fix tool status not stopping when aborting"
146- ❌ "Auto-convert reasoning_effort to reasoningEffort for OpenAI-compatible providers"
147- ✅ "Fix deep thinking mode not working with some providers"
148
149### Step 4: Update Files
150
1511. **`package.json`**: Update the `"version"` field to the new version.
1522. **`electron-builder.yml`**: Replace the content under `releaseInfo.releaseNotes: |` with the generated notes. Preserve the 4-space YAML indentation for the block scalar content.
1533. **`resources/cherry-studio/release-history.json`**: Never edit by hand; the notes must match `electron-builder.yml` byte for byte. Run `node scripts/release/sync-release-history.js --target-version {version}`, which prepends (or replaces) the entry for a stable release and leaves the file untouched for a prerelease. In GitHub Actions, the workflow runs this itself after the Claude step.
1544. **Validate source metadata**: For an interactive local run, run `node scripts/release/validate-prepared-release.js --target-version {version}` before generating the product manifest, and stop if it rejects the changed paths, version ordering, bilingual sections, or stable history. In GitHub Actions, leave validation to the workflow step that runs after Claude.
1555. **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.
156
157### Step 5: Present for Review
158
159Show the user:
160- The new version number.
161- The full generated release notes.
162- A summary of which files were modified.
163
164If `--dry-run` was specified, stop here.
165
166Otherwise, ask the user to confirm before proceeding to Step 6.
167
168### Step 6: Create Release Branch
169
1701. For an interactive local run, repeat Step 1 items 1-3 immediately before creating the branch. 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. Then create and push a signed, DCO-compliant release commit:
171 ```bash
172 git fetch origin refs/heads/main:refs/remotes/origin/main --tags
173 test "$(git branch --show-current)" = main
174 test "$(git rev-parse HEAD)" = "$(git rev-parse origin/main)"
175 test "$(node -p "require('./package.json').version")" = "{version}"
176 BASELINE_VERSION="$(git show HEAD:package.json | jq -r .version)"
177 BASELINE_TAG="v$BASELINE_VERSION"
178 git rev-parse --verify "refs/tags/$BASELINE_TAG"
179 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')"
180 test "$LATEST_PUBLISHED" = "$BASELINE_TAG"
181 REPO="$(gh repo view --json nameWithOwner --jq .nameWithOwner)"
182 gh api --paginate --slurp "repos/$REPO/releases?per_page=100" | TAG="v{version}" node scripts/release/validate-release-state.js prepare
183 test -z "$(git ls-remote --heads origin refs/heads/release/v{version})"
184 git status --short
185 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)"
186 test -z "$UNEXPECTED_RELEASE_PATHS"
187 git checkout -b release/v{version}
188 git add package.json electron-builder.yml resources/cherry-studio/release-history.json resources/builtin-agents/cherry-assistant/product-manifest.json
189 git commit -S --signoff -m "chore(release): prepare v{version}"
190 git cat-file commit HEAD | grep -q '^gpgsig '
191 git log -1 --format=%B | grep -q '^Signed-off-by: '
192 git push -u origin release/v{version}
193 ```
1942. In GitHub Actions, stop after updating `package.json` and `electron-builder.yml`. Temporary helper files and local Git operations are allowed; the workflow extracts those two file changes, restores the frozen source SHA, and discards everything else. It then derives the release history, validates, generates the product manifest, creates the branch, and uses GitHub's API to create and verify the signed, DCO-compliant commit. Never push from the Claude step.
1953. Report the release branch and next steps. Do not create a PR yet: the release must be built and published from this branch first.
196
197## CI Trigger Chain
198
199- Wait for the **CI** push run on the new `release/v{version}` commit to succeed. **`auto-release-build.yml`** revalidates that exact live branch head and dispatches **`release.yml`** with `all`; it builds macOS, Windows, and Linux and creates or updates the draft GitHub Release. Use **`release.yml`** manually only to retry a failed all-platform build or one platform for the unchanged tagged commit.
200- 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.
201- Review the backport PR, wait for its CI, and merge it. After the resulting release-branch push passes CI, the exact-head all-platform draft rebuild starts automatically.
202- A successful exact-head all-platform build starts **`publish-release.yml`**. Approve the `release` Environment deployment after inspecting the draft. Publication then acquires the release-state lock, revalidates the approved run, release branch, tag, draft, artifacts, open PRs, and pending hotfixes, and publishes only if they still agree. The draft body contains the bilingual `electron-builder.yml` notes followed by GitHub's generated changes. The final fetched `main` SHA is the hotfix cutoff; a hotfix merged after that snapshot belongs to the next release. Publication triggers **`post-release.yml`**, which uses the published tag as its source, applies only the release metadata delta to the latest `main`, and creates a `release-sync/v{version}` metadata-only PR.
203- 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.
204- 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.
205
206## Constraints
207
208- Always read `electron-builder.yml` before modifying it to understand the current format.
209- Never retain changes outside `package.json`, `electron-builder.yml`, `resources/cherry-studio/release-history.json`, and the generated `resources/builtin-agents/cherry-assistant/product-manifest.json`.
210- Never push directly to `main`.
211- Never create the release metadata PR before the GitHub Release is published; `post-release.yml` owns that step.
212- Always show the generated release notes to the user before creating the release branch (unless running in CI with no interactive user).