native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
Shell
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251#!/usr/bin/env bash# Generate release notes for the next Prowl version.## Usage: ./scripts/release-notes.sh [VERSION]## Compares HEAD against the previous release tag, gathers commits and PR# descriptions, and uses an LLM (claude CLI) to produce user-facing release# notes. Falls back to GitHub auto-notes if the LLM is unavailable.## Output: build/release-notes.md# The file can be reviewed and edited before running release.sh.set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"cd "$PROJECT_DIR"
log() { echo "[release-notes] $*"; }die() { echo "error: $*" >&2; exit 1; }
# ── Repository ───────────────────────────────────────────────────────────────
origin_repo_from_remote() { local remote_url remote_url="$(git remote get-url origin 2>/dev/null || true)" [[ -z "$remote_url" ]] && return 1 local repo repo="$(echo "$remote_url" | sed -E 's#^(git@github.com:|ssh://git@github.com/|https://github.com/)##; s#\.git$##')" [[ "$repo" == */* ]] && echo "$repo" && return 0 return 1}
REPO="${GH_REPO:-$(origin_repo_from_remote || true)}"[[ -z "$REPO" ]] && REPO="$(gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null || true)"[[ -z "$REPO" ]] && die "cannot determine GitHub repository"
# ── Version ──────────────────────────────────────────────────────────────────
if [[ -n "${1:-}" ]]; then VERSION="$1"else VERSION="$(date +%Y.%-m.%-d)" suffix=1 while git rev-parse "v$VERSION" >/dev/null 2>&1; do suffix=$((suffix + 1)) VERSION="$(date +%Y.%-m.%-d).$suffix" donefi
TAG="v$VERSION"
# ── Determine range ──────────────────────────────────────────────────────────
# If tag already exists, use it as the end point; otherwise use HEAD.if git rev-parse "$TAG" >/dev/null 2>&1; then END_REF="$TAG"else END_REF="HEAD"fi
PREV_TAG="$(git describe --tags --abbrev=0 "$END_REF^" 2>/dev/null || true)"if [[ -n "$PREV_TAG" ]]; then RANGE="$PREV_TAG..$END_REF"else RANGE=""fi
log "version: $VERSION"log "range: ${RANGE:-<all commits>}"
# ── LLM generation ───────────────────────────────────────────────────────────
generate_llm_notes() { local range="$1" local raw_context raw_context="$(mktemp)"
# Gather commit messages { echo "=== Commits ($range) ===" git log --pretty=format:'%s' "$range" echo "" } > "$raw_context"
# Gather merged PR details (title + body) { echo "" echo "=== Merged Pull Requests ===" local pr_numbers pr_numbers="$(git log --pretty=format:'%s' "$range" | grep -oE '#[0-9]+' | tr -d '#' | sort -u)" for pr in $pr_numbers; do local pr_json pr_json="$(gh pr view "$pr" --repo "$REPO" --json title,body 2>/dev/null || true)" if [[ -n "$pr_json" ]]; then echo "--- PR #$pr ---" echo "$pr_json" | jq -r '"Title: \(.title)\nBody:\n\(.body)"' echo "" fi done } >> "$raw_context"
# Gather diff stats { echo "" echo "=== Diff Stats ===" git diff --stat "$range" } >> "$raw_context"
local prompt prompt="$(cat <<'PROMPT'You are writing release notes for **Prowl**, a macOS app that runs multiplecoding agents in parallel, each in its own terminal tab.
Given the raw context below (commits, PR descriptions, diff stats), produce aconcise, user-facing changelog in Markdown.
## Rules
1. **Audience**: Prowl end-users (developers). They care about what changed in their day-to-day experience, not internal code structure.2. **Include only user-visible changes**: new features, behavior changes, UX improvements, notable bug fixes. Skip pure refactors, test-only changes, CI tweaks, dependency bumps, and code-style changes unless they affect the user.3. **Teach naturally**: when a change introduces a new shortcut, workflow, or setting, briefly explain how to use it (e.g. "Press ⌥⌘↩ to toggle Canvas view").4. **Tone**: clear, professional, friendly. No marketing fluff, no emoji, no exclamation marks, no "we're excited".5. **Format**: - Start with a one-line summary sentence of the release theme if there is a clear one; otherwise jump straight to the section. - Group items into sections using literal Markdown level-3 headings: `### New` for features/enhancements, `### Fixed` for bug fixes, and optionally `### Improved` for non-feature, non-bug enhancements. Omit a section if it has no items. Always use the `### ` heading syntax — never bold-paragraph forms like `**New**` or `**Fixed**`, and never `## ` (which is reserved for the version header). - Use a flat bullet list (`-`) within each section. - Each bullet should be one or two sentences maximum. - End with nothing — no sign-off, no footer.6. **Length**: aim for 3-8 bullets total. Merge trivial items. Omit if truly nothing is user-facing (output a single bullet: "- Internal improvements and stability fixes.").7. **Language**: English only.8. Output **only** the Markdown content. No preamble, no code fences.PROMPT)"
local notes notes="$(claude -p \ --model sonnet \ --allowedTools "" \ --output-format text \ "$prompt
--- RAW CONTEXT ---$(cat "$raw_context")--- END ---" 2>/dev/null || true)" rm -f "$raw_context"
if [[ -n "$notes" ]] && [[ "$(echo "$notes" | wc -l)" -ge 2 ]]; then echo "$notes" return 0 fi return 1}
generate_fallback_notes() { local range="$1" if [[ -n "$range" ]]; then gh api "repos/$REPO/releases/generate-notes" \ -f tag_name="$TAG" -f previous_tag_name="$PREV_TAG" \ --jq '.body' 2>/dev/null || \ git log --pretty=format:'- %s' "$range" else git log --pretty=format:'- %s' -20 fi}
# ── Validation ───────────────────────────────────────────────────────────────
# Lint a release-notes file against the CHANGELOG format used by Prowl-Site.# Returns 0 on clean, 1 if violations were found (also prints them).# Section headings must be `### New` / `### Fixed` / `### Improved` (level 3)# so they sit one level below the `## [VERSION]` header that release.sh# prepends. Bold paragraphs (`**Fixed**`) and `## Fixed` are both rejected:# the site CSS targets `:global(h3)`, so anything else renders unstyled.lint_release_notes() { local file="$1" local violations=()
# Bold-paragraph section headers (whole-line) if grep -nE '^\*\*(New|Fixed|Improved)\*\*[[:space:]]*$' "$file" >/dev/null; then while IFS= read -r line; do violations+=("$line (use '### …' instead of bold paragraph)") done < <(grep -nE '^\*\*(New|Fixed|Improved)\*\*[[:space:]]*$' "$file") fi
# Level-2 section headers (collide with the version header) if grep -nE '^## (New|Fixed|Improved)[[:space:]]*$' "$file" >/dev/null; then while IFS= read -r line; do violations+=("$line (use '### …' instead of '## …')") done < <(grep -nE '^## (New|Fixed|Improved)[[:space:]]*$' "$file") fi
if [[ ${#violations[@]} -gt 0 ]]; then echo "release-notes format violations in $file:" >&2 printf ' %s\n' "${violations[@]}" >&2 return 1 fi return 0}
# ── Generate ─────────────────────────────────────────────────────────────────
NOTES_FILE="build/release-notes.md"mkdir -p build
if [[ -n "$RANGE" ]]; then if command -v claude >/dev/null 2>&1; then log "generating release notes with LLM..." if generate_llm_notes "$RANGE" > "$NOTES_FILE"; then log "release notes generated by LLM" else log "LLM generation failed, falling back to GitHub auto-notes..." generate_fallback_notes "$RANGE" > "$NOTES_FILE" fi else generate_fallback_notes "$RANGE" > "$NOTES_FILE" fielse generate_fallback_notes "" > "$NOTES_FILE"fi
echoecho "──── Release Notes ($VERSION) ────"cat "$NOTES_FILE"echo "──────────────────────────────────"echolog "saved to $NOTES_FILE"
if ! lint_release_notes "$NOTES_FILE"; then log "fix the headings above (use '### New' / '### Fixed' / '### Improved')," log "then re-run this script or edit $NOTES_FILE before invoking release.sh." exit 1fi
log "review and edit the file if needed, then run:"log " ./scripts/release.sh $VERSION"