Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 57 additions & 8 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,15 @@ jobs:
# 1. Install Sentry CLI
curl -sL https://sentry.io/get-cli/ | SENTRY_CLI_VERSION="2.42.2" bash

# 2. Validate tag version matches pubspec.yaml version
TAG_VERSION="${GITHUB_REF_NAME#v}"
TAG_VERSION="${TAG_VERSION%%-rc*}"
# 2. Validate tag base version matches pubspec.yaml base version (ignoring -rc* suffix).
# pubspec version is always X.Y.Z (no -rc suffix) — RC distinction lives only in the git tag.
# SENTRY_RELEASE = pubspec version, matching exactly what the app reports at runtime.
TAG_BASE="${GITHUB_REF_NAME#v}"
TAG_BASE="${TAG_BASE%%-rc*}"
PUBSPEC_VERSION=$(awk '/^version:/{split($2,a,"+"); print a[1]; exit}' pubspec.yaml)
if [ "$PUBSPEC_VERSION" != "$TAG_VERSION" ]; then
echo "::error::Tag version ($TAG_VERSION) does not match pubspec version ($PUBSPEC_VERSION)."
PUBSPEC_BASE="${PUBSPEC_VERSION%%-rc*}"
if [ "$PUBSPEC_BASE" != "$TAG_BASE" ]; then
echo "::error::Tag version ($TAG_BASE) does not match pubspec version ($PUBSPEC_BASE)."
exit 1
fi
SENTRY_RELEASE="$PUBSPEC_VERSION"
Expand All @@ -117,7 +120,6 @@ jobs:

# 4. Upload ProGuard Mapping
MAPPING_FILE="build/app/outputs/mapping/release/mapping.txt"

if [ -f "$MAPPING_FILE" ]; then
echo "Found mapping.txt, uploading ProGuard mapping..."
sentry-cli upload-proguard "$MAPPING_FILE"
Expand All @@ -128,14 +130,61 @@ jobs:

# 5. Upload Dart Debug Symbols (Native Dart Stacktrace)
SYMBOLS_DIR="build/app/outputs/symbols"

if [ -d "$SYMBOLS_DIR" ]; then
echo "Uploading Dart debug symbols from $SYMBOLS_DIR..."
sentry-cli debug-files upload "$SYMBOLS_DIR"
else
echo "::error::Symbols directory not found at $SYMBOLS_DIR. Check your Fastfile build arguments."
exit 1
fi

# 6. Finalize
sentry-cli releases finalize "$SENTRY_RELEASE"

# --- UPLOAD DSYM TO SENTRY FOR IOS ---
# sentry-cli debug-files upload works independently of the release lifecycle,
# so no releases new/finalize needed here — avoids race condition with Android leg.
- name: Upload iOS dSYMs to Sentry
if: matrix.os == 'ios'
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
run: |
# 1. Install Sentry CLI
curl -sL https://sentry.io/get-cli/ | SENTRY_CLI_VERSION="2.42.2" bash

# 2. Validate tag base version matches pubspec.yaml base version (ignoring -rc* suffix).
# pubspec version is always X.Y.Z (no -rc suffix) — RC distinction lives only in the git tag.
# SENTRY_RELEASE = pubspec version, matching exactly what the app reports at runtime.
TAG_BASE="${GITHUB_REF_NAME#v}"
TAG_BASE="${TAG_BASE%%-rc*}"
PUBSPEC_VERSION=$(awk '/^version:/{split($2,a,"+"); print a[1]; exit}' pubspec.yaml)
PUBSPEC_BASE="${PUBSPEC_VERSION%%-rc*}"
if [ "$PUBSPEC_BASE" != "$TAG_BASE" ]; then
echo "::error::Tag version ($TAG_BASE) does not match pubspec version ($PUBSPEC_BASE)."
exit 1
fi
SENTRY_RELEASE="$PUBSPEC_VERSION"
echo "Processing Sentry Release: $SENTRY_RELEASE"

# 3. Find the xcarchive built by Fastlane.
# Xcode always archives to ~/Library/Developer/Xcode/Archives — pick the most recent one.
XCARCHIVE=$(find "$HOME/Library/Developer/Xcode/Archives" -name "Runner*.xcarchive" -maxdepth 3 2>/dev/null | sort -r | head -1)
if [ -z "$XCARCHIVE" ] || [ ! -d "$XCARCHIVE" ]; then
echo "::error::Runner xcarchive not found in ~/Library/Developer/Xcode/Archives"
exit 1
fi
echo "Found xcarchive: $XCARCHIVE"

# 4. Upload dSYMs (Native code: Swift, Objective-C, C++)
DSYM_DIR="$XCARCHIVE/dSYMs"
if [ -d "$DSYM_DIR" ]; then
echo "dSYM contents:"
ls -la "$DSYM_DIR"
echo "Uploading dSYMs from $DSYM_DIR..."
sentry-cli debug-files upload --include-sources --log-level info "$DSYM_DIR"
else
echo "::error::dSYMs directory not found inside xcarchive at $DSYM_DIR."
exit 1
fi
6 changes: 4 additions & 2 deletions android/fastlane/Fastfile
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@ platform :android do
track: track,
json_key_data: ENV["PLAY_STORE_CONFIG_JSON"]
)[0].to_i
build_name = last_git_tag.gsub("v", "").split("-").first
pubspec = File.read("../pubspec.yaml")
sentry_release = pubspec.match(/^version:\s+(\S+)/)[1].split("+").first
build_name = sentry_release.split("-").first
# Add: --obfuscate --split-debug-info=build/app/outputs/symbols
# Note: We run `cd ..` first, so paths are relative to repository root.
sh "cd .. && flutter build appbundle --verbose --release --obfuscate --split-debug-info=build/app/outputs/symbols --dart-define=SERVER_URL=$SERVER_URL --build-number=#{latest_build_number+1} --build-name=#{build_name}"
sh "cd .. && flutter build appbundle --verbose --release --obfuscate --split-debug-info=build/app/outputs/symbols --dart-define=SERVER_URL=$SERVER_URL --dart-define=SENTRY_RELEASE=#{sentry_release} --build-number=#{latest_build_number+1} --build-name=#{build_name}"
upload_to_play_store(
json_key_data: ENV["PLAY_STORE_CONFIG_JSON"],
track: track,
Expand Down
42 changes: 39 additions & 3 deletions core/lib/utils/sentry/sentry_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -78,21 +78,57 @@ class SentryConfig {
&& sentryEnvironment.trim().isNotEmpty;
if (!isConfigValid) return null;

final appVersion = await ApplicationManager().getAppVersion();
final release = await _resolveRelease();

const sentryDist = String.fromEnvironment('SENTRY_DIST');
logTrace(
'SentryConfig::load: sentryDist is $sentryDist, '
'appVersion is $appVersion',
'release is $release',
webConsoleEnabled: true,
);

return SentryConfig(
dsn: sentryDSN,
environment: sentryEnvironment,
release: appVersion,
release: release,
isAvailable: isAvailable,
dist: sentryDist.isNotEmpty ? sentryDist : null,
);
}

/// Resolves the Sentry release string.
///
/// Priority:
/// 1. `--dart-define=SENTRY_RELEASE=<version>` injected at build time by
/// Fastlane/CI — guarantees the same string the CI uses to upload symbols.
/// 2. `PackageInfo.version` as fallback (local dev / non-release builds).
///
/// Why dart-define takes priority: iOS CFBundleShortVersionString strips
/// pre-release suffixes (e.g. "0.28.3-rc09" → "0.28.3"), so PackageInfo
/// returns a different value than what CI tags the symbols with.
static Future<String> _resolveRelease() async {
const dartDefineRelease = String.fromEnvironment('SENTRY_RELEASE');
if (dartDefineRelease.isNotEmpty) return dartDefineRelease;
return ApplicationManager().getAppVersion();
}

static const String sentryConfigKeyChain = 'sentry_config_data';

Map<String, dynamic> toJson() {
return {
'dsn': dsn,
'environment': environment,
'release': release,
if (dist != null) 'dist': dist,
'tracesSampleRate': tracesSampleRate,
'profilesSampleRate': profilesSampleRate,
'sessionSampleRate': sessionSampleRate,
'onErrorSampleRate': onErrorSampleRate,
'enableLogs': enableLogs,
'isDebug': isDebug,
'attachScreenshot': attachScreenshot,
'isAvailable': isAvailable,
'enableFramesTracking': enableFramesTracking,
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
45 changes: 45 additions & 0 deletions docs/adr/0084-sentry-release-version-strategy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# 0084. Sentry Release Version Strategy (Mobile)

Date: 2026-04-22

## Status

Accepted

## Context

> **Scope: iOS and Android mobile only.** Web uses a separate Sentry project and version injection mechanism.

Sentry requires the release version reported by the app to exactly match the release version used when uploading debug symbols (dSYMs / source maps). A mismatch breaks symbolication — crash stack traces become unreadable.

There are three version sources that must stay in sync:

1. **Git tag** — Fastlane reads this via `last_git_tag` and injects it as `SENTRY_RELEASE` into the app via `dart-define` (base64-encoded `DART_DEFINES`)
2. **`pubspec.yaml` version** — used by CI only for base-version validation; **not** used as `SENTRY_RELEASE`
3. **`CFBundleShortVersionString` (iOS)** — derived from pubspec; Apple requires `X.Y.Z` format only — no `-rc` suffix allowed

The challenge: RC builds use a `-rcXX` suffix in the tag (e.g. `v0.28.5-rc01`), but Apple rejects that suffix in `CFBundleShortVersionString`.

## Decision

**Rule: pubspec version = git tag base version (no `-rc` suffix). The pubspec version is used as `SENTRY_RELEASE` in both the app and CI.**

| Source | RC build example | Final release example |
|---|---|---|
| Git tag | `v0.28.5-rc01` | `v0.28.5` |
| pubspec version | `0.28.5` | `0.28.5` |
| App Store / CFBundleShortVersionString | `0.28.5` ✅ | `0.28.5` ✅ |
| SENTRY_RELEASE (app + CI) | `0.28.5` | `0.28.5` |

### What breaks if you deviate

| Mistake | Result |
|---|---|
| pubspec = `0.28.5-rc01`, tag = `v0.28.5-rc01` | Apple rejects the iOS build (`CFBundleShortVersionString` must be `X.Y.Z`) |
| pubspec = `0.28.5-rc01`, tag = `v0.28.5` | Validation passes but app reports `"0.28.5"`, CI uploads to `"0.28.5-rc01"` → mismatch |

## Consequences

- pubspec always stays App Store-safe (`X.Y.Z`, no suffix)
- RC distinction lives only in the git tag
- CI validates base version consistency; pubspec version drives `SENTRY_RELEASE`
12 changes: 12 additions & 0 deletions ios/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ target 'Runner' do
end
end

target 'TwakeMailNSE' do
use_frameworks!
# Must use HybridSDK subspec at the same version as sentry_flutter requires.
# sentry_flutter 9.8.0 depends on Sentry/HybridSDK (= 8.56.2).
# Using plain `pod 'Sentry'` (Core) causes a version conflict and missing PrivateSentrySDKOnly errors.
#
# UPGRADE NOTE: When bumping sentry_flutter, update this version to match the new
# Sentry/HybridSDK constraint declared in Podfile.lock under DEPENDENCIES.
# Verify with: grep 'Sentry/HybridSDK' ios/Podfile.lock
pod 'Sentry/HybridSDK', '8.56.2'
end

post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
Expand Down
5 changes: 3 additions & 2 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ DEPENDENCIES:
- pointer_interceptor_ios (from `.symlinks/plugins/pointer_interceptor_ios/ios`)
- printing (from `.symlinks/plugins/printing/ios`)
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
- Sentry/HybridSDK (= 8.56.2)
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
Expand Down Expand Up @@ -418,7 +419,7 @@ SPEC CHECKSUMS:
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
package_info_plus: 580e9a5f1b6ca5594e7c9ed5f92d1dfb2a66b5e1
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
patrol: 5df5d241d7f95f0df12a6906bbf45acb43a1e537
patrol: cea8074f183a2a4232d0ebd10569ae05149ada42
pdfrx: 310e84d01e06fd2af26e16507a0e48c27e99195c
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
photo_manager: d2fbcc0f2d82458700ee6256a15018210a81d413
Expand All @@ -438,6 +439,6 @@ SPEC CHECKSUMS:
UniversalDetector2: 7c9ffd935cf050eeb19edf7e90f6febe3743a1af
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d

PODFILE CHECKSUM: 40b12ce0bc437886ee4f4050970375d7d253708d
PODFILE CHECKSUM: 01821a4f5c4164186c3bcdf0b69845d9f6e8604e

COCOAPODS: 1.16.2
Loading
Loading