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
If Release Script Fails
After Notarization Success
🎯 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.2
in 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
- Result: Sparkle won’t detect the update even with a new version
- Fix: Always increment CURRENT_PROJECT_VERSION in 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:
-
Verify configuration:
-
Run release:
📋 Pre-Release Checklist
Automated Checklist: Run./scripts/release-checklist.sh
for an interactive pre-release validation.
Before running ANY release commands, verify these items:
⚠️ CRITICAL: Sparkle Signature Verification
- Verify private key exists at
private/sparkle_private_key
- Confirm you will use the
-f
flag with ALL sign_update commands - Test sign a dummy file to ensure correct key:
- NEVER use sign_update without the
-f
flag! - The public key in Info.plist is:
AGCY8w5vHirVfGGDGc8Szc5iuOqupZSh9pMj/Qs67XI=
- Run signature validation script:
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
Version Verification
-
⚠️ CRITICAL: Version in version.xcconfig is EXACTLY what you want to release
⚠️ WARNING: The release script uses this version AS-IS. It will NOT add suffixes!
-
Build number is incremented
-
Web package.json version matches macOS version
⚠️ IMPORTANT: The web frontend version must be synchronized with the macOS app version!
-
CHANGELOG.md has entry for this version
Environment Variables
- Set required environment variables:
Clean Build
- Clean build and derived data if needed:
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 unexpected files in the app bundle
🚀 Creating a Release
Step 1: Pre-flight Check
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:
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 in the project root (/vibetunnel/CHANGELOG.md
) and contains a proper section for the version being released:
- Location: CHANGELOG.md must be at
/vibetunnel/CHANGELOG.md
(project root, NOT inmac/
) - No RELEASE_NOTES.md files: The release process does NOT use RELEASE_NOTES.md files
- Per-Version Extraction: The release script automatically extracts ONLY the changelog section for the specific version being released
- GitHub Release: Uses the extracted markdown content directly (via
generate-release-notes.sh
) - Sparkle Appcast: Converts the markdown to HTML for update dialogs
generate-release-notes.sh
- Extracts markdown release notes for GitHubchangelog-to-html.sh
- Converts markdown to HTML for Sparkle appcastfind-changelog.sh
- Reliably locates CHANGELOG.md from any directory
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
- Double-suffix detection (prevents 1.0.0-beta.2-beta.2)
- Build number uniqueness check
- Version consistency verification
- Notarization credential validation
- 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
Step 5: Verify Success
- Check the GitHub releases page
- IMPORTANT: Verify the GitHub release shows ONLY the current version’s changelog, not the entire history
- If it shows the full changelog, the release notes were not generated correctly
- The release should only show changes for that specific version (e.g., beta.10 shows only beta.10 changes)
- Monitor app size: Verify the DMG size is reasonable (expected: ~42-44 MB)
- If size increased significantly (>5MB), investigate for bundled development files
- Verify the appcast was updated correctly with proper changelog content
- Critical: Verify the Sparkle signature is correct:
- Test updating from a previous version
- Important: Verify that the Sparkle update dialog shows the formatted changelog, not HTML tags
- CRITICAL: Check that update installs without “improperly signed” errors
- If you get “improperly signed” error, the appcast has wrong signature
- Regenerate with:
sign_update -f private/sparkle_private_key [dmg-file]
- Update appcast XML with correct signature
- Run
./scripts/validate-sparkle-signature.sh
to verify all signatures
- Verify Stats.store is serving the updated appcast (1-minute cache):
If Interrupted
If the release script is interrupted:🛠️ 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/package.json
):
- Update “version” field to match MARKETING_VERSION
VibeTunnel-Mac.xcodeproj
2. Clean and Build Universal Binary
3. Sign and Notarize
4. Create DMG and ZIP
5. Sign DMG for Sparkle
6. Create GitHub Release
7. Update Appcast
🔍 Verification Commands
⚠️ 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
3. Prerequisites
- Xcode 16.4+ installed
- Node.js 20+ and Bun (for web frontend build)
- GitHub CLI authenticated:
gh auth status
- Apple Developer ID certificate in Keychain
- Sparkle tools in
~/.local/bin/
(sign_update, generate_appcast)
🔐 Sparkle Configuration
⚠️ CRITICAL: Sparkle Private Key Management
ALWAYS use the file-based private key for signing! VibeTunnel uses EdDSA signatures for Sparkle updates. The correct private key is stored at:private/sparkle_ed_private_key
(clean key file - REQUIRED for sign_update)private/sparkle_private_key
(commented version for documentation)
- File-based key (CORRECT) - Matches the public key in Info.plist
- Keychain key (WRONG) - May produce incompatible signatures
-f
flag when signing:
AGCY8w5vHirVfGGDGc8Szc5iuOqupZSh9pMj/Qs67XI=
Key File Format Requirements:
- The clean key file (
sparkle_ed_private_key
) must contain ONLY the base64 key - No comments, no extra lines, just the key:
SMYPxE98bJ5iLdHTLHTqGKZNFcZLgrT5Hyjh79h3TaU=
- The scripts handle this automatically by extracting from the commented file
Sparkle Requirements for Non-Sandboxed Apps
VibeTunnel is not sandboxed, which simplifies Sparkle configuration:1. Entitlements (VibeTunnel.entitlements)
2. Info.plist Configuration
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
notarize-app.sh
script should sign the 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
- 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:
- 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:App Size Issues
Unexpected Size Increase
Problem: DMG size increased significantly (>5MB) between releases. Common Causes:- Development dependencies bundled: node_modules, JAR files, or other dev files
- Build cache not cleaned: Old artifacts included
- New frameworks added: Legitimate size increase from new dependencies
- Add size checks to release script
- Ensure .gitignore includes all development paths
- Regular audits of app bundle contents
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 version.xcconfig was changed. Solution:-
Update package.json to match version.xcconfig:
-
Validate sync before building:
”Uncommitted changes detected”
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
Build Number Already Exists
Problem: Sparkle requires unique build numbers for each release. Solution:- Check existing build numbers:
- Update
mac/VibeTunnel/version.xcconfig
:
Notarization Failures
Problem: App notarization fails or takes too long. Common Causes:- Missing API credentials
- Network issues
- Apple service outages
- Unsigned frameworks or binaries
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
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-state
for the last successful step - Run
./scripts/release.sh --resume
to continue - Or manually complete remaining steps (see Manual Recovery below)
Manual Recovery Steps
If automated release fails after notarization:-
Create DMG (if missing):
-
Create GitHub Release:
-
Sign DMG for Sparkle:
-
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:--account VibeTunnel
.
Debug Sparkle Updates
Verify Signing and Notarization
Appcast Issues
📝 Appcast Entry Template
🎯 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
Missing from Appcast
Build Number Conflict
🔍 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.xml
appcast-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
📚 Helper Scripts
Changelog Management Scripts
generate-release-notes.sh
Extracts release notes for a specific version from CHANGELOG.md:
find-changelog.sh
Reliably locates CHANGELOG.md from any directory:
fix-release-changelogs.sh
Updates existing GitHub releases to use per-version changelogs:
📚 Common Commands
Test Sparkle Signature
Verify Appcast URLs
Manual Appcast Generation
Release Status Script
Createscripts/check-release-status.sh
:
📋 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:2. Better Progress Reporting
3. Parallel Operations
Where possible, run independent operations in parallel:📝 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 Recent Releases
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
screen
ortmux
for long operations - Add progress indicators to show the script is still running
Repository Name Parsing Issue
Issue:generate-appcast.sh
was including .git
suffix when parsing repository name from git remote URL.
Fix:
- Updated regex to strip
.git
suffix:${BASH_REMATCH[2]%.git}
- This caused GitHub API calls to fail with 404 errors
- Always test script changes with actual GitHub API calls
Private Key Format Requirements
Issue: The sign_update tool fails with “ERROR! Failed to decode base64 encoded key data” when the private key file contains comments. Solution:- Create a clean private key file containing ONLY the base64 key:
private/sparkle_ed_private_key
- The commented key file (
private/sparkle_private_key
) is kept for documentation - All scripts now use the clean key file automatically
- Scripts will extract the key from the commented file if the clean one doesn’t exist
State Tracking and Resume Capability
New Feature: Release process now supports interruption and resumption.- Added
release-state.sh
for state management - Tracks 9 major release steps with progress
- Use
./scripts/release.sh --resume
to continue interrupted release - Use
./scripts/release.sh --status
to check current state - State file at
.release-state
contains progress information
🚀 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