From 1ac2239111a8e52de6c0414bbca9d1b9b900092d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Tue, 21 Apr 2026 01:01:58 +0800 Subject: [PATCH] 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 --- .github/workflows/release-desktop.yml | 58 ++++++++++++++++----------- scripts/release.ts | 16 +++++++- 2 files changed, 50 insertions(+), 24 deletions(-) diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index f1661546..6603eef0 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -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 diff --git a/scripts/release.ts b/scripts/release.ts index efdfeb3b..dd13233d 100644 --- a/scripts/release.ts +++ b/scripts/release.ts @@ -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}`])