This document defines the release note conventions used by the repo-local
/release workflow and .github/workflows/release.yml.
The /release workflow is CPA Manager Plus specific. It is suitable as a
repo-local Claude command or repo-local Codex skill, but it should not be
installed as a global cross-project skill.
docs/release-notes/ is reserved for versioned release note files only. Do not
place process documentation or README files inside that directory.
Formal release notes are technical release records. Community-facing release copy is authored separately so it can prioritize user value and readability without weakening the technical notes.
main is the stable default branch. dev is the integration branch. Every
release follows this sequence:
release/<version> -> dev -> main -> v<version> tag -> GitHub Release
- Freeze the intended release scope on
dev; do not merge unrelated work until the tag is created. - Create
release/<version>fromdev, add the two release-note files and the Telegram post, then merge its release PR intodevwith a merge commit (not squash or rebase). - Record the resulting
devcommit from that release PR as the release SHA. Before promotion, confirm thatdevstill points to that exact SHA. - Before opening the promotion PR, derive its
Relatedsection from the exact range between the previousmaintip and the frozen releasedevSHA. Inspect first-parent integration merges and resolve each one to its GitHub PR using merge-commit and PR metadata. List every distinct PR merged intodevin that range, including the release PR and feature, fix, docs, or chore PRs; exclude branch-synchronization merges and work already present in the previousmaintip. Preserve integration order and stop if any merge cannot be mapped to a PR. Write oneRefs #<number>line per PR, usingN/Aonly when the range contains no qualifying PRs. - Open a same-repository
dev -> mainpromotion PR with the full repository template and the completeRelatedlist. It must pass the normal PR checks and theVerify dev promotion sourcegate before merging with a merge commit. Squash and rebase merges are rejected by release preflight. - Reconfirm that the resulting
maincommit contains the recorded releasedevSHA, then run the release workflow dry-run from thatmaincommit. - Create the tag from the exact
maincommit that passed the dry-run.
Do not promote dev immediately before starting this sequence. The release PR
must first add its three versioned files to dev, so a preliminary promotion
would be followed by a second mandatory promotion. The standard flow has one
dev -> main PR, after the Release PR is merged.
Do not open a release PR directly to main: branch protection permits only
the repository's dev branch to promote into main.
main can contain a prior dev -> main merge commit that is not an ancestor
of the current dev ref. This is normal. Before a new release, require that
main has no non-merge commits absent from dev; investigate and stop if it
does. The release scope is still invalid if dev advances after the release PR
is merged: refresh the notes and repeat the release preflight rather than
including unreviewed changes in the tag.
Normal releases use two explicit gates rather than one confirmation per remote operation:
- Integrate and validate: approve the exact release branch and files,
conditional Release PR merge, the single conditional
dev -> mainmerge, temporary clean-worktree lifecycle, dry-run dispatch, and any disclosed scoped fallback. Both merges proceed only while their recorded head/base SHAs remain unchanged and all required checks pass. - Tag and publish: after the dry-run succeeds, approve the exact
refs/tags/<tag> -> <main-promotion-sha>mapping. The tag-triggered run and read-only closeout then continue without further confirmation.
The operator keeps an in-session manifest containing the previous main SHA,
source dev SHA, the exact promotion range and Related PR numbers, release
content digests, Release PR head and merge SHAs, promotion SHA, dry-run ID, tag
target, and allowed fallback operations. Any SHA drift, failed check, scope
change, incomplete PR mapping, or new side effect invalidates the current gate
and stops the flow. A dirty developer checkout is recorded and preserved;
remote operations and a detached temporary worktree are used instead of
requiring unrelated local changes to be stashed or cleaned.
docs/release-notes/<tag>-<lang>.md
<tag>keeps thevprefix, for examplev1.0.2orv1.1.0-beta.1.<lang>iszhfor the authored Chinese source orenfor the English mirror translation. The release validator requires both exact filenames; historicalzh-CNfiles are not used as a publishing fallback.
Examples:
docs/release-notes/v1.0.2-zh.md
docs/release-notes/v1.0.2-en.md
Every new release's Chinese notes file (docs/release-notes/<tag>-zh.md) must contain exactly one cpamp-update HTML comment block containing JSON metadata.
This metadata serves as the reviewed input within the Release PR. PR validation CI validates it, and the release workflow reuses this exact block to generate the immutable release-info.json asset. No second metadata source is maintained.
Minimal template:
<!-- cpamp-update
{
"summary": {
"zh": "更新说明",
"en": "Release update"
},
"update": {
"breaking": false,
"migration_required": false,
"minimum_direct_upgrade_version": null,
"upgrade_guide_url": "https://github.com/seakee/CPA-Manager-Plus/releases/tag/v1.2.3"
},
"compatibility": {
"minimum_cpa_version": null
}
}
-->Field requirements:
summary.zh: Concise summary in Chinese (required non-empty string, max 4096 bytes).summary.en: Concise summary in English (required non-empty string, max 4096 bytes).update.breaking: Must be an explicit boolean (trueorfalse).update.migration_required: Must be an explicit boolean (trueorfalse).update.minimum_direct_upgrade_version: Valid SemVer tag string, ornullif there is no lower bound for direct upgrades.update.upgrade_guide_url: Valid HTTPS URL pointing to the upgrade guide withinhttps://github.com/seakee/CPA-Manager-Plus/.compatibility.minimum_cpa_version: Valid SemVer tag string, ornullif there is no minimum CPA version requirement.
Each new release must include a reviewed Telegram post:
docs/release-posts/<tag>-telegram.html
Example:
docs/release-posts/v1.0.2-telegram.html
The repo-local /release workflow drafts this file before confirmation and
shows the exact message in the release plan. Commit it in the same release PR
as the Chinese and English release notes. Do not generate or rewrite the post
inside GitHub Actions.
Write the post for users and community members rather than code reviewers:
- use
## <M> 月 <D> 日 v<version>as the Markdown heading, with更新内容,注意事项,发布截图, and致谢sections as applicable; end the Markdown summary and release closeout with the separate titleCPA-Manager-Plus [<M>月<D>日:<primary benefit>,<supporting benefit>]; - make the standalone title reflect the release's main user value, without implementation trivia; it is not part of the date/version heading;
- list every release-relevant, user-visible semantic change in
更新内容; do not impose a fixed item count, but merge implementation commits that result in the same user behavior; - keep each bullet to one concise sentence with the product subject and its user-visible result; retain necessary product terms but omit CRUD lists, commit/file counts, internal paths, tests, demo fixtures, and pure CI noise;
- include
注意事项only for upgrade, data, compatibility, configuration, or meaningful behavior changes that users need to act on or understand; - include
发布截图only when a specific screen or workflow should be shown; - keep
发布截图and the standalone community-summary title in the Markdown release summary only; Telegram HTML must omit both because the workflow does not attach media and the title is not part of the message format; - include acknowledgements only for external contributors and preserve their GitHub profile links;
- keep claims factual and grounded in the formal release notes;
- keep the complete HTML body within 3,500 characters.
Markdown community-summary template:
## <M> 月 <D> 日 v<version>
### 更新内容
- <用户可感知的更新>
### 注意事项
- <需要升级、配置或兼容性关注的事项>
### 发布截图
<具体页面或操作路径;没有推荐时省略本节>
### 致谢
- [@contributor](https://github.com/contributor) - <贡献带来的用户价值>
CPA-Manager-Plus [<M>月<D>日:<primary benefit>,<supporting benefit>]Omit optional sections that have no content. The Telegram HTML mirrors only
the applicable 更新内容, 注意事项, and 致谢 sections; do not append the
standalone Markdown community-summary title.
Telegram posts use a conservative HTML subset supported by the Bot API:
<b> <i> <code> <a href="https://example.com">...</a>
Escape other HTML characters. Do not include inline keyboard JSON, bot tokens,
chat IDs, thread IDs, or any other secret in the post file. The release
workflow adds one View Release button at send time.
After the GitHub Release job succeeds, .github/workflows/release.yml reads the
tag-matched post and sends it through Telegram Bot API sendMessage. Configure
these repository secrets:
TELEGRAM_BOT_TOKEN
TELEGRAM_CHAT_ID
TELEGRAM_MESSAGE_THREAD_ID # optional, for a forum topic
Missing configuration or a missing post file skips the notification with an Actions warning. Telegram delivery failure must not roll back or invalidate an otherwise successful GitHub Release.
.github/workflows/release.yml is intentionally fail-closed. A tag push must
use the strict v<major>.<minor>.<patch> format with an optional prerelease
suffix such as -rc.1; build metadata, floating tags, and malformed versions
are rejected. Numeric prerelease identifiers must not contain leading zeroes.
The PR workflow automatically validates newly added versioned files under
docs/release-notes/ and docs/release-posts/ before they can pass the stable
Required checks aggregate. The three files for each new release tag must
exist and satisfy the same content rules used by release preflight.
The preflight validates all of the following before building or publishing:
docs/release-notes/<tag>-zh.md,docs/release-notes/<tag>-en.md, anddocs/release-posts/<tag>-telegram.htmlexist and are non-empty;docs/release-notes/<tag>-zh.mdcontains exactly one validcpamp-updatemetadata JSON comment matching the update-contract specification;- the two release notes contain reciprocal tag-pinned GitHub blob links;
- the candidate SHA is the current
maintip; mainis a two-parentdev -> mainpromotion merge, anddevis the two-parent release-PR merge;- the final
maintree exactly matches the promoteddevtree; - the release merge introduces exactly the three versioned release files and no unrelated changes.
There is no commit-log or previous-tag fallback. A missing note or post stops the workflow before any asset or container publishing. Run the validator locally with:
npm run release:validate -- --tag v1.2.3 --content-onlyFor a complete topology check, provide the candidate SHA and fetched protected
refs. The workflow's workflow_dispatch path performs this same validation in
dry-run mode and may only be dispatched from main; it builds the HTML,
native packages, and Docker image with publishing disabled, while skipping
GitHub Release and Telegram delivery.
Release jobs share a non-canceling release-publish concurrency group, so two
tags cannot publish concurrently. Assets are uploaded as an Actions artifact
and reused by the GitHub Release job, which prevents a second build from
silently producing a different release payload. DockerHub publishing is
optional when its credentials are absent; GHCR remains the configured image
registry for normal tag runs.
After building the release artifact and before any Docker registry mutation, the workflow reads any existing GitHub Release for the tag. A missing release is created normally. An exact body, state, asset-name, size, and SHA-256 match is skipped idempotently. During an explicitly approved workflow rerun, a strict matching subset may resume by uploading only missing assets while the Release remains mutable. Unexpected assets, immutable incomplete Releases, changed metadata, changed content, or a partial Release on attempt 1 fail before Docker publishing. Same-name asset overwrites remain disabled.
Telegram delivery is deliberately non-blocking after the GitHub Release is
created. The job summary records sent, skipped-config,
skipped-missing-post, skipped-invalid-thread, or failed-delivery without
exposing secret values. A failed notification never rolls back a successful
release. Automatic Telegram delivery runs only during workflow attempt 1, so a
full workflow rerun cannot resend an already delivered post. The Bot API
sendMessage request itself is attempted once because it is non-idempotent; an
ambiguous network failure is handled as a possible delivery and requires the
explicit recovery workflow rather than an automatic HTTP retry.
Recovery rules:
- If pre-tag dry-run fails, fix the source or release files, repeat the Release PR/promotion as needed, and rerun the dry-run before creating a tag.
- Never rerun a completed successful tagged run. If a tagged run fails because the immutable source is invalid, fix it under a new version and create a new tag; never move the failed tag. For a transient asset or Docker failure, first identify which registries or Release stages already changed state, then obtain explicit recovery approval before rerunning the same tag. A rerun may fill only missing Release assets whose already-published siblings still match the checked payload exactly and whose Release remains mutable. An incomplete immutable Release requires a new version or separately approved administrative recovery. Publishing across registries is deterministic but not transactional.
- If GitHub Release succeeds and Telegram fails, repair the secret/post,
verify whether a message may already have been delivered, and explicitly
dispatch
Recover Telegram Release Notificationwith its resend confirmation. Dispatch it frommain. The recovery job validates the historical tag and complete published Release, extracts the post from the tag, and sends it with the current protectedmainhelper. Do not recover Telegram by rerunning the complete release workflow.
For Actions monitoring, use the run-level state as the canonical decision
surface. Require status=completed, a terminal conclusion, and the same
run_attempt on a second observation after a 30-60 second stabilization
interval. Job-level state may provide detail but must not override a completed
successful run.
Before enabling production releases, repository administrators must enforce
the remote controls that local files cannot provide: protected dev and
main branches, a tag ruleset that permits only the release automation to
create v* tags, and immutable GitHub Releases/tags. Never delete, retarget,
or overwrite a published release tag as a recovery step.
Chinese is the authored source. Other languages should preserve the same
structure, links, and statistics. Language switch links must be tag-pinned
GitHub blob URLs under docs/release-notes/ because GitHub Releases render the
curated note body outside that directory.
# CPA Manager Plus <version>
> <n> commits · <files> files changed · +<added> / -<deleted>
> [English ->](https://github.com/seakee/CPA-Manager-Plus/blob/<version>/docs/release-notes/<version>-en.md)
## Overview
<One short paragraph describing the release theme and context.>
## Highlights
### Features
- <User-facing capability description> (`<scope>`)
### Fixes
- <What was fixed and the affected scope>
<Keep only non-empty groups as needed: Performance / Refactor / Docs / Chore / CI / Build. Drop merge commits and noise.>
## Upgrade Notes
<Breaking changes, migration steps, or risk notes. Use "None" if not applicable.>
## Acknowledgements
<List external contributors only. Omit the section when there are none.>
- @<contributor> - <one sentence summarizing the contribution>
---
**Full Changelog**: https://github.com/seakee/CPA-Manager-Plus/compare/<previous tag>...<version>| Type | Group |
|---|---|
| feat | Features |
| fix | Fixes |
| perf | Performance |
| refactor | Refactor |
| docs | Docs |
| chore | Chore |
| ci | CI |
| build | Build |