|
| 1 | +#!/usr/bin/env bash |
| 2 | +set -euo pipefail |
| 3 | + |
| 4 | +# ─── Knot Code macOS Release Script ───────────────────────────────── |
| 5 | +# Usage: ./scripts/release-macos.sh <version> |
| 6 | +# Example: ./scripts/release-macos.sh 1.3.0 |
| 7 | +# |
| 8 | +# Prerequisites: |
| 9 | +# - Developer ID cert in keychain (Soul Protocol LLC) |
| 10 | +# - Notary keychain profile "notary" configured |
| 11 | +# - gh CLI authenticated |
| 12 | +# - Entitlements.plist at src-tauri/Entitlements.plist |
| 13 | +# ───────────────────────────────────────────────────────────────────── |
| 14 | + |
| 15 | +VERSION="${1:?Usage: $0 <version> (e.g. 1.3.0)}" |
| 16 | +IDENTITY="Developer ID Application: Soul Protocol LLC (9LR8Z8UQ9X)" |
| 17 | +ENTITLEMENTS="$(cd "$(dirname "$0")/.." && pwd)/src-tauri/Entitlements.plist" |
| 18 | +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" |
| 19 | +DMG_NAME="KnotCode_${VERSION}_aarch64.dmg" |
| 20 | +WORK="/tmp/knotcode-release-${VERSION}" |
| 21 | + |
| 22 | +cd "$REPO_ROOT" |
| 23 | + |
| 24 | +echo "═══════════════════════════════════════════════════════" |
| 25 | +echo " Knot Code Release — v${VERSION} (macOS aarch64)" |
| 26 | +echo "═══════════════════════════════════════════════════════" |
| 27 | + |
| 28 | +# ── 1. Preflight checks ───────────────────────────────────────────── |
| 29 | +echo "" |
| 30 | +echo "▸ [1/10] Preflight checks..." |
| 31 | + |
| 32 | +if [ ! -f "$ENTITLEMENTS" ]; then |
| 33 | + echo " ✗ Entitlements.plist not found at $ENTITLEMENTS" |
| 34 | + echo " Creating default entitlements..." |
| 35 | + cat > "$ENTITLEMENTS" << 'PLIST' |
| 36 | +<?xml version="1.0" encoding="UTF-8"?> |
| 37 | +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
| 38 | +<plist version="1.0"> |
| 39 | +<dict> |
| 40 | + <key>com.apple.security.cs.allow-jit</key> |
| 41 | + <true/> |
| 42 | + <key>com.apple.security.cs.allow-unsigned-executable-memory</key> |
| 43 | + <true/> |
| 44 | + <key>com.apple.security.cs.allow-dyld-environment-variables</key> |
| 45 | + <true/> |
| 46 | + <key>com.apple.security.network.client</key> |
| 47 | + <true/> |
| 48 | + <key>com.apple.security.network.server</key> |
| 49 | + <true/> |
| 50 | + <key>com.apple.security.files.user-selected.read-write</key> |
| 51 | + <true/> |
| 52 | +</dict> |
| 53 | +</plist> |
| 54 | +PLIST |
| 55 | +fi |
| 56 | + |
| 57 | +security find-identity -v -p codesigning | grep -q "9LR8Z8UQ9X" || { |
| 58 | + echo " ✗ Developer ID certificate not found in keychain"; exit 1 |
| 59 | +} |
| 60 | +xcrun notarytool store-credentials --help > /dev/null 2>&1 || { |
| 61 | + echo " ✗ notarytool not available"; exit 1 |
| 62 | +} |
| 63 | +command -v gh > /dev/null || { echo " ✗ gh CLI not found"; exit 1; } |
| 64 | +echo " ✓ All checks passed" |
| 65 | + |
| 66 | +# ── 2. Bump versions ──────────────────────────────────────────────── |
| 67 | +echo "" |
| 68 | +echo "▸ [2/10] Bumping version to ${VERSION}..." |
| 69 | + |
| 70 | +# package.json |
| 71 | +sed -i '' "s/\"version\": \"[^\"]*\"/\"version\": \"${VERSION}\"/" package.json |
| 72 | + |
| 73 | +# tauri.conf.json |
| 74 | +sed -i '' "s/\"version\": \"[^\"]*\"/\"version\": \"${VERSION}\"/" src-tauri/tauri.conf.json |
| 75 | + |
| 76 | +# Cargo.toml (only the first version line) |
| 77 | +sed -i '' "0,/^version = \".*\"/s//version = \"${VERSION}\"/" src-tauri/Cargo.toml |
| 78 | + |
| 79 | +echo " ✓ package.json, tauri.conf.json, Cargo.toml → ${VERSION}" |
| 80 | + |
| 81 | +# ── 3. Build frontend + Tauri ─────────────────────────────────────── |
| 82 | +echo "" |
| 83 | +echo "▸ [3/10] Building Tauri app..." |
| 84 | + |
| 85 | +# Clean Rust cache to force Info.plist regeneration |
| 86 | +cargo clean --manifest-path src-tauri/Cargo.toml --release -p app 2>/dev/null || true |
| 87 | + |
| 88 | +npx tauri build --bundles app 2>&1 | tail -3 |
| 89 | +echo " ✓ Build complete" |
| 90 | + |
| 91 | +# ── 4. Prepare app bundle ─────────────────────────────────────────── |
| 92 | +echo "" |
| 93 | +echo "▸ [4/10] Preparing app bundle..." |
| 94 | + |
| 95 | +rm -rf "$WORK" |
| 96 | +mkdir -p "$WORK" |
| 97 | + |
| 98 | +APP_SRC="src-tauri/target/release/bundle/macos/KnotCode.app" |
| 99 | +APP="$WORK/KnotCode.app" |
| 100 | +ditto "$APP_SRC" "$APP" |
| 101 | + |
| 102 | +# Patch version in Info.plist (Tauri sometimes caches old values) |
| 103 | +/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString ${VERSION}" "$APP/Contents/Info.plist" |
| 104 | +/usr/libexec/PlistBuddy -c "Set :CFBundleVersion ${VERSION}" "$APP/Contents/Info.plist" |
| 105 | + |
| 106 | +ACTUAL_VER=$(defaults read "$APP/Contents/Info.plist" CFBundleShortVersionString) |
| 107 | +echo " ✓ App bundle ready (CFBundleShortVersionString: ${ACTUAL_VER})" |
| 108 | + |
| 109 | +# ── 5. Code sign ──────────────────────────────────────────────────── |
| 110 | +echo "" |
| 111 | +echo "▸ [5/10] Signing..." |
| 112 | + |
| 113 | +codesign --force --options runtime \ |
| 114 | + --sign "$IDENTITY" \ |
| 115 | + --entitlements "$ENTITLEMENTS" \ |
| 116 | + "$APP/Contents/MacOS/app" |
| 117 | + |
| 118 | +codesign --force --options runtime \ |
| 119 | + --sign "$IDENTITY" \ |
| 120 | + --entitlements "$ENTITLEMENTS" \ |
| 121 | + "$APP" |
| 122 | + |
| 123 | +codesign --verify --deep --strict "$APP" 2>&1 | tail -1 |
| 124 | +echo " ✓ Signed with Developer ID" |
| 125 | + |
| 126 | +# ── 6. Notarize app ───────────────────────────────────────────────── |
| 127 | +echo "" |
| 128 | +echo "▸ [6/10] Notarizing app..." |
| 129 | + |
| 130 | +ditto -c -k --keepParent "$APP" "$WORK/KnotCode.zip" |
| 131 | +xcrun notarytool submit "$WORK/KnotCode.zip" --keychain-profile "notary" --wait 2>&1 | tail -3 |
| 132 | +xcrun stapler staple "$APP" 2>&1 | tail -1 |
| 133 | + |
| 134 | +# Verify Gatekeeper |
| 135 | +SPCTL=$(/usr/sbin/spctl --assess --type exec --verbose "$APP" 2>&1) |
| 136 | +echo " $SPCTL" |
| 137 | +echo "$SPCTL" | grep -q "accepted" || { echo " ✗ Gatekeeper rejected!"; exit 1; } |
| 138 | + |
| 139 | +# ── 7. Create DMG ─────────────────────────────────────────────────── |
| 140 | +echo "" |
| 141 | +echo "▸ [7/10] Creating DMG..." |
| 142 | + |
| 143 | +DMG_PATH="$WORK/$DMG_NAME" |
| 144 | +hdiutil create -volname "KnotCode" -srcfolder "$APP" -ov -format UDZO "$DMG_PATH" 2>&1 | tail -1 |
| 145 | + |
| 146 | +# ── 8. Notarize DMG ───────────────────────────────────────────────── |
| 147 | +echo "" |
| 148 | +echo "▸ [8/10] Notarizing DMG..." |
| 149 | + |
| 150 | +xcrun notarytool submit "$DMG_PATH" --keychain-profile "notary" --wait 2>&1 | tail -3 |
| 151 | +xcrun stapler staple "$DMG_PATH" 2>&1 | tail -1 |
| 152 | + |
| 153 | +DMG_SIZE=$(ls -lh "$DMG_PATH" | awk '{print $5}') |
| 154 | +echo " ✓ DMG ready: ${DMG_NAME} (${DMG_SIZE})" |
| 155 | + |
| 156 | +# ── 9. Git tag + push ─────────────────────────────────────────────── |
| 157 | +echo "" |
| 158 | +echo "▸ [9/10] Committing and tagging..." |
| 159 | + |
| 160 | +git add package.json src-tauri/tauri.conf.json src-tauri/Cargo.toml src-tauri/Cargo.lock src-tauri/Entitlements.plist 2>/dev/null || true |
| 161 | +git add package.json src-tauri/tauri.conf.json src-tauri/Cargo.toml |
| 162 | +git commit -m "chore: release v${VERSION}" --allow-empty 2>/dev/null || true |
| 163 | +git tag -f "v${VERSION}" -m "v${VERSION}" |
| 164 | +git push origin main 2>/dev/null || true |
| 165 | +git push origin "v${VERSION}" --force 2>/dev/null || true |
| 166 | + |
| 167 | +echo " ✓ Tagged v${VERSION}" |
| 168 | + |
| 169 | +# ── 10. Upload to GitHub Release ──────────────────────────────────── |
| 170 | +echo "" |
| 171 | +echo "▸ [10/10] Uploading to GitHub Release..." |
| 172 | + |
| 173 | +# Create release if it doesn't exist |
| 174 | +GH_TOKEN="" gh release create "v${VERSION}" \ |
| 175 | + --title "Knot Code v${VERSION}" \ |
| 176 | + --notes "Release v${VERSION} — see CHANGELOG.md for details." \ |
| 177 | + --latest 2>/dev/null || true |
| 178 | + |
| 179 | +# Upload DMG (clobber if exists) |
| 180 | +GH_TOKEN="" gh release upload "v${VERSION}" "$DMG_PATH" --clobber |
| 181 | + |
| 182 | +echo "" |
| 183 | +echo "═══════════════════════════════════════════════════════" |
| 184 | +echo " ✅ Knot Code v${VERSION} released!" |
| 185 | +echo "" |
| 186 | +echo " DMG: ${DMG_PATH}" |
| 187 | +echo " Release: https://github.com/OpenKnots/code-editor/releases/tag/v${VERSION}" |
| 188 | +echo " Size: ${DMG_SIZE}" |
| 189 | +echo "═══════════════════════════════════════════════════════" |
0 commit comments