#!/bin/bash # ============================================================================= # VibeTunnel Automated Release Script # ============================================================================= # # This script handles the complete end-to-end release process for VibeTunnel, # including building, signing, notarization, DMG creation, GitHub releases, # and appcast updates. It supports both stable and pre-release versions. # # USAGE: # ./scripts/release.sh [--dry-run] [number] # # ARGUMENTS: # type Release type: stable, beta, alpha, rc # number Pre-release number (required for beta/alpha/rc) # # OPTIONS: # --dry-run Preview what would be done without making changes # # IMPORTANT NOTES: # - This script can take 10-15 minutes due to notarization # - If running from Claude or other tools with timeouts, use a longer timeout # - If the script fails partway, use release-resume.sh to continue # # FEATURES: # - Complete build and release automation # - Automatic IS_PRERELEASE_BUILD flag handling # - Code signing and notarization # - DMG creation with signing # - GitHub release creation with assets # - Appcast XML generation and updates # - Git tag management and commit automation # - Comprehensive error checking and validation # # ENVIRONMENT VARIABLES: # APP_STORE_CONNECT_API_KEY_P8 App Store Connect API key (for notarization) # APP_STORE_CONNECT_KEY_ID API Key ID # APP_STORE_CONNECT_ISSUER_ID API Key Issuer ID # # DEPENDENCIES: # - preflight-check.sh (validates release readiness) # - Xcode workspace and project files # - build.sh (application building) # - sign-and-notarize.sh (code signing and notarization) # - create-dmg.sh (DMG creation) # - generate-appcast.sh (appcast updates) # - GitHub CLI (gh) for release creation # - Sparkle tools (sign_update) for EdDSA signatures # # RELEASE PROCESS: # 1. Pre-flight validation (git status, tools, certificates) # 2. Xcode project generation and commit if needed # 3. Application building with appropriate flags # 4. Code signing and notarization # 5. DMG creation and signing # 6. GitHub release creation with assets # 7. Appcast XML generation and updates # 8. Git commits and pushes # # EXAMPLES: # ./scripts/release.sh stable # Create stable release # ./scripts/release.sh beta 1 # Create beta.1 release # ./scripts/release.sh alpha 2 # Create alpha.2 release # ./scripts/release.sh rc 1 # Create rc.1 release # ./scripts/release.sh --dry-run stable # Preview stable release # ./scripts/release.sh --dry-run beta 1 # Preview beta.1 release # # OUTPUT: # - GitHub release at: https://github.com/amantus-ai/vibetunnel/releases # - Signed DMG file in build/ directory # - Updated appcast.xml and appcast-prerelease.xml files # - Git commits and tags pushed to repository # # ============================================================================= set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" # Source state management functions source "$SCRIPT_DIR/release-state.sh" # Color codes RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # Parse arguments and flags DRY_RUN=false RELEASE_TYPE="" PRERELEASE_NUMBER="" RESUME_MODE=false # Function to show usage show_usage() { echo "Usage:" echo " $0 [--dry-run] [number]" echo " $0 --resume" echo " $0 --status" echo "" echo "Arguments:" echo " release-type stable, beta, alpha, or rc" echo " number Pre-release number (required for beta/alpha/rc)" echo "" echo "Options:" echo " --dry-run Show what would be done without making changes" echo " --resume Resume an interrupted release" echo " --status Show current release progress" echo "" echo "Examples:" echo " $0 stable # Create stable release" echo " $0 beta 1 # Create beta.1 release" echo " $0 alpha 2 # Create alpha.2 release" echo " $0 rc 3 # Create rc.3 release" echo " $0 --dry-run stable # Preview stable release" echo " $0 --dry-run beta 1 # Preview beta.1 release" echo " $0 --resume # Resume interrupted release" echo " $0 --status # Check release progress" } # Parse command line arguments while [[ $# -gt 0 ]]; do case $1 in --dry-run) DRY_RUN=true shift ;; --resume) RESUME_MODE=true shift ;; --status) show_progress exit 0 ;; -h|--help) show_usage exit 0 ;; stable|beta|alpha|rc) if [[ -n "$RELEASE_TYPE" ]]; then echo -e "${RED}โŒ Error: Release type already specified as '$RELEASE_TYPE'${NC}" echo "" show_usage exit 1 fi RELEASE_TYPE="$1" shift ;; *) # Check if this could be a pre-release number if [[ -n "$RELEASE_TYPE" ]] && [[ "$RELEASE_TYPE" != "stable" ]] && [[ -z "$PRERELEASE_NUMBER" ]]; then # This might be intended as a pre-release number PRERELEASE_NUMBER="$1" shift else echo -e "${RED}โŒ Error: Unknown argument: $1${NC}" echo "" show_usage exit 1 fi ;; esac done # Handle resume mode if [[ "$RESUME_MODE" == "true" ]]; then if ! can_resume; then echo -e "${RED}โŒ Error: No release in progress to resume${NC}" echo "" echo "Start a new release with: $0 [number]" exit 1 fi # Load release info from state RELEASE_TYPE=$(get_release_info "release_type") RELEASE_VERSION=$(get_release_info "release_version") BUILD_NUMBER=$(get_release_info "build_number") TAG_NAME=$(get_release_info "tag_name") echo -e "${BLUE}๐Ÿ“‹ Resuming release${NC}" show_progress echo "" else # Normal mode - validate required arguments if [[ -z "$RELEASE_TYPE" ]]; then echo -e "${RED}โŒ Error: Release type is required${NC}" echo "" show_usage exit 1 fi fi # Validate release type case "$RELEASE_TYPE" in stable|beta|alpha|rc) # Valid release type ;; *) echo -e "${RED}โŒ Error: Invalid release type: $RELEASE_TYPE${NC}" echo "Valid types are: stable, beta, alpha, rc" echo "" show_usage exit 1 ;; esac # For pre-releases, validate number if [[ "$RELEASE_TYPE" != "stable" ]]; then if [[ -z "$PRERELEASE_NUMBER" ]]; then echo -e "${RED}โŒ Error: Pre-release number is required for $RELEASE_TYPE releases${NC}" echo "" echo "Example: $0 $RELEASE_TYPE 1" echo "" show_usage exit 1 fi # Validate that pre-release number is a positive integer if ! [[ "$PRERELEASE_NUMBER" =~ ^[0-9]+$ ]] || [[ "$PRERELEASE_NUMBER" -eq 0 ]]; then echo -e "${RED}โŒ Error: Pre-release number must be a positive integer${NC}" echo "Got: '$PRERELEASE_NUMBER'" echo "" show_usage exit 1 fi elif [[ -n "$PRERELEASE_NUMBER" ]]; then echo -e "${YELLOW}โš ๏ธ Warning: Pre-release number ignored for stable releases${NC}" PRERELEASE_NUMBER="" fi echo -e "${BLUE}๐Ÿš€ VibeTunnel Automated Release${NC}" echo "==============================" echo "" # Show dry-run mode if enabled if [[ "$DRY_RUN" == "true" ]]; then echo -e "${YELLOW}๐Ÿ” DRY RUN MODE - No changes will be made${NC}" echo "" fi # Additional strict pre-conditions before preflight check echo -e "${BLUE}๐Ÿ” Running strict pre-conditions...${NC}" # CHANGELOG.md will be checked later with proper fallback logic # Clean up any stuck VibeTunnel volumes before starting echo "๐Ÿงน Cleaning up any stuck DMG volumes..." for volume in /Volumes/VibeTunnel*; do if [ -d "$volume" ]; then echo " Unmounting $volume..." hdiutil detach "$volume" -force 2>/dev/null || true fi done # Check if we're on main branch CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) if [[ "$CURRENT_BRANCH" != "main" ]]; then echo -e "${RED}โŒ Error: Must be on main branch to release (current: $CURRENT_BRANCH)${NC}" echo " Run: git checkout main" exit 1 fi # Check for uncommitted changes if ! git diff-index --quiet HEAD --; then echo -e "${RED}โŒ Error: Uncommitted changes detected${NC}" echo " Please commit or stash your changes before releasing" git status --short exit 1 fi # Check if IS_PRERELEASE_BUILD is already set in environment if [[ -n "${IS_PRERELEASE_BUILD:-}" ]]; then echo -e "${YELLOW}โš ๏ธ Warning: IS_PRERELEASE_BUILD is already set to: $IS_PRERELEASE_BUILD${NC}" echo " This will be overridden by the release script" unset IS_PRERELEASE_BUILD fi # Check for required environment variables for notarization if [[ -z "${APP_STORE_CONNECT_API_KEY_P8:-}" ]] || \ [[ -z "${APP_STORE_CONNECT_KEY_ID:-}" ]] || \ [[ -z "${APP_STORE_CONNECT_ISSUER_ID:-}" ]]; then echo -e "${RED}โŒ Error: Missing notarization environment variables${NC}" echo " Required variables:" echo " - APP_STORE_CONNECT_API_KEY_P8" echo " - APP_STORE_CONNECT_KEY_ID" echo " - APP_STORE_CONNECT_ISSUER_ID" exit 1 fi # Check if notarize-dmg.sh exists if [[ ! -x "$SCRIPT_DIR/notarize-dmg.sh" ]]; then echo -e "${RED}โŒ Error: notarize-dmg.sh not found or not executable${NC}" echo " Expected at: $SCRIPT_DIR/notarize-dmg.sh" exit 1 fi # Check if GitHub CLI is installed and authenticated if ! command -v gh >/dev/null 2>&1; then echo -e "${RED}โŒ Error: GitHub CLI (gh) is not installed${NC}" echo " Install with: brew install gh" exit 1 fi if ! gh auth status >/dev/null 2>&1; then echo -e "${RED}โŒ Error: GitHub CLI is not authenticated${NC}" echo " Run: gh auth login" exit 1 fi # Check if changelog file exists in project root if [[ -f "$PROJECT_ROOT/../CHANGELOG.md" ]]; then CHANGELOG_PATH="$PROJECT_ROOT/../CHANGELOG.md" else echo -e "${YELLOW}โš ๏ธ Warning: CHANGELOG.md not found${NC}" echo " Expected location: $PROJECT_ROOT/../CHANGELOG.md" echo " Release notes will be basic" CHANGELOG_PATH="" fi # Check if we're up to date with origin/main git fetch origin main --quiet LOCAL=$(git rev-parse HEAD) REMOTE=$(git rev-parse origin/main) if [[ "$LOCAL" != "$REMOTE" ]]; then echo -e "${RED}โŒ Error: Not up to date with origin/main${NC}" echo " Run: git pull --rebase origin main" exit 1 fi echo -e "${GREEN}โœ… Strict pre-conditions passed${NC}" echo "" # Step 1: Run pre-flight check echo -e "${BLUE}๐Ÿ“‹ Step 1/8: Running pre-flight check...${NC}" if ! "$SCRIPT_DIR/preflight-check.sh"; then echo "" echo -e "${RED}โŒ Pre-flight check failed. Please fix the issues above.${NC}" exit 1 fi echo "" echo -e "${GREEN}โœ… Pre-flight check passed!${NC}" echo "" # Get version info VERSION_CONFIG="$PROJECT_ROOT/VibeTunnel/version.xcconfig" if [[ -f "$VERSION_CONFIG" ]]; then MARKETING_VERSION=$(grep 'MARKETING_VERSION' "$VERSION_CONFIG" | sed 's/.*MARKETING_VERSION = //') BUILD_NUMBER=$(grep 'CURRENT_PROJECT_VERSION' "$VERSION_CONFIG" | sed 's/.*CURRENT_PROJECT_VERSION = //') else echo -e "${RED}โŒ Error: Version configuration file not found at $VERSION_CONFIG${NC}" exit 1 fi # Determine release version if [[ "$RELEASE_TYPE" == "stable" ]]; then RELEASE_VERSION="$MARKETING_VERSION" TAG_NAME="v$RELEASE_VERSION" else # Check if MARKETING_VERSION already contains the pre-release suffix EXPECTED_SUFFIX="$RELEASE_TYPE.$PRERELEASE_NUMBER" if [[ "$MARKETING_VERSION" == *"-$EXPECTED_SUFFIX" ]]; then # Version already has the correct suffix, use as-is RELEASE_VERSION="$MARKETING_VERSION" else # Add the suffix RELEASE_VERSION="$MARKETING_VERSION-$RELEASE_TYPE.$PRERELEASE_NUMBER" fi TAG_NAME="v$RELEASE_VERSION" fi echo "๐Ÿ“ฆ Preparing release:" echo " Type: $RELEASE_TYPE" echo " Version: $RELEASE_VERSION" echo " Build: $BUILD_NUMBER" echo " Tag: $TAG_NAME" echo "" # Initialize state tracking for new releases if [[ "$RESUME_MODE" != "true" ]]; then init_state "$RELEASE_TYPE" "$RELEASE_VERSION" "$BUILD_NUMBER" "$TAG_NAME" fi # Additional validation after version determination echo -e "${BLUE}๐Ÿ” Validating release configuration...${NC}" # Check for double suffix issue if [[ "$RELEASE_VERSION" =~ -[a-zA-Z]+\.[0-9]+-[a-zA-Z]+\.[0-9]+ ]]; then echo -e "${RED}โŒ Error: Version has double suffix: $RELEASE_VERSION${NC}" echo " This indicates version.xcconfig already has a pre-release suffix" echo " Current MARKETING_VERSION: $MARKETING_VERSION" exit 1 fi # Verify build number hasn't been used echo "๐Ÿ” Checking build number uniqueness..." EXISTING_BUILDS="" if [[ -f "$PROJECT_ROOT/../appcast.xml" ]]; then APPCAST_BUILDS=$(grep -E '[0-9]+' "$PROJECT_ROOT/../appcast.xml" 2>/dev/null | sed 's/.*\([0-9]*\)<\/sparkle:version>.*/\1/' | tr '\n' ' ' || true) EXISTING_BUILDS+="$APPCAST_BUILDS" fi if [[ -f "$PROJECT_ROOT/../appcast-prerelease.xml" ]]; then PRERELEASE_BUILDS=$(grep -E '[0-9]+' "$PROJECT_ROOT/../appcast-prerelease.xml" 2>/dev/null | sed 's/.*\([0-9]*\)<\/sparkle:version>.*/\1/' | tr '\n' ' ' || true) EXISTING_BUILDS+="$PRERELEASE_BUILDS" fi for EXISTING_BUILD in $EXISTING_BUILDS; do if [[ "$BUILD_NUMBER" == "$EXISTING_BUILD" ]]; then echo -e "${RED}โŒ Error: Build number $BUILD_NUMBER already exists in appcast!${NC}" echo " Please increment CURRENT_PROJECT_VERSION in version.xcconfig" exit 1 fi done echo -e "${GREEN}โœ… Release configuration validated${NC}" echo "" # Step 2: Clean build directory echo -e "${BLUE}๐Ÿ“‹ Step 2/8: Cleaning build directory...${NC}" rm -rf "$PROJECT_ROOT/build" rm -rf "$PROJECT_ROOT/DerivedData" # rm -rf "$PROJECT_ROOT/.build" rm -rf ~/Library/Developer/Xcode/DerivedData/VibeTunnel-* echo "โœ“ Cleaned all build artifacts" # Step 3: Update version in version.xcconfig echo "" echo -e "${BLUE}๐Ÿ“‹ Step 3/8: Setting version...${NC}" # Determine the version string to set if [[ "$RELEASE_TYPE" == "stable" ]]; then # For stable releases, ensure MARKETING_VERSION doesn't have pre-release suffix # Extract base version (remove any existing pre-release suffix) BASE_VERSION=$(echo "$MARKETING_VERSION" | sed 's/-.*$//') VERSION_TO_SET="$BASE_VERSION" else # For pre-releases, use the RELEASE_VERSION we calculated above # (which already handles whether to add suffix or not) VERSION_TO_SET="$RELEASE_VERSION" fi if [[ "$DRY_RUN" == "true" ]]; then echo "๐Ÿ“ Would update MARKETING_VERSION to: $VERSION_TO_SET" echo " Current value: $MARKETING_VERSION" echo -e "${GREEN}โœ… Version would be set to: $VERSION_TO_SET${NC}" else # Backup version.xcconfig cp "$VERSION_CONFIG" "$VERSION_CONFIG.bak" # Update MARKETING_VERSION in version.xcconfig echo "๐Ÿ“ Updating MARKETING_VERSION to: $VERSION_TO_SET" sed -i '' "s/MARKETING_VERSION = .*/MARKETING_VERSION = $VERSION_TO_SET/" "$VERSION_CONFIG" # Verify the update NEW_MARKETING_VERSION=$(grep 'MARKETING_VERSION' "$VERSION_CONFIG" | sed 's/.*MARKETING_VERSION = //') if [[ "$NEW_MARKETING_VERSION" != "$VERSION_TO_SET" ]]; then echo -e "${RED}โŒ Failed to update MARKETING_VERSION${NC}" exit 1 fi echo -e "${GREEN}โœ… Version updated to: $VERSION_TO_SET${NC}" fi # Check if Xcode project was modified and commit if needed if ! git diff --quiet "$PROJECT_ROOT/VibeTunnel.xcodeproj/project.pbxproj"; then if [[ "$DRY_RUN" == "true" ]]; then echo "๐Ÿ“ Would commit Xcode project changes" echo " Commit message: Update Xcode project for build $BUILD_NUMBER" else echo "๐Ÿ“ Committing Xcode project changes..." git add "$PROJECT_ROOT/VibeTunnel.xcodeproj/project.pbxproj" git commit -m "Update Xcode project for build $BUILD_NUMBER" echo -e "${GREEN}โœ… Xcode project changes committed${NC}" fi fi # Step 4: Build the app echo "" echo -e "${BLUE}๐Ÿ“‹ Step 4/8: Building universal application...${NC}" if [[ "$DRY_RUN" == "true" ]]; then echo "๐Ÿ”จ Would build ARM64 binary with:" echo " Configuration: Release" echo " IS_PRERELEASE_BUILD: $([ "$RELEASE_TYPE" != "stable" ] && echo "YES" || echo "NO")" echo " Command: $SCRIPT_DIR/build.sh --configuration Release" echo "" echo " Would verify:" echo " - App exists at expected path" echo " - Build number matches $BUILD_NUMBER" echo " - Binary architecture is ARM64" echo -e "${GREEN}โœ… Build would be performed${NC}" else # Check for custom Node.js build echo "" echo "๐Ÿ” Checking for custom Node.js build..." WEB_DIR="$PROJECT_ROOT/../web" # Check if .node-builds directory exists if [[ -d "$WEB_DIR/.node-builds" ]]; then CUSTOM_NODE_PATH=$(find "$WEB_DIR/.node-builds" -name "node-v*-minimal" -type d 2>/dev/null | sort -V | tail -n1)/out/Release/node else CUSTOM_NODE_PATH="" fi if [[ ! -f "$CUSTOM_NODE_PATH" ]]; then echo -e "${YELLOW}โš ๏ธ Custom Node.js not found. Using system Node.js...${NC}" echo " Note: Release will work but app size will be larger." # Continue with default Node.js else CUSTOM_NODE_SIZE=$(ls -lh "$CUSTOM_NODE_PATH" | awk '{print $5}') CUSTOM_NODE_VERSION=$("$CUSTOM_NODE_PATH" --version 2>/dev/null || echo "unknown") echo -e "${GREEN}โœ… Found custom Node.js${NC}" echo " Version: $CUSTOM_NODE_VERSION" echo " Size: $CUSTOM_NODE_SIZE" fi # For pre-release builds, set the environment variable if [[ "$RELEASE_TYPE" != "stable" ]]; then echo "๐Ÿ“ Marking build as pre-release..." export IS_PRERELEASE_BUILD=YES else export IS_PRERELEASE_BUILD=NO fi # Build ARM64 binary echo "" echo "๐Ÿ”จ Building ARM64 binary..." "$SCRIPT_DIR/build.sh" --configuration Release # Find the built app - could be in build directory or DerivedData APP_PATH="$PROJECT_ROOT/build/Build/Products/Release/VibeTunnel.app" if [[ ! -d "$APP_PATH" ]]; then # Check DerivedData DEFAULT_DERIVED_DATA="$HOME/Library/Developer/Xcode/DerivedData" APP_PATH=$(find "$DEFAULT_DERIVED_DATA" -name "VibeTunnel.app" -path "*/Build/Products/Release/*" ! -path "*/Index.noindex/*" 2>/dev/null | head -n 1) if [[ ! -d "$APP_PATH" ]]; then echo -e "${RED}โŒ Build failed - app not found${NC}" exit 1 fi # Copy to expected location for consistency mkdir -p "$PROJECT_ROOT/build/Build/Products/Release" cp -R "$APP_PATH" "$PROJECT_ROOT/build/Build/Products/Release/" APP_PATH="$PROJECT_ROOT/build/Build/Products/Release/VibeTunnel.app" fi # Verify build number BUILT_VERSION=$(defaults read "$APP_PATH/Contents/Info.plist" CFBundleVersion) if [[ "$BUILT_VERSION" != "$BUILD_NUMBER" ]]; then echo -e "${RED}โŒ Build number mismatch! Expected $BUILD_NUMBER but got $BUILT_VERSION${NC}" exit 1 fi # Verify it's an ARM64 binary APP_BINARY="$APP_PATH/Contents/MacOS/VibeTunnel" if [[ -f "$APP_BINARY" ]]; then ARCH_INFO=$(lipo -info "$APP_BINARY" 2>/dev/null || echo "") if [[ "$ARCH_INFO" == *"arm64"* ]]; then echo "โœ… ARM64 binary created" else echo -e "${RED}โŒ Error: Binary is not ARM64: $ARCH_INFO${NC}" exit 1 fi fi echo -e "${GREEN}โœ… Build complete${NC}" fi # Step 5: Sign and notarize echo "" echo -e "${BLUE}๐Ÿ“‹ Step 5/8: Signing and notarizing...${NC}" if [[ "$DRY_RUN" == "true" ]]; then echo "๐Ÿ” Would sign and notarize the application" echo " Command: $SCRIPT_DIR/sign-and-notarize.sh --sign-and-notarize" echo -e "${GREEN}โœ… Signing and notarization would be performed${NC}" # For dry run, we need to exit early since we don't have actual build artifacts echo "" echo -e "${BLUE}๐Ÿ“‹ Remaining steps (would be performed):${NC}" echo " 6/8: Creating DMG and ZIP" echo " 7/8: Creating GitHub release with tag $TAG_NAME" echo " 8/8: Updating appcast files" echo " 9/9: Committing and pushing changes" echo "" echo "๐Ÿ“ฆ Release summary:" echo " Type: $RELEASE_TYPE" echo " Version: $RELEASE_VERSION" echo " Build: $BUILD_NUMBER" echo " Tag: $TAG_NAME" echo "" echo -e "${GREEN}๐ŸŽ‰ Dry run complete!${NC}" echo "" echo "To perform the actual release, run without --dry-run:" echo " $0 $RELEASE_TYPE${PRERELEASE_NUMBER:+ $PRERELEASE_NUMBER}" exit 0 fi "$SCRIPT_DIR/sign-and-notarize.sh" --sign-and-notarize # Verify Sparkle component signing echo "" echo -e "${BLUE}๐Ÿ” Verifying Sparkle component signatures...${NC}" SPARKLE_OK=true # Check each Sparkle component for proper signing with timestamps if [ -d "$APP_PATH/Contents/Frameworks/Sparkle.framework/Versions/B/XPCServices/Installer.xpc" ]; then CODESIGN_OUT=$(codesign -dv "$APP_PATH/Contents/Frameworks/Sparkle.framework/Versions/B/XPCServices/Installer.xpc" 2>&1) if echo "$CODESIGN_OUT" | grep -qE "(Timestamp|timestamp)"; then echo "โœ… Installer.xpc properly signed with timestamp" else echo -e "${RED}โŒ Installer.xpc missing timestamp signature${NC}" SPARKLE_OK=false fi fi if [ "$SPARKLE_OK" = false ]; then echo -e "${RED}โŒ Sparkle component signing verification failed!${NC}" exit 1 fi echo -e "${GREEN}โœ… All Sparkle components properly signed${NC}" # Step 6: Create DMG and ZIP echo "" echo -e "${BLUE}๐Ÿ“‹ Step 6/8: Creating DMG and ZIP...${NC}" DMG_NAME="VibeTunnel-$RELEASE_VERSION.dmg" DMG_PATH="$PROJECT_ROOT/build/$DMG_NAME" ZIP_NAME="VibeTunnel-$RELEASE_VERSION.zip" ZIP_PATH="$PROJECT_ROOT/build/$ZIP_NAME" "$SCRIPT_DIR/create-dmg.sh" "$APP_PATH" "$DMG_PATH" if [[ ! -f "$DMG_PATH" ]]; then echo -e "${RED}โŒ DMG creation failed${NC}" exit 1 fi "$SCRIPT_DIR/create-zip.sh" "$APP_PATH" "$ZIP_PATH" if [[ ! -f "$ZIP_PATH" ]]; then echo -e "${RED}โŒ ZIP creation failed${NC}" exit 1 fi echo -e "${GREEN}โœ… DMG and ZIP created${NC}" # Step 6.5: Notarize DMG echo "" echo -e "${BLUE}๐Ÿ“‹ Notarizing DMG...${NC}" "$SCRIPT_DIR/notarize-dmg.sh" "$DMG_PATH" echo -e "${GREEN}โœ… DMG notarized${NC}" # Verify DMG notarization echo "" echo -e "${BLUE}๐Ÿ” Verifying DMG notarization...${NC}" # Check if DMG is properly signed if codesign -dv "$DMG_PATH" &>/dev/null; then echo "โœ… DMG is signed" else echo -e "${RED}โŒ Error: DMG is not signed${NC}" exit 1 fi # Verify notarization with spctl if spctl -a -t open --context context:primary-signature -v "$DMG_PATH" 2>&1 | grep -q "accepted"; then echo "โœ… DMG notarization verified - accepted by Gatekeeper" else echo -e "${YELLOW}โš ๏ธ Warning: Could not verify DMG notarization with spctl${NC}" echo " This might be normal in some environments" fi # Check if notarization ticket is stapled if xcrun stapler validate "$DMG_PATH" 2>&1 | grep -q "The validate action worked"; then echo "โœ… Notarization ticket is properly stapled" else echo -e "${RED}โŒ Error: Notarization ticket is not stapled to DMG${NC}" echo " Users may experience delays when opening the DMG" exit 1 fi # Verify app inside DMG DMG_MOUNT=$(mktemp -d) if hdiutil attach "$DMG_PATH" -mountpoint "$DMG_MOUNT" -nobrowse -quiet; then DMG_APP="$DMG_MOUNT/VibeTunnel.app" # Check if app is notarized if spctl -a -t exec -vv "$DMG_APP" 2>&1 | grep -q "source=Notarized Developer ID"; then echo "โœ… App in DMG is properly notarized" else echo -e "${RED}โŒ App in DMG is not properly notarized!${NC}" hdiutil detach "$DMG_MOUNT" -quiet exit 1 fi hdiutil detach "$DMG_MOUNT" -quiet else echo -e "${RED}โŒ Failed to mount DMG for verification${NC}" exit 1 fi echo "" echo -e "${GREEN}โœ… DMG notarized and verified${NC}" # Size validation echo "" echo -e "${BLUE}๐Ÿ“ Validating release size...${NC}" DMG_SIZE=$(stat -f %z "$DMG_PATH" 2>/dev/null || stat -c %s "$DMG_PATH" 2>/dev/null) DMG_SIZE_MB=$((DMG_SIZE / 1024 / 1024)) echo "DMG size: ${DMG_SIZE_MB} MB" # Expected size range (42-50 MB based on recent releases) MIN_SIZE_MB=40 MAX_SIZE_MB=50 if [[ $DMG_SIZE_MB -lt $MIN_SIZE_MB ]]; then echo -e "${RED}โŒ DMG size is unexpectedly small (${DMG_SIZE_MB} MB < ${MIN_SIZE_MB} MB)${NC}" echo "This might indicate missing components." exit 1 elif [[ $DMG_SIZE_MB -gt $MAX_SIZE_MB ]]; then echo -e "${YELLOW}โš ๏ธ DMG size is larger than expected (${DMG_SIZE_MB} MB > ${MAX_SIZE_MB} MB)${NC}" echo "Checking for development files in app bundle..." # Mount DMG and check for common issues DMG_MOUNT_CHECK=$(mktemp -d) if hdiutil attach "$DMG_PATH" -mountpoint "$DMG_MOUNT_CHECK" -nobrowse -quiet; then APP_IN_DMG="$DMG_MOUNT_CHECK/VibeTunnel.app" # Check for node_modules if find "$APP_IN_DMG" -name "node_modules" -type d | grep -q .; then echo -e "${RED}โŒ Found node_modules in app bundle!${NC}" find "$APP_IN_DMG" -name "node_modules" -type d hdiutil detach "$DMG_MOUNT_CHECK" -quiet exit 1 fi # Check for JAR files if find "$APP_IN_DMG" -name "*.jar" -type f | grep -q .; then echo -e "${RED}โŒ Found JAR files in app bundle!${NC}" find "$APP_IN_DMG" -name "*.jar" -type f hdiutil detach "$DMG_MOUNT_CHECK" -quiet exit 1 fi # List large files echo "Large files in app bundle (>1MB):" find "$APP_IN_DMG" -type f -size +1M -exec ls -lh {} \; | awk '{print $5 " " $9}' hdiutil detach "$DMG_MOUNT_CHECK" -quiet fi echo "Consider investigating the size increase before proceeding." read -p "Continue anyway? (y/n): " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then echo -e "${RED}โŒ Release cancelled due to size concerns${NC}" exit 1 fi else echo -e "${GREEN}โœ… DMG size is within expected range${NC}" fi # Step 6: Create GitHub release echo "" echo -e "${BLUE}๐Ÿ“‹ Step 7/9: Creating GitHub release...${NC}" # Check if tag already exists locally if git rev-parse "$TAG_NAME" >/dev/null 2>&1; then echo -e "${YELLOW}โš ๏ธ Tag $TAG_NAME already exists locally!${NC}" DELETE_EXISTING=true else # Check if tag exists on remote if git ls-remote --tags origin | grep -q "refs/tags/$TAG_NAME"; then echo -e "${YELLOW}โš ๏ธ Tag $TAG_NAME already exists on remote!${NC}" DELETE_EXISTING=true else DELETE_EXISTING=false fi fi if [[ "$DELETE_EXISTING" == "true" ]]; then # Check if a release exists for this tag if gh release view "$TAG_NAME" >/dev/null 2>&1; then echo "" echo "A GitHub release already exists for this tag." echo "What would you like to do?" echo " 1) Delete the existing release and tag, then create new ones" echo " 2) Cancel the release" echo "" read -p "Enter your choice (1 or 2): " choice case $choice in 1) echo "๐Ÿ—‘๏ธ Deleting existing release and tag..." gh release delete "$TAG_NAME" --yes 2>/dev/null || true git tag -d "$TAG_NAME" git push origin :refs/tags/"$TAG_NAME" 2>/dev/null || true echo -e "${GREEN}โœ… Existing release and tag deleted${NC}" ;; 2) echo -e "${RED}โŒ Release cancelled${NC}" exit 1 ;; *) echo -e "${RED}โŒ Invalid choice. Release cancelled${NC}" exit 1 ;; esac else # Tag exists but no release - just delete the tag echo "๐Ÿ—‘๏ธ Deleting existing tag..." git tag -d "$TAG_NAME" git push origin :refs/tags/"$TAG_NAME" 2>/dev/null || true echo -e "${GREEN}โœ… Existing tag deleted${NC}" fi fi # Create and push tag echo "๐Ÿท๏ธ Creating tag $TAG_NAME..." git tag -a "$TAG_NAME" -m "Release $RELEASE_VERSION (build $BUILD_NUMBER)" git push origin "$TAG_NAME" # Create release echo "๐Ÿ“ค Creating GitHub release..." # Generate release notes from changelog echo "๐Ÿ“ Generating release notes from changelog..." RELEASE_NOTES="" # Use generate-release-notes.sh for better markdown output if [[ -x "$SCRIPT_DIR/generate-release-notes.sh" ]]; then echo " Using generate-release-notes.sh for version $RELEASE_VERSION" RELEASE_NOTES=$("$SCRIPT_DIR/generate-release-notes.sh" "$RELEASE_VERSION" 2>/dev/null || echo "") # Check if we got valid content if [[ -n "$RELEASE_NOTES" ]] && [[ "$RELEASE_NOTES" != *"This release includes various improvements and bug fixes"* ]]; then echo "โœ… Generated release notes from changelog" else echo "โš ๏ธ Could not extract specific changelog for version $RELEASE_VERSION" RELEASE_NOTES="" fi fi # Fallback to basic release notes if changelog extraction fails if [[ -z "$RELEASE_NOTES" ]]; then echo " Generating fallback release notes..." RELEASE_NOTES="## VibeTunnel $RELEASE_VERSION This release includes various improvements and bug fixes. For details, please see the [CHANGELOG](https://github.com/amantus-ai/vibetunnel/blob/main/CHANGELOG.md). **Build**: $BUILD_NUMBER" fi # Format the release title properly # Convert "1.0.0-beta.10" to "VibeTunnel 1.0.0 Beta 10" RELEASE_TITLE="VibeTunnel $RELEASE_VERSION" if [[ "$RELEASE_VERSION" =~ ^([0-9]+\.[0-9]+\.[0-9]+)-beta\.([0-9]+)$ ]]; then VERSION_BASE="${BASH_REMATCH[1]}" BETA_NUM="${BASH_REMATCH[2]}" RELEASE_TITLE="VibeTunnel $VERSION_BASE Beta $BETA_NUM" elif [[ "$RELEASE_VERSION" =~ ^([0-9]+\.[0-9]+\.[0-9]+)-alpha\.([0-9]+)$ ]]; then VERSION_BASE="${BASH_REMATCH[1]}" ALPHA_NUM="${BASH_REMATCH[2]}" RELEASE_TITLE="VibeTunnel $VERSION_BASE Alpha $ALPHA_NUM" elif [[ "$RELEASE_VERSION" =~ ^([0-9]+\.[0-9]+\.[0-9]+)-rc\.([0-9]+)$ ]]; then VERSION_BASE="${BASH_REMATCH[1]}" RC_NUM="${BASH_REMATCH[2]}" RELEASE_TITLE="VibeTunnel $VERSION_BASE RC $RC_NUM" fi if [[ "$RELEASE_TYPE" == "stable" ]]; then gh release create "$TAG_NAME" \ --title "$RELEASE_TITLE" \ --notes "$RELEASE_NOTES" \ "$DMG_PATH" \ "$ZIP_PATH" else gh release create "$TAG_NAME" \ --title "$RELEASE_TITLE" \ --notes "$RELEASE_NOTES" \ --prerelease \ "$DMG_PATH" \ "$ZIP_PATH" fi echo -e "${GREEN}โœ… GitHub release created${NC}" # Step 7: Update appcast echo "" echo -e "${BLUE}๐Ÿ“‹ Step 8/9: Updating appcast...${NC}" # Generate appcast echo "๐Ÿ” Generating appcast with EdDSA signatures..." # Set the Sparkle account for sign_update export SPARKLE_ACCOUNT="VibeTunnel" echo " Using Sparkle account: $SPARKLE_ACCOUNT" "$SCRIPT_DIR/generate-appcast.sh" # Verify the appcast was updated if [[ "$RELEASE_TYPE" == "stable" ]]; then if ! grep -q "$BUILD_NUMBER" "$PROJECT_ROOT/../appcast.xml"; then echo -e "${YELLOW}โš ๏ธ Appcast may not have been updated. Please check manually.${NC}" fi else if ! grep -q "$BUILD_NUMBER" "$PROJECT_ROOT/../appcast-prerelease.xml"; then echo -e "${YELLOW}โš ๏ธ Pre-release appcast may not have been updated. Please check manually.${NC}" fi fi echo -e "${GREEN}โœ… Appcast updated${NC}" # Commit and push appcast and version files echo "" echo "๐Ÿ“ค Committing and pushing changes..." # Add version.xcconfig changes git add "$VERSION_CONFIG" 2>/dev/null || true # Add appcast files (they're in project root, not mac/) if [[ -f "$PROJECT_ROOT/../appcast.xml" ]]; then git add "$PROJECT_ROOT/../appcast.xml" 2>/dev/null || true else echo -e "${YELLOW}โš ๏ธ Warning: appcast.xml not found in project root${NC}" fi if [[ -f "$PROJECT_ROOT/../appcast-prerelease.xml" ]]; then git add "$PROJECT_ROOT/../appcast-prerelease.xml" 2>/dev/null || true else echo -e "${YELLOW}โš ๏ธ Warning: appcast-prerelease.xml not found in project root${NC}" fi if ! git diff --cached --quiet; then git commit -m "Update appcast and version for $RELEASE_VERSION" git push origin main echo -e "${GREEN}โœ… Changes pushed${NC}" else echo "โ„น๏ธ No changes to commit" fi # For pre-releases, optionally restore base version if [[ "$RELEASE_TYPE" != "stable" ]]; then echo "" echo "๐Ÿ“ Note: MARKETING_VERSION is now set to '$VERSION_TO_SET'" echo " To restore base version for development, run:" echo " git checkout -- $VERSION_CONFIG" fi # Optional: Verify appcast echo "" echo "๐Ÿ” Verifying appcast files..." if "$SCRIPT_DIR/verify-appcast.sh" | grep -q "All appcast checks passed"; then echo -e "${GREEN}โœ… Appcast verification passed${NC}" else echo -e "${YELLOW}โš ๏ธ Some appcast issues detected. Please review the output above.${NC}" fi echo "" echo -e "${GREEN}๐ŸŽ‰ Release Complete!${NC}" echo "==================" echo "" echo -e "${GREEN}โœ… Successfully released VibeTunnel $RELEASE_VERSION${NC}" echo "" echo "Release details:" echo " - Version: $RELEASE_VERSION" echo " - Build: $BUILD_NUMBER" echo " - Tag: $TAG_NAME" echo " - GitHub: https://github.com/amantus-ai/vibetunnel/releases/tag/$TAG_NAME" echo "" echo "Release artifacts:" echo " - DMG: $(basename "$DMG_PATH")" echo " - ZIP: $(basename "$ZIP_PATH")" echo "" if [[ "$RELEASE_TYPE" != "stable" ]]; then echo "๐Ÿ“ Note: This is a pre-release. Users with 'Include Pre-releases' enabled will receive this update." else echo "๐Ÿ“ Note: This is a stable release. All users will receive this update." fi echo "" echo "๐Ÿ’ก Next steps:" echo " - Test the update from an older version" echo " - Monitor Console.app for any update errors" echo " - Update release notes on GitHub if needed"