native macOS codings agent orchestrator
1# Sensible defaults
2.ONESHELL:
3SHELL := bash
4.SHELLFLAGS := -e -u -c -o pipefail
5.DELETE_ON_ERROR:
6MAKEFLAGS += --warn-undefined-variables
7MAKEFLAGS += --no-builtin-rules
8
9# Derived values (DO NOT TOUCH).
10CURRENT_MAKEFILE_PATH := $(abspath $(lastword $(MAKEFILE_LIST)))
11CURRENT_MAKEFILE_DIR := $(patsubst %/,%,$(dir $(CURRENT_MAKEFILE_PATH)))
12GHOSTTY_XCFRAMEWORK_PATH := $(CURRENT_MAKEFILE_DIR)/Frameworks/GhosttyKit.xcframework
13GHOSTTY_RESOURCE_PATH := $(CURRENT_MAKEFILE_DIR)/Resources/ghostty
14GHOSTTY_TERMINFO_PATH := $(CURRENT_MAKEFILE_DIR)/Resources/terminfo
15GHOSTTY_BUILD_OUTPUTS := $(GHOSTTY_XCFRAMEWORK_PATH) $(GHOSTTY_RESOURCE_PATH) $(GHOSTTY_TERMINFO_PATH)
16GHOSTTY_BUILD_STAMP := $(CURRENT_MAKEFILE_DIR)/.ghostty_build_stamp
17GHOSTTY_HASH_FILE := $(CURRENT_MAKEFILE_DIR)/.ghostty_hash
18SPM_CACHE_DIR := /tmp/supacode-spm-cache/SourcePackages
19VERSION ?=
20BUILD ?=
21XCODEBUILD_FLAGS ?=
22FORMAT_BASE_REF ?= origin/main
23
24# Release-only analytics/crash credentials. Included from Config/Secrets.env if present,
25# or overridable from the environment (e.g. CI). Debug builds skip SDK init regardless.
26-include Config/Secrets.env
27PROWL_SENTRY_DSN ?=
28PROWL_POSTHOG_API_KEY ?=
29PROWL_POSTHOG_HOST ?=
30
31.DEFAULT_GOAL := help
32.PHONY: build-ghostty-xcframework ensure-ghostty sync-ghostty _check-ghostty-hash _record-ghostty-hash build-app build-cli build-cli-release embed-cli-debug embed-cli run-app install-dev-build install-release archive export-archive format format-changed format-lint lint check test test-cli-smoke test-cli-integration bump-version bump-and-release log-stream
33
34help: # Display this help.
35 @-+echo "Run make with one of the following targets:"
36 @-+echo
37 @-+grep -Eh "^[a-z-]+:.*#" $(CURRENT_MAKEFILE_PATH) | sed -E 's/^(.*:)(.*#+)(.*)/ \1 @@@ \3 /' | column -t -s "@@@"
38
39build-ghostty-xcframework: $(GHOSTTY_BUILD_STAMP) # Build ghostty framework
40 @$(MAKE) _record-ghostty-hash
41
42# Internal: actually rebuild ghostty.
43$(GHOSTTY_BUILD_STAMP):
44 @cd $(CURRENT_MAKEFILE_DIR)/ThirdParty/ghostty && mise exec -- zig build -Doptimize=ReleaseFast -Demit-xcframework=true -Dsentry=false
45 rsync -a ThirdParty/ghostty/macos/GhosttyKit.xcframework Frameworks
46 @src="$(CURRENT_MAKEFILE_DIR)/ThirdParty/ghostty/zig-out/share/ghostty"; \
47 dst="$(GHOSTTY_RESOURCE_PATH)"; \
48 terminfo_src="$(CURRENT_MAKEFILE_DIR)/ThirdParty/ghostty/zig-out/share/terminfo"; \
49 terminfo_dst="$(GHOSTTY_TERMINFO_PATH)"; \
50 mkdir -p "$$dst"; \
51 rsync -a --delete "$$src/" "$$dst/"; \
52 mkdir -p "$$terminfo_dst"; \
53 rsync -a --delete "$$terminfo_src/" "$$terminfo_dst/"
54 touch "$(GHOSTTY_BUILD_STAMP)"
55
56# Public entry point: only rebuilds ghostty if submodule SHA changed (or outputs are missing).
57ensure-ghostty: _check-ghostty-hash # Ensure GhosttyKit is up-to-date (fast path when unchanged)
58
59# Internal: compare current submodule SHA against the recorded one.
60_check-ghostty-hash:
61 @current_sha="$$(git -C $(CURRENT_MAKEFILE_DIR)/ThirdParty/ghostty rev-parse HEAD)"; \
62 last_sha=""; \
63 if [ -f "$(GHOSTTY_HASH_FILE)" ]; then \
64 last_sha="$$(cat "$(GHOSTTY_HASH_FILE)")"; \
65 fi; \
66 artifacts_ok=1; \
67 for path in "$(GHOSTTY_XCFRAMEWORK_PATH)" "$(GHOSTTY_RESOURCE_PATH)" "$(GHOSTTY_TERMINFO_PATH)"; do \
68 if [ ! -e "$$path" ]; then \
69 artifacts_ok=0; \
70 fi; \
71 done; \
72 if [ "$$current_sha" != "$$last_sha" ] || [ "$$artifacts_ok" -ne 1 ]; then \
73 echo "Syncing GhosttyKit for submodule $$current_sha"; \
74 $(MAKE) -B build-ghostty-xcframework; \
75 if [ "$$current_sha" != "$$last_sha" ]; then \
76 rm -rf ~/Library/Developer/Xcode/DerivedData/supacode-*; \
77 echo "Cleared Xcode DerivedData for ghostty header/module changes"; \
78 fi; \
79 else \
80 echo "GhosttyKit up-to-date (SHA unchanged)"; \
81 fi
82
83# Internal: record the current submodule SHA after a successful build.
84_record-ghostty-hash:
85 @git -C $(CURRENT_MAKEFILE_DIR)/ThirdParty/ghostty rev-parse HEAD > "$(GHOSTTY_HASH_FILE)"
86
87# Force a clean rebuild of GhosttyKit (ignores cached SHA, useful after submodule updates).
88sync-ghostty: # Force sync GhosttyKit to current submodule HEAD (always rebuilds)
89 @echo "Forcing GhosttyKit rebuild..."
90 $(MAKE) -B build-ghostty-xcframework
91 rm -rf ~/Library/Developer/Xcode/DerivedData/supacode-*
92 @echo "Done. Xcode module cache cleared for fresh compilation."
93
94build-app: ensure-ghostty embed-cli-debug # Build the macOS app (Debug)
95 bash -o pipefail -c 'xcodebuild -project supacode.xcodeproj -scheme supacode -configuration Debug build -skipMacroValidation -clonedSourcePackagesDirPath $(SPM_CACHE_DIR) 2>&1 | mise exec -- xcsift -qw --format toon'
96
97sync-cli-version: # Sync app MARKETING_VERSION into ProwlCLIShared/ProwlVersion.swift
98 @version="$$(/usr/bin/awk -F' = ' '/MARKETING_VERSION = [0-9.]*;/{gsub(/;/,"",$$2);print $$2; exit}' \
99 "$(CURRENT_MAKEFILE_DIR)/supacode.xcodeproj/project.pbxproj")"; \
100 dst="$(CURRENT_MAKEFILE_DIR)/supacode/CLIService/Shared/ProwlVersion.swift"; \
101 printf '// Auto-generated by Makefile (sync-cli-version). Do not edit.\n\npublic enum ProwlVersion {\n public static let current = "%s"\n}\n' "$$version" > "$$dst"
102
103build-cli: sync-cli-version # Build Swift CLI binary (SPM)
104 swift build --product prowl
105
106build-cli-release: sync-cli-version # Build universal CLI binary in release mode
107 swift build -c release --arch arm64 --arch x86_64 --product prowl
108
109embed-cli-debug: build-cli # Build debug CLI and copy into Resources for dev builds
110 @set -euo pipefail; \
111 bin="$$(swift build --show-bin-path)/prowl"; \
112 dst="$(CURRENT_MAKEFILE_DIR)/Resources/prowl-cli"; \
113 mkdir -p "$$dst"; \
114 cp "$$bin" "$$dst/prowl"; \
115 chmod +x "$$dst/prowl"; \
116 echo "embedded CLI binary at $$dst/prowl"
117
118embed-cli: build-cli-release # Build release CLI and copy into Resources for distribution
119 @set -euo pipefail; \
120 bin="$$(swift build -c release --arch arm64 --arch x86_64 --show-bin-path)/prowl"; \
121 dst="$(CURRENT_MAKEFILE_DIR)/Resources/prowl-cli"; \
122 mkdir -p "$$dst"; \
123 cp "$$bin" "$$dst/prowl"; \
124 chmod +x "$$dst/prowl"; \
125 echo "embedded CLI binary at $$dst/prowl"
126
127run-app: build-app # Build then launch (Debug) with log streaming
128 @set -euo pipefail; \
129 settings="$$(xcodebuild -project supacode.xcodeproj -scheme supacode -configuration Debug -showBuildSettings -json 2>/dev/null)"; \
130 build_dir="$$(echo "$$settings" | jq -er '.[0].buildSettings.BUILT_PRODUCTS_DIR')"; \
131 product="$$(echo "$$settings" | jq -er '.[0].buildSettings.FULL_PRODUCT_NAME')"; \
132 exec_name="$$(echo "$$settings" | jq -r '.[0].buildSettings.EXECUTABLE_NAME')"; \
133 if [ -z "$$build_dir" ] || [ -z "$$product" ] || [ "$$build_dir" = "null" ] || [ "$$product" = "null" ] || [ -z "$$exec_name" ] || [ "$$exec_name" = "null" ]; then \
134 echo "error: failed to resolve app path from build settings"; \
135 exit 1; \
136 fi; \
137 "$$build_dir/$$product/Contents/MacOS/$$exec_name"
138
139install-dev-build: build-app # install dev build to /Applications
140 @set -euo pipefail; \
141 settings="$$(xcodebuild -project supacode.xcodeproj -scheme supacode -configuration Debug -showBuildSettings -json 2>/dev/null)"; \
142 build_dir="$$(echo "$$settings" | jq -er '.[0].buildSettings.BUILT_PRODUCTS_DIR')"; \
143 product="$$(echo "$$settings" | jq -er '.[0].buildSettings.FULL_PRODUCT_NAME')"; \
144 if [ -z "$$build_dir" ] || [ -z "$$product" ] || [ "$$build_dir" = "null" ] || [ "$$product" = "null" ]; then \
145 echo "error: failed to resolve app path from build settings"; \
146 exit 1; \
147 fi; \
148 if [ "$$product" != "$$(basename "$$product")" ]; then \
149 echo "error: invalid product name (contains path separators): $$product"; \
150 exit 1; \
151 fi; \
152 if [[ "$$product" != *.app ]]; then \
153 echo "error: unexpected product name: $$product"; \
154 exit 1; \
155 fi; \
156 src="$$build_dir/$$product"; \
157 dst="/Applications/$$product"; \
158 dst_parent="$$(cd "$$(dirname "$$dst")" && pwd -P)"; \
159 if [ "$$dst_parent" != "/Applications" ]; then \
160 echo "error: refusing to install outside /Applications: $$dst"; \
161 exit 1; \
162 fi; \
163 if [ "$$src" = "/" ] || [ "$$dst" = "/Applications" ] || [ "$$dst" = "/Applications/" ]; then \
164 echo "error: unsafe install path (src=$$src, dst=$$dst)"; \
165 exit 1; \
166 fi; \
167 case "$$dst" in \
168 /Applications/*.app) ;; \
169 *) \
170 echo "error: refusing to install outside /Applications/*.app: $$dst"; \
171 exit 1; \
172 ;; \
173 esac; \
174 if [ ! -d "$$src" ]; then \
175 echo "app not found: $$src"; \
176 exit 1; \
177 fi; \
178 if [ ! -d "$$src/Contents" ]; then \
179 echo "error: source is not an app bundle: $$src"; \
180 exit 1; \
181 fi; \
182 echo "copying $$src -> $$dst"; \
183 if [ -e "$$dst" ]; then \
184 if ! command -v trash >/dev/null 2>&1; then \
185 echo "error: trash command not found; refusing to remove $$dst"; \
186 exit 1; \
187 fi; \
188 echo "moving existing app to Trash: $$dst"; \
189 trash "$$dst"; \
190 fi; \
191 ditto "$$src" "$$dst"; \
192 echo "installed $$dst"
193
194install-release: build-ghostty-xcframework # Build Release, sign locally, install to /Applications
195 @set -euo pipefail; \
196 SIGNING_IDENTITY="$$(security find-identity -v -p codesigning 2>/dev/null | awk -F'"' '/Developer ID Application/ {print $$2; exit}')"; \
197 if [ -z "$$SIGNING_IDENTITY" ]; then \
198 echo "error: no Developer ID Application identity found"; \
199 exit 1; \
200 fi; \
201 IDENTITY_SHA="$$(security find-identity -v -p codesigning 2>/dev/null | grep "$$SIGNING_IDENTITY" | head -1 | awk '{print $$2}')"; \
202 TEAM_ID="$$(echo "$$SIGNING_IDENTITY" | grep -oE '\([A-Z0-9]{10}\)$$' | tr -d '()')"; \
203 echo "identity: $$SIGNING_IDENTITY"; \
204 echo "team: $$TEAM_ID"; \
205 APPLE_TEAM_ID="$$TEAM_ID" DEVELOPER_ID_IDENTITY_SHA="$$IDENTITY_SHA" $(MAKE) archive; \
206 mkdir -p build; \
207 printf '%s\n' \
208 '<?xml version="1.0" encoding="UTF-8"?>' \
209 '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">' \
210 '<plist version="1.0">' \
211 '<dict>' \
212 ' <key>method</key>' \
213 ' <string>developer-id</string>' \
214 ' <key>signingStyle</key>' \
215 ' <string>manual</string>' \
216 ' <key>signingCertificate</key>' \
217 " <string>$$SIGNING_IDENTITY</string>" \
218 ' <key>teamID</key>' \
219 " <string>$$TEAM_ID</string>" \
220 '</dict>' \
221 '</plist>' > build/ExportOptions.plist; \
222 $(MAKE) export-archive; \
223 APP_PATH="$$(find build/export -name '*.app' -maxdepth 3 -print -quit)"; \
224 if [ ! -d "$$APP_PATH" ]; then \
225 echo "error: exported app not found"; \
226 exit 1; \
227 fi; \
228 SPARKLE="$$APP_PATH/Contents/Frameworks/Sparkle.framework/Versions/B"; \
229 if [ -d "$$SPARKLE" ]; then \
230 codesign -f -s "$$IDENTITY_SHA" -o runtime --timestamp -v "$$SPARKLE/XPCServices/Installer.xpc"; \
231 codesign -f -s "$$IDENTITY_SHA" -o runtime --timestamp --preserve-metadata=entitlements -v "$$SPARKLE/XPCServices/Downloader.xpc"; \
232 codesign -f -s "$$IDENTITY_SHA" -o runtime --timestamp -v "$$SPARKLE/Updater.app"; \
233 codesign -f -s "$$IDENTITY_SHA" -o runtime --timestamp -v "$$SPARKLE/Autoupdate"; \
234 codesign -f -s "$$IDENTITY_SHA" -o runtime --timestamp -v "$$SPARKLE/Sparkle"; \
235 codesign -f -s "$$IDENTITY_SHA" -o runtime --timestamp -v "$$APP_PATH/Contents/Frameworks/Sparkle.framework"; \
236 fi; \
237 SENTRY="$$APP_PATH/Contents/Frameworks/Sentry.framework"; \
238 if [ -d "$$SENTRY" ]; then \
239 codesign -f -s "$$IDENTITY_SHA" -o runtime --timestamp -v "$$SENTRY/Versions/A/Sentry"; \
240 codesign -f -s "$$IDENTITY_SHA" -o runtime --timestamp -v "$$SENTRY"; \
241 fi; \
242 codesign -f -s "$$IDENTITY_SHA" -o runtime --timestamp --preserve-metadata=entitlements,requirements,flags -v "$$APP_PATH"; \
243 codesign -vvv --deep --strict "$$APP_PATH"; \
244 PRODUCT="$$(basename "$$APP_PATH")"; \
245 if [ -z "$$PRODUCT" ] || [ "$$PRODUCT" = "." ] || [[ "$$PRODUCT" != *.app ]]; then \
246 echo "error: unexpected release product name: $$PRODUCT"; \
247 exit 1; \
248 fi; \
249 DST="/Applications/$$PRODUCT"; \
250 if [ "$$DST" = "/Applications" ] || [ "$$DST" = "/Applications/" ]; then \
251 echo "error: unsafe install destination: $$DST"; \
252 exit 1; \
253 fi; \
254 case "$$DST" in \
255 /Applications/*.app) ;; \
256 *) \
257 echo "error: refusing to install outside /Applications/*.app: $$DST"; \
258 exit 1; \
259 ;; \
260 esac; \
261 echo "copying $$APP_PATH -> $$DST"; \
262 if [ -e "$$DST" ]; then \
263 if ! command -v trash >/dev/null 2>&1; then \
264 echo "error: trash command not found; refusing to remove $$DST"; \
265 exit 1; \
266 fi; \
267 echo "moving existing app to Trash: $$DST"; \
268 trash "$$DST"; \
269 fi; \
270 ditto "$$APP_PATH" "$$DST"; \
271 echo "installed $$DST (Release build, locally signed)"
272
273archive: build-ghostty-xcframework embed-cli # Archive Release build for distribution
274 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" PROWL_SENTRY_DSN="$(PROWL_SENTRY_DSN)" PROWL_POSTHOG_API_KEY="$(PROWL_POSTHOG_API_KEY)" PROWL_POSTHOG_HOST="$(PROWL_POSTHOG_HOST)" -skipMacroValidation -clonedSourcePackagesDirPath $(SPM_CACHE_DIR) $(XCODEBUILD_FLAGS) 2>&1 | mise exec -- xcsift -qw --format toon'
275
276export-archive: # Export xarchive
277 bash -o pipefail -c 'xcodebuild -exportArchive -archivePath build/supacode.xcarchive -exportPath build/export -exportOptionsPlist build/ExportOptions.plist 2>&1 | mise exec -- xcsift -qw --format toon'
278
279test: ensure-ghostty
280 @set -euo pipefail; \
281 result_bundle="$(CURRENT_MAKEFILE_DIR)/build/test-results/supacode-tests.xcresult"; \
282 mkdir -p "$$(dirname "$$result_bundle")"; \
283 rm -rf "$$result_bundle"; \
284 set +e; \
285 xcodebuild test -project supacode.xcodeproj -scheme supacode -destination "platform=macOS" -resultBundlePath "$$result_bundle" CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY="" -skipMacroValidation -clonedSourcePackagesDirPath $(SPM_CACHE_DIR) 2>&1 | mise exec -- xcsift -w --format toon; \
286 xcodebuild_status=$${PIPESTATUS[0]}; \
287 set -e; \
288 if [ "$$xcodebuild_status" -ne 0 ]; then \
289 bash "$(CURRENT_MAKEFILE_DIR)/scripts/print-xcresult-failures.sh" "$$result_bundle" || true; \
290 fi; \
291 exit "$$xcodebuild_status"
292
293test-cli-smoke: build-cli # Smoke test CLI executable
294 @set -euo pipefail; \
295 bin="$$(swift build --show-bin-path)/prowl"; \
296 help_output="$$("$$bin" --help)"; \
297 version_output="$$("$$bin" --version)"; \
298 echo "$$help_output" | grep -q "USAGE:"; \
299 echo "$$version_output" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.]+)?$$'; \
300 socket="/tmp/prowl-cli-smoke-$$RANDOM.sock"; \
301 PROWL_CLI_SOCKET="$$socket" "$$bin" list --json >/tmp/prowl-cli-smoke.json || true; \
302 jq -e '.error.code == "APP_NOT_RUNNING"' /tmp/prowl-cli-smoke.json >/dev/null
303
304test-cli-integration: # Run CLI integration tests via SwiftPM
305 swift test --filter ProwlCLIIntegrationTests
306
307format: # Format all Swift code with swift-format (full-tree cleanup)
308 swift-format -p --in-place --recursive --configuration ./.swift-format.json supacode supacodeTests
309
310format-changed: # Format Swift files changed from FORMAT_BASE_REF (default: origin/main)
311 @base="$$(git merge-base HEAD "$(FORMAT_BASE_REF)" 2>/dev/null || git rev-parse HEAD)"; \
312 mapfile -t files < <( \
313 { \
314 git diff --name-only --diff-filter=ACMR "$$base" -- supacode supacodeTests; \
315 git ls-files --others --exclude-standard -- supacode supacodeTests; \
316 } | awk '/\.swift$$/' | sort -u \
317 ); \
318 if [ "$${#files[@]}" -eq 0 ]; then \
319 echo "No changed Swift files to format."; \
320 else \
321 printf 'Formatting %s changed Swift file(s) from %s\n' "$${#files[@]}" "$$base"; \
322 swift-format -p --in-place --configuration ./.swift-format.json "$${files[@]}"; \
323 fi
324
325format-lint: # Check Swift formatting without rewriting files
326 swift-format lint --strict --recursive --configuration ./.swift-format.json supacode supacodeTests
327
328lint: # Lint code with swiftlint
329 mise exec -- swiftlint lint --quiet --config .swiftlint.yml
330
331check: format-changed format-lint lint # Format changed Swift files, then run swift-format lint and SwiftLint
332
333log-stream: # Stream logs from the app via log stream
334 log stream --predicate 'subsystem == "com.onevcat.prowl"' --style compact --color always
335
336bump-version: # Bump app version (usage: make bump-version [VERSION=YYYY.M.DD] [BUILD=YYYYMMDD])
337 @if [ -z "$(VERSION)" ]; then \
338 version="$$(date +%Y.%-m.%-d)"; \
339 suffix=1; \
340 while git rev-parse "v$$version" >/dev/null 2>&1; do \
341 suffix=$$((suffix + 1)); \
342 version="$$(date +%Y.%-m.%-d).$$suffix"; \
343 done; \
344 else \
345 if ! echo "$(VERSION)" | grep -qE '^[0-9]{4}\.[0-9]{1,2}\.[0-9]{1,2}(\.[0-9]+)?$$'; then \
346 echo "error: VERSION must be in YYYY.M.DD or YYYY.M.DD.N format"; \
347 exit 1; \
348 fi; \
349 version="$(VERSION)"; \
350 fi; \
351 if [ -z "$(BUILD)" ]; then \
352 base_build="$$(date +%Y%m%d)"; \
353 current_build="$$(/usr/bin/awk -F' = ' '/CURRENT_PROJECT_VERSION = [0-9]+;/{gsub(/;/,"",$$2);print $$2; exit}' "$(CURRENT_MAKEFILE_DIR)/supacode.xcodeproj/project.pbxproj")"; \
354 if [ "$$current_build" -ge "$$base_build" ] 2>/dev/null; then \
355 build="$$((current_build + 1))"; \
356 else \
357 build="$$base_build"; \
358 fi; \
359 else \
360 if ! echo "$(BUILD)" | grep -qE '^[0-9]+$$'; then \
361 echo "error: BUILD must be an integer"; \
362 exit 1; \
363 fi; \
364 build="$(BUILD)"; \
365 fi; \
366 sed -i '' "s/MARKETING_VERSION = [0-9.]*;/MARKETING_VERSION = $$version;/g" \
367 "$(CURRENT_MAKEFILE_DIR)/supacode.xcodeproj/project.pbxproj"; \
368 sed -i '' "s/CURRENT_PROJECT_VERSION = [0-9]*;/CURRENT_PROJECT_VERSION = $$build;/g" \
369 "$(CURRENT_MAKEFILE_DIR)/supacode.xcodeproj/project.pbxproj"; \
370 printf '// Auto-generated by Makefile (sync-cli-version). Do not edit.\n\npublic enum ProwlVersion {\n public static let current = "%s"\n}\n' "$$version" > \
371 "$(CURRENT_MAKEFILE_DIR)/supacode/CLIService/Shared/ProwlVersion.swift"; \
372 git add "$(CURRENT_MAKEFILE_DIR)/supacode.xcodeproj/project.pbxproj" \
373 "$(CURRENT_MAKEFILE_DIR)/supacode/CLIService/Shared/ProwlVersion.swift"; \
374 git commit -m "bump v$$version"; \
375 git tag -s "v$$version" -m "v$$version"; \
376 echo "version bumped to $$version (build $$build), tagged v$$version"
377
378bump-and-release: bump-version # Bump version and push tags to trigger release
379 git push --follow-tags
380 @tag="$$(git describe --tags --abbrev=0)"; \
381 repo="$$(gh repo view --json nameWithOwner -q .nameWithOwner)"; \
382 prev="$$(gh release view --json tagName -q .tagName 2>/dev/null || echo '')"; \
383 tmp=$$(mktemp); \
384 if [ -n "$$prev" ]; then \
385 gh api "repos/$$repo/releases/generate-notes" -f tag_name="$$tag" -f previous_tag_name="$$prev" --jq '.body' > "$$tmp"; \
386 else \
387 gh api "repos/$$repo/releases/generate-notes" -f tag_name="$$tag" --jq '.body' > "$$tmp"; \
388 fi; \
389 $${EDITOR:-vim} "$$tmp"; \
390 gh release create "$$tag" --notes-file "$$tmp"; \
391 rm -f "$$tmp"