native macOS codings agent orchestrator
6
fork

Configure Feed

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

Add public release infrastructure with Sparkle auto-update support

- Configure Sparkle EdDSA key (SUPublicEDKey) and feed URL in Info.plist
- Switch version scheme from semver to date-based YYYY.M.DD format
- Add release.sh: full pipeline with archive, re-sign, DMG, notarize, appcast
- Add install-release Makefile target for local Release test builds
- Split /fork-release into /sync-upstream (sync only) and /release (publish)
- Deprecate release-to-fork.sh in favor of release.sh
- Update docs: fork-sync-and-release.md, change-list.md, CLAUDE.md, AGENTS.md

onevcat 1b0eb02b fe788b6e

+471 -114
+2 -3
.claude/commands/fork-release.md .claude/commands/sync-upstream.md
··· 1 - Sync upstream and publish a private fork release. 1 + Sync upstream changes into the fork's main branch. 2 2 3 3 Follow the workflow documented in `doc-onevcat/fork-sync-and-release.md`: 4 4 ··· 7 7 3. After resolving conflicts, stage the resolved files, then complete the merge commit with `git commit --no-edit` 8 8 4. Verify the build with `make build-app` (clear SPM cache at `/tmp/supacode-spm-cache/SourcePackages` if dependency resolution fails) 9 9 5. Push to origin: `git push origin main` 10 - 6. Run `./doc-onevcat/scripts/release-to-fork.sh` to build, sign, notarize, and publish the release 11 - 7. Report the release URL when done 10 + 6. Report what was synced (upstream commit range) when done
+14
.claude/commands/release.md
··· 1 + Build, sign, notarize, and publish a Prowl release. 2 + 3 + 1. Verify current branch is `main`: `git branch --show-current` 4 + - If not on main, abort and tell the user to switch first 5 + 2. Verify working tree is clean: `git status --porcelain` 6 + - If dirty, list the changes and ask whether to proceed or abort 7 + 3. Determine the version: 8 + - If `$ARGUMENTS` is provided, use it as the version (e.g., `2026.3.18`) 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 + 5. Report the GitHub release URL and remind the user to verify: 12 + - The DMG downloads and installs correctly 13 + - Sparkle update check works (launch app → Check for Updates) 14 + - The appcast at `https://prowl.onev.cat/appcast.xml` is updated
+3 -2
AGENTS.md
··· 6 6 make build-ghostty-xcframework # Rebuild GhosttyKit from Zig source (requires mise) 7 7 make build-app # Build macOS app (Debug) via xcodebuild 8 8 make run-app # Build and launch Debug app 9 - make install-dev-build # Build and copy to /Applications 9 + make install-dev-build # Build and copy to /Applications (Debug) 10 + make install-release # Build Release, sign locally, install to /Applications 10 11 make format # Run swift-format only 11 12 make lint # Run swiftlint only (fix + lint) 12 13 make check # Run both format and lint 13 14 make test # Run all tests 14 15 make log-stream # Stream app logs (subsystem: com.onevcat.prowl) 15 - make bump-version # Bump patch version and create git tag 16 + make bump-version # Bump version (date-based YYYY.M.DD) and create git tag 16 17 make bump-and-release # Bump version and push to trigger release 17 18 ``` 18 19
+73 -18
Makefile
··· 18 18 BUILD ?= 19 19 XCODEBUILD_FLAGS ?= 20 20 .DEFAULT_GOAL := help 21 - .PHONY: build-ghostty-xcframework build-app run-app install-dev-build archive export-archive format lint check test bump-version bump-and-release log-stream 21 + .PHONY: build-ghostty-xcframework build-app run-app install-dev-build install-release archive export-archive format lint check test bump-version bump-and-release log-stream 22 22 23 23 help: # Display this help. 24 24 @-+echo "Run make with one of the following targets:" ··· 64 64 ditto "$$src" "$$dst"; \ 65 65 echo "installed $$dst" 66 66 67 + install-release: build-ghostty-xcframework # Build Release, sign locally, install to /Applications 68 + @SIGNING_IDENTITY="$$(security find-identity -v -p codesigning 2>/dev/null | awk -F'"' '/Developer ID Application/ {print $$2; exit}')"; \ 69 + if [ -z "$$SIGNING_IDENTITY" ]; then \ 70 + echo "error: no Developer ID Application identity found"; \ 71 + exit 1; \ 72 + fi; \ 73 + IDENTITY_SHA="$$(security find-identity -v -p codesigning 2>/dev/null | grep "$$SIGNING_IDENTITY" | head -1 | awk '{print $$2}')"; \ 74 + TEAM_ID="$$(echo "$$SIGNING_IDENTITY" | grep -oE '\([A-Z0-9]{10}\)$$' | tr -d '()')"; \ 75 + echo "identity: $$SIGNING_IDENTITY"; \ 76 + echo "team: $$TEAM_ID"; \ 77 + APPLE_TEAM_ID="$$TEAM_ID" DEVELOPER_ID_IDENTITY_SHA="$$IDENTITY_SHA" $(MAKE) archive; \ 78 + mkdir -p build; \ 79 + cat > build/ExportOptions.plist <<-PLIST 80 + <?xml version="1.0" encoding="UTF-8"?> 81 + <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 82 + <plist version="1.0"> 83 + <dict> 84 + <key>method</key> 85 + <string>developer-id</string> 86 + <key>signingStyle</key> 87 + <string>manual</string> 88 + <key>signingCertificate</key> 89 + <string>$$SIGNING_IDENTITY</string> 90 + <key>teamID</key> 91 + <string>$$TEAM_ID</string> 92 + </dict> 93 + </plist> 94 + PLIST 95 + $(MAKE) export-archive; \ 96 + APP_PATH="$$(find build/export -name '*.app' -maxdepth 3 -print -quit)"; \ 97 + if [ ! -d "$$APP_PATH" ]; then \ 98 + echo "error: exported app not found"; \ 99 + exit 1; \ 100 + fi; \ 101 + SPARKLE="$$APP_PATH/Contents/Frameworks/Sparkle.framework/Versions/B"; \ 102 + if [ -d "$$SPARKLE" ]; then \ 103 + codesign -f -s "$$IDENTITY_SHA" -o runtime --timestamp -v "$$SPARKLE/XPCServices/Installer.xpc"; \ 104 + codesign -f -s "$$IDENTITY_SHA" -o runtime --timestamp --preserve-metadata=entitlements -v "$$SPARKLE/XPCServices/Downloader.xpc"; \ 105 + codesign -f -s "$$IDENTITY_SHA" -o runtime --timestamp -v "$$SPARKLE/Updater.app"; \ 106 + codesign -f -s "$$IDENTITY_SHA" -o runtime --timestamp -v "$$SPARKLE/Autoupdate"; \ 107 + codesign -f -s "$$IDENTITY_SHA" -o runtime --timestamp -v "$$SPARKLE/Sparkle"; \ 108 + codesign -f -s "$$IDENTITY_SHA" -o runtime --timestamp -v "$$APP_PATH/Contents/Frameworks/Sparkle.framework"; \ 109 + fi; \ 110 + SENTRY="$$APP_PATH/Contents/Frameworks/Sentry.framework"; \ 111 + if [ -d "$$SENTRY" ]; then \ 112 + codesign -f -s "$$IDENTITY_SHA" -o runtime --timestamp -v "$$SENTRY/Versions/A/Sentry"; \ 113 + codesign -f -s "$$IDENTITY_SHA" -o runtime --timestamp -v "$$SENTRY"; \ 114 + fi; \ 115 + codesign -f -s "$$IDENTITY_SHA" -o runtime --timestamp --preserve-metadata=entitlements,requirements,flags -v "$$APP_PATH"; \ 116 + codesign -vvv --deep --strict "$$APP_PATH"; \ 117 + PRODUCT="$$(basename "$$APP_PATH")"; \ 118 + DST="/Applications/$$PRODUCT"; \ 119 + echo "copying $$APP_PATH -> $$DST"; \ 120 + rm -rf "$$DST"; \ 121 + ditto "$$APP_PATH" "$$DST"; \ 122 + echo "installed $$DST (Release build, locally signed)" 123 + 67 124 archive: build-ghostty-xcframework # Archive Release build for distribution 68 125 bash -o pipefail -c 'xcodebuild -project supacode.xcodeproj -scheme supacode -configuration Release -archivePath build/supacode.xcarchive archive CODE_SIGN_STYLE=Manual DEVELOPMENT_TEAM="$$APPLE_TEAM_ID" CODE_SIGN_IDENTITY="$$DEVELOPER_ID_IDENTITY_SHA" OTHER_CODE_SIGN_FLAGS="--timestamp" -skipMacroValidation -clonedSourcePackagesDirPath $(SPM_CACHE_DIR) $(XCODEBUILD_FLAGS) 2>&1 | mise exec -- xcsift -qw --format toon' 69 126 ··· 85 142 log-stream: # Stream logs from the app via log stream 86 143 log stream --predicate 'subsystem == "com.onevcat.prowl"' --style compact --color always 87 144 88 - bump-version: # Bump app version (usage: make bump-version [VERSION=x.x.x] [BUILD=123]) 145 + bump-version: # Bump app version (usage: make bump-version [VERSION=YYYY.M.DD] [BUILD=YYYYMMDD]) 89 146 @if [ -z "$(VERSION)" ]; then \ 90 - current="$$(/usr/bin/awk -F' = ' '/MARKETING_VERSION = [0-9.]+;/{gsub(/;/,"",$$2);print $$2; exit}' "$(CURRENT_MAKEFILE_DIR)/supacode.xcodeproj/project.pbxproj")"; \ 91 - if [ -z "$$current" ]; then \ 92 - echo "error: MARKETING_VERSION not found"; \ 93 - exit 1; \ 94 - fi; \ 95 - major="$$(echo "$$current" | cut -d. -f1)"; \ 96 - minor="$$(echo "$$current" | cut -d. -f2)"; \ 97 - patch="$$(echo "$$current" | cut -d. -f3)"; \ 98 - version="$$major.$$minor.$$((patch + 1))"; \ 147 + version="$$(date +%Y.%-m.%-d)"; \ 148 + suffix=1; \ 149 + while git rev-parse "v$$version" >/dev/null 2>&1; do \ 150 + suffix=$$((suffix + 1)); \ 151 + version="$$(date +%Y.%-m.%-d).$$suffix"; \ 152 + done; \ 99 153 else \ 100 - if ! echo "$(VERSION)" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$$'; then \ 101 - echo "error: VERSION must be in x.x.x format"; \ 154 + if ! echo "$(VERSION)" | grep -qE '^[0-9]{4}\.[0-9]{1,2}\.[0-9]{1,2}(\.[0-9]+)?$$'; then \ 155 + echo "error: VERSION must be in YYYY.M.DD or YYYY.M.DD.N format"; \ 102 156 exit 1; \ 103 157 fi; \ 104 158 version="$(VERSION)"; \ 105 159 fi; \ 106 160 if [ -z "$(BUILD)" ]; then \ 107 - build="$$(/usr/bin/awk -F' = ' '/CURRENT_PROJECT_VERSION = [0-9]+;/{gsub(/;/,"",$$2);print $$2; exit}' "$(CURRENT_MAKEFILE_DIR)/supacode.xcodeproj/project.pbxproj")"; \ 108 - if [ -z "$$build" ]; then \ 109 - echo "error: CURRENT_PROJECT_VERSION not found"; \ 110 - exit 1; \ 161 + base_build="$$(date +%Y%m%d)"; \ 162 + current_build="$$(/usr/bin/awk -F' = ' '/CURRENT_PROJECT_VERSION = [0-9]+;/{gsub(/;/,"",$$2);print $$2; exit}' "$(CURRENT_MAKEFILE_DIR)/supacode.xcodeproj/project.pbxproj")"; \ 163 + if [ "$$current_build" -ge "$$base_build" ] 2>/dev/null; then \ 164 + build="$$((current_build + 1))"; \ 165 + else \ 166 + build="$$base_build"; \ 111 167 fi; \ 112 - build="$$((build + 1))"; \ 113 168 else \ 114 169 if ! echo "$(BUILD)" | grep -qE '^[0-9]+$$'; then \ 115 170 echo "error: BUILD must be an integer"; \
+1
doc-onevcat/change-list.md
··· 31 31 | Add PreToolUse hook to block `gh pr create` targeting upstream; PRs must explicitly target fork. | `9970560` | Fork only | 32 32 | Add PR target rule to CLAUDE.md: always target `onevcat/supacode`, never upstream. | `962ba62` | Fork only | 33 33 | Rebrand user-facing identity from Supacode to Prowl: app name, icon, bundle display name, settings file paths (`prowl.json`), subsystem identifiers, and about/UI strings. Keep module name as `supacode` for code compatibility. | `5f7d84a`…`5676418` | Fork only | 34 + | Add public release infrastructure: Sparkle EdDSA key setup, date-based version scheme (`YYYY.M.DD`), full release script with DMG/notarization/appcast, `install-release` Makefile target, `/release` and `/sync-upstream` commands. | — | Fork only |
+70 -91
doc-onevcat/fork-sync-and-release.md
··· 1 - # Fork Sync and Personal Release Workflow 1 + # Fork Sync and Public Release Workflow 2 2 3 3 ## Goal 4 4 5 - Keep `onevcat/supacode` close to `supabitapp/supacode` while preserving local customizations (keybindings and feature trims), and make personal releases easy to produce and download from the fork Release page. 5 + Keep `onevcat/Prowl` close to `supabitapp/supacode` while preserving local customizations, and publish public releases with Sparkle auto-update support. 6 + 7 + ## Release Infrastructure 6 8 7 - ## Current Release Build in This Repo 9 + ### Version Scheme 8 10 9 - The repository already has two production-grade workflows: 11 + Date-based versioning: `YYYY.M.DD` (e.g., `2026.3.18`). Same-day collisions append `.N` suffix (e.g., `2026.3.18.2`). Build number is `YYYYMMDD` integer, incrementing for same-day builds. 10 12 11 - - `.github/workflows/release.yml` 12 - - Trigger: GitHub Release published. 13 - - Build: `make archive` + `make export-archive` on `macos-26`. 14 - - Packaging: app zip, dmg, Sparkle appcast, delta files. 15 - - Signing/notarization: required (Apple cert + notary credentials). 16 - - Publish target in workflow: hard-coded `supabitapp/supacode`. 17 - - `.github/workflows/release-tip.yml` 18 - - Trigger: push to `main` (and manual dispatch). 19 - - Produces/updates `tip` prerelease assets and appcast. 20 - - Also requires signing/notarization secrets. 13 + ### Sparkle Auto-Update 21 14 22 - ### Fork Impact 15 + - Feed URL: `https://prowl.onev.cat/appcast.xml` (hosted via Prowl-Site on Netlify) 16 + - EdDSA public key configured in `supacode/Info.plist` as `SUPublicEDKey` 17 + - Private key stored in macOS Keychain and exported to `~/.prowl-sparkle-private-key` 18 + - Appcast generated by `bins/generate_appcast` during release 23 19 24 - Out of the box, these workflows are not fork-friendly: 20 + ### Release Artifacts 25 21 26 - - They require many signing/secrets that forks usually do not have. 27 - - Stable release publish target is hard-coded to upstream repo (`supabitapp/supacode`). 28 - - Some appcast URLs are hard-coded to `supacode.sh`. 22 + Each release produces: 23 + - `Prowl.dmg` — signed, notarized disk image for download 24 + - `Prowl.app.zip` — signed zip for Sparkle updates 25 + - `appcast.xml` — Sparkle feed with EdDSA signature 29 26 30 - ## Recommended Branch Strategy 27 + ## Branch Strategy 31 28 32 - - `upstream/main`: source of truth (read-only remote branch). 33 - - `main` (origin): your integration branch with custom patches. 34 - - `feat/onevcat-*`: optional short-lived branches per local customization. 29 + - `upstream/main`: source of truth (read-only remote branch) 30 + - `main` (origin): integration branch with custom patches 31 + - Feature branches as needed 35 32 36 33 ## One-Time Setup 37 34 ··· 42 39 git config rerere.autoupdate true 43 40 ``` 44 41 45 - `rerere` records your conflict resolutions so repeated upstream syncs become easier. 46 - 47 - ## Upstream Sync Runbook (Recommended: Merge) 42 + ## Upstream Sync Runbook 48 43 49 44 ```bash 50 45 git switch main ··· 57 52 git push origin main 58 53 ``` 59 54 55 + Or use the `/sync-upstream` command which automates these steps. 56 + 60 57 If conflicts happen, resolve once, commit, and `rerere` will likely auto-apply next time. 61 58 62 - ## Common Pitfalls and Fixes 59 + ## Release Workflow 63 60 64 - - `git fetch origin upstream --prune` is invalid for this use case. 65 - `upstream` is interpreted as a refspec, which may fail with `fatal: couldn't find remote ref upstream`. 66 - Use two fetch commands (or `git fetch --all --prune`) instead. 67 - - Prefer `git merge --ff-only origin/main` in scripted sync flow. 68 - It is deterministic and avoids `git pull` edge cases around `FETCH_HEAD`. 69 - - Keep working tree clean before sync (`git status --short` should be empty), otherwise abort and stash/commit first. 61 + ### Full Public Release 70 62 71 - ## Personal Release Strategy (Fork Release Page) 63 + ```bash 64 + # Run the release script (defaults to today's date as version) 65 + ./doc-onevcat/scripts/release.sh 72 66 73 - The release helper now supports automatic notarization for personal fork releases. 67 + # Or specify version explicitly 68 + ./doc-onevcat/scripts/release.sh 2026.3.18 69 + ``` 74 70 75 - Default flow: 71 + Or use the `/release` command. 76 72 77 - 1) Build app locally (`make build-app`). 78 - 2) Sign app with your `Developer ID Application` identity. 79 - 3) Notarize via `notarytool` and staple ticket to app. 80 - 4) Zip app bundle. 81 - 5) Create tag and upload zip to your fork GitHub Release page. 73 + The script handles: 74 + 1. Version bump (date-based) + signed git tag 75 + 2. Release archive (xcodebuild Release) 76 + 3. Re-sign Sparkle/Sentry frameworks 77 + 4. DMG creation with signing 78 + 5. Notarization + stapling 79 + 6. Sparkle appcast generation 80 + 7. GitHub Release with all artifacts 81 + 8. Prowl-Site appcast update (triggers Netlify deploy) 82 82 83 - Non-notarized publishing is intentionally disabled for this fork. 84 - 85 - ## Helper Scripts 86 - 87 - - Sync helper: `doc-onevcat/scripts/sync-upstream-main.sh` 88 - - Release helper: `doc-onevcat/scripts/release-to-fork.sh` 89 - - Default target repo: auto-detected from `origin` 90 - - Override target repo: `GH_REPO=owner/repo` 91 - - Release create fallback: if `gh release create` fails (for example token scope mismatch), script falls back to `gh api` and then uploads assets 92 - - Notarization: mandatory (the script exits if `ENABLE_NOTARIZATION!=1`) 93 - - Default keychain profile name: `supacode-notary` (override with `APPLE_NOTARY_KEYCHAIN_PROFILE`) 94 - 95 - ### Example 83 + ### Local Test Build (Release config, no publish) 96 84 97 85 ```bash 98 - # Sync upstream into local main and verify build 99 - ./doc-onevcat/scripts/sync-upstream-main.sh 100 - 101 - # Create a personal release on fork release page 102 - ./doc-onevcat/scripts/release-to-fork.sh 103 - 104 - # Or specify tag explicitly 105 - ./doc-onevcat/scripts/release-to-fork.sh onevcat-v2026.02.26-01 86 + make install-release 106 87 ``` 107 88 108 - ## Notarization Credentials 89 + Builds a Release archive, signs it locally, and installs to `/Applications`. 109 90 110 - The script first tries `xcrun notarytool submit --keychain-profile <profile>`. 111 - If profile is missing, it will create one using either: 91 + ### Environment Variables 112 92 113 - - App Store Connect API key: 114 - - `APPLE_NOTARIZATION_KEY_PATH` 115 - - `APPLE_NOTARIZATION_KEY_ID` 116 - - `APPLE_NOTARIZATION_ISSUER` 117 - - Or Apple ID credentials: 118 - - `APPLE_ID` 119 - - `APPLE_PASSWORD` (app-specific password) 120 - - `APPLE_TEAM_ID` (optional if inferable from signing identity) 93 + | Variable | Default | Description | 94 + | --- | --- | --- | 95 + | `APPLE_SIGNING_IDENTITY` | auto-detected | Developer ID Application identity | 96 + | `APPLE_TEAM_ID` | from identity | Apple Team ID | 97 + | `APPLE_NOTARY_KEYCHAIN_PROFILE` | `supacode-notary` | Keychain profile for notarytool | 98 + | `SPARKLE_PRIVATE_KEY_FILE` | `~/.prowl-sparkle-private-key` | EdDSA private key for appcast | 99 + | `PROWL_SITE_DIR` | `../Prowl-Site` | Path to Prowl-Site repo | 121 100 122 - Signing identity: 101 + ## Notarization Credentials 123 102 124 - - `APPLE_SIGNING_IDENTITY` (optional). If omitted, script auto-detects the first available `Developer ID Application` identity from keychain. 103 + The release script uses `xcrun notarytool submit --keychain-profile <profile>`. Store credentials with: 125 104 126 - ## Optional: Full Signed Release on Fork 105 + ```bash 106 + xcrun notarytool store-credentials supacode-notary \ 107 + --apple-id <email> \ 108 + --password <app-specific-password> \ 109 + --team-id <team-id> 110 + ``` 127 111 128 - If you need notarized DMG and Sparkle feed in your fork: 112 + ## Helper Scripts 129 113 130 - - Copy/adjust release workflows to publish to `${{ github.repository }}`. 131 - - Replace hard-coded download URL and release-notes URL with fork values. 132 - - Configure all required secrets: 133 - - `DEVELOPER_ID_CERT_P12` 134 - - `DEVELOPER_ID_CERT_PASSWORD` 135 - - `DEVELOPER_ID_IDENTITY` 136 - - `KEYCHAIN_PASSWORD` 137 - - `APPLE_TEAM_ID` 138 - - `APPLE_NOTARIZATION_ISSUER` 139 - - `APPLE_NOTARIZATION_KEY_ID` 140 - - `APPLE_NOTARIZATION_KEY` 141 - - `SPARKLE_PRIVATE_KEY` 142 - - plus telemetry/sentry secrets used by workflow 114 + - `doc-onevcat/scripts/sync-upstream-main.sh` — upstream sync automation 115 + - `doc-onevcat/scripts/release.sh` — full public release pipeline 116 + - `doc-onevcat/scripts/release-to-fork.sh` — (deprecated) legacy personal release script 143 117 144 - For your current goal (personal periodic builds), the unsigned release helper is usually enough. 118 + ## Common Pitfalls 119 + 120 + - `git fetch origin upstream --prune` is invalid. Use two separate fetch commands. 121 + - Prefer `git merge --ff-only origin/main` in scripted sync flow. 122 + - Keep working tree clean before sync or release. 123 + - Fork releases must be notarized. Non-notarized publishing is forbidden.
+3
doc-onevcat/scripts/release-to-fork.sh
··· 1 1 #!/usr/bin/env bash 2 + # DEPRECATED: Use release.sh instead for public releases with Sparkle appcast, 3 + # DMG packaging, and proper versioning. 4 + # This script is kept for reference only. 2 5 set -euo pipefail 3 6 4 7 origin_repo_from_remote() {
+301
doc-onevcat/scripts/release.sh
··· 1 + #!/usr/bin/env bash 2 + # Prowl public release script. 3 + # Usage: ./doc-onevcat/scripts/release.sh [VERSION] 4 + # 5 + # Environment variables: 6 + # APPLE_SIGNING_IDENTITY Developer ID identity (auto-detected if unset) 7 + # APPLE_TEAM_ID Apple Team ID (inferred from identity if unset) 8 + # APPLE_NOTARY_KEYCHAIN_PROFILE Keychain profile for notarytool (default: supacode-notary) 9 + # SPARKLE_PRIVATE_KEY_FILE Path to EdDSA private key file (default: ~/.prowl-sparkle-private-key) 10 + # PROWL_SITE_DIR Path to Prowl-Site repo (default: ../Prowl-Site) 11 + set -euo pipefail 12 + 13 + SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" 14 + PROJECT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" 15 + cd "$PROJECT_DIR" 16 + 17 + # ── Helpers ────────────────────────────────────────────────────────────────── 18 + 19 + origin_repo_from_remote() { 20 + local remote_url 21 + remote_url="$(git remote get-url origin 2>/dev/null || true)" 22 + [[ -z "$remote_url" ]] && return 1 23 + local repo 24 + repo="$(echo "$remote_url" | sed -E 's#^(git@github.com:|ssh://git@github.com/|https://github.com/)##; s#\.git$##')" 25 + [[ "$repo" == */* ]] && echo "$repo" && return 0 26 + return 1 27 + } 28 + 29 + default_signing_identity() { 30 + security find-identity -v -p codesigning 2>/dev/null \ 31 + | awk -F'"' '/Developer ID Application/ {print $2; exit}' 32 + } 33 + 34 + team_id_from_identity() { 35 + local identity="$1" 36 + if [[ "$identity" =~ \(([A-Z0-9]{10})\)$ ]]; then 37 + echo "${BASH_REMATCH[1]}" 38 + fi 39 + } 40 + 41 + signing_identity_sha() { 42 + security find-identity -v -p codesigning 2>/dev/null \ 43 + | grep "$1" | head -1 | awk '{print $2}' 44 + } 45 + 46 + log() { echo "[release] $*"; } 47 + die() { echo "error: $*" >&2; exit 1; } 48 + 49 + # ── Preflight ──────────────────────────────────────────────────────────────── 50 + 51 + log "preflight checks..." 52 + 53 + [[ "$(uname -s)" == "Darwin" ]] || die "macOS only" 54 + for cmd in gh jq codesign xcrun create-dmg; do 55 + command -v "$cmd" >/dev/null 2>&1 || die "$cmd is required but not found" 56 + done 57 + [[ -x "$PROJECT_DIR/bins/generate_appcast" ]] || die "bins/generate_appcast not found" 58 + 59 + if [[ -n "$(git status --porcelain)" ]]; then 60 + die "working tree is not clean — commit or stash changes first" 61 + fi 62 + 63 + REPO="${GH_REPO:-$(origin_repo_from_remote || true)}" 64 + [[ -z "$REPO" ]] && REPO="$(gh repo view --json nameWithOwner -q .nameWithOwner)" 65 + [[ -z "$REPO" ]] && die "cannot determine GitHub repository" 66 + 67 + KEYCHAIN_PROFILE="${APPLE_NOTARY_KEYCHAIN_PROFILE:-supacode-notary}" 68 + SIGNING_IDENTITY="${APPLE_SIGNING_IDENTITY:-$(default_signing_identity || true)}" 69 + [[ -z "$SIGNING_IDENTITY" ]] && die "no Developer ID Application identity found — set APPLE_SIGNING_IDENTITY" 70 + 71 + TEAM_ID="${APPLE_TEAM_ID:-$(team_id_from_identity "$SIGNING_IDENTITY" || true)}" 72 + [[ -z "$TEAM_ID" ]] && die "cannot determine Apple Team ID — set APPLE_TEAM_ID" 73 + 74 + IDENTITY_SHA="$(signing_identity_sha "$SIGNING_IDENTITY")" 75 + [[ -z "$IDENTITY_SHA" ]] && die "cannot find signing identity SHA for: $SIGNING_IDENTITY" 76 + 77 + SPARKLE_KEY_FILE="${SPARKLE_PRIVATE_KEY_FILE:-$HOME/.prowl-sparkle-private-key}" 78 + [[ -f "$SPARKLE_KEY_FILE" ]] || die "Sparkle private key not found: $SPARKLE_KEY_FILE" 79 + 80 + PROWL_SITE="${PROWL_SITE_DIR:-$PROJECT_DIR/../Prowl-Site}" 81 + 82 + log "repository: $REPO" 83 + log "signing identity: $SIGNING_IDENTITY" 84 + log "team ID: $TEAM_ID" 85 + 86 + # ── Version ────────────────────────────────────────────────────────────────── 87 + 88 + if [[ -n "${1:-}" ]]; then 89 + VERSION="$1" 90 + if ! echo "$VERSION" | grep -qE '^[0-9]{4}\.[0-9]{1,2}\.[0-9]{1,2}(\.[0-9]+)?$'; then 91 + die "VERSION must be in YYYY.M.DD or YYYY.M.DD.N format" 92 + fi 93 + else 94 + VERSION="$(date +%Y.%-m.%-d)" 95 + suffix=1 96 + while git rev-parse "v$VERSION" >/dev/null 2>&1; do 97 + suffix=$((suffix + 1)) 98 + VERSION="$(date +%Y.%-m.%-d).$suffix" 99 + done 100 + fi 101 + 102 + TAG="v$VERSION" 103 + git rev-parse "$TAG" >/dev/null 2>&1 && die "tag $TAG already exists" 104 + 105 + BUILD="$(date +%Y%m%d)" 106 + CURRENT_BUILD="$(/usr/bin/awk -F' = ' '/CURRENT_PROJECT_VERSION = [0-9]+;/{gsub(/;/,""); print $2; exit}' "$PROJECT_DIR/supacode.xcodeproj/project.pbxproj")" 107 + if [[ "$CURRENT_BUILD" -ge "$BUILD" ]] 2>/dev/null; then 108 + BUILD="$((CURRENT_BUILD + 1))" 109 + fi 110 + 111 + log "version: $VERSION (build $BUILD), tag: $TAG" 112 + 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 + if [[ -n "$PREV_TAG" ]]; then 125 + gh api "repos/$REPO/releases/generate-notes" \ 126 + -f tag_name="$TAG" -f previous_tag_name="$PREV_TAG" \ 127 + --jq '.body' > "$NOTES_FILE" 2>/dev/null || \ 128 + git log --pretty=format:'- %s' "$PREV_TAG..$TAG" > "$NOTES_FILE" 129 + else 130 + git log --pretty=format:'- %s' -20 > "$NOTES_FILE" 131 + fi 132 + log "release notes written to $NOTES_FILE" 133 + 134 + # ── Archive ────────────────────────────────────────────────────────────────── 135 + 136 + log "archiving Release build..." 137 + make archive APPLE_TEAM_ID="$TEAM_ID" DEVELOPER_ID_IDENTITY_SHA="$IDENTITY_SHA" 138 + 139 + # ── Export ─────────────────────────────────────────────────────────────────── 140 + 141 + log "generating ExportOptions.plist..." 142 + cat > build/ExportOptions.plist <<PLIST 143 + <?xml version="1.0" encoding="UTF-8"?> 144 + <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 145 + <plist version="1.0"> 146 + <dict> 147 + <key>method</key> 148 + <string>developer-id</string> 149 + <key>signingStyle</key> 150 + <string>manual</string> 151 + <key>signingCertificate</key> 152 + <string>$SIGNING_IDENTITY</string> 153 + <key>teamID</key> 154 + <string>$TEAM_ID</string> 155 + </dict> 156 + </plist> 157 + PLIST 158 + 159 + log "exporting archive..." 160 + make export-archive 161 + 162 + # ── Locate exported app ───────────────────────────────────────────────────── 163 + 164 + APP_PATH="$(find build/export -name "*.app" -maxdepth 3 -print -quit)" 165 + [[ -d "$APP_PATH" ]] || die "exported app not found in build/export" 166 + APP_NAME="$(basename "$APP_PATH")" 167 + log "exported app: $APP_PATH" 168 + 169 + # ── Re-sign Sparkle & Sentry frameworks ───────────────────────────────────── 170 + 171 + log "re-signing embedded frameworks..." 172 + SPARKLE="$APP_PATH/Contents/Frameworks/Sparkle.framework/Versions/B" 173 + 174 + if [[ -d "$SPARKLE" ]]; then 175 + codesign -f -s "$IDENTITY_SHA" -o runtime --timestamp -v "$SPARKLE/XPCServices/Installer.xpc" 176 + codesign -f -s "$IDENTITY_SHA" -o runtime --timestamp --preserve-metadata=entitlements -v "$SPARKLE/XPCServices/Downloader.xpc" 177 + codesign -f -s "$IDENTITY_SHA" -o runtime --timestamp -v "$SPARKLE/Updater.app" 178 + codesign -f -s "$IDENTITY_SHA" -o runtime --timestamp -v "$SPARKLE/Autoupdate" 179 + codesign -f -s "$IDENTITY_SHA" -o runtime --timestamp -v "$SPARKLE/Sparkle" 180 + codesign -f -s "$IDENTITY_SHA" -o runtime --timestamp -v "$APP_PATH/Contents/Frameworks/Sparkle.framework" 181 + fi 182 + 183 + SENTRY_FRAMEWORK="$APP_PATH/Contents/Frameworks/Sentry.framework" 184 + if [[ -d "$SENTRY_FRAMEWORK" ]]; then 185 + codesign -f -s "$IDENTITY_SHA" -o runtime --timestamp -v "$SENTRY_FRAMEWORK/Versions/A/Sentry" 186 + codesign -f -s "$IDENTITY_SHA" -o runtime --timestamp -v "$SENTRY_FRAMEWORK" 187 + fi 188 + 189 + # ── Re-sign app ───────────────────────────────────────────────────────────── 190 + 191 + log "re-signing app..." 192 + codesign -f -s "$IDENTITY_SHA" -o runtime --timestamp --preserve-metadata=entitlements,requirements,flags -v "$APP_PATH" 193 + codesign -vvv --deep --strict "$APP_PATH" 194 + log "signature verified" 195 + 196 + # ── DMG ────────────────────────────────────────────────────────────────────── 197 + 198 + log "building DMG..." 199 + DMG_PATH="build/Prowl.dmg" 200 + mise exec -- create-dmg "$APP_PATH" build/ \ 201 + --overwrite \ 202 + --dmg-title="Prowl" \ 203 + --identity="$IDENTITY_SHA" 204 + 205 + DMG_OUTPUT="$(find build -name "*.dmg" -maxdepth 1 -newer build/ExportOptions.plist | head -1)" 206 + if [[ "$DMG_OUTPUT" != "$DMG_PATH" ]] && [[ -n "$DMG_OUTPUT" ]]; then 207 + mv "$DMG_OUTPUT" "$DMG_PATH" 208 + fi 209 + [[ -f "$DMG_PATH" ]] || die "DMG not found at $DMG_PATH" 210 + 211 + # ── Notarize ───────────────────────────────────────────────────────────────── 212 + 213 + log "notarizing DMG..." 214 + for attempt in 1 2 3; do 215 + if xcrun notarytool submit "$DMG_PATH" --keychain-profile "$KEYCHAIN_PROFILE" --wait; then 216 + break 217 + fi 218 + if [[ $attempt -lt 3 ]]; then 219 + log "notarization attempt $attempt failed, retrying in 30s..." 220 + sleep 30 221 + else 222 + die "notarization failed after 3 attempts" 223 + fi 224 + done 225 + 226 + log "stapling notarization ticket..." 227 + xcrun stapler staple "$DMG_PATH" 228 + xcrun stapler staple "$APP_PATH" 229 + 230 + # ── Package zip for Sparkle ────────────────────────────────────────────────── 231 + 232 + ZIP_PATH="build/Prowl.app.zip" 233 + log "packaging $ZIP_PATH for Sparkle..." 234 + ditto -c -k --sequesterRsrc --keepParent "$APP_PATH" "$ZIP_PATH" 235 + 236 + # ── Appcast ────────────────────────────────────────────────────────────────── 237 + 238 + log "generating appcast..." 239 + STAGING="$(mktemp -d)" 240 + ARCHIVE_BASE="$(basename "$ZIP_PATH" .zip)" 241 + cp "$ZIP_PATH" "$STAGING/" 242 + cp "$NOTES_FILE" "$STAGING/$ARCHIVE_BASE.md" 243 + 244 + # Fetch existing appcast for history 245 + curl -fsSL "https://prowl.onev.cat/appcast.xml" -o "$STAGING/appcast.xml" 2>/dev/null || true 246 + 247 + "$PROJECT_DIR/bins/generate_appcast" \ 248 + --ed-key-file "$SPARKLE_KEY_FILE" \ 249 + --download-url-prefix "https://github.com/$REPO/releases/download/$TAG/" \ 250 + --embed-release-notes \ 251 + --maximum-versions 10 \ 252 + "$STAGING" 253 + 254 + cp "$STAGING/appcast.xml" build/appcast.xml 255 + find "$STAGING" -name "*.delta" -exec cp {} build/ \; 2>/dev/null || true 256 + rm -rf "$STAGING" 257 + log "appcast generated at build/appcast.xml" 258 + 259 + # ── Tag + push ─────────────────────────────────────────────────────────────── 260 + 261 + log "pushing tags..." 262 + git push --follow-tags 263 + 264 + # ── GitHub Release ─────────────────────────────────────────────────────────── 265 + 266 + log "creating GitHub Release..." 267 + UPLOAD_FILES=("$DMG_PATH" "$ZIP_PATH" "build/appcast.xml") 268 + DELTA_FILES=( $(find build -name "*.delta" -type f 2>/dev/null || true) ) 269 + UPLOAD_FILES+=("${DELTA_FILES[@]}") 270 + 271 + gh release create "$TAG" "${UPLOAD_FILES[@]}" \ 272 + --repo "$REPO" \ 273 + --title "Prowl $VERSION" \ 274 + --notes-file "$NOTES_FILE" 275 + 276 + RELEASE_URL="https://github.com/$REPO/releases/tag/$TAG" 277 + log "release created: $RELEASE_URL" 278 + 279 + # ── Update Prowl-Site ──────────────────────────────────────────────────────── 280 + 281 + if [[ -d "$PROWL_SITE" ]]; then 282 + log "updating Prowl-Site appcast..." 283 + mkdir -p "$PROWL_SITE/public" 284 + cp build/appcast.xml "$PROWL_SITE/public/appcast.xml" 285 + pushd "$PROWL_SITE" >/dev/null 286 + if [[ -n "$(git status --porcelain)" ]]; then 287 + git add public/appcast.xml 288 + git commit -m "Update appcast for Prowl $VERSION" 289 + git push 290 + log "Prowl-Site pushed (Netlify deploy will follow)" 291 + else 292 + log "Prowl-Site appcast unchanged" 293 + fi 294 + popd >/dev/null 295 + else 296 + log "Prowl-Site not found at $PROWL_SITE — skipping appcast deploy" 297 + log "copy build/appcast.xml manually or set PROWL_SITE_DIR" 298 + fi 299 + 300 + echo 301 + log "done! Release: $RELEASE_URL"
+4
supacode/Info.plist
··· 28 28 <string>A program running within Prowl would like to use speech recognition.</string> 29 29 <key>NSSystemAdministrationUsageDescription</key> 30 30 <string>A program running within Prowl requires elevated privileges.</string> 31 + <key>SUFeedURL</key> 32 + <string>https://prowl.onev.cat/appcast.xml</string> 33 + <key>SUPublicEDKey</key> 34 + <string>S5MT6SPxcuaERBJU7MZ9tSZ+RsEj2PqB7i9DowY3OQ4=</string> 31 35 <key>SUEnableAutomaticChecks</key> 32 36 <false/> 33 37 <key>SUAutomaticallyUpdate</key>