Automate release notes ingestion for tagged desktop releases

The release workflow was still publishing a hard-coded GitHub Release body,
which forced manual copy-paste of each version's markdown after the build
finished. This change makes tagged releases load release-notes/vX.Y.Z.md
from the tagged commit and fail fast when the file or version/tag alignment
is missing. The local release script now enforces the same contract and
stages the matching markdown file into the release commit automatically.

Constraint: GitHub Releases must be generated from the tagged commit so the workflow can only read files already present at that revision
Rejected: Keep a hard-coded releaseBody in workflow | still requires manual release-page editing every version
Rejected: Store release notes only outside git state | workflow cannot read local-only markdown during tag-triggered builds
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep release-notes/vX.Y.Z.md as the canonical source for GitHub Release bodies and preserve the tag/version validation gate
Tested: ruby -e 'require "yaml"; YAML.load_file(".github/workflows/release-desktop.yml"); puts "YAML OK"'
Tested: bun run scripts/release.ts 0.1.5 --dry
Tested: git diff --check
Not-tested: End-to-end GitHub Actions release run against a pushed tag
This commit is contained in:
程序员阿江(Relakkes) 2026-04-21 01:01:58 +08:00
parent 16d716e773
commit 1ac2239111
2 changed files with 50 additions and 24 deletions

View File

@ -61,6 +61,40 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Read version
id: version
shell: bash
run: |
VERSION=$(jq -r '.version' desktop/src-tauri/tauri.conf.json)
echo "value=$VERSION" >> "$GITHUB_OUTPUT"
- name: Validate tag matches version
if: github.event_name == 'push'
shell: bash
run: |
EXPECTED_TAG="v${{ steps.version.outputs.value }}"
if [ "${GITHUB_REF_NAME}" != "$EXPECTED_TAG" ]; then
echo "::error::Tag ${GITHUB_REF_NAME} does not match app version ${EXPECTED_TAG}"
exit 1
fi
- name: Load release notes
id: release_notes
shell: bash
run: |
NOTES_FILE="release-notes/v${{ steps.version.outputs.value }}.md"
if [ ! -f "$NOTES_FILE" ]; then
echo "::error::Missing release notes file: $NOTES_FILE"
exit 1
fi
{
echo 'body<<__RELEASE_NOTES__'
cat "$NOTES_FILE"
echo
echo '__RELEASE_NOTES__'
} >> "$GITHUB_OUTPUT"
# ── System dependencies (Linux) ──────────────────────────
- name: Install Linux dependencies
if: contains(matrix.platform, 'ubuntu')
@ -134,29 +168,7 @@ jobs:
tauriScript: bunx tauri
tagName: v__VERSION__
releaseName: 'Claude Code Haha v__VERSION__'
releaseBody: |
## Claude Code Haha v__VERSION__
### Downloads
| Platform | File |
|----------|------|
| macOS (Apple Silicon) | `.dmg` |
| macOS (Intel) | `.dmg` |
| Windows | `.exe` (NSIS installer) |
| Linux | `.deb` |
### First-time installation
**macOS**: This build is unsigned. If you see "app is damaged" or "unidentified developer", run:
```bash
xattr -cr /Applications/Claude\ Code\ Haha.app
```
**Windows**: If SmartScreen blocks the app, click "More info" → "Run anyway".
---
See [Installation Guide](https://github.com/NanmiCoder/cc-haha/blob/main/docs/desktop/04-installation.md) for details.
releaseBody: ${{ steps.release_notes.outputs.body }}
releaseDraft: ${{ github.event_name == 'workflow_dispatch' && inputs.draft || false }}
prerelease: false
args: ${{ matrix.tauri_args }} --config src-tauri/tauri.release-ci.json

View File

@ -10,7 +10,7 @@
* bun run scripts/release.ts patch --dry # preview without changes
*/
import { readFileSync, writeFileSync } from 'node:fs'
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
import path from 'node:path'
const root = path.resolve(import.meta.dir, '..')
@ -43,6 +43,10 @@ function getCurrentVersion(): string {
return tauriConf.version
}
function getReleaseNotesPath(version: string): string {
return path.join(root, 'release-notes', `v${version}.md`)
}
function bumpVersion(current: string, bump: string): string {
if (/^\d+\.\d+\.\d+$/.test(bump)) {
return bump
@ -88,9 +92,11 @@ if (!bumpArg) {
const current = getCurrentVersion()
const next = bumpVersion(current, bumpArg)
const releaseNotesPath = getReleaseNotesPath(next)
console.log(`\n Version: ${current}${next}`)
console.log(` Tag: v${next}`)
console.log(` Notes: ${path.relative(root, releaseNotesPath)}`)
console.log(` Dry run: ${dryRun}\n`)
if (dryRun) {
@ -98,9 +104,16 @@ if (dryRun) {
for (const file of VERSION_FILES) {
console.log(` - ${path.relative(root, file.path)}`)
}
console.log(` - ${path.relative(root, releaseNotesPath)} ${existsSync(releaseNotesPath) ? '(present)' : '(missing)'}`)
process.exit(0)
}
if (!existsSync(releaseNotesPath)) {
console.error(`Missing release notes file: ${path.relative(root, releaseNotesPath)}`)
console.error(`Create it before releasing so GitHub Release can use it automatically.`)
process.exit(1)
}
// Update version in all files
for (const file of VERSION_FILES) {
const content = readFileSync(file.path, 'utf-8')
@ -122,6 +135,7 @@ await run([
'desktop/src-tauri/tauri.conf.json',
'desktop/src-tauri/Cargo.toml',
'desktop/src-tauri/Cargo.lock',
path.relative(root, releaseNotesPath),
])
await run(['git', 'commit', '-m', `release: v${next}`])
await run(['git', 'tag', '-a', `v${next}`, '-m', `Release v${next}`])