diff --git a/scripts/upload-symbols.ps1 b/scripts/upload-symbols.ps1 index 21375392..ce40ba1c 100755 --- a/scripts/upload-symbols.ps1 +++ b/scripts/upload-symbols.ps1 @@ -16,124 +16,11 @@ $Root = Split-Path -Parent $ScriptDir Set-Location $Root if ([string]::IsNullOrEmpty($SourceRoot)) { - $SearchRoot = $Root -} -elseif ([System.IO.Path]::IsPathRooted($SourceRoot)) { - $SearchRoot = $SourceRoot + & dart run scripts/upload_symbols.dart $Platform } else { - $SearchRoot = [System.IO.Path]::GetFullPath((Join-Path $Root $SourceRoot)) + & dart run scripts/upload_symbols.dart $Platform $SourceRoot } - -$BuildRoot = Join-Path $SearchRoot 'build' -$SymbolRoot = Join-Path (Join-Path $SearchRoot 'debug-info') $Platform -$DryRun = -not [string]::IsNullOrEmpty($env:BUGS_UPLOAD_DRY_RUN) - -$SearchRoots = [System.Collections.Generic.List[string]]::new() -function Add-ExistingRoot([string]$Path) { - if (Test-Path -Path $Path -PathType Container) { - $SearchRoots.Add($Path) - } -} - -Add-ExistingRoot $SymbolRoot - -switch ($Platform) { - { $_ -eq 'windows-x64' -or $_ -eq 'windows-arm64' } { - Add-ExistingRoot (Join-Path $BuildRoot 'windows') - } - default { - Write-Error "unknown platform: $Platform" - exit 2 - } -} - -function Has-SymbolFile { - foreach ($RootPath in $SearchRoots) { - $First = Get-ChildItem -Path $RootPath -File -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 - if ($null -ne $First) { - return $true - } - } - return $false -} - -if (-not (Has-SymbolFile)) { - Write-Error "no symbols found for platform $Platform" - exit 3 -} - -if ([string]::IsNullOrEmpty($env:SENTRY_URL)) { - $env:SENTRY_URL = if ([string]::IsNullOrEmpty($env:BUGS_URL)) { 'https://bugs.plezy.app' } else { $env:BUGS_URL } -} - -if ([string]::IsNullOrEmpty($env:SENTRY_RELEASE)) { - $env:SENTRY_RELEASE = "plezy@$(git rev-parse --short HEAD)" -} - -if ([string]::IsNullOrEmpty($env:SENTRY_LOG_LEVEL)) { - $env:SENTRY_LOG_LEVEL = 'info' -} - -if ([string]::IsNullOrEmpty($env:SENTRY_AUTH_TOKEN) -and -not [string]::IsNullOrEmpty($env:BUGS_ADMIN_TOKEN)) { - $env:SENTRY_AUTH_TOKEN = $env:BUGS_ADMIN_TOKEN -} - -if (-not $DryRun -and [string]::IsNullOrEmpty($env:SENTRY_AUTH_TOKEN)) { - Write-Error 'SENTRY_AUTH_TOKEN or BUGS_ADMIN_TOKEN env var required' - exit 1 -} - -$DartSymbolMapPath = $env:SENTRY_DART_SYMBOL_MAP_PATH -if ([string]::IsNullOrEmpty($DartSymbolMapPath)) { - $Candidates = @( - (Join-Path $SymbolRoot 'obfuscation.map.json'), - (Join-Path (Join-Path $BuildRoot 'app\obfuscation') "$Platform.map.json"), - (Join-Path (Join-Path $BuildRoot 'app') 'obfuscation.map.json') - ) - - foreach ($Candidate in $Candidates) { - if (Test-Path -Path $Candidate -PathType Leaf) { - $DartSymbolMapPath = $Candidate - break - } - } -} - -$PluginArgs = @( - "--sentry-define=release=$($env:SENTRY_RELEASE)", - "--sentry-define=url=$($env:SENTRY_URL)", - "--sentry-define=build_path=$BuildRoot" -) - -if (-not [string]::IsNullOrEmpty($env:SENTRY_DIST)) { - $PluginArgs += "--sentry-define=dist=$($env:SENTRY_DIST)" -} - -if (Test-Path -Path $SymbolRoot -PathType Container) { - $PluginArgs += "--sentry-define=symbols_path=$SymbolRoot" -} - -if (-not [string]::IsNullOrEmpty($DartSymbolMapPath)) { - $PluginArgs += "--sentry-define=dart_symbol_map_path=$DartSymbolMapPath" -} - -if ($DryRun) { - Write-Host "dry-run: would upload symbols for $Platform" - Write-Host "dry-run: release=$($env:SENTRY_RELEASE)" - Write-Host "dry-run: dist=$($env:SENTRY_DIST)" - Write-Host "dry-run: source_root=$SearchRoot" - Write-Host "dry-run: build_path=$BuildRoot" - Write-Host "dry-run: symbols_path=$SymbolRoot" - Write-Host "dry-run: dart_symbol_map_path=$DartSymbolMapPath" - foreach ($RootPath in $SearchRoots) { - Get-ChildItem -Path $RootPath -File -Recurse -ErrorAction SilentlyContinue | ForEach-Object { Write-Host $_.FullName } - } - exit 0 -} - -Write-Host "uploading symbols for $Platform release $($env:SENTRY_RELEASE) dist $($env:SENTRY_DIST)" -& dart run sentry_dart_plugin @PluginArgs if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } diff --git a/scripts/upload-symbols.sh b/scripts/upload-symbols.sh index 2a2d14f5..c6ee2a55 100755 --- a/scripts/upload-symbols.sh +++ b/scripts/upload-symbols.sh @@ -5,121 +5,9 @@ # Platforms: macos | ios | android-apk | android-aab | linux-x64 | linux-arm64 set -euo pipefail -PLATFORM="${1:?platform arg required}" -DRY_RUN="${BUGS_UPLOAD_DRY_RUN:-}" +: "${1:?platform arg required}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" cd "$ROOT" - -SOURCE_ROOT="${2:-$ROOT}" -if [[ "$SOURCE_ROOT" != /* ]]; then - SOURCE_ROOT="$ROOT/$SOURCE_ROOT" -fi - -BUILD_ROOT="$SOURCE_ROOT/build" -SYMBOL_ROOT="$SOURCE_ROOT/debug-info/$PLATFORM" -DART_SYMBOL_MAP_PATH="${SENTRY_DART_SYMBOL_MAP_PATH:-}" - -if [ -z "$DART_SYMBOL_MAP_PATH" ]; then - for candidate in \ - "$SYMBOL_ROOT/obfuscation.map.json" \ - "$BUILD_ROOT/app/obfuscation/$PLATFORM.map.json" \ - "$BUILD_ROOT/app/obfuscation.map.json"; do - if [ -f "$candidate" ]; then - DART_SYMBOL_MAP_PATH="$candidate" - break - fi - done -fi - -SEARCH_ROOTS=() - -add_existing_root() { - if [ -d "$1" ]; then - SEARCH_ROOTS+=("$1") - fi -} - -add_existing_root "$SYMBOL_ROOT" - -case "$PLATFORM" in - macos) - add_existing_root "$BUILD_ROOT/macos" - ;; - ios) - add_existing_root "$BUILD_ROOT/ios" - add_existing_root "$SOURCE_ROOT/ios/build" - ;; - linux-x64|linux-arm64) - add_existing_root "$BUILD_ROOT/linux" - ;; - android-apk|android-aab) - add_existing_root "$BUILD_ROOT/app" - ;; - *) - echo "unknown platform: $PLATFORM" >&2 - exit 2 - ;; -esac - -found_symbol_file() { - local first - if [ "${#SEARCH_ROOTS[@]}" -eq 0 ]; then - return 1 - fi - - first="$(find "${SEARCH_ROOTS[@]}" -type f -print -quit 2>/dev/null || true)" - [ -n "$first" ] -} - -if ! found_symbol_file; then - echo "no symbols found for platform ${PLATFORM}" >&2 - exit 3 -fi - -export SENTRY_URL="${SENTRY_URL:-${BUGS_URL:-https://bugs.plezy.app}}" -export SENTRY_RELEASE="${SENTRY_RELEASE:-plezy@$(git rev-parse --short HEAD)}" -export SENTRY_LOG_LEVEL="${SENTRY_LOG_LEVEL:-info}" - -if [ -z "${SENTRY_AUTH_TOKEN:-}" ] && [ -n "${BUGS_ADMIN_TOKEN:-}" ]; then - export SENTRY_AUTH_TOKEN="$BUGS_ADMIN_TOKEN" -fi - -if [ -z "$DRY_RUN" ] && [ -z "${SENTRY_AUTH_TOKEN:-}" ]; then - echo "SENTRY_AUTH_TOKEN or BUGS_ADMIN_TOKEN env var required" >&2 - exit 1 -fi - -PLUGIN_ARGS=( - "--sentry-define=release=${SENTRY_RELEASE}" - "--sentry-define=url=${SENTRY_URL}" - "--sentry-define=build_path=${BUILD_ROOT}" -) - -if [ -n "${SENTRY_DIST:-}" ]; then - PLUGIN_ARGS+=("--sentry-define=dist=${SENTRY_DIST}") -fi - -if [ -d "$SYMBOL_ROOT" ]; then - PLUGIN_ARGS+=("--sentry-define=symbols_path=${SYMBOL_ROOT}") -fi - -if [ -n "$DART_SYMBOL_MAP_PATH" ]; then - PLUGIN_ARGS+=("--sentry-define=dart_symbol_map_path=${DART_SYMBOL_MAP_PATH}") -fi - -if [ -n "$DRY_RUN" ]; then - echo "dry-run: would upload symbols for ${PLATFORM}" - echo "dry-run: release=${SENTRY_RELEASE}" - echo "dry-run: dist=${SENTRY_DIST:-}" - echo "dry-run: source_root=${SOURCE_ROOT}" - echo "dry-run: build_path=${BUILD_ROOT}" - echo "dry-run: symbols_path=${SYMBOL_ROOT}" - echo "dry-run: dart_symbol_map_path=${DART_SYMBOL_MAP_PATH}" - find "${SEARCH_ROOTS[@]}" -type f -print 2>/dev/null || true - exit 0 -fi - -echo "uploading symbols for ${PLATFORM} release ${SENTRY_RELEASE} dist ${SENTRY_DIST:-}" -dart run sentry_dart_plugin "${PLUGIN_ARGS[@]}" +exec dart run scripts/upload_symbols.dart "$@" diff --git a/scripts/upload_symbols.dart b/scripts/upload_symbols.dart new file mode 100644 index 00000000..4b7e910f --- /dev/null +++ b/scripts/upload_symbols.dart @@ -0,0 +1,203 @@ +import 'dart:io'; + +import 'package:path/path.dart' as path; + +typedef SymbolUploader = + Future Function({ + required List arguments, + required Map environment, + required String workingDirectory, + }); + +const _defaultSentryUrl = 'https://bugs.plezy.app'; + +Future main(List arguments) async { + final scriptDirectory = File.fromUri(Platform.script).parent; + final result = await runUploadSymbols(arguments, repositoryRoot: scriptDirectory.parent); + if (result != 0) { + exitCode = result; + } +} + +Future runUploadSymbols( + List arguments, { + required Directory repositoryRoot, + Map? environment, + SymbolUploader uploader = _runSentryPlugin, + Future Function()? revisionProvider, + StringSink? output, + StringSink? errors, +}) async { + final out = output ?? stdout; + final err = errors ?? stderr; + if (arguments.isEmpty || arguments.first.isEmpty) { + err.writeln('platform arg required'); + return 1; + } + + final platform = arguments.first; + final root = path.normalize(path.absolute(repositoryRoot.path)); + final sourceArgument = arguments.length > 1 ? arguments[1] : ''; + final sourceRoot = sourceArgument.isEmpty + ? root + : path.normalize(path.isAbsolute(sourceArgument) ? sourceArgument : path.join(root, sourceArgument)); + final buildRoot = path.join(sourceRoot, 'build'); + final symbolRoot = path.join(sourceRoot, 'debug-info', platform); + + final searchRoots = []; + void addExistingRoot(String candidate) { + if (Directory(candidate).existsSync()) { + searchRoots.add(candidate); + } + } + + addExistingRoot(symbolRoot); + switch (platform) { + case 'macos': + addExistingRoot(path.join(buildRoot, 'macos')); + case 'ios': + addExistingRoot(path.join(buildRoot, 'ios')); + addExistingRoot(path.join(sourceRoot, 'ios', 'build')); + case 'linux-x64' || 'linux-arm64': + addExistingRoot(path.join(buildRoot, 'linux')); + case 'android-apk' || 'android-aab': + addExistingRoot(path.join(buildRoot, 'app')); + case 'windows-x64' || 'windows-arm64': + addExistingRoot(path.join(buildRoot, 'windows')); + default: + err.writeln('unknown platform: $platform'); + return 2; + } + + final symbolFiles = _discoverFiles(searchRoots); + if (symbolFiles.isEmpty) { + err.writeln('no symbols found for platform $platform'); + return 3; + } + + final childEnvironment = Map.of(environment ?? Platform.environment); + final dryRun = _isNotEmpty(childEnvironment['BUGS_UPLOAD_DRY_RUN']); + childEnvironment['SENTRY_URL'] = _firstNotEmpty([ + childEnvironment['SENTRY_URL'], + childEnvironment['BUGS_URL'], + _defaultSentryUrl, + ]); + childEnvironment['SENTRY_LOG_LEVEL'] = _firstNotEmpty([childEnvironment['SENTRY_LOG_LEVEL'], 'info']); + if (!_isNotEmpty(childEnvironment['SENTRY_AUTH_TOKEN']) && _isNotEmpty(childEnvironment['BUGS_ADMIN_TOKEN'])) { + childEnvironment['SENTRY_AUTH_TOKEN'] = childEnvironment['BUGS_ADMIN_TOKEN']!; + } + if (!dryRun && !_isNotEmpty(childEnvironment['SENTRY_AUTH_TOKEN'])) { + err.writeln('SENTRY_AUTH_TOKEN or BUGS_ADMIN_TOKEN env var required'); + return 1; + } + + if (!_isNotEmpty(childEnvironment['SENTRY_RELEASE'])) { + final revision = await (revisionProvider?.call() ?? _gitRevision(root)); + if (!_isNotEmpty(revision)) { + err.writeln('failed to determine SENTRY_RELEASE from git'); + return 1; + } + childEnvironment['SENTRY_RELEASE'] = 'plezy@$revision'; + } + + var dartSymbolMapPath = childEnvironment['SENTRY_DART_SYMBOL_MAP_PATH'] ?? ''; + if (dartSymbolMapPath.isEmpty) { + for (final candidate in [ + path.join(symbolRoot, 'obfuscation.map.json'), + path.join(buildRoot, 'app', 'obfuscation', '$platform.map.json'), + path.join(buildRoot, 'app', 'obfuscation.map.json'), + ]) { + if (File(candidate).existsSync()) { + dartSymbolMapPath = candidate; + break; + } + } + } + + final release = childEnvironment['SENTRY_RELEASE']!; + final dist = childEnvironment['SENTRY_DIST'] ?? ''; + final pluginArguments = [ + '--sentry-define=release=$release', + '--sentry-define=url=${childEnvironment['SENTRY_URL']}', + '--sentry-define=build_path=$buildRoot', + if (dist.isNotEmpty) '--sentry-define=dist=$dist', + if (Directory(symbolRoot).existsSync()) '--sentry-define=symbols_path=$symbolRoot', + if (dartSymbolMapPath.isNotEmpty) '--sentry-define=dart_symbol_map_path=$dartSymbolMapPath', + ]; + + if (dryRun) { + out.writeln('dry-run: would upload symbols for $platform'); + out.writeln('dry-run: release=$release'); + out.writeln('dry-run: dist=$dist'); + out.writeln('dry-run: source_root=$sourceRoot'); + out.writeln('dry-run: build_path=$buildRoot'); + out.writeln('dry-run: symbols_path=$symbolRoot'); + out.writeln('dry-run: dart_symbol_map_path=$dartSymbolMapPath'); + for (final file in symbolFiles) { + out.writeln(file); + } + return 0; + } + + out.writeln('uploading symbols for $platform release $release dist $dist'); + try { + final result = await uploader(arguments: pluginArguments, environment: childEnvironment, workingDirectory: root); + if (result != 0) { + err.writeln('symbol upload failed for $platform (exit code $result)'); + } + return result; + } on ProcessException catch (error) { + err.writeln('failed to start symbol upload: $error'); + return 1; + } +} + +List _discoverFiles(List roots) { + final files = []; + for (final root in roots) { + try { + files.addAll( + Directory(root) + .listSync(recursive: true, followLinks: false) + .whereType() + .map((file) => path.normalize(path.absolute(file.path))), + ); + } on FileSystemException { + // Match the wrappers' previous best-effort discovery behavior. + } + } + files.sort(); + return files; +} + +Future _gitRevision(String root) async { + try { + final result = await Process.run('git', const ['rev-parse', '--short', 'HEAD'], workingDirectory: root); + if (result.exitCode == 0) { + return (result.stdout as String).trim(); + } + } on ProcessException { + return null; + } + return null; +} + +Future _runSentryPlugin({ + required List arguments, + required Map environment, + required String workingDirectory, +}) async { + final process = await Process.start( + Platform.resolvedExecutable, + ['run', 'sentry_dart_plugin', ...arguments], + workingDirectory: workingDirectory, + environment: environment, + includeParentEnvironment: false, + mode: ProcessStartMode.inheritStdio, + ); + return process.exitCode; +} + +bool _isNotEmpty(String? value) => value != null && value.isNotEmpty; + +String _firstNotEmpty(List values) => values.firstWhere(_isNotEmpty)!; diff --git a/test/scripts/upload_symbols_test.dart b/test/scripts/upload_symbols_test.dart new file mode 100644 index 00000000..4c0235db --- /dev/null +++ b/test/scripts/upload_symbols_test.dart @@ -0,0 +1,107 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:path/path.dart' as path; + +import '../../scripts/upload_symbols.dart'; + +void main() { + late Directory repository; + + setUp(() async { + repository = await Directory.systemTemp.createTemp('plezy_upload_symbols_'); + }); + + tearDown(() async { + await repository.delete(recursive: true); + }); + + test('discovers symbols and passes the complete upload request to the plugin', () async { + final symbolRoot = Directory(path.join(repository.path, 'debug-info', 'linux-x64'))..createSync(recursive: true); + final archive = File(path.join(symbolRoot.path, 'symbols.zip'))..writeAsStringSync('symbols'); + final symbolMap = File(path.join(symbolRoot.path, 'obfuscation.map.json'))..writeAsStringSync('{}'); + late List uploadArguments; + late Map uploadEnvironment; + late String uploadWorkingDirectory; + + final result = await runUploadSymbols( + const ['linux-x64'], + repositoryRoot: repository, + environment: const { + 'BUGS_ADMIN_TOKEN': 'admin-token', + 'BUGS_URL': 'https://bugs.example.test', + 'SENTRY_RELEASE': 'plezy@test', + 'SENTRY_DIST': 'test-linux', + }, + uploader: ({required arguments, required environment, required workingDirectory}) async { + uploadArguments = arguments; + uploadEnvironment = environment; + uploadWorkingDirectory = workingDirectory; + return 0; + }, + ); + + expect(result, 0); + expect(archive.existsSync(), isTrue); + expect(uploadWorkingDirectory, path.normalize(path.absolute(repository.path))); + expect(uploadEnvironment['SENTRY_AUTH_TOKEN'], 'admin-token'); + expect(uploadEnvironment['SENTRY_LOG_LEVEL'], 'info'); + expect(uploadArguments, [ + '--sentry-define=release=plezy@test', + '--sentry-define=url=https://bugs.example.test', + '--sentry-define=build_path=${path.join(repository.path, 'build')}', + '--sentry-define=dist=test-linux', + '--sentry-define=symbols_path=${symbolRoot.path}', + '--sentry-define=dart_symbol_map_path=${symbolMap.path}', + ]); + }); + + test('rejects missing platform, symbols, and credentials', () async { + final errors = StringBuffer(); + expect(await runUploadSymbols(const [], repositoryRoot: repository, errors: errors), 1); + expect(errors.toString(), contains('platform arg required')); + + errors.clear(); + expect( + await runUploadSymbols( + const ['windows-x64'], + repositoryRoot: repository, + environment: const {'SENTRY_RELEASE': 'plezy@test'}, + errors: errors, + ), + 3, + ); + expect(errors.toString(), contains('no symbols found')); + + Directory(path.join(repository.path, 'debug-info', 'windows-x64')).createSync(recursive: true); + File(path.join(repository.path, 'debug-info', 'windows-x64', 'symbols.zip')).writeAsStringSync('symbols'); + errors.clear(); + expect( + await runUploadSymbols( + const ['windows-x64'], + repositoryRoot: repository, + environment: const {'SENTRY_RELEASE': 'plezy@test'}, + errors: errors, + ), + 1, + ); + expect(errors.toString(), contains('SENTRY_AUTH_TOKEN or BUGS_ADMIN_TOKEN env var required')); + }); + + test('propagates an HTTP upload failure without network access', () async { + final symbolRoot = Directory(path.join(repository.path, 'debug-info', 'android-apk'))..createSync(recursive: true); + File(path.join(symbolRoot.path, 'symbols.zip')).writeAsStringSync('symbols'); + final errors = StringBuffer(); + + final result = await runUploadSymbols( + const ['android-apk'], + repositoryRoot: repository, + environment: const {'SENTRY_AUTH_TOKEN': 'token', 'SENTRY_RELEASE': 'plezy@test'}, + errors: errors, + uploader: ({required arguments, required environment, required workingDirectory}) async => 22, + ); + + expect(result, 22); + expect(errors.toString(), contains('symbol upload failed for android-apk (exit code 22)')); + }); +}