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.
This commit is contained in:
Peter Steinberger 2025-06-19 05:14:09 +02:00
parent b2b340fd1e
commit ba7d66aa88
9 changed files with 1196 additions and 27 deletions

329
docs/release-guide.md Normal file
View file

@ -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="<your-api-key>"
export APP_STORE_CONNECT_KEY_ID="<your-key-id>"
export APP_STORE_CONNECT_ISSUER_ID="<your-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 | `<app_path> [output_path]` |
| `notarize-dmg.sh` | Notarize DMG | `<dmg_path>` |
| `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.

230
docs/sparkle-keys.md Normal file
View file

@ -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.

173
scripts/clean.sh Executable file
View file

@ -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

287
scripts/common.sh Normal file
View file

@ -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=<value>"
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

View file

@ -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 <app_path> [output_path]
#
# ARGUMENTS:
# app_path Path to the .app bundle
# output_path Path for output DMG (optional, defaults to build/VibeTunnel-<version>.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 <app_path> [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 <app_path> [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 \

View file

@ -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

View file

@ -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!"
print_success "Linting complete!"

View file

@ -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 <app_path>
#
# 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"

View file

@ -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 ""