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:

  1. ✅ Update core version files (package.json, Cargo.toml, tauri.conf.json)
  2. ✅ Commit the changes
  3. ✅ Create and push git tag v1.2.3
  4. ✅ Build Desktop, iOS, and Android simultaneously
    • iOS updates project.yml during build
    • Android calculates versionCode from git commit count during build
  5. ✅ Create GitHub release with all artifacts
  6. ✅ Upload iOS build to TestFlight
  7. ✅ Upload Android build to Play Store (Internal)

Version Management

Version Files

Our project has multiple version files with different update strategies:

FileUpdate MethodPurpose
package.jsonrelease.yml 🤖Frontend/Node version
src-tauri/Cargo.tomlrelease.yml 🤖Rust backend version
src-tauri/tauri.conf.jsonrelease.yml 🤖Tauri config (source of truth)
src-tauri/gen/apple/project.ymlios-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.propertiesTauri CLI ⚙️Android version (auto-generated)

Update Strategy:

  1. Core version files (package.json, Cargo.toml, tauri.conf.json):
    • Updated by release.yml workflow via scripts/create-release.ts
    • Committed to git with the version bump
  2. Platform-specific versions:
    • iOS: project.yml updated during ios-release.yml build (not committed)
    • Android: versionCode calculated from git commit count during android-release.yml build (not committed)
  3. Auto-generated files:
    • tauri.properties: Generated by Tauri CLI from tauri.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 contain 1.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

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:

  1. Version Bump (via version-bump.yml workflow):

    • 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
  2. Platform Builds (runs in parallel):

    • Desktop: Builds all platforms (Linux, macOS, Windows)
    • iOS: Updates project.yml, builds, and uploads to TestFlight
    • Android: Calculates versionCode from git commit count, builds, and uploads to Play Store (Internal)
  3. 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 --version or --type is 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 --push flag 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:

When you trigger via GitHub Actions (workflow_dispatch):

  1. 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
  2. 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):

  1. Version Validation

    • Extracts version from tag (e.g., v1.2.31.2.3)
    • Validates that all version files match the tag
    • Fails fast if versions don't match with helpful error messages
  2. 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
  • iOS: .ipa uploaded to TestFlight
  • Android: .aab uploaded 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 with ci- (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

  1. Check GitHub Actions logs for detailed error messages
  2. Verify secrets are configured (especially for iOS/Android signing)
  3. Test workflow on a branch starting with ci- branch first
  4. 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_PROFILE secret
  • 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 signing
  • TAURI_SIGNING_PRIVATE_KEY_PASSWORD - Password for signing key

iOS

  • APPLE_CERTIFICATE_P12 - Apple distribution certificate (base64 encoded)
  • APPLE_CERTIFICATE_PASSWORD - Certificate password
  • APP_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 ID
  • APP_STORE_CONNECT_ISSUER_ID - API issuer ID
  • APPLE_TEAM_ID - Apple Developer Team ID

Android (coming soon)

  • ANDROID_KEY_ALIAS - Android keystore alias
  • ANDROID_KEY_PASSWORD - Keystore password
  • ANDROID_KEY_BASE64 - Keystore file (base64 encoded)

Environment Variables

  • VITE_THUNDERBOLT_CLOUD_URL - Set in repository variables (not secrets)

Best Practices

  1. Always use the helper workflow for version bumps (it updates all 4 files for you)
  2. Test on a branch starting with ci-* before merging workflow changes
  3. Use semantic versioning (major.minor.patch)
  4. Write good commit messages for auto-detection to work
  5. Check version files are in sync before manually tagging
  6. Review the release on GitHub before making it public
  7. Never force push to main or version tags
  8. 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:

  1. Test locally with --dry-run flag to see what changes
  2. Run the script without --push to commit and tag locally
  3. Review the changes
  4. Push when ready (manually or with --push flag)
  5. 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:

  1. Check the GitHub Actions logs
  2. Review this guide
  3. Check Tauri documentation: https://tauri.app
  4. Open an issue with detailed logs and steps to reproduce