cmux/scripts/build-sign-upload.sh
Lawrence Chen f451766d12
Add cmux <path> to open directories and Homebrew binary stanza (#705)
* Add `cmux <path>` to open directories and Homebrew binary stanza

CLI: `cmux .` or `cmux /path/to/dir` opens a new workspace at the
given directory. If the app isn't running, it launches first and waits
for the socket. Also adds `--cwd` flag to `new-workspace`.

Server: `workspace.create` now accepts an optional `cwd` parameter,
passed through to `TabManager.addWorkspace(workingDirectory:)`.

Homebrew: adds `binary` stanza to the cask so `cmux` CLI is globally
available after `brew install --cask cmux`. Updated both the cask file,
the CI workflow template, and the manual release script so automated
version bumps preserve the stanza.

* Address review: validate cwd type, fix socket detection, propagate errors

- looksLikePath now also matches paths containing `/` (e.g. `foo/bar`)
- openPath uses socket connection attempt instead of fileExists to detect
  whether the app is running (Unix sockets may not appear on filesystem)
- launchApp/activateApp now throw instead of swallowing errors with try?
- Server validates that cwd param is a string, returns invalid_params error
  if wrong type is passed
2026-02-28 20:25:41 -08:00

208 lines
6.5 KiB
Bash
Executable file

#!/usr/bin/env bash
set -euo pipefail
# Build, sign, notarize, create DMG, generate appcast, and upload to GitHub release.
# Usage: ./scripts/build-sign-upload.sh <tag> [--allow-overwrite]
# Requires: source ~/.secrets/cmuxterm.env && export SPARKLE_PRIVATE_KEY
usage() {
cat <<'EOF'
Usage: ./scripts/build-sign-upload.sh <tag> [--allow-overwrite]
Options:
--allow-overwrite Permit replacing existing release assets for the same tag.
Use only for emergency rerolls.
EOF
}
ALLOW_OVERWRITE="false"
POSITIONAL=()
while [[ $# -gt 0 ]]; do
case "$1" in
--allow-overwrite)
ALLOW_OVERWRITE="true"
shift
;;
-h|--help)
usage
exit 0
;;
-*)
echo "Unknown option: $1" >&2
usage >&2
exit 1
;;
*)
POSITIONAL+=("$1")
shift
;;
esac
done
set -- "${POSITIONAL[@]}"
if [[ $# -ne 1 ]]; then
usage >&2
exit 1
fi
TAG="$1"
SIGN_HASH="A050CC7E193C8221BDBA204E731B046CDCCC1B30"
ENTITLEMENTS="cmux.entitlements"
APP_PATH="build/Build/Products/Release/cmux.app"
# --- Pre-flight ---
source ~/.secrets/cmuxterm.env
export SPARKLE_PRIVATE_KEY
for tool in zig xcodebuild create-dmg xcrun codesign ditto gh; do
command -v "$tool" >/dev/null || { echo "MISSING: $tool" >&2; exit 1; }
done
echo "Pre-flight checks passed"
# --- Build GhosttyKit (if needed) ---
if [ ! -d "GhosttyKit.xcframework" ]; then
echo "Building GhosttyKit..."
cd ghostty && zig build -Demit-xcframework=true -Demit-macos-app=false -Dxcframework-target=native -Doptimize=ReleaseFast && cd ..
rm -rf GhosttyKit.xcframework
cp -R ghostty/macos/GhosttyKit.xcframework GhosttyKit.xcframework
else
echo "GhosttyKit.xcframework exists, skipping build"
fi
# --- Build app (Release, unsigned) ---
echo "Building app..."
rm -rf build/
xcodebuild -scheme cmux -configuration Release -derivedDataPath build CODE_SIGNING_ALLOWED=NO build 2>&1 | tail -5
echo "Build succeeded"
# --- Inject Sparkle keys ---
echo "Injecting Sparkle keys..."
SPARKLE_PUBLIC_KEY_DERIVED=$(swift scripts/derive_sparkle_public_key.swift "$SPARKLE_PRIVATE_KEY")
APP_PLIST="$APP_PATH/Contents/Info.plist"
/usr/libexec/PlistBuddy -c "Delete :SUPublicEDKey" "$APP_PLIST" 2>/dev/null || true
/usr/libexec/PlistBuddy -c "Delete :SUFeedURL" "$APP_PLIST" 2>/dev/null || true
/usr/libexec/PlistBuddy -c "Add :SUPublicEDKey string $SPARKLE_PUBLIC_KEY_DERIVED" "$APP_PLIST"
/usr/libexec/PlistBuddy -c "Add :SUFeedURL string https://github.com/manaflow-ai/cmux/releases/latest/download/appcast.xml" "$APP_PLIST"
echo "Sparkle keys injected"
# --- Codesign ---
echo "Codesigning..."
CLI_PATH="$APP_PATH/Contents/Resources/bin/cmux"
if [ -f "$CLI_PATH" ]; then
/usr/bin/codesign --force --options runtime --timestamp --sign "$SIGN_HASH" --entitlements "$ENTITLEMENTS" "$CLI_PATH"
fi
/usr/bin/codesign --force --options runtime --timestamp --sign "$SIGN_HASH" --entitlements "$ENTITLEMENTS" --deep "$APP_PATH"
/usr/bin/codesign --verify --deep --strict --verbose=2 "$APP_PATH"
echo "Codesign verified"
# --- Notarize app ---
echo "Notarizing app..."
ditto -c -k --sequesterRsrc --keepParent "$APP_PATH" cmux-notary.zip
xcrun notarytool submit cmux-notary.zip \
--apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID" --password "$APPLE_APP_SPECIFIC_PASSWORD" --wait
xcrun stapler staple "$APP_PATH"
xcrun stapler validate "$APP_PATH"
rm -f cmux-notary.zip
echo "App notarized"
# --- Create and notarize DMG ---
echo "Creating DMG..."
rm -f cmux-macos.dmg
create-dmg --codesign "$SIGN_HASH" cmux-macos.dmg "$APP_PATH"
echo "Notarizing DMG..."
xcrun notarytool submit cmux-macos.dmg \
--apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID" --password "$APPLE_APP_SPECIFIC_PASSWORD" --wait
xcrun stapler staple cmux-macos.dmg
xcrun stapler validate cmux-macos.dmg
echo "DMG notarized"
# --- Generate Sparkle appcast ---
echo "Generating appcast..."
./scripts/sparkle_generate_appcast.sh cmux-macos.dmg "$TAG" appcast.xml
# --- Create GitHub release (if needed) and upload ---
if gh release view "$TAG" >/dev/null 2>&1; then
echo "Release $TAG already exists"
EXISTING_ASSETS="$(gh release view "$TAG" --json assets --jq '.assets[].name' || true)"
HAS_CONFLICTING_ASSET="false"
for asset in cmux-macos.dmg appcast.xml; do
if printf '%s\n' "$EXISTING_ASSETS" | grep -Fxq "$asset"; then
HAS_CONFLICTING_ASSET="true"
break
fi
done
if [[ "$HAS_CONFLICTING_ASSET" == "true" && "$ALLOW_OVERWRITE" != "true" ]]; then
echo "ERROR: Refusing to overwrite signed release assets for existing tag $TAG." >&2
echo "Use a new tag, or rerun with --allow-overwrite for an emergency reroll." >&2
exit 1
fi
if [[ "$ALLOW_OVERWRITE" == "true" ]]; then
echo "Uploading with overwrite enabled for existing release $TAG..."
gh release upload "$TAG" cmux-macos.dmg appcast.xml --clobber
else
echo "Uploading to existing release $TAG..."
gh release upload "$TAG" cmux-macos.dmg appcast.xml
fi
else
echo "Creating release $TAG and uploading..."
gh release create "$TAG" cmux-macos.dmg appcast.xml --title "$TAG" --notes "See CHANGELOG.md for details"
fi
# --- Verify ---
gh release view "$TAG"
# --- Update Homebrew cask (skip for nightlies) ---
if [[ "$TAG" != *"-nightly"* ]]; then
VERSION="${TAG#v}"
DMG_SHA256=$(shasum -a 256 cmux-macos.dmg | cut -d' ' -f1)
echo "Updating homebrew cask to $VERSION (SHA: $DMG_SHA256)..."
CASK_FILE="homebrew-cmux/Casks/cmux.rb"
if [ -f "$CASK_FILE" ]; then
cat > "$CASK_FILE" << CASKEOF
cask "cmux" do
version "${VERSION}"
sha256 "${DMG_SHA256}"
url "https://github.com/manaflow-ai/cmux/releases/download/v#{version}/cmux-macos.dmg"
name "cmux"
desc "Lightweight native macOS terminal with vertical tabs for AI coding agents"
homepage "https://github.com/manaflow-ai/cmux"
livecheck do
url :url
strategy :github_latest
end
depends_on macos: ">= :ventura"
app "cmux.app"
binary "#{appdir}/cmux.app/Contents/Resources/bin/cmux"
zap trash: [
"~/Library/Application Support/cmux",
"~/Library/Caches/cmux",
"~/Library/Preferences/ai.manaflow.cmuxterm.plist",
]
end
CASKEOF
cd homebrew-cmux
git add Casks/cmux.rb
if git diff --staged --quiet; then
echo "Homebrew cask already up to date"
else
git commit -m "Update cmux to ${VERSION}"
git push
echo "Homebrew cask updated"
fi
cd ..
else
echo "WARNING: homebrew-cmux submodule not found, skipping cask update"
fi
fi
# --- Cleanup ---
rm -rf build/ cmux-macos.dmg appcast.xml
echo ""
echo "=== Release $TAG complete ==="
say "cmux release complete"