Release Guide
May 12, 2026 · View on GitHub
This guide explains how to create and publish releases for Thunderbolt using GitHub Actions.
Overview
Our release process uses GitHub Actions to automate building and publishing releases for all platforms:
- Desktop: Linux, macOS (Intel + Apple Silicon), Windows (x64 + ARM64)
- Mobile: iOS (TestFlight), Android (Play Store Internal Track)
PR Title Convention
PR titles are validated by .github/workflows/lint-pr-title.yml and drive automated changelog generation (since the repo uses squash-merge, the PR title becomes the commit on main).
Two formats are accepted:
Conventional Commits (preferred):
<type>[(<scope>)]: <description>
feat(THU-58): add changelog automation
fix: remove invalid header on mobile
chore(deps): bump @tauri-apps/api to 2.11.0
Allowed types: feat, fix, chore, docs, refactor, perf, test, ci, build, style.
Legacy THU-XXX: format (accepted, prefer Conventional going forward):
THU-58: add changelog automation
Invalid titles (e.g. random text, missing type, missing description) fail the Lint PR Title check and block merge.
Quick Start
The easiest way to create a release is via GitHub Actions:
# Via GitHub CLI (recommended)
# Release to ALL platforms (Desktop + iOS + Android)
gh workflow run release.yml -f version_type=auto # Auto-detect from commits
gh workflow run release.yml -f version=1.2.3 # Explicit version
gh workflow run release.yml -f version_type=minor # Specific bump type
# Or via GitHub UI: Actions → Release → Run workflow
Alternative: Local Script (for testing)
# Auto-detect version from commits and push
bun run scripts/create-release.ts --push
# Specify exact version
bun run scripts/create-release.ts --version 1.2.3 --push
That's it! The workflow will:
- ✅ Update core version files (package.json, Cargo.toml, tauri.conf.json)
- ✅ Commit the changes
- ✅ Create and push git tag
v1.2.3 - ✅ Build Desktop, iOS, and Android simultaneously
- iOS updates project.yml during build
- Android calculates versionCode from git commit count during build
- ✅ Create GitHub release with all artifacts
- ✅ Upload iOS build to TestFlight
- ✅ Upload Android build to Play Store (Internal)
Version Management
Version Files
Our project has multiple version files with different update strategies:
| File | Update Method | Purpose |
|---|---|---|
package.json | release.yml 🤖 | Frontend/Node version |
src-tauri/Cargo.toml | release.yml 🤖 | Rust backend version |
src-tauri/tauri.conf.json | release.yml 🤖 | Tauri config (source of truth) |
src-tauri/gen/apple/project.yml | ios-release.yml 🍎 | iOS version (set during build) |
bundle.android.versionCode (tauri.conf) | Git commit count 📊 | Android build number (calculated) |
src-tauri/gen/android/app/tauri.properties | Tauri CLI ⚙️ | Android version (auto-generated) |
Update Strategy:
- Core version files (
package.json,Cargo.toml,tauri.conf.json):- Updated by
release.ymlworkflow viascripts/create-release.ts - Committed to git with the version bump
- Updated by
- Platform-specific versions:
- iOS:
project.ymlupdated duringios-release.ymlbuild (not committed) - Android:
versionCodecalculated from git commit count duringandroid-release.ymlbuild (not committed)
- iOS:
- Auto-generated files:
tauri.properties: Generated by Tauri CLI fromtauri.conf.json(never edit manually)
Important: tauri.conf.json is the source of truth. Platform workflows read from it and update their platform-specific files as needed.
Version Philosophy
- Version files always contain the next version to be released
- When you tag
v1.2.3, all files should already contain1.2.3 - Tags point to commits with matching version numbers
- This prevents version mismatches and extra commits
Android versionCode Calculation
The Android versionCode is automatically calculated using the formula: BASE_OFFSET + git commit count
Current formula: 1000 + $(git rev-list --count HEAD)
This approach:
- Always unique & monotonically increasing - Satisfies Google Play Store requirements
- Deterministic & reproducible - Same commit = same versionCode
- No git pollution - No commits needed just for version bumps
- Aligned with iOS - Both platforms handle versions locally during builds
- Future-proof - Base offset (1000) ensures we stay above previous manual versionCode values
The versionCode in tauri.conf.json is updated only during builds and is never committed back to the repository.
Release Methods
Method 1: GitHub Actions (Recommended)
Trigger a unified release for all platforms via GitHub Actions:
# Via GitHub CLI - Release workflow (simplest)
gh workflow run release.yml -f version_type=auto # Auto-detect (default)
gh workflow run release.yml -f version_type=patch # Patch bump
gh workflow run release.yml -f version_type=minor # Minor bump
gh workflow run release.yml -f version_type=major # Major bump
gh workflow run release.yml -f version=1.2.3 # Explicit version
# Or via GitHub UI:
# 1. Go to Actions tab
# 2. Select "Release" workflow
# 3. Click "Run workflow"
# 4. Choose version type or enter explicit version
What happens:
-
Version Bump (via
version-bump.ymlworkflow):- Updates core version files:
package.json,Cargo.toml,tauri.conf.json - Commits changes and creates unified tag
v1.2.3 - Pushes to remote
- Creates draft GitHub release with changelog
- Updates core version files:
-
Platform Builds (runs in parallel):
- Desktop: Builds all platforms (Linux, macOS, Windows)
- iOS: Updates
project.yml, builds, and uploads to TestFlight - Android: Calculates
versionCodefrom git commit count, builds, and uploads to Play Store (Internal)
-
Final Release:
- All builds upload artifacts to the GitHub release
Method 2: Local Script (Best for Testing)
Use the local TypeScript script to test version bumping before pushing:
# Dry run first to see what would happen (safe!)
bun run scripts/create-release.ts --dry-run
# Auto-detect from commits (default behavior)
bun run scripts/create-release.ts
# Auto-detect and push immediately
bun run scripts/create-release.ts --push
# Specify exact version
bun run scripts/create-release.ts --version 1.2.3 --push
# Specify bump type
bun run scripts/create-release.ts --type minor --push
Default behavior:
- If no
--versionor--typeis specified, defaults to--type auto(auto-detection from commits) - Updates all version files including
tauri.conf.json - Commits the changes
- Creates the tag locally
- Tells you how to push when ready (unless
--pushflag is used)
With --push flag:
- Does everything above
- Pushes commit and tag to remote
- Triggers the GitHub Actions workflow automatically
Why local-first is useful:
- Review the changes before they go remote
- Test the tag locally
- Avoid triggering expensive CI builds until you're ready
Method 3: Manual Process
If you prefer complete manual control:
# 1. Update version in all files to 1.2.3
# Edit these files manually:
# - package.json
# - src-tauri/Cargo.toml
# - src-tauri/tauri.conf.json
# - src-tauri/gen/apple/project.yml
# 2. Commit and push
git add package.json src-tauri/Cargo.toml src-tauri/tauri.conf.json src-tauri/gen/apple/project.yml
git commit -m "chore: bump version to 1.2.3"
git push origin main
# 3. Create and push tag
git tag v1.2.3
git push origin v1.2.3
The tag push will automatically trigger the release workflow.
What Happens During a Release
The release.yml workflow handles the entire release process:
Option A: Manual Trigger (Recommended)
When you trigger via GitHub Actions (workflow_dispatch):
-
Version Bump Job
- Updates core version files:
package.json,Cargo.toml,tauri.conf.json - Commits changes with message like "chore: bump version to 1.2.3 for release"
- Creates tag
v1.2.3 - Pushes commit and tag to remote
- Creates draft GitHub release with changelog
- Updates core version files:
-
Tag triggers platform builds (see below)
Option B: Tag Push (Manual or Script)
When you push a version tag manually (or via create-release.ts script):
-
Version Validation
- Extracts version from tag (e.g.,
v1.2.3→1.2.3) - Validates that all version files match the tag
- Fails fast if versions don't match with helpful error messages
- Extracts version from tag (e.g.,
-
Parallel Platform Builds (see below)
Platform Builds (runs for both options)
Once the tag exists, all platform builds run in parallel:
- Desktop:
- Linux:
.deb,.AppImage,.rpm - macOS (Apple Silicon):
.dmg,.app.tar.gz - macOS (Intel):
.dmg,.app.tar.gz - Windows (x64):
.msi,.exe - Windows (ARM64):
.msi,.exe
- Linux:
- iOS:
.ipauploaded to TestFlight - Android:
.aabuploaded to Play Store (Internal Track)
Final Steps
- GitHub Release: All artifacts uploaded to the release
- iOS TestFlight: Testers with access are notified
- Android Play Store: Available on internal track for testing
Platform-Specific Releases
iOS-Only Releases
To release only iOS to TestFlight without a full release:
# Via version-bump workflow (recommended)
gh workflow run version-bump.yml -f platform=ios -f version_type=auto
# Or direct iOS release workflow
gh workflow run ios-release.yml -f version=1.2.3 -f notify_testers=true
Note: Creates RC tag like v1.2.3-ios-rc
Android-Only Releases
To release only Android to Play Store without a full release:
# Via version-bump workflow (recommended)
gh workflow run version-bump.yml -f platform=android -f version_type=auto
# Or direct Android release workflow
gh workflow run android-release.yml -f version=1.2.3 -f notify_testers=true
Note: Creates RC tag like v1.2.3-android-rc
Desktop-Only Releases
To release only desktop without mobile:
gh workflow run version-bump.yml -f platform=desktop -f version_type=auto
Note: Creates RC tag like v1.2.3-desktop-rc
Testing Workflow Changes
Never push workflow changes directly to main!
Instead, use test branches that start with ci-:
# 1. Create test branch (can be any ci-* branch)
git checkout -b ci-my-test
# 2. Make your workflow changes
# Edit .github/workflows/*.yml files
# 3. Push to test branch
git commit -am "test: update workflow"
git push origin ci-my-test
# 4. Manually trigger workflows to test
gh workflow run release.yml --ref ci-my-test
# 5. Once verified, merge to main
git checkout main
git merge ci-my-test
git push origin main
All workflows are configured to run on:
ci-*- Any branch starting withci-(recommended for parallel testing)
Troubleshooting
Version Mismatch Error
If you get a version validation error:
❌ Error: Tag version (1.2.3) doesn't match package.json (1.2.2)
Solution: Use the helper workflow to update all files:
gh workflow run create-version-tag.yml -f version=1.2.3
Or update all files manually and recommit before tagging.
Build Failures
- Check GitHub Actions logs for detailed error messages
- Verify secrets are configured (especially for iOS/Android signing)
- Test workflow on a branch starting with
ci-branch first - Check recent changes that might have broken the build
iOS Build Issues
Common issues:
- Signing certificates expired: Update secrets in GitHub repository settings
- Provisioning profile issues: Regenerate and update
APP_STORE_PROVISIONING_PROFILEsecret - Xcode version: Workflow uses Xcode 26.2 (iOS 26 SDK)
Manual Workflow Trigger
If automatic triggers aren't working:
# Manually trigger release workflow
gh workflow run release.yml
# Manually trigger iOS release
gh workflow run ios-release.yml -f version=1.2.3
Required GitHub Secrets
The following secrets must be configured in your repository:
Desktop (all platforms)
TAURI_SIGNING_PRIVATE_KEY- For code signingTAURI_SIGNING_PRIVATE_KEY_PASSWORD- Password for signing key
iOS
APPLE_CERTIFICATE_P12- Apple distribution certificate (base64 encoded)APPLE_CERTIFICATE_PASSWORD- Certificate passwordAPP_STORE_PROVISIONING_PROFILE- Provisioning profile (base64 encoded)APP_STORE_CONNECT_API_KEY- App Store Connect API key (base64 or raw .p8)APP_STORE_CONNECT_API_KEY_ID- API key IDAPP_STORE_CONNECT_ISSUER_ID- API issuer IDAPPLE_TEAM_ID- Apple Developer Team ID
Android (coming soon)
ANDROID_KEY_ALIAS- Android keystore aliasANDROID_KEY_PASSWORD- Keystore passwordANDROID_KEY_BASE64- Keystore file (base64 encoded)
Environment Variables
VITE_THUNDERBOLT_CLOUD_URL- Set in repository variables (not secrets)
Best Practices
- ✅ Always use the helper workflow for version bumps (it updates all 4 files for you)
- ✅ Test on a branch starting with
ci-*before merging workflow changes - ✅ Use semantic versioning (major.minor.patch)
- ✅ Write good commit messages for auto-detection to work
- ✅ Check version files are in sync before manually tagging
- ✅ Review the release on GitHub before making it public
- ❌ Never force push to main or version tags
- ❌ Don't manually edit
gen/android/app/tauri.properties(auto-generated by Tauri CLI)
TL;DR on version updates:
- Use the local script (
scripts/create-release.ts) → everything updates automatically ✅ - Use the GitHub Actions workflow → everything updates automatically ✅
- Manual updates → you must update 4 files (package.json, Cargo.toml, tauri.conf.json, project.yml) ✏️
- Never touch → tauri.properties (Tauri handles this) ⚙️
Recommended workflow:
- Test locally with
--dry-runflag to see what changes - Run the script without
--pushto commit and tag locally - Review the changes
- Push when ready (manually or with
--pushflag) - GitHub Actions automatically builds and publishes
Rollback
If you need to rollback a release:
# Delete the tag locally and remotely
git tag -d v1.2.3
git push origin :refs/tags/v1.2.3
# Delete the GitHub release (via UI or CLI)
gh release delete v1.2.3
# Revert version changes if already merged
git revert <commit-hash>
git push origin main
Support
If you encounter issues:
- Check the GitHub Actions logs
- Review this guide
- Check Tauri documentation: https://tauri.app
- Open an issue with detailed logs and steps to reproduce