diff --git a/docs/RELEASING.md b/docs/RELEASING.md index f9dcfaffd..b1db4af95 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -32,7 +32,7 @@ Use `pnpm` (Node 22+) from the repo root. Keep the working tree clean before tag 5) **macOS app (Sparkle)** - [ ] Build + sign the macOS app, then zip it for distribution. -- [ ] Generate the Sparkle signature and update `appcast.xml`. +- [ ] Generate the Sparkle appcast (HTML notes via `scripts/make_appcast.sh`) and update `appcast.xml`. - [ ] Keep the app zip (and optional dSYM zip) ready to attach to the GitHub release. - [ ] Follow `docs/mac/release.md` for the exact commands and required env vars. diff --git a/docs/mac/release.md b/docs/mac/release.md index 4e110cd62..9372f25b7 100644 --- a/docs/mac/release.md +++ b/docs/mac/release.md @@ -49,28 +49,12 @@ ditto -c -k --keepParent apps/macos/.build/release/Clawdis.app.dSYM dist/Clawdis ``` ## Appcast entry -1. Generate the ed25519 signature (requires `SPARKLE_PRIVATE_KEY_FILE`): - ```bash - SPARKLE_PRIVATE_KEY_FILE=/Users/steipete/Library/CloudStorage/Dropbox/Backup/Sparkle/ed25519-private-key \ - apps/macos/.build/artifacts/sparkle/Sparkle/bin/sign_update dist/Clawdis-0.1.0.zip - ``` - Copy the reported signature and file size. -2. Edit `appcast.xml` (root of repo), add a new `` at the top pointing to the GitHub release asset. Example snippet to adapt: - ```xml - - Clawdis 0.1.0 - https://github.com/steipete/clawdis/releases/tag/v0.1.0 - Sun, 07 Dec 2025 12:00:00 +0000 - - - ``` - Keep the newest item first; leave the channel metadata intact. -3. Commit the updated `appcast.xml` alongside the release assets (zip + dSYM) when publishing. +Use the release note generator so Sparkle renders formatted HTML notes: +```bash +SPARKLE_PRIVATE_KEY_FILE=/Users/steipete/Library/CloudStorage/Dropbox/Backup/Sparkle/ed25519-private-key scripts/make_appcast.sh dist/Clawdis-0.1.0.zip https://raw.githubusercontent.com/steipete/clawdis/main/appcast.xml +``` +Generates HTML release notes from `CHANGELOG.md` (via `scripts/changelog-to-html.sh`) and embeds them in the appcast entry. +Commit the updated `appcast.xml` alongside the release assets (zip + dSYM) when publishing. ## Publish & verify - Upload `Clawdis-0.1.0.zip` (and `Clawdis-0.1.0.dSYM.zip`) to the GitHub release for tag `v0.1.0`. diff --git a/scripts/changelog-to-html.sh b/scripts/changelog-to-html.sh new file mode 100755 index 000000000..2f48c3617 --- /dev/null +++ b/scripts/changelog-to-html.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +set -euo pipefail + +VERSION=${1:-} +CHANGELOG_FILE=${2:-} + +if [[ -z "$VERSION" ]]; then + echo "Usage: $0 [changelog_file]" >&2 + exit 1 +fi + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +if [[ -z "$CHANGELOG_FILE" ]]; then + if [[ -f "$SCRIPT_DIR/../CHANGELOG.md" ]]; then + CHANGELOG_FILE="$SCRIPT_DIR/../CHANGELOG.md" + elif [[ -f "CHANGELOG.md" ]]; then + CHANGELOG_FILE="CHANGELOG.md" + elif [[ -f "../CHANGELOG.md" ]]; then + CHANGELOG_FILE="../CHANGELOG.md" + else + echo "Error: Could not find CHANGELOG.md" >&2 + exit 1 + fi +fi + +if [[ ! -f "$CHANGELOG_FILE" ]]; then + echo "Error: Changelog file '$CHANGELOG_FILE' not found" >&2 + exit 1 +fi + +extract_version_section() { + local version=$1 + local file=$2 + awk -v version="$version" ' + BEGIN { found=0 } + /^## / { + if ($0 ~ "^##[[:space:]]+" version "([[:space:]].*|$)") { found=1; next } + if (found) { exit } + } + found { print } + ' "$file" +} + +markdown_to_html() { + local text=$1 + text=$(echo "$text" | sed 's/^### \(.*\)$/

\1<\/h3>/') + text=$(echo "$text" | sed 's/^## \(.*\)$/

\1<\/h2>/') + text=$(echo "$text" | sed 's/^- \*\*\([^*]*\)\*\*\(.*\)$/
  • \1<\/strong>\2<\/li>/') + text=$(echo "$text" | sed 's/^- \([^*].*\)$/
  • \1<\/li>/') + text=$(echo "$text" | sed 's/\*\*\([^*]*\)\*\*/\1<\/strong>/g') + text=$(echo "$text" | sed 's/`\([^`]*\)`/\1<\/code>/g') + text=$(echo "$text" | sed 's/\[\([^]]*\)\](\([^)]*\))/\1<\/a>/g') + echo "$text" +} + +version_content=$(extract_version_section "$VERSION" "$CHANGELOG_FILE") +if [[ -z "$version_content" ]]; then + echo "

    Clawdis $VERSION

    " + echo "

    Latest Clawdis update.

    " + echo "

    View full changelog

    " + exit 0 +fi + +echo "

    Clawdis $VERSION

    " + +in_list=false +while IFS= read -r line; do + if [[ "$line" =~ ^- ]]; then + if [[ "$in_list" == false ]]; then + echo "
      " + in_list=true + fi + markdown_to_html "$line" + else + if [[ "$in_list" == true ]]; then + echo "
    " + in_list=false + fi + if [[ -n "$line" ]]; then + markdown_to_html "$line" + fi + fi +done <<< "$version_content" + +if [[ "$in_list" == true ]]; then + echo "" +fi + +echo "

    View full changelog

    " diff --git a/scripts/make_appcast.sh b/scripts/make_appcast.sh new file mode 100755 index 000000000..21a586ac8 --- /dev/null +++ b/scripts/make_appcast.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT=$(cd "$(dirname "$0")/.." && pwd) +ZIP=${1:?"Usage: $0 Clawdis-.zip"} +FEED_URL=${2:-"https://raw.githubusercontent.com/steipete/clawdis/main/appcast.xml"} +PRIVATE_KEY_FILE=${SPARKLE_PRIVATE_KEY_FILE:-} +if [[ -z "$PRIVATE_KEY_FILE" ]]; then + echo "Set SPARKLE_PRIVATE_KEY_FILE to your ed25519 private key (Sparkle)." >&2 + exit 1 +fi +if [[ ! -f "$ZIP" ]]; then + echo "Zip not found: $ZIP" >&2 + exit 1 +fi + +ZIP_DIR=$(cd "$(dirname "$ZIP")" && pwd) +ZIP_NAME=$(basename "$ZIP") +ZIP_BASE="${ZIP_NAME%.zip}" +VERSION=${SPARKLE_RELEASE_VERSION:-} +if [[ -z "$VERSION" ]]; then + if [[ "$ZIP_NAME" =~ ^Clawdis-([0-9]+(\.[0-9]+){1,2}([-.][^.]*)?)\.zip$ ]]; then + VERSION="${BASH_REMATCH[1]}" + else + echo "Could not infer version from $ZIP_NAME; set SPARKLE_RELEASE_VERSION." >&2 + exit 1 + fi +fi + +NOTES_HTML="${ZIP_DIR}/${ZIP_BASE}.html" +KEEP_NOTES=${KEEP_SPARKLE_NOTES:-0} +if [[ -x "$ROOT/scripts/changelog-to-html.sh" ]]; then + "$ROOT/scripts/changelog-to-html.sh" "$VERSION" >"$NOTES_HTML" +else + echo "Missing scripts/changelog-to-html.sh; cannot generate HTML release notes." >&2 + exit 1 +fi +if [[ "$KEEP_NOTES" != "1" ]]; then + trap 'rm -f "$NOTES_HTML"' EXIT +fi + +DOWNLOAD_URL_PREFIX=${SPARKLE_DOWNLOAD_URL_PREFIX:-"https://github.com/steipete/clawdis/releases/download/v${VERSION}/"} + +export PATH="$ROOT/apps/macos/.build/artifacts/sparkle/Sparkle/bin:$PATH" +if ! command -v generate_appcast >/dev/null; then + echo "generate_appcast not found in PATH. Build Sparkle tools via SwiftPM." >&2 + exit 1 +fi + +generate_appcast \ + --ed-key-file "$PRIVATE_KEY_FILE" \ + --download-url-prefix "$DOWNLOAD_URL_PREFIX" \ + --embed-release-notes \ + --link "$FEED_URL" \ + "$ZIP_DIR" + +echo "Appcast generated (appcast.xml). Upload alongside $ZIP at $FEED_URL"