Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
31 KiB
VibeTunnel Release Documentation
This guide provides comprehensive documentation for creating and publishing releases for VibeTunnel, a macOS menu bar application using Sparkle 2.x for automatic updates.
🚀 Quick Release Commands
Standard Release Flow
# 1. Update versions
vim VibeTunnel/version.xcconfig # Set MARKETING_VERSION and increment CURRENT_PROJECT_VERSION
vim ../web/package.json # Match version with MARKETING_VERSION
# 2. Update changelog
vim CHANGELOG.md # Add entry for new version
# 3. Run release
export SPARKLE_ACCOUNT="VibeTunnel"
./scripts/release.sh beta 5 # For beta.5
./scripts/release.sh stable # For stable release
If Release Script Fails
After Notarization Success
# 1. Create DMG (if missing)
./scripts/create-dmg.sh build/Build/Products/Release/VibeTunnel.app
# 2. Create GitHub release
gh release create "v1.0.0-beta.5" \
--title "VibeTunnel 1.0.0-beta.5" \
--prerelease \
--notes-file RELEASE_NOTES.md \
build/VibeTunnel-*.dmg \
build/VibeTunnel-*.zip
# 3. Get Sparkle signature
sign_update build/VibeTunnel-*.dmg --account VibeTunnel
# 4. Update appcast manually (add to appcast-prerelease.xml)
# 5. Commit and push
git add ../appcast-prerelease.xml
git commit -m "Update appcast for v1.0.0-beta.5"
git push
🎯 Release Process Overview
VibeTunnel uses an automated release process that handles all the complexity of:
- Building universal binaries containing both arm64 (Apple Silicon) and x86_64 (Intel)
- Code signing and notarization with Apple
- Creating DMG and ZIP files
- Publishing to GitHub
- Updating Sparkle appcast files with EdDSA signatures
⚠️ Version Management Best Practices
Critical Version Rules
-
Version Configuration Source of Truth
- ALL version information is stored in
VibeTunnel/version.xcconfig - The Xcode project must reference these values using
$(MARKETING_VERSION)and$(CURRENT_PROJECT_VERSION) - NEVER hardcode versions in the Xcode project
- ALL version information is stored in
-
Pre-release Version Suffixes
- For pre-releases, the suffix MUST be in version.xcconfig BEFORE running release.sh
- Example: To release beta 2, set
MARKETING_VERSION = 1.0.0-beta.2in version.xcconfig - The release script will NOT add suffixes - it uses the version exactly as configured
-
Build Number Management
- Build numbers MUST be incremented for EVERY release (including pre-releases)
- Build numbers MUST be monotonically increasing
- Sparkle uses build numbers, not version strings, to determine if an update is available
Common Version Management Mistakes
❌ MISTAKE: Running ./scripts/release.sh beta 2 when version.xcconfig already has 1.0.0-beta.2
- Result: Creates version
1.0.0-beta.2-beta.2(double suffix) - Fix: The release type and number are only for tagging, not version modification
❌ MISTAKE: Forgetting to increment build number
- Result: Sparkle won't detect the update even with a new version
- Fix: Always increment CURRENT_PROJECT_VERSION in version.xcconfig
❌ MISTAKE: Hardcoding versions in Xcode project instead of using version.xcconfig
- Result: Version mismatches between built app and expected version
- Fix: Ensure Xcode project uses
$(MARKETING_VERSION)and$(CURRENT_PROJECT_VERSION)
Version Workflow Example
For releasing 1.0.0-beta.2:
-
Edit version.xcconfig:
MARKETING_VERSION = 1.0.0-beta.2 # Add suffix here CURRENT_PROJECT_VERSION = 105 # Increment from previous build -
Verify configuration:
./scripts/preflight-check.sh # This will warn if version already has a suffix -
Run release:
./scripts/release.sh beta 2 # The "beta 2" parameters are ONLY for git tagging
📋 Pre-Release Checklist
Before running ANY release commands, verify these items:
Environment Setup
- Ensure stable internet connection (notarization requires consistent connectivity)
- Check Apple Developer status page for any service issues
- Have at least 30 minutes available (full release takes 15-20 minutes)
- Close other resource-intensive applications
- Ensure you're on main branch
git checkout main git pull --rebase origin main git status # Check for uncommitted changes
Version Verification
-
⚠️ CRITICAL: Version in version.xcconfig is EXACTLY what you want to release
grep MARKETING_VERSION VibeTunnel/version.xcconfig # For beta.2 should show: MARKETING_VERSION = 1.0.0-beta.2 # NOT: MARKETING_VERSION = 1.0.0⚠️ WARNING: The release script uses this version AS-IS. It will NOT add suffixes!
-
Build number is incremented
grep CURRENT_PROJECT_VERSION VibeTunnel/version.xcconfig # Must be higher than the last release -
Web package.json version matches macOS version
# Check web version matches macOS version grep '"version"' ../web/package.json # Should match MARKETING_VERSION from version.xcconfig⚠️ IMPORTANT: The web frontend version must be synchronized with the macOS app version!
-
CHANGELOG.md has entry for this version
grep "## \[1.0.0-beta.2\]" CHANGELOG.md # Must exist with release notes
Environment Variables
- Set required environment variables:
export SPARKLE_ACCOUNT="VibeTunnel" export APP_STORE_CONNECT_KEY_ID="YOUR_KEY_ID" export APP_STORE_CONNECT_ISSUER_ID="YOUR_ISSUER_ID" export APP_STORE_CONNECT_API_KEY_P8="-----BEGIN PRIVATE KEY----- YOUR_PRIVATE_KEY_CONTENT -----END PRIVATE KEY-----"
Clean Build
- Clean build and derived data if needed:
./scripts/clean.sh rm -rf build DerivedData rm -rf ~/Library/Developer/Xcode/DerivedData/VibeTunnel-*
File Verification
- CHANGELOG.md exists and has entry for new version
- Sparkle private key exists at expected location
- No stuck DMG volumes in /Volumes/
# Check for stuck volumes ls /Volumes/VibeTunnel* # Unmount if needed for volume in /Volumes/VibeTunnel*; do hdiutil detach "$volume" -force done
🚀 Creating a Release
Step 1: Pre-flight Check
./scripts/preflight-check.sh
This validates your environment is ready for release.
Step 2: CRITICAL Pre-Release Version Check
IMPORTANT: Before running the release script, ensure your version.xcconfig is set correctly:
- For beta releases: The MARKETING_VERSION should already include the suffix (e.g.,
1.0.0-beta.2) - The release script will NOT add additional suffixes - it uses the version as-is
- Always verify the version before proceeding:
grep MARKETING_VERSION VibeTunnel/version.xcconfig # Should show: MARKETING_VERSION = 1.0.0-beta.2
Common Mistake: If the version is already 1.0.0-beta.2 and you run ./scripts/release.sh beta 2,
it will create 1.0.0-beta.2-beta.2 which is wrong!
Step 3: Create/Update CHANGELOG.md
Before creating any release, ensure the CHANGELOG.md file exists and contains a proper section for the version being released. If this is your first release, create a CHANGELOG.md file in the project root:
# Changelog
All notable changes to VibeTunnel will be documented in this file.
## [1.0.0-beta.2] - 2025-06-19
### 🎨 UI Improvements
- **Enhanced feature** - Description of the improvement
...
CRITICAL: The appcast generation relies on the local CHANGELOG.md file, NOT the GitHub release description. The changelog must be added to CHANGELOG.md BEFORE running the release script.
Step 4: Create the Release
⚠️ CRITICAL UNDERSTANDING: The release script parameters are ONLY for:
- Git tag creation
- Determining if it's a pre-release on GitHub
- Validation that your version.xcconfig matches your intent
The script will NEVER modify the version - it uses version.xcconfig exactly as configured!
For long-running operations, consider using screen or tmux:
# Run in a screen/tmux session to prevent disconnection
screen -S release
./scripts/release.sh beta 5 --verbose --log
# For stable releases:
./scripts/release.sh stable
# Expects version.xcconfig to have a stable version like "1.0.0"
# For pre-releases:
./scripts/release.sh beta 2
# Expects version.xcconfig to ALREADY have "1.0.0-beta.2"
# If version.xcconfig has "1.0.0", the script will add suffix and create "1.0.0-beta.2"
# If version.xcconfig already has "1.0.0-beta.2", it will use it as-is
Script Validation: The release script now includes:
- Double-suffix detection (prevents 1.0.0-beta.2-beta.2)
- Build number uniqueness check
- Version consistency verification
- Notarization credential validation
IMPORTANT: The release script does NOT automatically increment build numbers. You must manually update the build number in VibeTunnel.xcodeproj before running the script, or it will fail the pre-flight check.
The script will:
- Validate build number is unique and incrementing
- Build, sign, and notarize the app
- Create a DMG
- Publish to GitHub
- Update the appcast files with EdDSA signatures
- Commit and push all changes
Note: Notarization can take 5-10 minutes depending on Apple's servers. This is normal.
Step 5: Verify Success
- Check the GitHub releases page
- Verify the appcast was updated correctly with proper changelog content
- Critical: Verify the Sparkle signature is correct:
# Download and verify the DMG signature curl -L -o test.dmg <github-dmg-url> sign_update test.dmg --account VibeTunnel # Compare with appcast sparkle:edSignature - Test updating from a previous version
- Important: Verify that the Sparkle update dialog shows the formatted changelog, not HTML tags
- Check that update installs without "improperly signed" errors
If Interrupted
If the release script is interrupted:
./scripts/check-release-status.sh 1.0.0-beta.5
./scripts/release.sh --resume
🛠️ Manual Process (If Needed)
If the automated script fails, here's the manual process:
1. Update Version Numbers
Edit version configuration files:
macOS App (VibeTunnel/version.xcconfig):
- Update MARKETING_VERSION
- Update CURRENT_PROJECT_VERSION (build number)
Web Frontend (../web/package.json):
- Update "version" field to match MARKETING_VERSION
Note: The Xcode project file is named VibeTunnel-Mac.xcodeproj
2. Clean and Build Universal Binary
rm -rf build DerivedData
./scripts/build.sh --configuration Release
3. Sign and Notarize
./scripts/sign-and-notarize.sh build/Build/Products/Release/VibeTunnel.app
4. Create DMG and ZIP
./scripts/create-dmg.sh build/Build/Products/Release/VibeTunnel.app
./scripts/create-zip.sh build/Build/Products/Release/VibeTunnel.app
5. Sign DMG for Sparkle
export PATH="$HOME/.local/bin:$PATH"
sign_update build/VibeTunnel-X.X.X.dmg
6. Create GitHub Release
gh release create "v1.0.0-beta.1" \
--title "VibeTunnel 1.0.0-beta.1" \
--notes "Beta release 1" \
--prerelease \
build/VibeTunnel-*.dmg \
build/VibeTunnel-*.zip
7. Update Appcast
./scripts/update-appcast.sh
git add appcast*.xml
git commit -m "Update appcast for v1.0.0-beta.1"
git push
🔍 Verification Commands
# Check release artifacts
ls -la build/VibeTunnel-*.dmg
ls -la build/VibeTunnel-*.zip
# Check GitHub release
gh release view v1.0.0-beta.5
# Verify Sparkle signature
curl -L -o test.dmg [github-dmg-url]
sign_update test.dmg --account VibeTunnel
# Check appcast
grep "1.0.0-beta.5" ../appcast-prerelease.xml
# Verify app in DMG
hdiutil attach test.dmg
spctl -a -vv /Volumes/VibeTunnel/VibeTunnel.app
hdiutil detach /Volumes/VibeTunnel
⚠️ Critical Requirements
1. Build Numbers MUST Increment
Sparkle uses build numbers (CFBundleVersion) to determine updates, NOT version strings!
| Version | Build | Result |
|---|---|---|
| 1.0.0-beta.1 | 100 | ✅ |
| 1.0.0-beta.2 | 101 | ✅ |
| 1.0.0-beta.3 | 99 | ❌ Build went backwards |
| 1.0.0 | 101 | ❌ Duplicate build number |
2. Required Environment Variables
export APP_STORE_CONNECT_KEY_ID="YOUR_KEY_ID"
export APP_STORE_CONNECT_ISSUER_ID="YOUR_ISSUER_ID"
export APP_STORE_CONNECT_API_KEY_P8="-----BEGIN PRIVATE KEY-----
YOUR_PRIVATE_KEY_CONTENT
-----END PRIVATE KEY-----"
3. Prerequisites
- Xcode 16.4+ installed
- Node.js 20+ and Bun (for web frontend build)
# Install Bun curl -fsSL https://bun.sh/install | bash - GitHub CLI authenticated:
gh auth status - Apple Developer ID certificate in Keychain
- Sparkle tools in
~/.local/bin/(sign_update, generate_appcast)
🔐 Sparkle Configuration
Sparkle Requirements for Non-Sandboxed Apps
VibeTunnel is not sandboxed, which simplifies Sparkle configuration:
1. Entitlements (VibeTunnel.entitlements)
<!-- App is NOT sandboxed -->
<key>com.apple.security.app-sandbox</key>
<false/>
<!-- Required for code injection/library validation -->
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
2. Info.plist Configuration
"SUEnableInstallerLauncherService": false, // Not needed for non-sandboxed apps
"SUEnableDownloaderService": false, // Not needed for non-sandboxed apps
3. Code Signing Requirements
The notarization script handles all signing correctly:
- Do NOT use --deep flag when signing the app
- Sign the app with hardened runtime and entitlements
The notarize-app.sh script should sign the app:
# Sign the app WITHOUT --deep flag
codesign --force --sign "Developer ID Application" --entitlements VibeTunnel.entitlements --options runtime VibeTunnel.app
Architecture Support
VibeTunnel uses universal binaries that include both architectures:
- Apple Silicon (arm64): Optimized for M1+ Macs
- Intel (x86_64): For Intel-based Macs
The build system creates a single universal binary that works on all Mac architectures. This approach:
- Simplifies distribution with one DMG/ZIP per release
- Works seamlessly with Sparkle auto-updates
- Provides optimal performance on each architecture
- Follows Apple's recommended best practices
📋 Update Channels
VibeTunnel supports two update channels:
-
Stable Channel (
appcast.xml)- Production releases only
- Default for all users
-
Pre-release Channel (
appcast-prerelease.xml)- Includes beta, alpha, and RC versions
- Users opt-in via Settings
🐛 Common Issues and Solutions
Version and Build Number Issues
Double Version Suffix (e.g., 1.0.0-beta.2-beta.2)
Problem: Version has double suffix after running release script.
Cause: version.xcconfig already had the suffix, and you provided the same suffix to release.sh.
Solution:
- Clean up the botched release:
# Delete the bad tag git tag -d v1.0.0-beta.2-beta.2 git push origin :refs/tags/v1.0.0-beta.2-beta.2 # Delete the GitHub release gh release delete v1.0.0-beta.2-beta.2 --yes # Fix version.xcconfig # Set to the correct version without double suffix - Re-run the release with correct parameters
Build Script Reports Version Mismatch
Problem: Build script warns that built version doesn't match version.xcconfig.
Cause: Xcode project is not properly configured to use version.xcconfig values.
Solution:
- Open VibeTunnel.xcodeproj in Xcode
- Select the project, then the target
- In Build Settings, ensure:
- MARKETING_VERSION =
$(MARKETING_VERSION) - CURRENT_PROJECT_VERSION =
$(CURRENT_PROJECT_VERSION)
- MARKETING_VERSION =
Preflight Check Warns About Existing Suffix
Problem: Preflight check shows "Version already contains pre-release suffix".
Solution: This is a helpful warning! It reminds you to use matching parameters:
# If version.xcconfig has "1.0.0-beta.2"
./scripts/release.sh beta 2 # Correct - matches the suffix
Common Version Sync Issues
Web Version Out of Sync
Problem: Web server shows different version than macOS app (e.g., "beta.3" when app is "beta.4").
Cause: web/package.json was not updated when BuildNumber.xcconfig was changed.
Solution:
-
Update package.json to match BuildNumber.xcconfig:
# Check current versions grep MARKETING_VERSION VibeTunnel/version.xcconfig grep "version" ../web/package.json # Update web version to match vim ../web/package.json -
Validate sync before building:
cd ../web && node scripts/validate-version-sync.js
Note: The web UI automatically displays the version from package.json (injected at build time).
"Uncommitted changes detected"
git status --porcelain # Check what's changed
git stash # Temporarily store changes
# Run release
git stash pop # Restore changes
Appcast Shows HTML Tags Instead of Formatted Text
Problem: Sparkle update dialog shows escaped HTML like <h2> instead of formatted text.
Root Cause: The generate-appcast.sh script is escaping HTML content from GitHub release descriptions.
Solution:
- Ensure CHANGELOG.md has the proper section for the release version BEFORE running release script
- The appcast should use local CHANGELOG.md, not GitHub release body
- If the appcast is wrong, manually fix the generate-appcast.sh script to use local changelog content
Build Numbers Not Incrementing
Problem: Sparkle doesn't detect new version as an update.
Solution: Always increment the build number in the Xcode project before releasing.
Stuck DMG Volumes
Problem: "Resource temporarily unavailable" errors when creating DMG.
Symptoms:
hdiutil: create failed - Resource temporarily unavailable- Multiple VibeTunnel volumes visible in Finder
- DMG creation fails repeatedly
Solution:
# Manually unmount all VibeTunnel volumes
for volume in /Volumes/VibeTunnel*; do
hdiutil detach "$volume" -force
done
# Kill any stuck DMG processes
pkill -f "VibeTunnel.*\.dmg"
Prevention: Scripts now clean up volumes automatically before DMG creation.
Build Number Already Exists
Problem: Sparkle requires unique build numbers for each release.
Solution:
- Check existing build numbers:
grep -E '<sparkle:version>[0-9]+</sparkle:version>' ../appcast*.xml - Update
mac/VibeTunnel/version.xcconfig:CURRENT_PROJECT_VERSION = <new_unique_number>
Notarization Failures
Problem: App notarization fails or takes too long.
Common Causes:
- Missing API credentials
- Network issues
- Apple service outages
- Unsigned frameworks or binaries
Solution:
# Check notarization status
xcrun notarytool history --key-id "$APP_STORE_CONNECT_KEY_ID" \
--key "$APP_STORE_CONNECT_API_KEY_P8" \
--issuer-id "$APP_STORE_CONNECT_ISSUER_ID"
# Get detailed log for failed submission
xcrun notarytool log <submission-id> --key-id ...
Normal Duration: Notarization typically takes 2-10 minutes. If it's taking longer than 15 minutes, check Apple System Status.
GitHub Release Already Exists
Problem: Tag or release already exists on GitHub.
Solution: The release script now prompts you to:
- Delete the existing release and tag
- Cancel the release
Prevention: Always pull latest changes before releasing.
DMG Shows "Unnotarized Developer ID"
Problem: The DMG shows as "Unnotarized Developer ID" when checked with spctl.
Explanation: This is NORMAL - DMGs are not notarized themselves, only the app inside is notarized. Check the app inside: it should show "Notarized Developer ID".
Generate Appcast Fails
Problem: generate-appcast.sh failed with GitHub API error despite valid authentication.
Workaround:
- Manually add entry to appcast-prerelease.xml
- Use signature from:
sign_update [dmg] --account VibeTunnel - Follow existing entry format (see template below)
🔧 Troubleshooting Common Issues
Script Timeouts
If the release script times out:
- Check
.release-statefor the last successful step - Run
./scripts/release.sh --resumeto continue - Or manually complete remaining steps (see Manual Recovery below)
Manual Recovery Steps
If automated release fails after notarization:
-
Create DMG (if missing):
./scripts/create-dmg.sh build/Build/Products/Release/VibeTunnel.app -
Create GitHub Release:
gh release create "v$VERSION" \ --title "VibeTunnel $VERSION" \ --notes-file RELEASE_NOTES.md \ --prerelease \ build/VibeTunnel-*.dmg \ build/VibeTunnel-*.zip -
Sign DMG for Sparkle:
export SPARKLE_ACCOUNT="VibeTunnel" sign_update build/VibeTunnel-$VERSION.dmg --account VibeTunnel -
Update Appcast Manually:
- Add entry to appcast-prerelease.xml with signature from step 3
- Commit and push:
git add appcast*.xml && git commit -m "Update appcast" && git push
"Update is improperly signed" Error
Problem: Users see "The update is improperly signed and could not be validated."
Cause: The DMG was signed with the wrong Sparkle key (default instead of VibeTunnel account).
Quick Fix:
# 1. Download the DMG from GitHub
curl -L -o fix.dmg <github-dmg-url>
# 2. Generate correct signature
sign_update fix.dmg --account VibeTunnel
# 3. Update appcast-prerelease.xml with the new sparkle:edSignature
# 4. Commit and push
Prevention: The updated scripts now always use --account VibeTunnel.
Debug Sparkle Updates
# Monitor VibeTunnel logs
log stream --predicate 'process == "VibeTunnel"' --level debug
# Check XPC errors
log stream --predicate 'process == "VibeTunnel"' | grep -i -E "(sparkle|xpc|installer)"
# Verify XPC services
codesign -dvv "VibeTunnel.app/Contents/Frameworks/Sparkle.framework/Versions/B/XPCServices/Installer.xpc"
Verify Signing and Notarization
# Check app signature
./scripts/verify-app.sh build/VibeTunnel-1.0.0.dmg
# Verify XPC bundle IDs (should be org.sparkle-project.*)
/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" \
"VibeTunnel.app/Contents/Frameworks/Sparkle.framework/Versions/B/XPCServices/Installer.xpc/Contents/Info.plist"
Appcast Issues
# Verify appcast has correct build numbers
./scripts/verify-appcast.sh
# Check if build number is "1" (common bug)
grep '<sparkle:version>' appcast-prerelease.xml
📝 Appcast Entry Template
<item>
<title>VibeTunnel VERSION</title>
<link>https://github.com/amantus-ai/vibetunnel/releases/download/vVERSION/VibeTunnel-VERSION.dmg</link>
<sparkle:version>BUILD_NUMBER</sparkle:version>
<sparkle:shortVersionString>VERSION</sparkle:shortVersionString>
<description><![CDATA[
<h2>VibeTunnel VERSION</h2>
<p><strong>Pre-release version</strong></p>
<!-- Copy from CHANGELOG.md -->
]]></description>
<pubDate>DATE</pubDate>
<enclosure url="https://github.com/amantus-ai/vibetunnel/releases/download/vVERSION/VibeTunnel-VERSION.dmg"
sparkle:version="BUILD_NUMBER"
sparkle:shortVersionString="VERSION"
length="SIZE_IN_BYTES"
type="application/x-apple-diskimage"
sparkle:edSignature="SIGNATURE_FROM_SIGN_UPDATE"/>
</item>
🎯 Release Success Criteria
- GitHub release created with both DMG and ZIP
- DMG downloads and mounts correctly
- App inside DMG shows as notarized
- Appcast updated and pushed
- Sparkle signature in appcast matches DMG
- Version and build numbers correct everywhere
- Previous version can update via Sparkle
🚨 Emergency Fixes
Wrong Sparkle Signature
# 1. Get correct signature
sign_update [dmg-url] --account VibeTunnel
# 2. Update appcast-prerelease.xml with correct signature
# 3. Commit and push immediately
Missing from Appcast
# Users won't see update until appcast is fixed
# Add entry manually following template above
# Test with: curl https://raw.githubusercontent.com/amantus-ai/vibetunnel/main/appcast-prerelease.xml
Build Number Conflict
# If Sparkle complains about duplicate build number
# Increment build number in version.xcconfig
# Create new release with higher build number
# Old release will be ignored by Sparkle
🔍 Key File Locations
Important: Files are not always where scripts expect them to be.
Key Locations:
- Appcast files: Located in project root (
/vibetunnel/), NOT inmac/appcast.xmlappcast-prerelease.xml
- CHANGELOG.md: Can be in either:
mac/CHANGELOG.md(preferred by release script)- Project root
/vibetunnel/CHANGELOG.md(common location)
- Sparkle private key: Usually in
mac/private/sparkle_private_key
📚 Common Commands
Test Sparkle Signature
# Find sign_update binary
find . -name sign_update -type f
# Test signing with specific account
./path/to/sign_update file.dmg -f private/sparkle_private_key -p --account VibeTunnel
Verify Appcast URLs
# Check that appcast files are accessible
curl -I https://raw.githubusercontent.com/amantus-ai/vibetunnel/main/appcast.xml
curl -I https://raw.githubusercontent.com/amantus-ai/vibetunnel/main/appcast-prerelease.xml
Manual Appcast Generation
# If automatic generation fails
cd mac
export SPARKLE_ACCOUNT="VibeTunnel"
./scripts/generate-appcast.sh
Release Status Script
Create scripts/check-release-status.sh:
#!/bin/bash
VERSION=$1
echo "Checking release status for v$VERSION..."
# Check local artifacts
echo -n "✓ Local DMG: "
[ -f "build/VibeTunnel-$VERSION.dmg" ] && echo "EXISTS" || echo "MISSING"
echo -n "✓ Local ZIP: "
[ -f "build/VibeTunnel-$VERSION.zip" ] && echo "EXISTS" || echo "MISSING"
# Check GitHub
echo -n "✓ GitHub Release: "
gh release view "v$VERSION" &>/dev/null && echo "EXISTS" || echo "MISSING"
# Check appcast
echo -n "✓ Appcast Entry: "
grep -q "$VERSION" ../appcast-prerelease.xml && echo "EXISTS" || echo "MISSING"
📋 Post-Release Verification
-
Check GitHub Release:
- Verify assets are attached
- Check file sizes match
- Ensure release notes are formatted correctly
-
Test Update in App:
- Install previous version
- Check for updates
- Verify update downloads and installs
- Check signature verification in Console.app
-
Monitor for Issues:
- Watch Console.app for Sparkle errors
- Check GitHub issues for user reports
- Verify download counts on GitHub
🛠️ Recommended Script Improvements
Based on release experience, consider implementing:
1. Release Script Enhancements
Add state tracking for resumability:
# Add to release.sh
# State file to track progress
STATE_FILE=".release-state"
# Save state after each major step
save_state() {
echo "$1" > "$STATE_FILE"
}
# Resume from last state
resume_from_state() {
if [ -f "$STATE_FILE" ]; then
LAST_STATE=$(cat "$STATE_FILE")
echo "Resuming from: $LAST_STATE"
fi
}
# Add --resume flag handling
if [[ "$1" == "--resume" ]]; then
resume_from_state
shift
fi
2. Better Progress Reporting
# Add progress function
progress() {
local step=$1
local total=$2
local message=$3
echo "[${step}/${total}] ${message}"
}
# Use throughout script
progress 1 8 "Running pre-flight checks..."
progress 2 8 "Building application..."
3. Parallel Operations
Where possible, run independent operations in parallel:
# Run signing and changelog generation in parallel
{
sign_app &
PID1=$!
generate_changelog &
PID2=$!
wait $PID1 $PID2
}
📝 Key Learnings
- Always use explicit accounts when dealing with signing operations
- Clean up resources (volumes, processes) before operations
- Verify file locations - don't assume standard paths
- Test the full update flow before announcing the release
- Keep credentials secure but easily accessible for scripts
- Document everything - future you will thank present you
- Plan for long-running operations - notarization can take 10+ minutes
- Implement resumable workflows - scripts should handle interruptions gracefully
- DMG signing is separate from notarization - DMGs themselves aren't notarized, only the app inside
- Command timeouts are a real issue - use screen/tmux for releases
Additional Lessons from v1.0.0-beta.5 Release
DMG Notarization Confusion
Issue: The DMG shows as "Unnotarized Developer ID" when checked with spctl, but this is normal. Explanation:
- DMGs are not notarized themselves - only the app inside is notarized
- The app inside the DMG shows correctly as "Notarized Developer ID"
- This is expected behavior and not an error
Release Script Timeout Handling
Issue: Release script timed out during notarization (took ~5 minutes). Solution:
- Run release scripts in a terminal without timeout constraints
- Consider using
screenortmuxfor long operations - Add progress indicators to show the script is still running
Appcast Generation Failures
Issue: generate-appcast.sh failed with GitHub API errors despite valid auth.
Workaround:
- Manually create appcast entries when automation fails
- Always verify the Sparkle signature with
sign_update --account VibeTunnel - Keep a template of appcast entries for quick manual updates
🚀 Long-term Improvements
- CI/CD Integration: Move releases to GitHub Actions for reliability
- Release Dashboard: Web UI showing release progress and status
- Automated Testing: Test Sparkle updates in CI before publishing
- Rollback Capability: Script to quickly revert a bad release
- Release Templates: Pre-configured release notes and changelog formats
- Monitoring Improvements: Add detailed logging with timestamps and metrics
Summary
The VibeTunnel release process is complex but well-automated. The main challenges are:
- Command timeouts during long operations (especially notarization)
- Lack of resumability after failures
- Missing progress indicators
- No automated recovery options
- File location confusion
Following this guide and implementing the suggested improvements will make releases more reliable and less stressful, especially when using tools with timeout constraints.
Remember: Always use the automated release script, ensure build numbers increment, and test updates before announcing!