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.✅ Standard Release Flow (RepoBar parity)
-
Version + changelog
- Update
VibeTunnel/version.xcconfig(MARKETING_VERSION,CURRENT_PROJECT_VERSION). - Sync
../web/package.jsonversion. - Finalize the top section in
CHANGELOG.md(no “Unreleased”).
- Update
-
Run the full release script
./scripts/release.sh beta <n>or./scripts/release.sh stable- Generates appcast entries with HTML notes from
CHANGELOG.md. - Release notes helper:
./mac/scripts/generate-release-notes.sh <version> > RELEASE_NOTES.md
-
Sparkle UX verification
- About → “Check for Updates…”
- Menu only shows “Update ready, restart now?” once the update is downloaded.
- Sparkle dialog shows formatted release notes (not escaped HTML).
🚀 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 the supported Apple Silicon (arm64) application and embedded server resources
- 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
- 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
-fflag with ALL sign_update commands - Test sign a dummy file to ensure correct key:
- NEVER use sign_update without the
-fflag! - 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.shto 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!2. Required Environment Variables
3. Prerequisites
- Xcode 16.4+ installed
- Node.js 24 and Bun (for web frontend build)
- Rustup;
native/vt-fwd/rust-toolchain.tomlpins the release forwarder toolchain - 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
The native VibeTunnel app supports Apple Silicon (arm64) Macs. The npm package remains available for Intel Macs. The release build creates one arm64 application and matching embedded server resources. This approach:- Simplifies distribution with one DMG/ZIP per release
- Works seamlessly with Sparkle auto-updates
- Prevents host-architecture native resources from being embedded in an incompatible app
📋 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-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):
-
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.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
📚 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
screenortmuxfor 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
.gitsuffix:${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.shfor state management - Tracks 9 major release steps with progress
- Use
./scripts/release.sh --resumeto continue interrupted release - Use
./scripts/release.sh --statusto check current state - State file at
.release-statecontains 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