From f656ad3030c26ad569842132663d381251f1d11f Mon Sep 17 00:00:00 2001 From: jdx <216188+jdx@users.noreply.github.com> Date: Sun, 18 Jan 2026 16:10:16 -0600 Subject: [PATCH 1/9] feat(release): add LLM-generated prose summary to release notes - New script `scripts/gen-release-summary.sh`: Generates user-friendly editorialized release notes using Claude Code (sandboxed with read-only tools) - Updated `tasks/release-plz`: Integrates LLM generation into the release process, replacing raw git-cliff output with editorialized content - Updated `.github/workflows/publish-cli.yml`: Extracts release notes from CHANGELOG.md instead of regenerating Co-Authored-By: Claude Opus 4.5 --- .github/workflows/publish-cli.yml | 4 ++- scripts/gen-release-summary.sh | 48 +++++++++++++++++++++++++ tasks/release-plz | 60 ++++++++++++++++++++++++++++--- 3 files changed, 107 insertions(+), 5 deletions(-) create mode 100755 scripts/gen-release-summary.sh diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml index 705f7554..c65e6c5d 100644 --- a/.github/workflows/publish-cli.yml +++ b/.github/workflows/publish-cli.yml @@ -29,9 +29,11 @@ jobs: - name: Create draft release if: ${{ startsWith(github.ref, 'refs/tags/v') }} run: | + # Extract the latest release section from CHANGELOG.md (includes LLM summary) + awk '/^## \[/{if(found) exit; found=1} found{print}' CHANGELOG.md >/tmp/release-notes.txt gh release create --draft ${{ github.ref_name }} \ --title "${{ github.ref_name }}" \ - --notes "$(git cliff --latest --strip all)" + --notes-file /tmp/release-notes.txt build-and-publish: needs: [create-release] strategy: diff --git a/scripts/gen-release-summary.sh b/scripts/gen-release-summary.sh new file mode 100755 index 00000000..fe2c70ef --- /dev/null +++ b/scripts/gen-release-summary.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Generate editorialized release notes using Claude Code +# Usage: ./scripts/gen-release-summary.sh [prev_version] + +version="${1:-}" +prev_version="${2:-}" + +if [[ -z $version ]]; then + echo "Usage: $0 [prev_version]" >&2 + exit 1 +fi + +# Get the git-cliff changelog for context +changelog=$(git cliff --unreleased --strip all 2>/dev/null || echo "") + +if [[ -z $changelog ]]; then + echo "Error: No unreleased changes found" >&2 + exit 1 +fi + +# Use Claude Code to editorialize the release notes +# Sandboxed: only read-only tools allowed (no Bash, Edit, Write) +claude -p \ + --model claude-opus-4-20250514 \ + --output-format text \ + --allowedTools "Read,Grep,Glob" \ + <&2 + exit 1 +fi + +# Create the release header with appropriate URL format +RELEASE_DATE="$(date +%Y-%m-%d)" +if [[ -n $prev_tag ]]; then + RELEASE_HEADER="## [$version](https://github.com/jdx/usage/compare/$prev_tag..$version) - $RELEASE_DATE" +else + # No previous tag - link to the tag directly instead of a compare URL + RELEASE_HEADER="## [$version](https://github.com/jdx/usage/releases/tag/$version) - $RELEASE_DATE" +fi + +# Insert the new release before the first existing release section (## [) +# This handles any CHANGELOG format as long as releases start with ## [ +# Use ENVIRON for notes to avoid awk interpreting backslash escape sequences +export RELEASE_HEADER EDITORIALIZED_NOTES +awk_exit_code=0 +awk ' + /^## \[/ && !inserted { + # Insert new release before the first existing release + print ENVIRON["RELEASE_HEADER"] + print "" + print ENVIRON["EDITORIALIZED_NOTES"] + print "" + inserted = 1 + } + { print } + END { + # Signal whether insertion happened via exit code + exit (inserted ? 0 : 1) + } +' CHANGELOG.md >CHANGELOG.md.tmp || awk_exit_code=$? + +if [[ $awk_exit_code -ne 0 ]]; then + echo "Error: Failed to insert release notes into CHANGELOG.md (no existing release section found)" >&2 + rm -f CHANGELOG.md.tmp + exit 1 +fi +mv CHANGELOG.md.tmp CHANGELOG.md +echo "Editorialized release notes added to CHANGELOG.md" + cargo set-version "${version#v}" --exclude clap_usage mise run render mise run lint-fix @@ -42,5 +90,9 @@ git add \ git checkout -B release git commit -m "chore: release $version" git push origin release --force -gh pr create --title "chore: release $version" --body "$changelog" --label "release" || - gh pr edit --title "chore: release $version" --body "$changelog" + +if [[ "$(gh pr list --label release)" == "" ]]; then + gh pr create --title "chore: release $version" --body "$EDITORIALIZED_NOTES" --label "release" --head release +else + gh pr edit --title "chore: release $version" --body "$EDITORIALIZED_NOTES" +fi From 6fca43f607621222d7a850efc777659b60c5ebb7 Mon Sep 17 00:00:00 2001 From: jdx <216188+jdx@users.noreply.github.com> Date: Sun, 18 Jan 2026 16:25:44 -0600 Subject: [PATCH 2/9] fix: validate LLM output doesn't contain ## [ pattern Add prompt instruction and runtime validation to prevent LLM from generating "## [" patterns that would corrupt changelog processing: - Added explicit instruction in prompt to never use "## [" - Added grep validation that rejects output containing the pattern - Exits with error if pattern detected Co-Authored-By: Claude Opus 4.5 --- scripts/gen-release-summary.sh | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/scripts/gen-release-summary.sh b/scripts/gen-release-summary.sh index fe2c70ef..42a545cd 100755 --- a/scripts/gen-release-summary.sh +++ b/scripts/gen-release-summary.sh @@ -22,11 +22,12 @@ fi # Use Claude Code to editorialize the release notes # Sandboxed: only read-only tools allowed (no Bash, Edit, Write) -claude -p \ - --model claude-opus-4-20250514 \ - --output-format text \ - --allowedTools "Read,Grep,Glob" \ - <&2 + exit 1 +fi + +echo "$output" From 62b25709a477bd4064a23db9a72ad003705f3b29 Mon Sep 17 00:00:00 2001 From: jdx <216188+jdx@users.noreply.github.com> Date: Sun, 18 Jan 2026 17:02:12 -0600 Subject: [PATCH 3/9] fix: prevent command substitution and add PR/doc links to release notes - Use printf-based prompt construction to avoid backtick command substitution - Add instructions for PR links and documentation links in release notes Co-Authored-By: Claude Opus 4.5 --- scripts/gen-release-summary.sh | 36 +++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/scripts/gen-release-summary.sh b/scripts/gen-release-summary.sh index 42a545cd..53cc06f6 100755 --- a/scripts/gen-release-summary.sh +++ b/scripts/gen-release-summary.sh @@ -20,19 +20,16 @@ if [[ -z $changelog ]]; then exit 1 fi -# Use Claude Code to editorialize the release notes -# Sandboxed: only read-only tools allowed (no Bash, Edit, Write) -output=$( - claude -p \ - --model claude-opus-4-20250514 \ - --output-format text \ - --allowedTools "Read,Grep,Glob" \ - < Date: Sun, 18 Jan 2026 17:27:46 -0600 Subject: [PATCH 4/9] refactor: split release notes into concise changelog and rich GitHub releases - gen-release-summary.sh: concise 1-paragraph + bullets for CHANGELOG.md - gen-release-notes.sh: rich detailed notes for GitHub releases - Update publish-cli.yml to generate rich notes at release time Co-Authored-By: Claude Opus 4.5 --- .github/workflows/publish-cli.yml | 19 ++++++++-- scripts/gen-release-notes.sh | 60 +++++++++++++++++++++++++++++++ scripts/gen-release-summary.sh | 28 +++++++-------- 3 files changed, 89 insertions(+), 18 deletions(-) create mode 100755 scripts/gen-release-notes.sh diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml index c65e6c5d..71ba60bd 100644 --- a/.github/workflows/publish-cli.yml +++ b/.github/workflows/publish-cli.yml @@ -26,14 +26,29 @@ jobs: with: submodules: recursive - uses: jdx/mise-action@c37c93293d6b742fc901e1406b8f764f6fb19dac # v2 + - name: Install Claude Code CLI + if: ${{ startsWith(github.ref, 'refs/tags/v') }} + run: npm install -g @anthropic-ai/claude-code - name: Create draft release if: ${{ startsWith(github.ref, 'refs/tags/v') }} run: | - # Extract the latest release section from CHANGELOG.md (includes LLM summary) - awk '/^## \[/{if(found) exit; found=1} found{print}' CHANGELOG.md >/tmp/release-notes.txt + TAG_NAME="${{ github.ref_name }}" + PREV_TAG="$(git describe --tags --abbrev=0 "$TAG_NAME^" 2>/dev/null || echo "")" + # Generate rich release notes using LLM + if [[ -n "${ANTHROPIC_API_KEY:-}" ]]; then + ./scripts/gen-release-notes.sh "$TAG_NAME" "$PREV_TAG" >/tmp/release-notes.txt || { + echo "LLM generation failed, falling back to CHANGELOG.md" + awk '/^## \[/{if(found) exit; found=1} found{print}' CHANGELOG.md >/tmp/release-notes.txt + } + else + echo "ANTHROPIC_API_KEY not set, using CHANGELOG.md" + awk '/^## \[/{if(found) exit; found=1} found{print}' CHANGELOG.md >/tmp/release-notes.txt + fi gh release create --draft ${{ github.ref_name }} \ --title "${{ github.ref_name }}" \ --notes-file /tmp/release-notes.txt + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} build-and-publish: needs: [create-release] strategy: diff --git a/scripts/gen-release-notes.sh b/scripts/gen-release-notes.sh new file mode 100755 index 00000000..8a547ca7 --- /dev/null +++ b/scripts/gen-release-notes.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Generate rich release notes for GitHub releases using Claude Code +# Usage: ./scripts/gen-release-notes.sh [prev_version] + +version="${1:-}" +prev_version="${2:-}" + +if [[ -z $version ]]; then + echo "Usage: $0 [prev_version]" >&2 + exit 1 +fi + +# Get the git-cliff changelog for context +# Use the tag range if prev_version provided, otherwise unreleased +if [[ -n $prev_version ]]; then + changelog=$(git cliff --strip all "${prev_version}..${version}" 2>/dev/null || echo "") +else + changelog=$(git cliff --unreleased --strip all 2>/dev/null || echo "") +fi + +if [[ -z $changelog ]]; then + echo "Error: No changes found for release" >&2 + exit 1 +fi + +# Build prompt safely using printf to avoid command substitution on backticks in changelog +prompt=$( + printf '%s\n' "You are writing release notes for usage version ${version}${prev_version:+ (previous version: ${prev_version})}." + printf '\n' + printf '%s\n' "usage is a CLI argument parser library for Rust that generates completions, man pages, and markdown docs from a simple spec format." + printf '\n' + printf '%s\n' "Here is the raw changelog from git-cliff:" + printf '%s\n' "$changelog" + printf '\n' + cat <<'INSTRUCTIONS' +Write user-friendly release notes: + +1. Start with 1-2 paragraphs summarizing key changes +2. Organize into ### sections (Highlights, Bug Fixes, etc.) +3. Explain WHY changes matter to users +4. Include PR links and documentation links (https://usage.jdx.dev/) +5. Include contributor usernames (@username) +6. Skip internal changes + +Output ONLY the release notes, no preamble. +INSTRUCTIONS +) + +# Use Claude Code to generate the release notes +# Sandboxed: only read-only tools allowed (no Bash, Edit, Write) +output=$( + printf '%s' "$prompt" | claude -p \ + --model claude-opus-4-20250514 \ + --output-format text \ + --allowedTools "Read,Grep,Glob" +) + +echo "$output" diff --git a/scripts/gen-release-summary.sh b/scripts/gen-release-summary.sh index 53cc06f6..8fafa453 100755 --- a/scripts/gen-release-summary.sh +++ b/scripts/gen-release-summary.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -# Generate editorialized release notes using Claude Code +# Generate concise changelog entry using Claude Code # Usage: ./scripts/gen-release-summary.sh [prev_version] version="${1:-}" @@ -30,27 +30,23 @@ prompt=$( printf '%s\n' "$changelog" printf '\n' cat <<'INSTRUCTIONS' -Rewrite this into user-friendly release notes. The format should be: +Write a brief changelog entry: -1. Start with 1-2 paragraphs summarizing the most important changes -2. Then organize into sections using ### headers (e.g., "### Highlights", "### Bug Fixes") -3. Write in clear, user-focused language (not developer commit messages) -4. Explain WHY changes matter to users, not just what changed -5. Group related changes together logically -6. Skip minor/internal changes that don't affect users -7. Include contributor attribution where appropriate (@username) -8. Include links to PRs (e.g., [#123](https://github.com/jdx/usage/pull/123)) for significant changes -9. Where applicable, link to relevant documentation at https://usage.jdx.dev/ +1. One short paragraph (2-3 sentences) summarizing the release +2. Categorized bullet points (### Features, ### Bug Fixes, etc.) +3. One line per change, no explanations +4. Skip minor/internal changes +5. Include PR links for significant changes +6. Include contributor usernames (@username) +7. Link to relevant documentation at https://usage.jdx.dev/ where applicable -IMPORTANT: Use only ### for section headers. NEVER use "## [" as this pattern is reserved for version headers and will corrupt changelog processing. +IMPORTANT: Use only ### for section headers. NEVER use "## [" as this pattern is reserved for version headers. -Keep the tone professional but approachable. Focus on what users care about. - -Output ONLY the editorialized release notes, no preamble. +Output ONLY the brief changelog, no preamble. INSTRUCTIONS ) -# Use Claude Code to editorialize the release notes +# Use Claude Code to generate the changelog entry # Sandboxed: only read-only tools allowed (no Bash, Edit, Write) output=$( printf '%s' "$prompt" | claude -p \ From 01e8f01332ea5aa1b8477f04100d5756dade8fdc Mon Sep 17 00:00:00 2001 From: jdx <216188+jdx@users.noreply.github.com> Date: Sun, 18 Jan 2026 18:20:51 -0600 Subject: [PATCH 5/9] refactor: remove LLM generation from release-plz LLM-generated changelog content should only be created after the release PR is merged, not during PR creation. Now: - release-plz uses git-cliff directly for CHANGELOG.md - LLM editorialization only happens in release.yml for GitHub releases - Removes gen-release-summary script (no longer needed) Co-Authored-By: Claude Opus 4.5 --- scripts/gen-release-summary.sh | 64 ---------------------------------- tasks/release-plz | 55 ++++------------------------- 2 files changed, 6 insertions(+), 113 deletions(-) delete mode 100755 scripts/gen-release-summary.sh diff --git a/scripts/gen-release-summary.sh b/scripts/gen-release-summary.sh deleted file mode 100755 index 8fafa453..00000000 --- a/scripts/gen-release-summary.sh +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Generate concise changelog entry using Claude Code -# Usage: ./scripts/gen-release-summary.sh [prev_version] - -version="${1:-}" -prev_version="${2:-}" - -if [[ -z $version ]]; then - echo "Usage: $0 [prev_version]" >&2 - exit 1 -fi - -# Get the git-cliff changelog for context -changelog=$(git cliff --unreleased --strip all 2>/dev/null || echo "") - -if [[ -z $changelog ]]; then - echo "Error: No unreleased changes found" >&2 - exit 1 -fi - -# Build prompt safely using printf to avoid command substitution on backticks in changelog -prompt=$( - printf '%s\n' "You are writing release notes for usage version ${version}${prev_version:+ (previous version: ${prev_version})}." - printf '\n' - printf '%s\n' "usage is a CLI argument parser library for Rust that generates completions, man pages, and markdown docs from a simple spec format." - printf '\n' - printf '%s\n' "Here is the raw changelog from git-cliff:" - printf '%s\n' "$changelog" - printf '\n' - cat <<'INSTRUCTIONS' -Write a brief changelog entry: - -1. One short paragraph (2-3 sentences) summarizing the release -2. Categorized bullet points (### Features, ### Bug Fixes, etc.) -3. One line per change, no explanations -4. Skip minor/internal changes -5. Include PR links for significant changes -6. Include contributor usernames (@username) -7. Link to relevant documentation at https://usage.jdx.dev/ where applicable - -IMPORTANT: Use only ### for section headers. NEVER use "## [" as this pattern is reserved for version headers. - -Output ONLY the brief changelog, no preamble. -INSTRUCTIONS -) - -# Use Claude Code to generate the changelog entry -# Sandboxed: only read-only tools allowed (no Bash, Edit, Write) -output=$( - printf '%s' "$prompt" | claude -p \ - --model claude-opus-4-20250514 \ - --output-format text \ - --allowedTools "Read,Grep,Glob" -) - -# Validate output doesn't contain patterns that would corrupt changelog processing -if echo "$output" | grep -qE '^## \['; then - echo "Error: LLM output contains '## [' pattern which would corrupt changelog processing" >&2 - exit 1 -fi - -echo "$output" diff --git a/tasks/release-plz b/tasks/release-plz index 58fd7ddb..00983224 100755 --- a/tasks/release-plz +++ b/tasks/release-plz @@ -16,7 +16,6 @@ if ! echo "$released_versions" | grep -q "^v$cur_version$"; then fi version="$(git cliff --bumped-version)" -prev_tag="$(git describe --tags --abbrev=0 2>/dev/null || echo "")" if [ "${usage_dry_run:-}" == 1 ]; then echo "version: $version" @@ -25,53 +24,11 @@ if [ "${usage_dry_run:-}" == 1 ]; then exit 0 fi -# Generate editorialized release notes using LLM -echo "Generating editorialized release notes..." -EDITORIALIZED_NOTES="$(./scripts/gen-release-summary.sh "$version" "$prev_tag")" +# Generate changelog using git-cliff (LLM editorialization happens in release.yml after merge) +git cliff --bump -o CHANGELOG.md -# Validate we got non-empty release notes -if [[ -z $EDITORIALIZED_NOTES ]]; then - echo "Error: Failed to generate release notes (empty output)" >&2 - exit 1 -fi - -# Create the release header with appropriate URL format -RELEASE_DATE="$(date +%Y-%m-%d)" -if [[ -n $prev_tag ]]; then - RELEASE_HEADER="## [$version](https://github.com/jdx/usage/compare/$prev_tag..$version) - $RELEASE_DATE" -else - # No previous tag - link to the tag directly instead of a compare URL - RELEASE_HEADER="## [$version](https://github.com/jdx/usage/releases/tag/$version) - $RELEASE_DATE" -fi - -# Insert the new release before the first existing release section (## [) -# This handles any CHANGELOG format as long as releases start with ## [ -# Use ENVIRON for notes to avoid awk interpreting backslash escape sequences -export RELEASE_HEADER EDITORIALIZED_NOTES -awk_exit_code=0 -awk ' - /^## \[/ && !inserted { - # Insert new release before the first existing release - print ENVIRON["RELEASE_HEADER"] - print "" - print ENVIRON["EDITORIALIZED_NOTES"] - print "" - inserted = 1 - } - { print } - END { - # Signal whether insertion happened via exit code - exit (inserted ? 0 : 1) - } -' CHANGELOG.md >CHANGELOG.md.tmp || awk_exit_code=$? - -if [[ $awk_exit_code -ne 0 ]]; then - echo "Error: Failed to insert release notes into CHANGELOG.md (no existing release section found)" >&2 - rm -f CHANGELOG.md.tmp - exit 1 -fi -mv CHANGELOG.md.tmp CHANGELOG.md -echo "Editorialized release notes added to CHANGELOG.md" +# Get the unreleased notes for PR body +PR_BODY="$(git cliff --bump --unreleased --strip all)" cargo set-version "${version#v}" --exclude clap_usage mise run render @@ -92,7 +49,7 @@ git commit -m "chore: release $version" git push origin release --force if [[ "$(gh pr list --label release)" == "" ]]; then - gh pr create --title "chore: release $version" --body "$EDITORIALIZED_NOTES" --label "release" --head release + gh pr create --title "chore: release $version" --body "$PR_BODY" --label "release" --head release else - gh pr edit --title "chore: release $version" --body "$EDITORIALIZED_NOTES" + gh pr edit --title "chore: release $version" --body "$PR_BODY" fi From 1be21d3ac556814d5b7bef996aeef023f0da0903 Mon Sep 17 00:00:00 2001 From: jdx <216188+jdx@users.noreply.github.com> Date: Sun, 18 Jan 2026 19:10:38 -0600 Subject: [PATCH 6/9] fix: add fetch-depth and validate Claude output - Add fetch-depth: 0 to checkout step so git describe and git cliff have access to full git history - Validate Claude output is non-empty before using it, exit with error if empty so workflow falls back to CHANGELOG.md Co-Authored-By: Claude Opus 4.5 --- .github/workflows/publish-cli.yml | 1 + scripts/gen-release-notes.sh | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml index 71ba60bd..bebb1169 100644 --- a/.github/workflows/publish-cli.yml +++ b/.github/workflows/publish-cli.yml @@ -25,6 +25,7 @@ jobs: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 with: submodules: recursive + fetch-depth: 0 - uses: jdx/mise-action@c37c93293d6b742fc901e1406b8f764f6fb19dac # v2 - name: Install Claude Code CLI if: ${{ startsWith(github.ref, 'refs/tags/v') }} diff --git a/scripts/gen-release-notes.sh b/scripts/gen-release-notes.sh index 8a547ca7..4bec96f5 100755 --- a/scripts/gen-release-notes.sh +++ b/scripts/gen-release-notes.sh @@ -57,4 +57,10 @@ output=$( --allowedTools "Read,Grep,Glob" ) +# Validate we got non-empty output +if [[ -z $output ]]; then + echo "Error: Claude returned empty output" >&2 + exit 1 +fi + echo "$output" From ce72428a4a0dba81a398bb10d1f10b7cda5b123f Mon Sep 17 00:00:00 2001 From: jdx <216188+jdx@users.noreply.github.com> Date: Sun, 18 Jan 2026 19:46:18 -0600 Subject: [PATCH 7/9] fix: strip version header from PR body The PR title already contains the version, so strip the redundant version header (## [version] - date) from the PR body. Co-Authored-By: Claude Opus 4.5 --- tasks/release-plz | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tasks/release-plz b/tasks/release-plz index 00983224..8a9ccc42 100755 --- a/tasks/release-plz +++ b/tasks/release-plz @@ -28,7 +28,8 @@ fi git cliff --bump -o CHANGELOG.md # Get the unreleased notes for PR body -PR_BODY="$(git cliff --bump --unreleased --strip all)" +# Strip version header since PR title already has version +PR_BODY="$(git cliff --bump --unreleased --strip all | tail -n +3)" cargo set-version "${version#v}" --exclude clap_usage mise run render From 968207416aa667bd3ba3ae08bb2c34e62d73b248 Mon Sep 17 00:00:00 2001 From: jdx <216188+jdx@users.noreply.github.com> Date: Sun, 18 Jan 2026 20:01:42 -0600 Subject: [PATCH 8/9] fix: add verbose error handling to gen-release-notes - Log version, previous version, and changelog length before calling Claude - Capture stderr from Claude CLI and display on failure - Helps diagnose why LLM generation fails in CI Co-Authored-By: Claude Opus 4.5 --- scripts/gen-release-notes.sh | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/scripts/gen-release-notes.sh b/scripts/gen-release-notes.sh index 4bec96f5..eab46271 100755 --- a/scripts/gen-release-notes.sh +++ b/scripts/gen-release-notes.sh @@ -50,12 +50,21 @@ INSTRUCTIONS # Use Claude Code to generate the release notes # Sandboxed: only read-only tools allowed (no Bash, Edit, Write) -output=$( +echo "Generating release notes with Claude..." >&2 +echo "Version: $version" >&2 +echo "Previous version: ${prev_version:-none}" >&2 +echo "Changelog length: ${#changelog} chars" >&2 + +if ! output=$( printf '%s' "$prompt" | claude -p \ --model claude-opus-4-20250514 \ --output-format text \ - --allowedTools "Read,Grep,Glob" -) + --allowedTools "Read,Grep,Glob" 2>&1 +); then + echo "Error: Claude CLI failed" >&2 + echo "Output: $output" >&2 + exit 1 +fi # Validate we got non-empty output if [[ -z $output ]]; then From 8cec42d8125a225d4764b75cdaa4e2dd14eeeaff Mon Sep 17 00:00:00 2001 From: jdx <216188+jdx@users.noreply.github.com> Date: Sun, 18 Jan 2026 20:08:42 -0600 Subject: [PATCH 9/9] fix: capture stderr separately in gen-release-notes Avoid polluting release notes output by redirecting stderr to a temp file instead of merging with stdout (2>&1). This prevents Claude CLI diagnostic messages from appearing in the release notes. Co-Authored-By: Claude Opus 4.5 --- scripts/gen-release-notes.sh | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/gen-release-notes.sh b/scripts/gen-release-notes.sh index eab46271..27c84465 100755 --- a/scripts/gen-release-notes.sh +++ b/scripts/gen-release-notes.sh @@ -55,20 +55,25 @@ echo "Version: $version" >&2 echo "Previous version: ${prev_version:-none}" >&2 echo "Changelog length: ${#changelog} chars" >&2 +# Capture stderr separately to avoid polluting output +stderr_file=$(mktemp) +trap 'rm -f "$stderr_file"' EXIT + if ! output=$( printf '%s' "$prompt" | claude -p \ --model claude-opus-4-20250514 \ --output-format text \ - --allowedTools "Read,Grep,Glob" 2>&1 + --allowedTools "Read,Grep,Glob" 2>"$stderr_file" ); then echo "Error: Claude CLI failed" >&2 - echo "Output: $output" >&2 + cat "$stderr_file" >&2 exit 1 fi # Validate we got non-empty output if [[ -z $output ]]; then echo "Error: Claude returned empty output" >&2 + cat "$stderr_file" >&2 exit 1 fi