From ba7d66aa88728dca3945b5f50c76bd751eb704d0 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 19 Jun 2025 05:14:09 +0200 Subject: [PATCH] Improve release scripts and documentation after successful beta 2 release Major improvements: - Add common.sh library for consistent error handling and logging - Fix hardcoded values in scripts (signing identity, volume names, GitHub repo) - Add comprehensive release documentation with lessons learned - Create Sparkle key management guide - Add clean.sh script for managing build artifacts - Improve error handling and validation across all scripts - Update lint.sh with proper documentation and better error handling - Make generate-appcast.sh fail fast if private key is missing Script improvements: - release.sh: Add GitHub CLI auth check, remote tag validation - notarize-app.sh: Auto-detect signing identity from keychain - create-dmg.sh: Make volume name configurable - generate-appcast.sh: Extract GitHub info from git remote - All scripts: Add proper documentation headers This ensures more reliable and maintainable release process. --- docs/release-guide.md | 329 ++++++++++++++++++++++++++++++++++++ docs/sparkle-keys.md | 230 +++++++++++++++++++++++++ scripts/clean.sh | 173 +++++++++++++++++++ scripts/common.sh | 287 +++++++++++++++++++++++++++++++ scripts/create-dmg.sh | 31 +++- scripts/generate-appcast.sh | 23 ++- scripts/lint.sh | 73 ++++++-- scripts/notarize-app.sh | 41 ++++- scripts/release.sh | 36 +++- 9 files changed, 1196 insertions(+), 27 deletions(-) create mode 100644 docs/release-guide.md create mode 100644 docs/sparkle-keys.md create mode 100755 scripts/clean.sh create mode 100644 scripts/common.sh diff --git a/docs/release-guide.md b/docs/release-guide.md new file mode 100644 index 00000000..182c3c1b --- /dev/null +++ b/docs/release-guide.md @@ -0,0 +1,329 @@ +# VibeTunnel Release Process + +This document describes the complete release process for VibeTunnel, including all prerequisites, steps, and troubleshooting information. + +## Table of Contents + +1. [Prerequisites](#prerequisites) +2. [Pre-Release Checklist](#pre-release-checklist) +3. [Release Process](#release-process) +4. [Post-Release Steps](#post-release-steps) +5. [Troubleshooting](#troubleshooting) +6. [Lessons Learned](#lessons-learned) + +## Prerequisites + +### Required Tools + +- **Xcode** (latest stable version) +- **GitHub CLI** (`brew install gh`) +- **Apple Developer Account** with valid certificates +- **Sparkle EdDSA Keys** (see [Sparkle Key Management](#sparkle-key-management)) + +### Environment Variables + +```bash +# Required for notarization +export APP_STORE_CONNECT_API_KEY_P8="" +export APP_STORE_CONNECT_KEY_ID="" +export APP_STORE_CONNECT_ISSUER_ID="" + +# Optional - will be auto-detected if not set +export SIGN_IDENTITY="Developer ID Application: Your Name (TEAMID)" +``` + +### Sparkle Key Management + +VibeTunnel uses EdDSA signatures for secure updates via Sparkle framework. + +#### Key Storage + +- **Private Key**: `private/sparkle_private_key` (NEVER commit this!) +- **Public Key**: `VibeTunnel/sparkle-public-ed-key.txt` (committed to repo) + +#### Generating New Keys + +If you need to generate new keys: + +```bash +# Generate keys using Sparkle's tool +./build/SourcePackages/artifacts/sparkle/Sparkle/bin/generate_keys + +# This creates a key pair - save the private key securely! +``` + +#### Restoring Keys + +To restore keys from backup: + +```bash +# Create private directory +mkdir -p private + +# Copy your private key (base64 encoded, no comments) +echo "YOUR_PRIVATE_KEY_BASE64" > private/sparkle_private_key + +# Ensure it's in .gitignore +echo "private/" >> .gitignore +``` + +## Pre-Release Checklist + +Before starting a release, ensure: + +1. **Version Configuration** + - [ ] Update `MARKETING_VERSION` in `VibeTunnel/version.xcconfig` + - [ ] Increment `CURRENT_PROJECT_VERSION` (build number) + - [ ] Ensure version follows semantic versioning + +2. **Code Quality** + - [ ] All tests pass: `npm test` (in web/) and Swift tests + - [ ] Linting passes: `./scripts/lint.sh` + - [ ] No uncommitted changes: `git status` + +3. **Documentation** + - [ ] Update `CHANGELOG.md` with release notes + - [ ] Version header format: `## X.Y.Z (YYYY-MM-DD)` + - [ ] Include sections: Features, Improvements, Bug Fixes + +4. **Authentication** + - [ ] GitHub CLI authenticated: `gh auth status` + - [ ] Signing certificates valid: `security find-identity -v -p codesigning` + - [ ] Notarization credentials set (environment variables) + +## Release Process + +### Automated Release + +The easiest way to create a release is using the automated script: + +```bash +# For stable release +./scripts/release.sh stable + +# For pre-release (beta, alpha, rc) +./scripts/release.sh beta 1 # Creates 1.0.0-beta.1 +./scripts/release.sh rc 2 # Creates 1.0.0-rc.2 +``` + +The script will: +1. Run pre-flight checks +2. Build the application +3. Sign and notarize +4. Create DMG +5. Upload to GitHub +6. Update appcast files + +### Manual Release Steps + +If you need to run steps manually: + +1. **Run Pre-flight Checks** + ```bash + ./scripts/preflight-check.sh + ``` + +2. **Build Application** + ```bash + # For stable release + ./scripts/build.sh --configuration Release + + # For pre-release + IS_PRERELEASE_BUILD=YES ./scripts/build.sh --configuration Release + ``` + +3. **Sign and Notarize** + ```bash + ./scripts/sign-and-notarize.sh --sign-and-notarize + ``` + +4. **Create DMG** + ```bash + ./scripts/create-dmg.sh build/Build/Products/Release/VibeTunnel.app + ``` + +5. **Notarize DMG** + ```bash + ./scripts/notarize-dmg.sh build/VibeTunnel-X.Y.Z.dmg + ``` + +6. **Create GitHub Release** + ```bash + # Create tag + git tag -a "vX.Y.Z" -m "Release X.Y.Z" + git push origin "vX.Y.Z" + + # Create release + gh release create "vX.Y.Z" \ + --title "VibeTunnel X.Y.Z" \ + --notes "Release notes here" \ + build/VibeTunnel-X.Y.Z.dmg + ``` + +7. **Update Appcast** + ```bash + ./scripts/generate-appcast.sh + git add appcast*.xml + git commit -m "Update appcast for vX.Y.Z" + git push + ``` + +## Post-Release Steps + +1. **Verify Release** + - [ ] Check GitHub release page + - [ ] Download and test the DMG + - [ ] Verify auto-update works from previous version + +2. **Clean Up** + ```bash + # Clean build artifacts (keeps DMG) + ./scripts/clean.sh --keep-dmg + + # Restore development version if needed + git checkout -- VibeTunnel/version.xcconfig + ``` + +3. **Announce Release** + - [ ] Update website/documentation + - [ ] Send release announcement + - [ ] Update issue tracker milestones + +## Troubleshooting + +### Common Issues + +#### "Update isn't properly signed" Error + +This indicates an EdDSA signature mismatch. Causes: +- Wrong private key used for signing +- Appcast not updated after DMG creation +- Cached signatures from different key + +Solution: +1. Ensure correct private key in `private/sparkle_private_key` +2. Regenerate appcast: `./scripts/generate-appcast.sh` +3. Commit and push appcast changes + +#### Build Number Already Exists + +Error: "Build number X already exists in appcast" + +Solution: +1. Increment `CURRENT_PROJECT_VERSION` in `version.xcconfig` +2. Each release must have a unique build number + +#### Notarization Fails + +Common causes: +- Invalid or expired certificates +- Missing API credentials +- Network issues + +Solution: +1. Check credentials: `xcrun notarytool history` +2. Verify certificates: `security find-identity -v` +3. Check console logs for specific errors + +#### Xcode Project Version Mismatch + +If build shows wrong version: +1. Ensure Xcode project uses `$(CURRENT_PROJECT_VERSION)` +2. Not hardcoded values +3. Clean and rebuild + +### Verification Commands + +```bash +# Check signing +codesign -dv --verbose=4 build/VibeTunnel.app + +# Check notarization +spctl -a -t exec -vv build/VibeTunnel.app + +# Verify DMG +hdiutil verify build/VibeTunnel-X.Y.Z.dmg + +# Test EdDSA signature +./build/SourcePackages/artifacts/sparkle/Sparkle/bin/sign_update \ + build/VibeTunnel-X.Y.Z.dmg \ + -f private/sparkle_private_key +``` + +## Lessons Learned + +### Critical Points + +1. **Always Use Correct Sparkle Keys** + - The private key must match the public key in the app + - Store private key securely, never in version control + - Test signature generation before release + +2. **Timestamp All Code Signatures** + - Required for Sparkle components + - Use `--timestamp` flag on all codesign operations + - Prevents "update isn't properly signed" errors + +3. **Version Management** + - Use xcconfig for centralized version control + - Never hardcode versions in Xcode project + - Increment build number for every release + +4. **Pre-flight Validation** + - Always run pre-flight checks + - Ensure clean git state + - Verify all credentials before starting + +5. **Appcast Synchronization** + - Push appcast updates immediately after release + - GitHub serves as the appcast host + - Users fetch from raw.githubusercontent.com + +### Best Practices + +- **Automate Everything**: Use the release script for consistency +- **Test Updates**: Always test auto-update from previous version +- **Keep Logs**: Save notarization logs for debugging +- **Document Issues**: Update this guide when new issues arise +- **Clean Regularly**: Use `clean.sh` to manage disk space + +## Script Reference + +| Script | Purpose | Key Options | +|--------|---------|-------------| +| `release.sh` | Complete automated release | `stable`, `beta N`, `alpha N`, `rc N` | +| `preflight-check.sh` | Validate release readiness | None | +| `build.sh` | Build application | `--configuration Release/Debug` | +| `sign-and-notarize.sh` | Sign and notarize app | `--sign-and-notarize` | +| `create-dmg.sh` | Create DMG installer | ` [output_path]` | +| `notarize-dmg.sh` | Notarize DMG | `` | +| `generate-appcast.sh` | Update appcast files | None | +| `verify-appcast.sh` | Verify appcast validity | None | +| `clean.sh` | Clean build artifacts | `--all`, `--keep-dmg`, `--dry-run` | +| `lint.sh` | Run code linters | None | + +## Environment Setup + +For team members setting up for releases: + +```bash +# 1. Install dependencies +brew install gh +npm install -g swiftformat swiftlint + +# 2. Authenticate GitHub CLI +gh auth login + +# 3. Set up notarization credentials +# Add to ~/.zshrc or ~/.bash_profile: +export APP_STORE_CONNECT_API_KEY_P8="..." +export APP_STORE_CONNECT_KEY_ID="..." +export APP_STORE_CONNECT_ISSUER_ID="..." + +# 4. Get Sparkle private key from secure storage +# Contact team lead for access +``` + +--- + +For questions or issues, consult the script headers or create an issue in the repository. \ No newline at end of file diff --git a/docs/sparkle-keys.md b/docs/sparkle-keys.md new file mode 100644 index 00000000..cae96b10 --- /dev/null +++ b/docs/sparkle-keys.md @@ -0,0 +1,230 @@ +# Sparkle Key Management Guide + +This guide covers the management of EdDSA keys used for signing VibeTunnel updates with the Sparkle framework. + +## Overview + +VibeTunnel uses Sparkle's EdDSA (Ed25519) signatures for secure software updates. This system requires: +- A **private key** (kept secret) for signing updates +- A **public key** (distributed with the app) for verifying signatures + +## Key Locations + +### Public Key +- **Location**: `VibeTunnel/sparkle-public-ed-key.txt` +- **Status**: Committed to repository +- **Usage**: Embedded in app via `SUPublicEDKey` in Info.plist +- **Current Value**: `AGCY8w5vHirVfGGDGc8Szc5iuOqupZSh9pMj/Qs67XI=` + +### Private Key +- **Location**: `private/sparkle_private_key` +- **Status**: NOT in version control (in .gitignore) +- **Usage**: Required for signing updates during release +- **Format**: Base64-encoded key data (no comments or headers) + +## Initial Setup + +### For New Team Members + +1. **Request Access** + ```bash + # Contact team lead for secure key transfer + # Keys are stored in: Dropbox/Backup/Sparkle-VibeTunnel/ + ``` + +2. **Install Private Key** + ```bash + # Create private directory + mkdir -p private + + # Add key file (get content from secure backup) + echo "BASE64_PRIVATE_KEY_HERE" > private/sparkle_private_key + + # Verify it's ignored by git + git status # Should not show private/ + ``` + +3. **Verify Setup** + ```bash + # Test signing with your key + ./build/SourcePackages/artifacts/sparkle/Sparkle/bin/sign_update \ + any_file.dmg \ + -f private/sparkle_private_key + ``` + +### For New Projects + +1. **Generate New Keys** + ```bash + # Build Sparkle tools first + ./scripts/build.sh + + # Generate new key pair + ./build/SourcePackages/artifacts/sparkle/Sparkle/bin/generate_keys + ``` + +2. **Save Keys** + ```bash + # Copy displayed keys: + # Private key: [base64 string] + # Public key: [base64 string] + + # Save private key + mkdir -p private + echo "PRIVATE_KEY_BASE64" > private/sparkle_private_key + + # Save public key + echo "PUBLIC_KEY_BASE64" > VibeTunnel/sparkle-public-ed-key.txt + ``` + +3. **Update App Configuration** + - Add public key to Info.plist under `SUPublicEDKey` + - Commit public key file to repository + +## Key Security + +### Best Practices + +1. **Never Commit Private Keys** + - Private directory is in .gitignore + - Double-check before committing + +2. **Secure Backup** + - Store in encrypted location + - Use password manager or secure cloud storage + - Keep multiple secure backups + +3. **Limited Access** + - Only release managers need private key + - Use secure channels for key transfer + - Rotate keys if compromised + +4. **Key Format** + - Private key file must contain ONLY the base64 key + - No comments, headers, or extra whitespace + - Single line of base64 data + +### Example Private Key Format +``` +SMYPxE98bJ5iLdHTLHTqGKZNFcZLgrT5Hyjh79h3TaU= +``` + +## Troubleshooting + +### "EdDSA signature does not match" Error + +**Cause**: Wrong private key or key format issues + +**Solution**: +1. Verify private key matches public key +2. Check key file has no extra characters +3. Regenerate appcast with correct key + +### "Failed to decode base64 encoded key data" + +**Cause**: Private key file contains comments or headers + +**Solution**: +```bash +# Extract just the key +grep -v '^#' your_key_backup.txt | grep -v '^$' > private/sparkle_private_key +``` + +### Testing Key Pair Match + +```bash +# Sign a test file +echo "test" > test.txt +./build/SourcePackages/artifacts/sparkle/Sparkle/bin/sign_update \ + test.txt \ + -f private/sparkle_private_key + +# The signature should generate successfully +# Compare with production signatures to verify +``` + +## Key Rotation + +If keys need to be rotated: + +1. **Generate New Keys** + ```bash + ./build/SourcePackages/artifacts/sparkle/Sparkle/bin/generate_keys + ``` + +2. **Update App** + - Change `SUPublicEDKey` in Info.plist + - Update `sparkle-public-ed-key.txt` + - Release new version with new public key + +3. **Transition Period** + - Keep old private key for emergency updates + - Sign new updates with new key + - After all users update, retire old key + +## Integration with Release Process + +The release scripts automatically use the private key: + +1. **generate-appcast.sh** + - Expects key at `private/sparkle_private_key` + - Fails if key missing or invalid + - Signs all DMG files in releases + +2. **release.sh** + - Calls generate-appcast.sh after creating DMG + - Ensures signatures are created before pushing + +## Recovery Procedures + +### Lost Private Key + +If private key is lost: +1. Generate new key pair +2. Update app with new public key +3. Release update signed with old key (if possible) +4. All future updates use new key + +### Compromised Private Key + +If private key is compromised: +1. Generate new key pair immediately +2. Release security update with new public key +3. Notify users of security update +4. Revoke compromised key (document publicly) + +## Verification Commands + +### Verify Current Setup +```bash +# Check public key in app +/usr/libexec/PlistBuddy -c "Print :SUPublicEDKey" \ + build/Build/Products/Release/VibeTunnel.app/Contents/Info.plist + +# Check private key exists +ls -la private/sparkle_private_key + +# Test signing +./scripts/generate-appcast.sh --dry-run +``` + +### Verify Release Signatures +```bash +# Check signature in appcast +grep "sparkle:edSignature" appcast-prerelease.xml + +# Manually verify a DMG +./build/SourcePackages/artifacts/sparkle/Sparkle/bin/sign_update \ + build/VibeTunnel-1.0.0.dmg \ + -f private/sparkle_private_key +``` + +## Additional Resources + +- [Sparkle Documentation](https://sparkle-project.org/documentation/) +- [EdDSA on Wikipedia](https://en.wikipedia.org/wiki/EdDSA) +- [Ed25519 Key Security](https://ed25519.cr.yp.to/) + +--- + +For questions about key management, contact the release team lead. \ No newline at end of file diff --git a/scripts/clean.sh b/scripts/clean.sh new file mode 100755 index 00000000..acd10bd1 --- /dev/null +++ b/scripts/clean.sh @@ -0,0 +1,173 @@ +#!/bin/bash + +# ============================================================================= +# VibeTunnel Build Cleanup Script +# ============================================================================= +# +# This script cleans up build artifacts and temporary files to free up disk space. +# +# USAGE: +# ./scripts/clean.sh [options] +# +# OPTIONS: +# --all Clean everything including release DMGs +# --keep-dmg Keep release DMG files (default) +# --dry-run Show what would be deleted without actually deleting +# +# FEATURES: +# - Removes build directories and DerivedData +# - Cleans temporary files and caches +# - Preserves release DMGs by default +# - Shows disk space freed +# +# ============================================================================= + +set -euo pipefail + +# Source common functions +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +[[ -f "$SCRIPT_DIR/common.sh" ]] && source "$SCRIPT_DIR/common.sh" || true + +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +# Parse arguments +CLEAN_ALL=false +KEEP_DMG=true +DRY_RUN=false + +while [[ $# -gt 0 ]]; do + case $1 in + --all) + CLEAN_ALL=true + KEEP_DMG=false + shift + ;; + --keep-dmg) + KEEP_DMG=true + shift + ;; + --dry-run) + DRY_RUN=true + shift + ;; + *) + echo "Unknown option: $1" + echo "Usage: $0 [--all] [--keep-dmg] [--dry-run]" + exit 1 + ;; + esac +done + +# Function to get directory size +get_size() { + local path="$1" + if [[ -d "$path" ]]; then + du -sh "$path" 2>/dev/null | awk '{print $1}' + else + echo "0" + fi +} + +# Function to remove with dry-run support +remove_item() { + local item="$1" + local description="${2:-$item}" + + if [[ -e "$item" ]]; then + local size=$(get_size "$item") + if [[ "$DRY_RUN" == "true" ]]; then + print_info "[DRY RUN] Would remove $description ($size)" + else + print_info "Removing $description ($size)..." + rm -rf "$item" + print_success "Removed $description" + fi + fi +} + +cd "$PROJECT_ROOT" + +print_info "Starting cleanup..." +[[ "$DRY_RUN" == "true" ]] && print_warning "DRY RUN MODE - Nothing will be deleted" + +# Get initial disk usage +INITIAL_SIZE=$(du -sh . 2>/dev/null | awk '{print $1}') + +# Clean build directories +remove_item "build/Build" "Xcode build artifacts" +remove_item "build/ModuleCache" "Module cache" +remove_item "build/SourcePackages" "Source packages" +remove_item "build/dmg-temp" "DMG temporary files" +remove_item "DerivedData" "DerivedData" + +# Clean tty-fwd Rust target (but keep the built binaries) +if [[ "$CLEAN_ALL" == "true" ]]; then + remove_item "tty-fwd/target" "Rust build artifacts" +else + # Keep the release binaries + find tty-fwd/target -type f -name "*.d" -delete 2>/dev/null || true + find tty-fwd/target -type f -name "*.rmeta" -delete 2>/dev/null || true + find tty-fwd/target -type d -name "incremental" -exec rm -rf {} + 2>/dev/null || true + [[ "$DRY_RUN" == "false" ]] && print_success "Cleaned Rust intermediate files" +fi + +# Clean SPM build artifacts +remove_item ".build" "Swift Package Manager build" + +# Clean user-specific Xcode DerivedData +XCODE_DERIVED_DATA="$HOME/Library/Developer/Xcode/DerivedData" +if [[ -d "$XCODE_DERIVED_DATA" ]]; then + for dir in "$XCODE_DERIVED_DATA"/VibeTunnel-*; do + if [[ -d "$dir" ]]; then + remove_item "$dir" "Xcode DerivedData for VibeTunnel" + fi + done +fi + +# Clean temporary files +find . -name ".DS_Store" -delete 2>/dev/null || true +find . -name "*.swp" -delete 2>/dev/null || true +find . -name "*~" -delete 2>/dev/null || true +find . -name "*.tmp" -delete 2>/dev/null || true +[[ "$DRY_RUN" == "false" ]] && print_success "Cleaned temporary files" + +# Clean old DMGs (keep latest) +if [[ "$KEEP_DMG" == "false" ]]; then + remove_item "build/*.dmg" "All DMG files" +else + # Keep only the latest DMG + DMG_COUNT=$(ls -1 build/*.dmg 2>/dev/null | wc -l | tr -d ' ') + if [[ $DMG_COUNT -gt 1 ]]; then + print_info "Keeping latest DMG, removing older ones..." + ls -t build/*.dmg | tail -n +2 | while read dmg; do + remove_item "$dmg" "Old DMG: $(basename "$dmg")" + done + fi +fi + +# Clean node_modules if requested +if [[ "$CLEAN_ALL" == "true" ]]; then + remove_item "web/node_modules" "Node.js dependencies" + remove_item "web/.next" "Next.js build cache" +fi + +# Clean Python caches +find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true +find . -type f -name "*.pyc" -delete 2>/dev/null || true +[[ "$DRY_RUN" == "false" ]] && print_success "Cleaned Python caches" + +# Get final disk usage +FINAL_SIZE=$(du -sh . 2>/dev/null | awk '{print $1}') + +print_success "Cleanup complete!" +print_info "Disk usage: $INITIAL_SIZE → $FINAL_SIZE" + +# Suggest additional cleanups if not using --all +if [[ "$CLEAN_ALL" == "false" ]]; then + echo "" + print_info "For more aggressive cleanup, use: $0 --all" + print_info "This will also remove:" + print_info " - Release DMG files" + print_info " - Node.js dependencies" + print_info " - Rust target directory" +fi \ No newline at end of file diff --git a/scripts/common.sh b/scripts/common.sh new file mode 100644 index 00000000..d2c2e45d --- /dev/null +++ b/scripts/common.sh @@ -0,0 +1,287 @@ +#!/bin/bash + +# ============================================================================= +# VibeTunnel Common Script Library +# ============================================================================= +# +# This file provides common functions and utilities for all VibeTunnel scripts +# to ensure consistency in error handling, logging, and output formatting. +# +# USAGE: +# Source this file at the beginning of your script: +# source "$(dirname "${BASH_SOURCE[0]}")/common.sh" +# +# FEATURES: +# - Consistent color codes for output +# - Error handling and logging functions +# - Common validation functions +# - Progress indicators +# - Platform detection utilities +# +# ============================================================================= + +# Color codes for consistent output +export RED='\033[0;31m' +export GREEN='\033[0;32m' +export YELLOW='\033[1;33m' +export BLUE='\033[0;34m' +export PURPLE='\033[0;35m' +export CYAN='\033[0;36m' +export NC='\033[0m' # No Color + +# Logging levels +export LOG_LEVEL="${LOG_LEVEL:-INFO}" +export LOG_DEBUG=0 +export LOG_INFO=1 +export LOG_WARN=2 +export LOG_ERROR=3 + +# Get current log level +get_log_level() { + case "$LOG_LEVEL" in + DEBUG) echo $LOG_DEBUG ;; + INFO) echo $LOG_INFO ;; + WARN) echo $LOG_WARN ;; + ERROR) echo $LOG_ERROR ;; + *) echo $LOG_INFO ;; + esac +} + +# Logging functions +log_debug() { + [[ $(get_log_level) -le $LOG_DEBUG ]] && echo -e "${CYAN}[DEBUG]${NC} $*" >&2 +} + +log_info() { + [[ $(get_log_level) -le $LOG_INFO ]] && echo -e "${BLUE}[INFO]${NC} $*" +} + +log_warn() { + [[ $(get_log_level) -le $LOG_WARN ]] && echo -e "${YELLOW}[WARN]${NC} $*" >&2 +} + +log_error() { + [[ $(get_log_level) -le $LOG_ERROR ]] && echo -e "${RED}[ERROR]${NC} $*" >&2 +} + +# Success/failure indicators +print_success() { + echo -e "${GREEN}✅ $*${NC}" +} + +print_error() { + echo -e "${RED}❌ $*${NC}" >&2 +} + +print_warning() { + echo -e "${YELLOW}⚠️ $*${NC}" >&2 +} + +print_info() { + echo -e "${BLUE}ℹ️ $*${NC}" +} + +# Error handling with cleanup +error_exit() { + local message="${1:-Unknown error}" + local exit_code="${2:-1}" + print_error "Error: $message" + # Call cleanup function if it exists + if declare -f cleanup >/dev/null; then + log_debug "Running cleanup function" + cleanup + fi + exit "$exit_code" +} + +# Trap handler for errors +setup_error_trap() { + trap 'error_exit "Script failed at line $LINENO"' ERR +} + +# Validate required commands +require_command() { + local cmd="$1" + local install_hint="${2:-}" + + if ! command -v "$cmd" >/dev/null 2>&1; then + print_error "Required command not found: $cmd" + [[ -n "$install_hint" ]] && echo " Install with: $install_hint" + exit 1 + fi +} + +# Validate required environment variables +require_env_var() { + local var_name="$1" + local description="${2:-$var_name}" + + if [[ -z "${!var_name:-}" ]]; then + print_error "Required environment variable not set: $description" + echo " Export $var_name=" + exit 1 + fi +} + +# Validate file exists +require_file() { + local file="$1" + local description="${2:-$file}" + + if [[ ! -f "$file" ]]; then + print_error "Required file not found: $description" + echo " Expected at: $file" + exit 1 + fi +} + +# Validate directory exists +require_dir() { + local dir="$1" + local description="${2:-$dir}" + + if [[ ! -d "$dir" ]]; then + print_error "Required directory not found: $description" + echo " Expected at: $dir" + exit 1 + fi +} + +# Platform detection +is_macos() { + [[ "$OSTYPE" == "darwin"* ]] +} + +is_linux() { + [[ "$OSTYPE" == "linux"* ]] +} + +# Get platform name +get_platform() { + if is_macos; then + echo "macos" + elif is_linux; then + echo "linux" + else + echo "unknown" + fi +} + +# Progress indicator +show_progress() { + local message="$1" + echo -ne "${BLUE}⏳ $message...${NC}\r" +} + +end_progress() { + local message="$1" + local status="${2:-success}" + + # Clear the line + echo -ne "\033[2K\r" + + case "$status" in + success) print_success "$message" ;; + error) print_error "$message" ;; + warning) print_warning "$message" ;; + *) print_info "$message" ;; + esac +} + +# Confirmation prompt +confirm() { + local prompt="${1:-Are you sure?}" + local default="${2:-n}" + + local yn_prompt="[y/N]" + [[ "$default" == "y" ]] && yn_prompt="[Y/n]" + + read -p "$prompt $yn_prompt " -n 1 -r + echo + + if [[ "$default" == "y" ]]; then + [[ ! $REPLY =~ ^[Nn]$ ]] + else + [[ $REPLY =~ ^[Yy]$ ]] + fi +} + +# Version comparison +version_compare() { + # Returns 0 if $1 = $2, 1 if $1 > $2, 2 if $1 < $2 + if [[ "$1" == "$2" ]]; then + return 0 + fi + + local IFS=. + local i ver1=($1) ver2=($2) + + # Fill empty fields in ver1 with zeros + for ((i=${#ver1[@]}; i<${#ver2[@]}; i++)); do + ver1[i]=0 + done + + for ((i=0; i<${#ver1[@]}; i++)); do + if [[ -z ${ver2[i]} ]]; then + # Fill empty fields in ver2 with zeros + ver2[i]=0 + fi + if ((10#${ver1[i]} > 10#${ver2[i]})); then + return 1 + fi + if ((10#${ver1[i]} < 10#${ver2[i]})); then + return 2 + fi + done + return 0 +} + +# Safe temporary file/directory creation +create_temp_file() { + local prefix="${1:-vibetunnel}" + mktemp -t "${prefix}.XXXXXX" +} + +create_temp_dir() { + local prefix="${1:-vibetunnel}" + mktemp -d -t "${prefix}.XXXXXX" +} + +# Cleanup registration +CLEANUP_ITEMS=() + +register_cleanup() { + CLEANUP_ITEMS+=("$1") +} + +cleanup() { + log_debug "Running cleanup for ${#CLEANUP_ITEMS[@]} items" + for item in "${CLEANUP_ITEMS[@]}"; do + if [[ -f "$item" ]]; then + log_debug "Removing file: $item" + rm -f "$item" + elif [[ -d "$item" ]]; then + log_debug "Removing directory: $item" + rm -rf "$item" + fi + done +} + +# Set up cleanup trap +trap cleanup EXIT + +# Export functions for use in subshells +export -f log_debug log_info log_warn log_error +export -f print_success print_error print_warning print_info +export -f error_exit require_command require_env_var require_file require_dir +export -f is_macos is_linux get_platform +export -f show_progress end_progress confirm +export -f version_compare create_temp_file create_temp_dir +export -f register_cleanup cleanup + +# Verify bash version +BASH_MIN_VERSION="4.0" +if ! version_compare "$BASH_VERSION" "$BASH_MIN_VERSION" || [[ $? -eq 2 ]]; then + print_warning "Bash version $BASH_VERSION is older than recommended $BASH_MIN_VERSION" + print_warning "Some features may not work as expected" +fi \ No newline at end of file diff --git a/scripts/create-dmg.sh b/scripts/create-dmg.sh index 0237e20c..bf07ef80 100755 --- a/scripts/create-dmg.sh +++ b/scripts/create-dmg.sh @@ -1,9 +1,28 @@ #!/bin/bash +# ============================================================================= +# VibeTunnel DMG Creation Script +# ============================================================================= +# +# This script creates a DMG disk image for VibeTunnel distribution. +# +# USAGE: +# ./scripts/create-dmg.sh [output_path] +# +# ARGUMENTS: +# app_path Path to the .app bundle +# output_path Path for output DMG (optional, defaults to build/VibeTunnel-.dmg) +# +# ENVIRONMENT VARIABLES: +# DMG_VOLUME_NAME Name for the DMG volume (optional, defaults to app name) +# +# ============================================================================= + set -euo pipefail -# Script to create a DMG for VibeTunnel -# Usage: ./scripts/create-dmg.sh [output_path] +# Source common functions +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +[[ -f "$SCRIPT_DIR/common.sh" ]] && source "$SCRIPT_DIR/common.sh" if [[ $# -lt 1 ]] || [[ $# -gt 2 ]]; then echo "Usage: $0 [output_path]" @@ -20,9 +39,11 @@ if [[ ! -d "$APP_PATH" ]]; then exit 1 fi -# Get version info +# Get app name and version info +APP_NAME=$(/usr/libexec/PlistBuddy -c "Print CFBundleName" "$APP_PATH/Contents/Info.plist" 2>/dev/null || echo "VibeTunnel") VERSION=$(/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" "$APP_PATH/Contents/Info.plist") -DMG_NAME="VibeTunnel-${VERSION}.dmg" +DMG_NAME="${APP_NAME}-${VERSION}.dmg" +DMG_VOLUME_NAME="${DMG_VOLUME_NAME:-$APP_NAME}" # Use provided output path or default if [[ $# -eq 2 ]]; then @@ -46,7 +67,7 @@ ln -s /Applications "$DMG_TEMP/Applications" # Create DMG hdiutil create \ - -volname "VibeTunnel" \ + -volname "$DMG_VOLUME_NAME" \ -srcfolder "$DMG_TEMP" \ -ov \ -format UDZO \ diff --git a/scripts/generate-appcast.sh b/scripts/generate-appcast.sh index 4155cdd7..aadf6dea 100755 --- a/scripts/generate-appcast.sh +++ b/scripts/generate-appcast.sh @@ -19,9 +19,20 @@ if [ -f "$CONFIG_FILE" ]; then fi # Configuration -GITHUB_USERNAME="${GITHUB_USERNAME:-amantus-ai}" -GITHUB_REPO="${GITHUB_USERNAME}/${GITHUB_REPO:-vibetunnel}" -SPARKLE_PRIVATE_KEY_PATH="private/sparkle_private_key" +# Try to extract from git remote if not set +if [[ -z "${GITHUB_USERNAME:-}" ]] || [[ -z "${GITHUB_REPO:-}" ]]; then + GIT_REMOTE_URL=$(git remote get-url origin 2>/dev/null || echo "") + if [[ "$GIT_REMOTE_URL" =~ github\.com[:/]([^/]+)/([^/]+)(\.git)?$ ]]; then + GITHUB_USERNAME="${GITHUB_USERNAME:-${BASH_REMATCH[1]}}" + GITHUB_REPO="${GITHUB_REPO:-${BASH_REMATCH[2]}}" + else + GITHUB_USERNAME="${GITHUB_USERNAME:-amantus-ai}" + GITHUB_REPO="${GITHUB_REPO:-vibetunnel}" + fi +fi + +GITHUB_REPO_FULL="${GITHUB_USERNAME}/${GITHUB_REPO}" +SPARKLE_PRIVATE_KEY_PATH="${SPARKLE_PRIVATE_KEY_PATH:-private/sparkle_private_key}" # Verify private key exists if [ ! -f "$SPARKLE_PRIVATE_KEY_PATH" ]; then @@ -302,7 +313,7 @@ EOF # Main function main() { - print_info "Generating appcast files for $GITHUB_REPO" + print_info "Generating appcast files for $GITHUB_REPO_FULL" # Create temporary directory local temp_dir=$(mktemp -d) @@ -311,13 +322,13 @@ main() { # Fetch all releases from GitHub with error handling print_info "Fetching releases from GitHub..." local releases - if ! releases=$(gh api "repos/$GITHUB_REPO/releases" --paginate 2>/dev/null); then + if ! releases=$(gh api "repos/$GITHUB_REPO_FULL/releases" --paginate 2>/dev/null); then print_error "Failed to fetch releases from GitHub. Please check your GitHub CLI authentication and network connection." exit 1 fi if [ -z "$releases" ] || [ "$releases" = "[]" ]; then - print_warning "No releases found for repository $GITHUB_REPO" + print_warning "No releases found for repository $GITHUB_REPO_FULL" exit 0 fi diff --git a/scripts/lint.sh b/scripts/lint.sh index 685759dc..28720aa2 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -1,28 +1,81 @@ #!/bin/bash -# Swift linting and formatting script +# ============================================================================= +# VibeTunnel Swift Linting and Formatting Script +# ============================================================================= +# +# This script runs SwiftFormat and SwiftLint on the VibeTunnel codebase +# to ensure consistent code style and catch potential issues. +# +# USAGE: +# ./scripts/lint.sh +# +# DEPENDENCIES: +# - swiftformat (brew install swiftformat) +# - swiftlint (brew install swiftlint) +# +# FEATURES: +# - Automatically formats Swift code with SwiftFormat +# - Fixes auto-correctable SwiftLint issues +# - Reports remaining SwiftLint warnings and errors +# +# EXIT CODES: +# 0 - Success (all checks passed) +# 1 - Missing dependencies or linting errors +# +# ============================================================================= + set -euo pipefail -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -PROJECT_ROOT="$( cd "$SCRIPT_DIR/.." && pwd )" +# Source common functions +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +[[ -f "$SCRIPT_DIR/common.sh" ]] && source "$SCRIPT_DIR/common.sh" || true +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +# Change to project root cd "$PROJECT_ROOT" -echo "Running SwiftFormat..." +# Check if project has Swift files +if ! find . -name "*.swift" -not -path "./.build/*" -not -path "./build/*" | head -1 | grep -q .; then + print_warning "No Swift files found in project" + exit 0 +fi + +# Run SwiftFormat +print_info "Running SwiftFormat..." if command -v swiftformat &> /dev/null; then - swiftformat . --verbose + if swiftformat . --verbose; then + print_success "SwiftFormat completed successfully" + else + print_error "SwiftFormat encountered errors" + exit 1 + fi else - echo "SwiftFormat not installed. Install with: brew install swiftformat" + print_error "SwiftFormat not installed" + echo " Install with: brew install swiftformat" exit 1 fi -echo "Running SwiftLint..." +# Run SwiftLint +print_info "Running SwiftLint..." if command -v swiftlint &> /dev/null; then + # First run auto-corrections + print_info "Applying auto-corrections..." swiftlint --fix - swiftlint + + # Then run full lint check + print_info "Checking for remaining issues..." + if swiftlint; then + print_success "SwiftLint completed successfully" + else + print_warning "SwiftLint found issues that require manual attention" + # Don't exit with error as these may be warnings + fi else - echo "SwiftLint not installed. Install with: brew install swiftlint" + print_error "SwiftLint not installed" + echo " Install with: brew install swiftlint" exit 1 fi -echo "✅ Linting complete!" \ No newline at end of file +print_success "Linting complete!" \ No newline at end of file diff --git a/scripts/notarize-app.sh b/scripts/notarize-app.sh index b8345794..110ce236 100755 --- a/scripts/notarize-app.sh +++ b/scripts/notarize-app.sh @@ -1,9 +1,34 @@ #!/bin/bash -# notarize-app.sh - Complete notarization script for VibeTunnel with Sparkle -# Handles hardened runtime, proper signing of all components, and notarization + +# ============================================================================= +# VibeTunnel App Notarization Script +# ============================================================================= +# +# This script handles complete notarization for VibeTunnel including: +# - Hardened runtime signing +# - Proper signing of all components (including Sparkle) +# - Apple notarization submission and stapling +# +# USAGE: +# ./scripts/notarize-app.sh +# +# ARGUMENTS: +# app_path Path to the .app bundle to notarize +# +# ENVIRONMENT VARIABLES: +# SIGN_IDENTITY Developer ID identity (optional) +# APP_STORE_CONNECT_API_KEY_P8 App Store Connect API key +# APP_STORE_CONNECT_KEY_ID API Key ID +# APP_STORE_CONNECT_ISSUER_ID API Issuer ID +# +# ============================================================================= set -eo pipefail +# Source common functions +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +[[ -f "$SCRIPT_DIR/common.sh" ]] && source "$SCRIPT_DIR/common.sh" + # ============================================================================ # Configuration # ============================================================================ @@ -26,9 +51,19 @@ success() { } APP_BUNDLE="${1:-build/Build/Products/Release/VibeTunnel.app}" -SIGN_IDENTITY="Developer ID Application: Peter Steinberger (Y5PE65HELJ)" +# Use environment variable or detect from keychain +SIGN_IDENTITY="${SIGN_IDENTITY:-$(security find-identity -v -p codesigning | grep "Developer ID Application" | head -1 | awk -F'"' '{print $2}')}" TIMEOUT_MINUTES=30 +# Validate signing identity +if [[ -z "$SIGN_IDENTITY" ]]; then + error "No signing identity found. Please set SIGN_IDENTITY environment variable." + echo "Example: export SIGN_IDENTITY=\"Developer ID Application: Your Name (TEAMID)\"" + exit 1 +fi + +log "Using signing identity: $SIGN_IDENTITY" + # Check if app bundle exists if [ ! -d "$APP_BUNDLE" ]; then error "App bundle not found at $APP_BUNDLE" diff --git a/scripts/release.sh b/scripts/release.sh index 7ba61a71..232fe868 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -155,6 +155,25 @@ if [[ ! -x "$SCRIPT_DIR/notarize-dmg.sh" ]]; then 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 +if [[ ! -f "$PROJECT_ROOT/CHANGELOG.md" ]]; then + echo -e "${YELLOW}⚠️ Warning: CHANGELOG.md not found${NC}" + echo " Release notes will be basic" +fi + # Check if we're up to date with origin/main git fetch origin main --quiet LOCAL=$(git rev-parse HEAD) @@ -482,10 +501,21 @@ fi echo "" echo -e "${BLUE}📋 Step 7/9: Creating GitHub release...${NC}" -# Check if tag already exists +# 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!${NC}" - + 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 ""