Skip to content

Latest commit

 

History

History
303 lines (224 loc) · 21.3 KB

RELEASES.md

File metadata and controls

303 lines (224 loc) · 21.3 KB

Release Process

This document covers how to create releases for the web, desktop, and mobile applications.

Release Flows

All platforms share a single version. Running pnpm release bumps package.json, tauri.conf.json, and Cargo.toml together via the version lifecycle hook.

Desktop Release: desktop-v* tag

The desktop release workflow is release-desktop.yml. It creates or updates a draft GitHub Release and uploads the Tauri desktop artifacts for the full desktop matrix.

The unified release.yml runs the webview E2E gate before it calls the reusable desktop workflow, so it passes skip_e2e_gate: true to avoid running the same suite twice. GitHub therefore shows the nested build-desktop / E2E webview (gate) as skipped by design. The six macOS, Windows, and Linux desktop build rows are still created; a skipped nested gate does not mean desktop CI was removed.

Platform Architecture Installer bundles Updater/signature assets
macOS arm64 Forward.Email_<version>_aarch64.dmg Forward.Email_aarch64.app.tar.gz and .sig
macOS x64 Forward.Email_<version>_x64.dmg Forward.Email_x64.app.tar.gz and .sig
Windows x64 Forward.Email_<version>_x64_en-US.msi, Forward.Email_<version>_x64-setup.exe matching .sig sidecars
Windows arm64 Forward.Email_<version>_arm64-setup.exe matching .sig sidecar
Linux x64 Forward.Email_<version>_amd64.AppImage, Forward.Email_<version>_amd64.deb, Forward.Email-<version>-1.x86_64.rpm, forwardemail-mail_<version>_amd64.snap matching .sig sidecars
Linux arm64 Forward.Email_<version>_arm64.deb, Forward.Email-<version>-1.aarch64.rpm, forwardemail-mail_<version>_arm64.snap matching .sig sidecars

The workflow uses native GitHub-hosted runners for macos-15, macos-15-intel, ubuntu-22.04, ubuntu-22.04-arm, windows-latest, and windows-11-arm. Linux arm64 intentionally limits the native Tauri bundles to Debian and RPM because the broader Linux bundle set is not reliable on that ARM runner; it additionally builds the native arm64 Snap. Windows arm64 currently publishes the NSIS setup executable rather than MSI so the arm64 path stays aligned with the documented Windows installer support in Tauri.

pnpm release:desktop patch

Terminal client: part of every v* release

release.yml calls release-cli.yml after the release is created. It attaches these assets to the same GitHub Release:

Asset What it is
forwardemail-{linux,darwin}-{x64,arm64}.gz Standalone executables (Node.js single executable applications)
forwardemail-win-{x64,arm64}.exe.gz Standalone Windows executables, Authenticode-signed like the desktop installers
install.sh, install.ps1 The one-line installers

The executables are listed in SHA256SUMS.txt, which the installers and the self-updater verify against, and they carry build provenance attestations. The same version is published to npm as forwardemail on every release, from the bundle CI built and tested; np itself does not publish. The installers and the updater only see a release once it is published, not while it is a draft. The terminal client guide has the details.

CI Pipeline

Every push to main and every PR triggers ci.yml:

  • Lint + format checks
  • Unit tests (Vitest)
  • Build
  • E2E tests (Playwright: desktop Chromium, mobile Android, mobile iOS viewports)

Versioning

We use np for version management. It handles version bumping, git tagging, and pushing.

pnpm release

np prompts for patch/minor/major, creates the v* tag, and pushes to trigger the full release flow.

Version Sync

The version lifecycle hook in package.json runs scripts/sync-version.cjs, which updates:

File Field Example
src-tauri/tauri.conf.json version 0.8.2
src-tauri/Cargo.toml version 0.8.2
src-tauri/gen/android/app/tauri.properties versionName + versionCode 0.8.2 / 802

The Android versionCode is derived as major * 10000 + minor * 100 + patch (e.g., 0.8.2 = 802, 1.0.0 = 10000). This ensures it always increments with each semver bump, as required by the Play Store.

Manual Release

Trigger the desktop workflow manually via GitHub Actions:

  1. Open Actions → Release Desktop (Tauri)
  2. Choose Run workflow
  3. Enter a tag such as desktop-v0.7.0

Artifacts

Desktop

The exact desktop asset basenames are listed in the matrix above. Tauri also uploads latest.json, and the unified release workflow adds SHA256SUMS.txt. Linux arm64 intentionally has no AppImage; Windows arm64 intentionally has no MSI.

Android

File Description
forwardemail-mail_<version>_android.apk Signed sideloadable APK (arm64-v8a + armeabi-v7a) containing both FCM and UnifiedPush; users may select a UnifiedPush distributor
forwardemail-mail_<version>_android.aab The same dual-provider application as an Android App Bundle (all ABIs) for Google Play
forwardemail-mail_<version>_fdroid.apk Signed Google-free UnifiedPush-only APK (arm64-v8a + armeabi-v7a) for the self-hosted F-Droid repository and Obtainium

Built by release-mobile.yml. The dual-provider Android release job requires Android signing secrets, Firebase client configuration, and the matching VAPID public key before toolchain setup (see SECRETS.md). The separate android-fdroid job uses the same Android release certificate and pnpm tauri:android:build:fdroid, but excludes Firebase and Google Play Services.

The sideloadable APKs are universal APKs, so every ABI they contain is downloaded by every user; they ship only the two ARM ABIs that real devices use. The AAB keeps x86 and x86_64 as well because Play serves each device only its matching split. In all three, the Rust library is stripped by Gradle and its debug symbols are packaged separately (scripts/inject-android-native-symbols.cjs): inside the AAB for Play Console crash symbolication, and as the android-native-debug-symbols workflow artifact for tombstones from sideloaded installs.

Additional distribution channels

The regular v* orchestrator invokes all configured downstream distribution lanes after the GitHub Release artifacts are ready. The table intentionally distinguishes asset production from store publication: each native Linux row attaches its architecture-matched Snap to the GitHub Release, while every store-facing action remains opt-in until its account and credentials are configured.

Channel Release behavior Opt-in control Operator action after a release
Snap Store Builds a strict Snap and adds it to the GitHub Release; publishes it to the stable store channel when enabled PUBLISH_SNAP_STORE=true Verify the stable listing and test snap install
Flathub Flathub rebuilds the submitted source manifest; the source workflow provides only PR verification Flathub application acceptance Review the External Data Checker update PR
F-Droid repository Downloads the signed Google-free APK, creates signed indexes, and deploys the repository to GitHub Pages PUBLISH_FDROID_REPOSITORY=true Check Pages, index signature/fingerprint, and client refresh
Homebrew tap Downloads both macOS DMGs, calculates checksums, and opens/updates a cask pull request in the separate tap PUBLISH_HOMEBREW_TAP=true Review and merge the tap pull request
Obtainium Tracks the Google-free GitHub Release APK directly Always available after the APK exists No publisher action required

The full one-time setup, secret generation, installation commands, and failure handling are in distribution-publishing.md.

iOS

File Description
forwardemail-mail_<version>_ios.ipa Signed IPA (archival; primary distribution is TestFlight below)

The same IPA is uploaded to App Store Connect → TestFlight via xcrun altool using the App Store Connect API key. Testers install through the TestFlight app rather than downloading from the GitHub Release. See ios-setup.md for the post-release flow (processing wait, inviting testers, beta review).

Code Signing

Local and non-release builds can be unsigned. CI release behavior is stricter: the Android release fails its preflight unless all four keystore values are configured, while the iOS release skips when its signing and App Store Connect values are unavailable. Desktop signing and notarization remain conditional on their platform credentials. See the workflow-specific requirements below before creating a release tag.

Platform Signing Notes
macOS Apple Developer ID + notarization Users won't see Gatekeeper warnings
Windows SSL.com certificate via eSigner Signed in CI through eSigner cloud signing; the Windows rows fail closed without the ESIGNER_* secrets. SmartScreen reputation still builds over time after signing
Linux None needed .deb and .rpm work unsigned; trust is handled by the host package flow
Android Self-managed keystore (.jks) Required for Play Store; optional for APK
iOS Apple Distribution + ASC API key Required for TestFlight; job skips gracefully when secrets aren't set
Auto-updater Ed25519 key Required for .sig files

See SECRETS.md for the full list of required secrets, desktop-ci-secrets.md for desktop signing setup, and ios-setup.md for the iOS signing and TestFlight flow.

Tauri Pre-Release Checks

Tauri v2 has a handful of platform-specific bugs that are easy to ship past in tauri dev and only show up in signed/notarized production builds. Run through this list before promoting a draft GitHub release.

macOS: entitlements + updater smoke test

The Developer ID macOS bundle is not sandboxed. src-tauri/Entitlements.plist carries only com.apple.security.network.client and the two Hardened Runtime JIT entitlements; com.apple.security.app-sandbox was removed on 2026-06-02 because a sandbox without file-access entitlements crashes the file picker (see the postmortem). A separate Entitlements.appstore.plist (sandbox plus user-selected file access) exists for the future Mac App Store lane and must never be referenced from tauri.conf.json. Entitlement mistakes only show up in signed builds (tauri-apps/tauri#13878), so run this on a notarized build.

Smoke test, on a notarized signed build (not tauri dev):

  1. Install the signed .dmg from the draft release.
  2. Open Console.app → filter for "Forward Email".
  3. Launch the app. Confirm:
    • The updater check fires and either reports "up to date" or surfaces a new version prompt, not a network error.
    • The login flow reaches api.forwardemail.net (sign in with a test account).
  4. If outbound traffic is silently failing, re-check Entitlements.plist ships in the bundle (codesign -d --entitlements - /Applications/Forward\ Email.app).
  5. Open Compose and add an attachment. The native file picker must open; a crash here means the sandbox entitlement crept back in.

Windows: mailto: handler smoke test

Windows ships both NSIS (-setup.exe) and MSI installers. The deep-link plugin's compile-time scheme registration only works with MSI on Windows (tauri-apps/plugins-workspace#10095), so we register mailto: at runtime via direct registry mutation in set_default_mailto_handler (src-tauri/src/lib.rs).

Smoke test, on a fresh Windows 11 VM (don't reuse a dev machine: stale registry entries from prior installs hide the bug):

  1. Install via -setup.exe (NSIS).
  2. Launch the app, sign in.
  3. Settings → make Forward Email the default mail handler. Accept the Windows Settings prompt that pops.
  4. Open cmd and run start mailto:test@example.com. Confirm:
    • Forward Email comes to the foreground.
    • The compose window opens pre-populated with test@example.com in the To field.
  5. Repeat with a single-instance test: with the app already open, run the start mailto: command again. Confirm a new compose window opens in the existing instance, not a second app process.

If step 4 silently does nothing, the runtime registry write isn't taking. Fall back to MSI (.msi) and reproduce there before shipping.

Updater endpoint resilience

The updater pulls its manifest from a single GitHub URL (tauri.conf.json → plugins.updater.endpoints):

https://github.com/forwardemail/mail.forwardemail.net/releases/latest/download/latest.json

GitHub's redirect chain has historically returned 401s/403s for some clients (tauri-apps/tauri#2579 lineage). To add resilience, host a mirror of latest.json at a stable URL on forwardemail.net infra (e.g. a Cloudflare Worker or static R2/S3 object) and add it as a second entry in endpoints:

"endpoints": [
  "https://releases.forwardemail.net/latest.json",
  "https://github.com/forwardemail/mail.forwardemail.net/releases/latest/download/latest.json"
]

Tauri tries each endpoint in order and falls through on network or non-200 errors, so the self-hosted mirror becomes primary and the GitHub URL is the backstop.

The mirror needs to publish the same manifest JSON the desktop release workflow already produces; the simplest path is a CI job that re-uploads latest.json to the mirror after the GitHub release is published.

Antivirus reputation (Windows)

Authenticode signing alone doesn't suppress every Windows Defender / third-party AV false-positive against WebView2 binaries (tauri-apps/wry#2486). On each release, proactively submit the new -setup.exe and .msi to:

Reputation builds over weeks, so submitting on each release is more useful than batching after user reports.

Linux: webkit2gtk version

tauri.conf.json declares libwebkit2gtk-4.1-0 as the deb dependency. Don't drop back to 4.0; it's no longer in Ubuntu 24 / Debian 13 repos (tauri-apps/wry#9662). When testing the AppImage, use a fresh Ubuntu 24 VM (not Ubuntu 22) so we catch any 4.1 incompatibilities before users do.

Known limitations (document, don't fix)

  • ChromeOS Android intent filter (tauri-apps/plugins-workspace#3207): deep links don't fire for ChromeOS users running our Android build. Niche; no upstream fix yet.
  • Notification plugin on Android (tauri-apps/plugins-workspace#2341): the cancelAll, pending, active, and channels APIs are broken. Don't add call sites for any of them. notification-bridge.js already avoids them and logs explicitly when channel creation fails on Android.
  • macOS WKWebView pinned to OS version: users on old macOS get old WebKit. We currently set bundle.macOS.minimumSystemVersion = "10.15". Bumping to 11.0 would drop Catalina users in exchange for fewer JS-feature edge cases. Defer until telemetry shows Catalina usage is negligible.

Latest release promotion

The release stays a draft until every build row, the mobile jobs, and the checksum file have landed. The publish job then flips it to published with make_latest=true set explicitly, so Latest does not depend on GitHub's creation-date default (a draft created before a hotfix and published after it would otherwise not become Latest). release-summary ends by verifying that the tag is published and is what releases/latest returns, because the desktop updater endpoint follows Latest. A green run therefore always means the new version is what users are offered.

GitHub asset upload failures

GitHub's release upload endpoint returns transient 5xx responses a few times a month ("Unicorn!", "Error saving asset", "Error creating asset temp dir"). In v0.13.9 (run 35251791779) three rows failed this way after their builds had already succeeded. Two defenses are in place:

  • tauri-action runs with retryAttempts: 3, so a failed upload (or build) is retried inside the same job without losing the warm Rust target directory.
  • Every first-party upload (Snap, APK, AAB, Google-free APK, IPA, checksums) goes through .github/actions/upload-release-asset, which deletes any partial asset of the same name, retries with backoff, and verifies the uploaded size.

If a row still fails, use Re-run failed jobs on the run. GitHub re-runs the failed rows and every job that was skipped downstream (checksums, publish, deploy), and a re-uploaded asset replaces the earlier one by name. A re-run executes the workflow files from the tagged commit, so it will not pick up workflow fixes merged since the tag.

Related Documentation