This guide explains how to create and publish releases for Thunderbolt using GitHub Actions.
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 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.
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 workflowAlternative: 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 --pushThat'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)
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 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
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.
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 versionWhat 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
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 --pushDefault 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
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.3The tag push will automatically trigger the release workflow.
The release.yml workflow handles the entire release process:
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)
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)
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)
- GitHub Release: All artifacts uploaded to the release
- iOS TestFlight: Testers with access are notified
- Android Play Store: Available on internal track for testing
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=trueNote: Creates RC tag like v1.2.3-ios-rc
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=trueNote: Creates RC tag like v1.2.3-android-rc
To release only desktop without mobile:
gh workflow run version-bump.yml -f platform=desktop -f version_type=autoNote: Creates RC tag like v1.2.3-desktop-rc
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 mainAll workflows are configured to run on:
ci-*- Any branch starting withci-(recommended for parallel testing)
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.3Or update all files manually and recommit before tagging.
- 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
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)
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.3The following secrets must be configured in your repository:
TAURI_SIGNING_PRIVATE_KEY- For code signingTAURI_SIGNING_PRIVATE_KEY_PASSWORD- Password for signing key
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_KEY_ALIAS- Android keystore aliasANDROID_KEY_PASSWORD- Keystore passwordANDROID_KEY_BASE64- Keystore file (base64 encoded)
VITE_THUNDERBOLT_CLOUD_URL- Set in repository variables (not secrets)
- ✅ 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
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 mainIf 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