mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-03-25 09:25:50 +00:00
- Created normalize-release-titles.sh script to update existing releases - Updated all 9 beta releases to use consistent title format - Modified release.sh to use normalized format for future releases - Changed from "VibeTunnel 1.0.0-beta.10" to "VibeTunnel 1.0.0 Beta 10" This makes release titles more readable and consistent across all releases.
927 lines
No EOL
32 KiB
Bash
Executable file
927 lines
No EOL
32 KiB
Bash
Executable file
#!/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] <type> [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] <release-type> [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 <release-type> [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 '<sparkle:version>[0-9]+</sparkle:version>' "$PROJECT_ROOT/../appcast.xml" 2>/dev/null | sed 's/.*<sparkle:version>\([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 '<sparkle:version>[0-9]+</sparkle:version>' "$PROJECT_ROOT/../appcast-prerelease.xml" 2>/dev/null | sed 's/.*<sparkle:version>\([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-Mac.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-Mac.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}"
|
||
|
||
|
||
# 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..."
|
||
CHANGELOG_HTML=""
|
||
if [[ -x "$SCRIPT_DIR/changelog-to-html.sh" ]] && [[ -n "$CHANGELOG_PATH" ]] && [[ -f "$CHANGELOG_PATH" ]]; then
|
||
# Extract version for changelog (remove any pre-release suffixes for lookup)
|
||
CHANGELOG_VERSION="$RELEASE_VERSION"
|
||
if [[ "$CHANGELOG_VERSION" =~ ^([0-9]+\.[0-9]+\.[0-9]+) ]]; then
|
||
CHANGELOG_BASE="${BASH_REMATCH[1]}"
|
||
# Try full version first, then base version
|
||
CHANGELOG_HTML=$("$SCRIPT_DIR/changelog-to-html.sh" "$CHANGELOG_VERSION" "$CHANGELOG_PATH" 2>/dev/null || \
|
||
"$SCRIPT_DIR/changelog-to-html.sh" "$CHANGELOG_BASE" "$CHANGELOG_PATH" 2>/dev/null || \
|
||
echo "")
|
||
fi
|
||
fi
|
||
|
||
# Fallback to basic release notes if changelog extraction fails
|
||
if [[ -z "$CHANGELOG_HTML" ]]; then
|
||
echo "⚠️ Could not extract changelog, using basic release notes"
|
||
RELEASE_NOTES="Release $RELEASE_VERSION (build $BUILD_NUMBER)"
|
||
else
|
||
echo "✅ Generated release notes from changelog"
|
||
RELEASE_NOTES="$CHANGELOG_HTML"
|
||
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 "<sparkle:version>$BUILD_NUMBER</sparkle:version>" "$PROJECT_ROOT/../appcast.xml"; then
|
||
echo -e "${YELLOW}⚠️ Appcast may not have been updated. Please check manually.${NC}"
|
||
fi
|
||
else
|
||
if ! grep -q "<sparkle:version>$BUILD_NUMBER</sparkle:version>" "$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" |