native macOS codings agent orchestrator
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: split release into two scripts for safer workflow

- release-notes.sh: generates build/release-notes.md from commits since
last tag via LLM, without bumping version or creating tags
- release.sh: requires release-notes.md to exist, refuses without it.
Skips bump if tag already exists (safe re-run). Deletes and recreates
GitHub Release if it already exists (idempotent).
- Update /release skill to match the two-step flow

This prevents the previous failure mode where a monolithic script would
run bump+tag+build before notes could be reviewed.

onevcat 1a7c2e77 2dca1440

+246 -148
+12 -9
.claude/commands/release.md
··· 7 7 3. Determine the version: 8 8 - If `$ARGUMENTS` is provided, use it as the version (e.g., `2026.3.18`) 9 9 - Otherwise, default to today's date format and confirm with the user before proceeding 10 - 4. Run the release script: `./doc-onevcat/scripts/release.sh <VERSION>` 11 - - The script will pause after generating release notes and prompt for confirmation 12 - - If running non-interactively (e.g., through Claude), the script skips the interactive prompt. 13 - In that case, **you must** read `build/release-notes.md` after the script generates it, 14 - show the release notes to the user, and get explicit confirmation before the build proceeds. 15 - To do this, run the script in two phases: 16 - 1. Generate notes only: run up to note generation, then read and display `build/release-notes.md` 17 - 2. After user confirms, run the full release script (it will reuse the confirmed notes via `--notes-file build/release-notes.md`) 18 - 5. Report the GitHub release URL and remind the user to verify: 10 + 4. Generate release notes: `./doc-onevcat/scripts/release-notes.sh <VERSION>` 11 + - This script compares HEAD against the previous release tag, gathers commits and 12 + PR descriptions, and generates user-facing notes via LLM into `build/release-notes.md`. 13 + - Read the generated `build/release-notes.md`, show the content to the user, and wait 14 + for explicit confirmation. If the user wants changes, edit the file directly. 15 + - **Do NOT proceed to the next step until the user confirms the release notes.** 16 + 5. Run the release script: `./doc-onevcat/scripts/release.sh <VERSION>` 17 + - The script reads `build/release-notes.md` (required — refuses to run without it). 18 + - It handles: version bump, build, sign, notarize, DMG, appcast, GitHub Release, and 19 + Prowl-Site update. If the tag already exists (e.g., from a prior interrupted run), 20 + it skips the bump step automatically. 21 + 6. Report the GitHub release URL and remind the user to verify: 19 22 - The DMG downloads and installs correctly 20 23 - Sparkle update check works (launch app → Check for Updates) 21 24 - The appcast at `https://prowl.onev.cat/appcast.xml` is updated
+205
doc-onevcat/scripts/release-notes.sh
··· 1 + #!/usr/bin/env bash 2 + # Generate release notes for the next Prowl version. 3 + # 4 + # Usage: ./doc-onevcat/scripts/release-notes.sh [VERSION] 5 + # 6 + # Compares HEAD against the previous release tag, gathers commits and PR 7 + # descriptions, and uses an LLM (claude CLI) to produce user-facing release 8 + # notes. Falls back to GitHub auto-notes if the LLM is unavailable. 9 + # 10 + # Output: build/release-notes.md 11 + # The file can be reviewed and edited before running release.sh. 12 + set -euo pipefail 13 + 14 + SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" 15 + PROJECT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" 16 + cd "$PROJECT_DIR" 17 + 18 + log() { echo "[release-notes] $*"; } 19 + die() { echo "error: $*" >&2; exit 1; } 20 + 21 + # ── Repository ─────────────────────────────────────────────────────────────── 22 + 23 + origin_repo_from_remote() { 24 + local remote_url 25 + remote_url="$(git remote get-url origin 2>/dev/null || true)" 26 + [[ -z "$remote_url" ]] && return 1 27 + local repo 28 + repo="$(echo "$remote_url" | sed -E 's#^(git@github.com:|ssh://git@github.com/|https://github.com/)##; s#\.git$##')" 29 + [[ "$repo" == */* ]] && echo "$repo" && return 0 30 + return 1 31 + } 32 + 33 + REPO="${GH_REPO:-$(origin_repo_from_remote || true)}" 34 + [[ -z "$REPO" ]] && REPO="$(gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null || true)" 35 + [[ -z "$REPO" ]] && die "cannot determine GitHub repository" 36 + 37 + # ── Version ────────────────────────────────────────────────────────────────── 38 + 39 + if [[ -n "${1:-}" ]]; then 40 + VERSION="$1" 41 + else 42 + VERSION="$(date +%Y.%-m.%-d)" 43 + suffix=1 44 + while git rev-parse "v$VERSION" >/dev/null 2>&1; do 45 + suffix=$((suffix + 1)) 46 + VERSION="$(date +%Y.%-m.%-d).$suffix" 47 + done 48 + fi 49 + 50 + TAG="v$VERSION" 51 + 52 + # ── Determine range ────────────────────────────────────────────────────────── 53 + 54 + # If tag already exists, use it as the end point; otherwise use HEAD. 55 + if git rev-parse "$TAG" >/dev/null 2>&1; then 56 + END_REF="$TAG" 57 + else 58 + END_REF="HEAD" 59 + fi 60 + 61 + PREV_TAG="$(git describe --tags --abbrev=0 "$END_REF^" 2>/dev/null || true)" 62 + if [[ -n "$PREV_TAG" ]]; then 63 + RANGE="$PREV_TAG..$END_REF" 64 + else 65 + RANGE="" 66 + fi 67 + 68 + log "version: $VERSION" 69 + log "range: ${RANGE:-<all commits>}" 70 + 71 + # ── LLM generation ─────────────────────────────────────────────────────────── 72 + 73 + generate_llm_notes() { 74 + local range="$1" 75 + local raw_context 76 + raw_context="$(mktemp)" 77 + 78 + # Gather commit messages 79 + { 80 + echo "=== Commits ($range) ===" 81 + git log --pretty=format:'%s' "$range" 82 + echo "" 83 + } > "$raw_context" 84 + 85 + # Gather merged PR details (title + body) 86 + { 87 + echo "" 88 + echo "=== Merged Pull Requests ===" 89 + local pr_numbers 90 + pr_numbers="$(git log --pretty=format:'%s' "$range" | grep -oE '#[0-9]+' | tr -d '#' | sort -u)" 91 + for pr in $pr_numbers; do 92 + local pr_json 93 + pr_json="$(gh pr view "$pr" --repo "$REPO" --json title,body 2>/dev/null || true)" 94 + if [[ -n "$pr_json" ]]; then 95 + echo "--- PR #$pr ---" 96 + echo "$pr_json" | jq -r '"Title: \(.title)\nBody:\n\(.body)"' 97 + echo "" 98 + fi 99 + done 100 + } >> "$raw_context" 101 + 102 + # Gather diff stats 103 + { 104 + echo "" 105 + echo "=== Diff Stats ===" 106 + git diff --stat "$range" 107 + } >> "$raw_context" 108 + 109 + local prompt 110 + prompt="$(cat <<'PROMPT' 111 + You are writing release notes for **Prowl**, a macOS app that runs multiple 112 + coding agents in parallel, each in its own terminal tab. 113 + 114 + Given the raw context below (commits, PR descriptions, diff stats), produce a 115 + concise, user-facing changelog in Markdown. 116 + 117 + ## Rules 118 + 119 + 1. **Audience**: Prowl end-users (developers). They care about what changed in 120 + their day-to-day experience, not internal code structure. 121 + 2. **Include only user-visible changes**: new features, behavior changes, UX 122 + improvements, notable bug fixes. Skip pure refactors, test-only changes, 123 + CI tweaks, dependency bumps, and code-style changes unless they affect 124 + the user. 125 + 3. **Teach naturally**: when a change introduces a new shortcut, workflow, or 126 + setting, briefly explain how to use it (e.g. "Press ⌥⌘↩ to toggle 127 + Canvas view"). 128 + 4. **Tone**: clear, professional, friendly. No marketing fluff, no emoji, no 129 + exclamation marks, no "we're excited". 130 + 5. **Format**: 131 + - Start with a one-line summary sentence of the release theme if there is a 132 + clear one; otherwise jump straight to the list. 133 + - Group into two sections: **New** for features/enhancements, and **Fixed** 134 + for bug fixes. Omit a section if it has no items. 135 + - Use a flat bullet list (`-`) within each section. 136 + - Each bullet should be one or two sentences maximum. 137 + - End with nothing — no sign-off, no footer. 138 + 6. **Length**: aim for 3-8 bullets total. Merge trivial items. Omit if truly 139 + nothing is user-facing (output a single bullet: "- Internal improvements 140 + and stability fixes."). 141 + 7. **Language**: English only. 142 + 8. Output **only** the Markdown content. No preamble, no code fences. 143 + PROMPT 144 + )" 145 + 146 + local notes 147 + notes="$(claude -p \ 148 + --model sonnet \ 149 + --allowedTools "" \ 150 + --output-format text \ 151 + "$prompt 152 + 153 + --- RAW CONTEXT --- 154 + $(cat "$raw_context") 155 + --- END ---" 2>/dev/null || true)" 156 + rm -f "$raw_context" 157 + 158 + if [[ -n "$notes" ]] && [[ "$(echo "$notes" | wc -l)" -ge 2 ]]; then 159 + echo "$notes" 160 + return 0 161 + fi 162 + return 1 163 + } 164 + 165 + generate_fallback_notes() { 166 + local range="$1" 167 + if [[ -n "$range" ]]; then 168 + gh api "repos/$REPO/releases/generate-notes" \ 169 + -f tag_name="$TAG" -f previous_tag_name="$PREV_TAG" \ 170 + --jq '.body' 2>/dev/null || \ 171 + git log --pretty=format:'- %s' "$range" 172 + else 173 + git log --pretty=format:'- %s' -20 174 + fi 175 + } 176 + 177 + # ── Generate ───────────────────────────────────────────────────────────────── 178 + 179 + NOTES_FILE="build/release-notes.md" 180 + mkdir -p build 181 + 182 + if [[ -n "$RANGE" ]]; then 183 + if command -v claude >/dev/null 2>&1; then 184 + log "generating release notes with LLM..." 185 + if generate_llm_notes "$RANGE" > "$NOTES_FILE"; then 186 + log "release notes generated by LLM" 187 + else 188 + log "LLM generation failed, falling back to GitHub auto-notes..." 189 + generate_fallback_notes "$RANGE" > "$NOTES_FILE" 190 + fi 191 + else 192 + generate_fallback_notes "$RANGE" > "$NOTES_FILE" 193 + fi 194 + else 195 + generate_fallback_notes "" > "$NOTES_FILE" 196 + fi 197 + 198 + echo 199 + echo "──── Release Notes ($VERSION) ────" 200 + cat "$NOTES_FILE" 201 + echo "──────────────────────────────────" 202 + echo 203 + log "saved to $NOTES_FILE" 204 + log "review and edit the file if needed, then run:" 205 + log " ./doc-onevcat/scripts/release.sh $VERSION"
+29 -139
doc-onevcat/scripts/release.sh
··· 1 1 #!/usr/bin/env bash 2 - # Prowl public release script. 2 + # Prowl release script: bump, build, sign, notarize, and publish. 3 + # 3 4 # Usage: ./doc-onevcat/scripts/release.sh [VERSION] 5 + # 6 + # Prerequisites: 7 + # Run ./doc-onevcat/scripts/release-notes.sh first to generate and review 8 + # build/release-notes.md. This script will refuse to proceed without it. 4 9 # 5 10 # Environment variables: 6 11 # APPLE_SIGNING_IDENTITY Developer ID identity (auto-detected if unset) ··· 79 84 80 85 PROWL_SITE="${PROWL_SITE_DIR:-$PROJECT_DIR/../Prowl-Site}" 81 86 87 + NOTES_FILE="build/release-notes.md" 88 + [[ -s "$NOTES_FILE" ]] || die "$NOTES_FILE not found — run release-notes.sh first" 89 + 82 90 log "repository: $REPO" 83 91 log "signing identity: $SIGNING_IDENTITY" 84 92 log "team ID: $TEAM_ID" ··· 100 108 fi 101 109 102 110 TAG="v$VERSION" 103 - git rev-parse "$TAG" >/dev/null 2>&1 && die "tag $TAG already exists" 104 111 105 112 BUILD="$(date +%Y%m%d)" 106 113 CURRENT_BUILD="$(/usr/bin/awk -F' = ' '/CURRENT_PROJECT_VERSION = [0-9]+;/{gsub(/;/,""); print $2; exit}' "$PROJECT_DIR/supacode.xcodeproj/project.pbxproj")" ··· 110 117 111 118 log "version: $VERSION (build $BUILD), tag: $TAG" 112 119 113 - # ── Bump version ───────────────────────────────────────────────────────────── 114 - 115 - log "bumping version in project..." 116 - make bump-version VERSION="$VERSION" BUILD="$BUILD" 117 - 118 - # ── Changelog ──────────────────────────────────────────────────────────────── 119 - 120 - log "generating release notes..." 121 - NOTES_FILE="build/release-notes.md" 122 - mkdir -p build 123 - PREV_TAG="$(git describe --tags --abbrev=0 "$TAG^" 2>/dev/null || true)" 124 - 125 - generate_llm_notes() { 126 - local range="$1" 127 - local raw_context 128 - raw_context="$(mktemp)" 129 - 130 - # Gather commit messages 131 - { 132 - echo "=== Commits ($range) ===" 133 - git log --pretty=format:'%s' "$range" 134 - echo "" 135 - } > "$raw_context" 136 - 137 - # Gather merged PR details (title + body) 138 - { 139 - echo "" 140 - echo "=== Merged Pull Requests ===" 141 - local pr_numbers 142 - pr_numbers="$(git log --pretty=format:'%s' "$range" | grep -oE '#[0-9]+' | tr -d '#' | sort -u)" 143 - for pr in $pr_numbers; do 144 - local pr_json 145 - pr_json="$(gh pr view "$pr" --repo "$REPO" --json title,body 2>/dev/null || true)" 146 - if [[ -n "$pr_json" ]]; then 147 - echo "--- PR #$pr ---" 148 - echo "$pr_json" | jq -r '"Title: \(.title)\nBody:\n\(.body)"' 149 - echo "" 150 - fi 151 - done 152 - } >> "$raw_context" 153 - 154 - # Gather diff stats 155 - { 156 - echo "" 157 - echo "=== Diff Stats ===" 158 - git diff --stat "$range" 159 - } >> "$raw_context" 160 - 161 - local prompt 162 - prompt="$(cat <<'PROMPT' 163 - You are writing release notes for **Prowl**, a macOS app that runs multiple 164 - coding agents in parallel, each in its own terminal tab. 165 - 166 - Given the raw context below (commits, PR descriptions, diff stats), produce a 167 - concise, user-facing changelog in Markdown. 168 - 169 - ## Rules 170 - 171 - 1. **Audience**: Prowl end-users (developers). They care about what changed in 172 - their day-to-day experience, not internal code structure. 173 - 2. **Include only user-visible changes**: new features, behavior changes, UX 174 - improvements, notable bug fixes. Skip pure refactors, test-only changes, 175 - CI tweaks, dependency bumps, and code-style changes unless they affect 176 - the user. 177 - 3. **Teach naturally**: when a change introduces a new shortcut, workflow, or 178 - setting, briefly explain how to use it (e.g. "Press ⌥⌘↩ to toggle 179 - Canvas view"). 180 - 4. **Tone**: clear, professional, friendly. No marketing fluff, no emoji, no 181 - exclamation marks, no "we're excited". 182 - 5. **Format**: 183 - - Start with a one-line summary sentence of the release theme if there is a 184 - clear one; otherwise jump straight to the list. 185 - - Use a flat bullet list (`-`). Group related items under a single bullet. 186 - - Each bullet should be one or two sentences maximum. 187 - - If a change is a bug fix, start the bullet with "Fixed: …". 188 - - End with nothing — no sign-off, no footer. 189 - 6. **Length**: aim for 3-8 bullets. Merge trivial items. Omit if truly nothing 190 - is user-facing (output a single bullet: "- Internal improvements and 191 - stability fixes."). 192 - 7. **Language**: English only. 193 - 8. Output **only** the Markdown content. No preamble, no code fences. 194 - PROMPT 195 - )" 196 - 197 - local notes 198 - notes="$(claude -p \ 199 - --model sonnet \ 200 - --allowedTools "" \ 201 - --output-format text \ 202 - "$prompt 203 - 204 - --- RAW CONTEXT --- 205 - $(cat "$raw_context") 206 - --- END ---" 2>/dev/null || true)" 207 - rm -f "$raw_context" 208 - 209 - if [[ -n "$notes" ]] && [[ "$(echo "$notes" | wc -l)" -ge 2 ]]; then 210 - echo "$notes" 211 - return 0 212 - fi 213 - return 1 214 - } 215 - 216 - generate_fallback_notes() { 217 - local range="$1" 218 - if [[ -n "$range" ]]; then 219 - gh api "repos/$REPO/releases/generate-notes" \ 220 - -f tag_name="$TAG" -f previous_tag_name="$PREV_TAG" \ 221 - --jq '.body' 2>/dev/null || \ 222 - git log --pretty=format:'- %s' "$range" 223 - else 224 - git log --pretty=format:'- %s' -20 225 - fi 226 - } 120 + # ── Bump version (skip if tag already exists) ──────────────────────────────── 227 121 228 - if [[ -s "$NOTES_FILE" ]]; then 229 - log "using existing release notes from $NOTES_FILE" 230 - elif [[ -n "$PREV_TAG" ]]; then 231 - RANGE="$PREV_TAG..$TAG" 232 - if command -v claude >/dev/null 2>&1; then 233 - log "generating release notes with LLM..." 234 - if generate_llm_notes "$RANGE" > "$NOTES_FILE"; then 235 - log "release notes generated by LLM" 236 - else 237 - log "LLM generation failed, falling back to GitHub auto-notes..." 238 - generate_fallback_notes "$RANGE" > "$NOTES_FILE" 239 - fi 240 - else 241 - generate_fallback_notes "$RANGE" > "$NOTES_FILE" 242 - fi 122 + if git rev-parse "$TAG" >/dev/null 2>&1; then 123 + log "tag $TAG already exists, skipping bump" 243 124 else 244 - generate_fallback_notes "" > "$NOTES_FILE" 125 + log "bumping version in project..." 126 + make bump-version VERSION="$VERSION" BUILD="$BUILD" 245 127 fi 246 - log "release notes written to $NOTES_FILE" 128 + 129 + # ── Show release notes ─────────────────────────────────────────────────────── 130 + 131 + echo 132 + echo "──── Release Notes ────" 133 + cat "$NOTES_FILE" 134 + echo "───────────────────────" 135 + echo 247 136 248 - # Show release notes and ask for confirmation (skip in non-interactive / CI) 137 + # Confirm interactively if possible 249 138 if [[ -t 0 ]]; then 250 - echo 251 - echo "──── Release Notes ────" 252 - cat "$NOTES_FILE" 253 - echo "───────────────────────" 254 - echo 255 139 read -rp "Proceed with release? [Y/n] " confirm 256 140 case "${confirm:-Y}" in 257 141 [Yy]*) ;; ··· 395 279 UPLOAD_FILES=("$DMG_PATH" "$ZIP_PATH" "build/appcast.xml") 396 280 DELTA_FILES=( $(find build -name "*.delta" -type f 2>/dev/null || true) ) 397 281 UPLOAD_FILES+=("${DELTA_FILES[@]}") 282 + 283 + # Delete existing release if present (idempotent re-run) 284 + if gh release view "$TAG" --repo "$REPO" >/dev/null 2>&1; then 285 + log "deleting existing release $TAG for re-creation..." 286 + gh release delete "$TAG" --repo "$REPO" --yes 287 + fi 398 288 399 289 gh release create "$TAG" "${UPLOAD_FILES[@]}" \ 400 290 --repo "$REPO" \