diff --git a/packages/shorebird_cli/lib/src/archive_analysis/ios_archive_differ.dart b/packages/shorebird_cli/lib/src/archive_analysis/ios_archive_differ.dart index 27834834..c8c9ab7a 100644 --- a/packages/shorebird_cli/lib/src/archive_analysis/ios_archive_differ.dart +++ b/packages/shorebird_cli/lib/src/archive_analysis/ios_archive_differ.dart @@ -138,9 +138,6 @@ class IosArchiveDiffer extends ArchiveDiffer { if (Platform.isMacOS) { Process.runSync('assetutil', ['--info', outPath, '-o', assetInfoPath]); - } else { - // This is just for testing - File(assetInfoPath).createSync(recursive: true); } // Remove the timestamp line from the json file diff --git a/packages/shorebird_cli/lib/src/command_runner.dart b/packages/shorebird_cli/lib/src/command_runner.dart index 2538a8f7..a100c986 100644 --- a/packages/shorebird_cli/lib/src/command_runner.dart +++ b/packages/shorebird_cli/lib/src/command_runner.dart @@ -7,7 +7,6 @@ import 'package:mason_logger/mason_logger.dart'; import 'package:scoped/scoped.dart'; import 'package:shorebird_cli/src/commands/commands.dart'; import 'package:shorebird_cli/src/commands/patch_new/patch_new.dart'; -import 'package:shorebird_cli/src/commands/release_new/release_new.dart'; import 'package:shorebird_cli/src/engine_config.dart'; import 'package:shorebird_cli/src/logger.dart'; import 'package:shorebird_cli/src/platform.dart'; @@ -78,7 +77,6 @@ class ShorebirdCliCommandRunner extends CompletionCommandRunner { addCommand(PatchNewCommand()); addCommand(PreviewCommand()); addCommand(ReleaseCommand()); - addCommand(ReleaseNewCommand()); addCommand(RunCommand()); addCommand(UpgradeCommand()); } diff --git a/packages/shorebird_cli/lib/src/commands/release_new/aar_releaser.dart b/packages/shorebird_cli/lib/src/commands/release/aar_releaser.dart similarity index 98% rename from packages/shorebird_cli/lib/src/commands/release_new/aar_releaser.dart rename to packages/shorebird_cli/lib/src/commands/release/aar_releaser.dart index e2603a34..246dce61 100644 --- a/packages/shorebird_cli/lib/src/commands/release_new/aar_releaser.dart +++ b/packages/shorebird_cli/lib/src/commands/release/aar_releaser.dart @@ -4,7 +4,7 @@ import 'package:mason_logger/mason_logger.dart'; import 'package:path/path.dart' as p; import 'package:shorebird_cli/src/artifact_builder.dart'; import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; -import 'package:shorebird_cli/src/commands/release_new/releaser.dart'; +import 'package:shorebird_cli/src/commands/release/releaser.dart'; import 'package:shorebird_cli/src/logger.dart'; import 'package:shorebird_cli/src/platform.dart'; import 'package:shorebird_cli/src/platform/platform.dart'; diff --git a/packages/shorebird_cli/lib/src/commands/release_new/android_releaser.dart b/packages/shorebird_cli/lib/src/commands/release/android_releaser.dart similarity index 97% rename from packages/shorebird_cli/lib/src/commands/release_new/android_releaser.dart rename to packages/shorebird_cli/lib/src/commands/release/android_releaser.dart index b32321b4..704f61e0 100644 --- a/packages/shorebird_cli/lib/src/commands/release_new/android_releaser.dart +++ b/packages/shorebird_cli/lib/src/commands/release/android_releaser.dart @@ -1,8 +1,8 @@ import 'package:mason_logger/mason_logger.dart'; import 'package:shorebird_cli/src/artifact_builder.dart'; import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; -import 'package:shorebird_cli/src/commands/release_new/release_new.dart'; -import 'package:shorebird_cli/src/commands/release_new/releaser.dart'; +import 'package:shorebird_cli/src/commands/release/release.dart'; +import 'package:shorebird_cli/src/commands/release/releaser.dart'; import 'package:shorebird_cli/src/doctor.dart'; import 'package:shorebird_cli/src/logger.dart'; import 'package:shorebird_cli/src/platform.dart'; diff --git a/packages/shorebird_cli/lib/src/commands/release_new/ios_framework_releaser.dart b/packages/shorebird_cli/lib/src/commands/release/ios_framework_releaser.dart similarity index 98% rename from packages/shorebird_cli/lib/src/commands/release_new/ios_framework_releaser.dart rename to packages/shorebird_cli/lib/src/commands/release/ios_framework_releaser.dart index e3b11a15..b3d981c6 100644 --- a/packages/shorebird_cli/lib/src/commands/release_new/ios_framework_releaser.dart +++ b/packages/shorebird_cli/lib/src/commands/release/ios_framework_releaser.dart @@ -5,7 +5,7 @@ import 'package:platform/platform.dart'; import 'package:shorebird_cli/src/artifact_builder.dart'; import 'package:shorebird_cli/src/artifact_manager.dart'; import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; -import 'package:shorebird_cli/src/commands/release_new/releaser.dart'; +import 'package:shorebird_cli/src/commands/release/releaser.dart'; import 'package:shorebird_cli/src/doctor.dart'; import 'package:shorebird_cli/src/executables/xcodebuild.dart'; import 'package:shorebird_cli/src/logger.dart'; diff --git a/packages/shorebird_cli/lib/src/commands/release_new/ios_releaser.dart b/packages/shorebird_cli/lib/src/commands/release/ios_releaser.dart similarity index 93% rename from packages/shorebird_cli/lib/src/commands/release_new/ios_releaser.dart rename to packages/shorebird_cli/lib/src/commands/release/ios_releaser.dart index dc7334d0..3e20c59f 100644 --- a/packages/shorebird_cli/lib/src/commands/release_new/ios_releaser.dart +++ b/packages/shorebird_cli/lib/src/commands/release/ios_releaser.dart @@ -1,11 +1,12 @@ import 'package:mason_logger/mason_logger.dart'; import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; +import 'package:pub_semver/pub_semver.dart'; import 'package:shorebird_cli/src/archive_analysis/plist.dart'; import 'package:shorebird_cli/src/artifact_builder.dart'; import 'package:shorebird_cli/src/artifact_manager.dart'; import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; -import 'package:shorebird_cli/src/commands/release_new/releaser.dart'; +import 'package:shorebird_cli/src/commands/release/releaser.dart'; import 'package:shorebird_cli/src/doctor.dart'; import 'package:shorebird_cli/src/executables/xcodebuild.dart'; import 'package:shorebird_cli/src/logger.dart'; @@ -61,6 +62,17 @@ class IosReleaser extends Releaser { } on PreconditionFailedException catch (e) { exit(e.exitCode.code); } + + final flutterVersionArg = argResults['flutter-version'] as String?; + if (flutterVersionArg != null) { + if (Version.parse(flutterVersionArg) < + minimumSupportedIosFlutterVersion) { + logger.err( + '''iOS releases are not supported with Flutter versions older than $minimumSupportedIosFlutterVersion.''', + ); + exit(ExitCode.usage.code); + } + } } @override diff --git a/packages/shorebird_cli/lib/src/commands/release/release.dart b/packages/shorebird_cli/lib/src/commands/release/release.dart index d5d83ea7..9ca9dd96 100644 --- a/packages/shorebird_cli/lib/src/commands/release/release.dart +++ b/packages/shorebird_cli/lib/src/commands/release/release.dart @@ -1,5 +1,6 @@ -export 'release_aar_command.dart'; -export 'release_android_command.dart'; +export 'aar_releaser.dart'; +export 'android_releaser.dart'; +export 'ios_framework_releaser.dart'; +export 'ios_releaser.dart'; export 'release_command.dart'; -export 'release_ios_command.dart'; -export 'release_ios_framework_command.dart'; +export 'releaser.dart'; diff --git a/packages/shorebird_cli/lib/src/commands/release/release_aar_command.dart b/packages/shorebird_cli/lib/src/commands/release/release_aar_command.dart deleted file mode 100644 index 9555f961..00000000 --- a/packages/shorebird_cli/lib/src/commands/release/release_aar_command.dart +++ /dev/null @@ -1,324 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:archive/archive_io.dart'; -import 'package:io/io.dart' show copyPath; -import 'package:mason_logger/mason_logger.dart'; -import 'package:path/path.dart' as p; -import 'package:scoped/scoped.dart'; -import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; -import 'package:shorebird_cli/src/command.dart'; -import 'package:shorebird_cli/src/commands/release/release.dart'; -import 'package:shorebird_cli/src/config/config.dart'; -import 'package:shorebird_cli/src/logger.dart'; -import 'package:shorebird_cli/src/platform.dart'; -import 'package:shorebird_cli/src/platform/platform.dart'; -import 'package:shorebird_cli/src/shorebird_artifact_mixin.dart'; -import 'package:shorebird_cli/src/shorebird_build_mixin.dart'; -import 'package:shorebird_cli/src/shorebird_env.dart'; -import 'package:shorebird_cli/src/shorebird_flutter.dart'; -import 'package:shorebird_cli/src/shorebird_release_version_mixin.dart'; -import 'package:shorebird_cli/src/shorebird_validator.dart'; -import 'package:shorebird_cli/src/version.dart'; -import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; - -/// {@template release_aar_command} -/// `shorebird release aar` -/// Create new Android archive releases. -/// {@endtemplate} -class ReleaseAarCommand extends ShorebirdCommand - with - ShorebirdBuildMixin, - ShorebirdReleaseVersionMixin, - ShorebirdArtifactMixin { - /// {@macro release_aar_command} - ReleaseAarCommand({ - UnzipFn? unzipFn, - }) : _unzipFn = unzipFn ?? extractFileToDisk { - argParser - ..addOption( - 'release-version', - help: ''' -The version of the associated release (e.g. "1.0.0"). This should be the version -of the Android app that is using this module.''', - mandatory: true, - ) - // `flutter build aar` defaults to a build number of 1.0, so we do the - // same. - ..addOption( - 'build-number', - help: 'The build number of the aar', - defaultsTo: '1.0', - ) - ..addOption( - 'flutter-version', - help: 'The Flutter version to use when building the app (e.g: 3.16.3).', - ) - ..addMultiOption( - 'target-platform', - help: 'The target platform(s) for which the app is compiled.', - defaultsTo: Arch.values.map((arch) => arch.targetPlatformCliArg), - allowed: Arch.values.map((arch) => arch.targetPlatformCliArg), - ); - } - - @override - String get name => 'aar'; - - @override - String get description => ''' -Builds and submits your Android archive to Shorebird. -Shorebird saves the compiled Dart code from your application in order to -make smaller updates to your app. -'''; - - final UnzipFn _unzipFn; - - @override - Future run() async { - try { - await shorebirdValidator.validatePreconditions( - checkUserIsAuthenticated: true, - checkShorebirdInitialized: true, - ); - } on PreconditionFailedException catch (e) { - return e.exitCode.code; - } - - if (shorebirdEnv.androidPackageName == null) { - logger.err('Could not find androidPackage in pubspec.yaml.'); - return ExitCode.config.code; - } - - const releasePlatform = ReleasePlatform.android; - final buildNumber = results['build-number'] as String; - final releaseVersion = results['release-version'] as String; - final flutterVersion = results['flutter-version'] as String?; - final shorebirdYaml = shorebirdEnv.getShorebirdYaml()!; - final appId = shorebirdYaml.getAppId(); - final app = await codePushClientWrapper.getApp(appId: appId); - final architectures = (results['target-platform'] as List) - .map( - (platform) => AndroidArch.availableAndroidArchs - .firstWhere((arch) => arch.targetPlatformCliArg == platform), - ) - .toSet(); - - final existingRelease = await codePushClientWrapper.maybeGetRelease( - appId: appId, - releaseVersion: releaseVersion, - ); - - if (existingRelease != null) { - codePushClientWrapper.ensureReleaseIsNotActive( - release: existingRelease, - platform: releasePlatform, - ); - } - - var flutterRevisionForRelease = shorebirdEnv.flutterRevision; - if (flutterVersion != null) { - final String? revision; - try { - revision = await shorebirdFlutter.getRevisionForVersion( - flutterVersion, - ); - } catch (error) { - logger.err( - ''' -Unable to determine revision for Flutter version: $flutterVersion. -$error''', - ); - return ExitCode.software.code; - } - - if (revision == null) { - final openIssueLink = link( - uri: Uri.parse( - 'https://github.com/shorebirdtech/shorebird/issues/new?assignees=&labels=feature&projects=&template=feature_request.md&title=feat%3A+', - ), - message: 'open an issue', - ); - logger.err(''' -Version $flutterVersion not found. Please $openIssueLink to request a new version. -Use `shorebird flutter versions list` to list available versions. -'''); - return ExitCode.software.code; - } - - flutterRevisionForRelease = revision; - } - - try { - await shorebirdFlutter.installRevision( - revision: flutterRevisionForRelease, - ); - } catch (_) { - return ExitCode.software.code; - } - - final releaseFlutterShorebirdEnv = shorebirdEnv.copyWith( - flutterRevisionOverride: flutterRevisionForRelease, - ); - - return await runScoped( - () async { - final flutterVersionString = - await shorebirdFlutter.getVersionAndRevision(); - - final buildProgress = logger.progress( - 'Building release with Flutter $flutterVersionString', - ); - - try { - await buildAar( - buildNumber: buildNumber, - targetPlatforms: architectures, - ); - } on ProcessException catch (error) { - buildProgress.fail('Failed to build: ${error.message}'); - return ExitCode.software.code; - } - buildProgress.complete(); - - final archNames = architectures.map((arch) => arch.name); - final summary = [ - '''šŸ“± App: ${lightCyan.wrap(app.displayName)} ${lightCyan.wrap('(${app.appId})')}''', - 'šŸ“¦ Release Version: ${lightCyan.wrap(releaseVersion)}', - '''šŸ•¹ļø Platform: ${lightCyan.wrap(releasePlatform.name)} ${lightCyan.wrap('(${archNames.join(', ')})')}''', - '🐦 Flutter Version: ${lightCyan.wrap(flutterVersionString)}', - ]; - - logger.info(''' - -${styleBold.wrap(lightGreen.wrap('šŸš€ Ready to create a new release!'))} - -${summary.join('\n')} -'''); - - if (shorebirdEnv.canAcceptUserInput) { - final confirm = logger.confirm('Would you like to continue?'); - - if (!confirm) { - logger.info('Aborting.'); - return ExitCode.success.code; - } - } - - final Release release; - if (existingRelease != null) { - release = existingRelease; - await codePushClientWrapper.updateReleaseStatus( - appId: appId, - releaseId: release.id, - platform: releasePlatform, - status: ReleaseStatus.draft, - ); - } else { - release = await codePushClientWrapper.createRelease( - appId: appId, - version: releaseVersion, - flutterRevision: shorebirdEnv.flutterRevision, - platform: releasePlatform, - ); - } - - // Copy release AAR to a new directory to avoid overwriting with - // subsequent patch builds. - final sourceLibraryDirectory = Directory(aarLibraryPath); - final targetLibraryDirectory = Directory( - p.join(shorebirdEnv.getShorebirdProjectRoot()!.path, 'release'), - ); - await copyPath( - sourceLibraryDirectory.path, - targetLibraryDirectory.path, - ); - - final extractAarProgress = logger.progress('Creating artifacts'); - final extractedAarDir = await extractAar( - packageName: shorebirdEnv.androidPackageName!, - buildNumber: buildNumber, - unzipFn: _unzipFn, - ); - extractAarProgress.complete(); - - await codePushClientWrapper.createAndroidArchiveReleaseArtifacts( - appId: app.appId, - releaseId: release.id, - platform: releasePlatform, - aarPath: aarArtifactPath( - packageName: shorebirdEnv.androidPackageName!, - buildNumber: buildNumber, - ), - extractedAarDir: extractedAarDir, - architectures: architectures, - ); - - await codePushClientWrapper.updateReleaseStatus( - appId: app.appId, - releaseId: release.id, - platform: releasePlatform, - status: ReleaseStatus.active, - metadata: UpdateReleaseMetadata( - releasePlatform: releasePlatform, - flutterVersionOverride: flutterVersion, - generatedApks: false, - environment: BuildEnvironmentMetadata( - operatingSystem: platform.operatingSystem, - operatingSystemVersion: platform.operatingSystemVersion, - shorebirdVersion: packageVersion, - xcodeVersion: null, - ), - ), - ); - - logger - ..success('\nāœ… Published Release ${release.version}!') - ..info(''' - -Your next steps: - -1. Add the aar repo and Shorebird's maven url to your app's settings.gradle: - -Note: The maven url needs to be a relative path from your settings.gradle file to the aar library. The code below assumes your Flutter module is in a sibling directory of your Android app. - -${lightCyan.wrap(''' -dependencyResolutionManagement { - repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) - repositories { - google() - mavenCentral() -+ maven { -+ url '../${p.basename(shorebirdEnv.getShorebirdProjectRoot()!.path)}/${p.relative(targetLibraryDirectory.path)}' -+ } -+ maven { -- url 'https://storage.googleapis.com/download.flutter.io' -+ url 'https://download.shorebird.dev/download.flutter.io' -+ } - } -} -''')} - -2. Add this module as a dependency in your app's build.gradle: -${lightCyan.wrap(''' -dependencies { - // ... - releaseImplementation '${shorebirdEnv.androidPackageName}:flutter_release:$buildNumber' - // ... -}''')} -'''); - - ReleaseCommand.printPatchInstructions( - name: name, - releaseVersion: release.version, - requiresReleaseVersion: true, - ); - - return ExitCode.success.code; - }, - values: { - shorebirdEnvRef.overrideWith(() => releaseFlutterShorebirdEnv), - }, - ); - } -} diff --git a/packages/shorebird_cli/lib/src/commands/release/release_android_command.dart b/packages/shorebird_cli/lib/src/commands/release/release_android_command.dart deleted file mode 100644 index 4190fb93..00000000 --- a/packages/shorebird_cli/lib/src/commands/release/release_android_command.dart +++ /dev/null @@ -1,343 +0,0 @@ -import 'dart:io'; - -import 'package:mason_logger/mason_logger.dart'; -import 'package:scoped/scoped.dart'; -import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; -import 'package:shorebird_cli/src/command.dart'; -import 'package:shorebird_cli/src/commands/release/release.dart'; -import 'package:shorebird_cli/src/config/shorebird_yaml.dart'; -import 'package:shorebird_cli/src/doctor.dart'; -import 'package:shorebird_cli/src/extensions/arg_results.dart'; -import 'package:shorebird_cli/src/logger.dart'; -import 'package:shorebird_cli/src/platform.dart'; -import 'package:shorebird_cli/src/platform/platform.dart'; -import 'package:shorebird_cli/src/shorebird_build_mixin.dart'; -import 'package:shorebird_cli/src/shorebird_env.dart'; -import 'package:shorebird_cli/src/shorebird_flutter.dart'; -import 'package:shorebird_cli/src/shorebird_release_version_mixin.dart'; -import 'package:shorebird_cli/src/shorebird_validator.dart'; -import 'package:shorebird_cli/src/version.dart'; -import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; - -/// {@template release_android_command} -/// `shorebird release android` -/// Create new app releases for Android. -/// {@endtemplate} -class ReleaseAndroidCommand extends ShorebirdCommand - with ShorebirdBuildMixin, ShorebirdReleaseVersionMixin { - /// {@macro release_android_command} - ReleaseAndroidCommand() { - argParser - ..addOption( - 'target', - abbr: 't', - help: 'The main entrypoint file of the application.', - ) - ..addOption( - 'flavor', - help: 'The product flavor to use when building the app.', - ) - ..addOption( - 'artifact', - help: 'They type of artifact to generate.', - allowed: ['aab', 'apk'], - defaultsTo: 'aab', - allowedHelp: { - 'aab': 'Android App Bundle', - 'apk': 'Android Package Kit', - }, - ) - ..addOption( - 'flutter-version', - help: 'The Flutter version to use when building the app (e.g: 3.16.3).', - ) - ..addFlag( - 'split-per-abi', - help: 'Whether to split the APKs per ABIs. ' - 'To learn more, see: https://developer.android.com/studio/build/configure-apk-splits#configure-abi-split', - hide: true, - negatable: false, - ) - ..addMultiOption( - 'target-platform', - help: 'The target platform(s) for which the app is compiled.', - defaultsTo: Arch.values.map((arch) => arch.targetPlatformCliArg), - allowed: Arch.values.map((arch) => arch.targetPlatformCliArg), - ); - } - - @override - String get description => ''' -Builds and submits your Android app to Shorebird. -Shorebird saves the compiled Dart code from your application in order to -make smaller updates to your app. -'''; - - @override - String get name => 'android'; - - @override - Future run() async { - try { - await shorebirdValidator.validatePreconditions( - checkUserIsAuthenticated: true, - checkShorebirdInitialized: true, - validators: doctor.androidCommandValidators, - ); - } on PreconditionFailedException catch (e) { - return e.exitCode.code; - } - - const releasePlatform = ReleasePlatform.android; - final flavor = results.findOption('flavor', argParser: argParser); - final target = results.findOption('target', argParser: argParser); - final generateApk = results['artifact'] as String == 'apk'; - final splitApk = results['split-per-abi'] == true; - final flutterVersion = results['flutter-version'] as String?; - final architectures = (results['target-platform'] as List) - .map( - (platform) => AndroidArch.availableAndroidArchs - .firstWhere((arch) => arch.targetPlatformCliArg == platform), - ) - .toSet(); - - if (generateApk && splitApk) { - logger - ..err( - 'Shorebird does not support the split-per-abi option at this time', - ) - ..info( - ''' -Split APKs are each given a different release version than what is specified in the pubspec.yaml. - -See ${link(uri: Uri.parse('https://github.com/flutter/flutter/issues/39817'))} for more information about this issue. -Please comment and upvote ${link(uri: Uri.parse('https://github.com/shorebirdtech/shorebird/issues/1141'))} if you would like shorebird to support this.''', - ); - return ExitCode.unavailable.code; - } - - var flutterRevisionForRelease = shorebirdEnv.flutterRevision; - if (flutterVersion != null) { - final String? revision; - try { - revision = await shorebirdFlutter.getRevisionForVersion( - flutterVersion, - ); - } catch (error) { - logger.err( - ''' -Unable to determine revision for Flutter version: $flutterVersion. -$error''', - ); - return ExitCode.software.code; - } - - if (revision == null) { - final openIssueLink = link( - uri: Uri.parse( - 'https://github.com/shorebirdtech/shorebird/issues/new?assignees=&labels=feature&projects=&template=feature_request.md&title=feat%3A+', - ), - message: 'open an issue', - ); - logger.err(''' -Version $flutterVersion not found. Please $openIssueLink to request a new version. -Use `shorebird flutter versions list` to list available versions. -'''); - return ExitCode.software.code; - } - - flutterRevisionForRelease = revision; - } - - try { - await shorebirdFlutter.installRevision( - revision: flutterRevisionForRelease, - ); - } catch (_) { - return ExitCode.software.code; - } - - final releaseFlutterShorebirdEnv = shorebirdEnv.copyWith( - flutterRevisionOverride: flutterRevisionForRelease, - ); - - return await runScoped( - () async { - final flutterVersionString = - await shorebirdFlutter.getVersionAndRevision(); - - final buildProgress = logger.progress( - 'Building release with Flutter $flutterVersionString', - ); - - late final File apkFile; - final File aabFile; - - try { - aabFile = await buildAppBundle( - flavor: flavor, - target: target, - targetPlatforms: architectures, - ); - if (generateApk) { - apkFile = await buildApk( - flavor: flavor, - target: target, - targetPlatforms: architectures, - ); - } - } on BuildException catch (error) { - buildProgress.fail(error.message); - return ExitCode.software.code; - } - buildProgress.complete(); - - final projectRoot = shorebirdEnv.getShorebirdProjectRoot()!; - final shorebirdYaml = shorebirdEnv.getShorebirdYaml()!; - final appId = shorebirdYaml.getAppId(flavor: flavor); - final app = await codePushClientWrapper.getApp(appId: appId); - - final String releaseVersion; - final detectReleaseVersionProgress = logger.progress( - 'Detecting release version', - ); - try { - releaseVersion = await extractReleaseVersionFromAppBundle( - aabFile.path, - ); - detectReleaseVersionProgress.complete(); - } catch (error) { - detectReleaseVersionProgress.fail('$error'); - return ExitCode.software.code; - } - - final existingRelease = await codePushClientWrapper.maybeGetRelease( - appId: appId, - releaseVersion: releaseVersion, - ); - - if (existingRelease != null) { - codePushClientWrapper.ensureReleaseIsNotActive( - release: existingRelease, - platform: releasePlatform, - ); - - // All artifacts associated with a given release must be built - // with the same Flutter revision. - if (existingRelease.flutterRevision != flutterRevisionForRelease) { - ReleaseCommand.printConflictingFlutterRevisionError( - existingFlutterRevision: existingRelease.flutterRevision, - currentFlutterRevision: flutterRevisionForRelease, - releaseVersion: releaseVersion, - ); - return ExitCode.software.code; - } - } - - final archNames = architectures.map((a) => a.name); - final summary = [ - '''šŸ“± App: ${lightCyan.wrap(app.displayName)} ${lightCyan.wrap('(${app.appId})')}''', - if (flavor != null) 'šŸ§ Flavor: ${lightCyan.wrap(flavor)}', - 'šŸ“¦ Release Version: ${lightCyan.wrap(releaseVersion)}', - '''šŸ•¹ļø Platform: ${lightCyan.wrap(releasePlatform.name)} ${lightCyan.wrap('(${archNames.join(', ')})')}''', - '🐦 Flutter Version: ${lightCyan.wrap(flutterVersionString)}', - ]; - - logger.info(''' - -${styleBold.wrap(lightGreen.wrap('šŸš€ Ready to create a new release!'))} - -${summary.join('\n')} -'''); - - if (shorebirdEnv.canAcceptUserInput) { - final confirm = logger.confirm('Would you like to continue?'); - - if (!confirm) { - logger.info('Aborting.'); - return ExitCode.success.code; - } - } - - final Release release; - if (existingRelease != null) { - release = existingRelease; - await codePushClientWrapper.updateReleaseStatus( - appId: appId, - releaseId: release.id, - platform: releasePlatform, - status: ReleaseStatus.draft, - ); - } else { - release = await codePushClientWrapper.createRelease( - appId: appId, - version: releaseVersion, - flutterRevision: shorebirdEnv.flutterRevision, - platform: releasePlatform, - ); - } - - await codePushClientWrapper.createAndroidReleaseArtifacts( - appId: app.appId, - releaseId: release.id, - projectRoot: projectRoot.path, - aabPath: aabFile.path, - platform: releasePlatform, - architectures: architectures, - flavor: flavor, - ); - - await codePushClientWrapper.updateReleaseStatus( - appId: app.appId, - releaseId: release.id, - platform: releasePlatform, - status: ReleaseStatus.active, - metadata: UpdateReleaseMetadata( - releasePlatform: releasePlatform, - flutterVersionOverride: flutterVersion, - generatedApks: generateApk, - environment: BuildEnvironmentMetadata( - operatingSystem: platform.operatingSystem, - operatingSystemVersion: platform.operatingSystemVersion, - shorebirdVersion: packageVersion, - xcodeVersion: null, - ), - ), - ); - - // The extra newline before and no newline after is intentional. See - // unit tests for testing of output. - final apkText = generateApk - ? ''' - -Or distribute the apk: -${lightCyan.wrap(apkFile.path)} -''' - : ''; - - logger - ..success('\nāœ… Published Release ${release.version}!') - ..info(''' - -Your next step is to upload the app bundle to the Play Store: -${lightCyan.wrap(aabFile.path)} -$apkText -For information on uploading to the Play Store, see: -${link(uri: Uri.parse('https://support.google.com/googleplay/android-developer/answer/9859152?hl=en'))} -'''); - - ReleaseCommand.printPatchInstructions( - name: name, - flavor: flavor, - target: target, - releaseVersion: release.version, - ); - - return ExitCode.success.code; - }, - values: { - shorebirdEnvRef.overrideWith(() => releaseFlutterShorebirdEnv), - }, - ); - } -} diff --git a/packages/shorebird_cli/lib/src/commands/release/release_command.dart b/packages/shorebird_cli/lib/src/commands/release/release_command.dart index 3165f463..1434402f 100644 --- a/packages/shorebird_cli/lib/src/commands/release/release_command.dart +++ b/packages/shorebird_cli/lib/src/commands/release/release_command.dart @@ -1,58 +1,440 @@ +import 'dart:async'; + import 'package:mason_logger/mason_logger.dart'; +import 'package:meta/meta.dart'; +import 'package:scoped/scoped.dart'; +import 'package:shorebird_cli/src/cache.dart'; +import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; import 'package:shorebird_cli/src/command.dart'; -import 'package:shorebird_cli/src/commands/commands.dart'; +import 'package:shorebird_cli/src/commands/release/release.dart'; +import 'package:shorebird_cli/src/config/config.dart'; +import 'package:shorebird_cli/src/extensions/arg_results.dart'; import 'package:shorebird_cli/src/logger.dart'; +import 'package:shorebird_cli/src/platform/platform.dart'; +import 'package:shorebird_cli/src/release_type.dart'; +import 'package:shorebird_cli/src/shorebird_env.dart'; +import 'package:shorebird_cli/src/shorebird_flutter.dart'; +import 'package:shorebird_cli/src/third_party/flutter_tools/lib/flutter_tools.dart'; +import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; +import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart'; + +typedef ResolveReleaser = Releaser Function(ReleaseType releaseType); /// {@template release_command} -/// `shorebird release` -/// Create new app releases. +/// Creates a new app release for the specified platform(s). /// {@endtemplate} class ReleaseCommand extends ShorebirdCommand { /// {@macro release_command} - ReleaseCommand() { - addSubcommand(ReleaseAarCommand()); - addSubcommand(ReleaseAndroidCommand()); - addSubcommand(ReleaseIosCommand()); - addSubcommand(ReleaseIosFrameworkCommand()); + ReleaseCommand({ResolveReleaser? resolveReleaser}) { + _resolveReleaser = resolveReleaser ?? getReleaser; + argParser + ..addOption( + 'target', + abbr: 't', + help: 'The main entrypoint file of the application.', + ) + ..addOption( + 'flavor', + help: 'The product flavor to use when building the app.', + ) + ..addOption( + 'build-number', + help: ''' +An identifier used as an internal version number. +Each build must have a unique identifier to differentiate it from previous builds. +It is used to determine whether one build is more recent than another, with higher numbers indicating more recent build. +On Android it is used as "versionCode". +On Xcode builds it is used as "CFBundleVersion".''', + defaultsTo: '1.0', + ) + ..addFlag( + 'codesign', + help: 'Codesign the application bundle.', + defaultsTo: true, + ) + ..addFlag( + 'dry-run', + abbr: 'n', + negatable: false, + help: 'Validate but do not upload the release.', + ) + ..addOption( + exportOptionsPlistArgName, + help: + '''Export an IPA with these options. See "xcodebuild -h" for available exportOptionsPlist keys.''', + ) + ..addOption( + 'flutter-version', + help: 'The Flutter version to use when building the app (e.g: 3.16.3).', + ) + ..addOption( + 'android-artifact', + help: + '''The type of artifact to generate. Only relevant for Android releases.''', + allowed: ['aab', 'apk'], + defaultsTo: 'aab', + allowedHelp: { + 'aab': 'Android App Bundle', + 'apk': 'Android Package Kit', + }, + ) + ..addMultiOption( + 'platform', + abbr: 'p', + help: 'The platform(s) to to build this release for.', + allowed: ReleaseType.values.map((e) => e.cliName).toList(), + // TODO(bryanoltman): uncomment this once https://github.com/dart-lang/args/pull/273 lands + // mandatory: true. + ) + ..addOption( + 'release-version', + help: ''' +The version of the associated release (e.g. "1.0.0"). This should be the version +of the iOS app that is using this module.''', + ) + ..addMultiOption( + 'target-platform', + help: 'The target platform(s) for which the app is compiled.', + defaultsTo: Arch.values.map((arch) => arch.targetPlatformCliArg), + allowed: Arch.values.map((arch) => arch.targetPlatformCliArg), + ); } + late final ResolveReleaser _resolveReleaser; + @override - String get description => 'Manage your Shorebird app releases.'; + bool get hidden => true; + + @override + String get description => + 'Creates a shorebird release for the provided target platforms'; @override String get name => 'release'; - static void printConflictingFlutterRevisionError({ - required String existingFlutterRevision, - required String currentFlutterRevision, - required String releaseVersion, - }) { - logger.err( - ''' -${styleBold.wrap(lightRed.wrap('A release with version $releaseVersion already exists but was built using a different Flutter revision.'))} + @override + Future run() async { + final releaserFutures = + results.releaseTypes.map(_resolveReleaser).map(createRelease); - Existing release built with: ${lightCyan.wrap(existingFlutterRevision)} - Current release built with: ${lightCyan.wrap(currentFlutterRevision)} + for (final future in releaserFutures) { + await future; + } + + return ExitCode.success.code; + } + + @visibleForTesting + Releaser getReleaser(ReleaseType releaseType) { + switch (releaseType) { + case ReleaseType.android: + return AndroidReleaser( + argResults: results, + flavor: flavor, + target: target, + ); + case ReleaseType.ios: + return IosReleaser( + argResults: results, + flavor: flavor, + target: target, + ); + case ReleaseType.iosFramework: + return IosFrameworkReleaser( + argResults: results, + flavor: flavor, + target: target, + ); + case ReleaseType.aar: + return AarReleaser( + argResults: results, + flavor: flavor, + target: target, + ); + } + } + + /// The shorebird app ID for the current project. + String get appId => shorebirdEnv.getShorebirdYaml()!.getAppId(flavor: flavor); + + /// The build flavor, if provided. + late String? flavor = results.findOption('flavor', argParser: argParser); + + /// The target script, if provided. + late String? target = results.findOption('target', argParser: argParser); + + /// The flutter version specified by the user, if any. + late String? flutterVersionArg = results['flutter-version'] as String?; + + /// The workflow to create a new release for a Shorebird app. + /// + /// Expectations for methods invoked by this command: + /// - They perform their own logging. If an error occurs, they are + /// responsible for properly logging the error, cleaning up running + /// [Progress]es, etc. + /// - They handle their own exceptions and exit with a non-zero exit code if + /// an error occurs *instead of* throwing an exception. + @visibleForTesting + Future createRelease(Releaser releaser) async { + await releaser.assertPreconditions(); + await releaser.assertArgsAreValid(); + + await cache.updateAll(); + + // This command handles logging, we don't need to provide our own + // progress, error logs, etc. + final app = await codePushClientWrapper.getApp(appId: appId); + final targetFlutterRevision = await resolveTargetFlutterRevision(); + try { + await shorebirdFlutter.installRevision(revision: targetFlutterRevision); + } catch (_) { + exit(ExitCode.software.code); + } + + final releaseFlutterShorebirdEnv = shorebirdEnv.copyWith( + flutterRevisionOverride: targetFlutterRevision, + ); + return await runScoped( + () async { + await cache.updateAll(); + + final releaseArtifact = await releaser.buildReleaseArtifacts(); + final releaseVersion = await releaser.getReleaseVersion( + releaseArtifactRoot: releaseArtifact, + ); + + // Ensure we can create a release from what we've built. + await ensureVersionIsReleasable( + version: releaseVersion, + flutterRevision: targetFlutterRevision, + releasePlatform: releaser.releaseType.releasePlatform, + ); + + final dryRun = results['dry-run'] == true; + if (dryRun) { + logger + ..info('No issues detected.') + ..info('The server may enforce additional checks.'); + exit(ExitCode.success.code); + } + + // Ask the user to proceed (this is skipped when running via CI). + await confirmCreateRelease( + app: app, + releaseVersion: releaseVersion, + flutterVersion: targetFlutterRevision, + releasePlatform: releaser.releaseType.releasePlatform, + ); + + final release = await getOrCreateRelease( + version: releaseVersion, + releasePlatform: releaser.releaseType.releasePlatform, + ); + await prepareRelease(release: release, releaser: releaser); + await releaser.uploadReleaseArtifacts(release: release, appId: appId); + await finalizeRelease(release: release, releaser: releaser); + + logger + ..success(''' + +āœ… Published Release ${release.version}!''') + ..info(releaser.postReleaseInstructions); + + printPatchInstructions( + releaser: releaser, + releaseVersion: releaseVersion, + releaseType: releaser.releaseType, + flavor: flavor, + target: target, + ); + }, + values: { + shorebirdEnvRef.overrideWith(() => releaseFlutterShorebirdEnv), + }, + ); + } + + /// Determines which Flutter version to use for the release. This will be + /// either the version specified by the user or the version provided by + /// [shorebirdEnv]. Will exit with [ExitCode.software] if the version + /// specified by the user is not found/supported. + Future resolveTargetFlutterRevision() async { + if (flutterVersionArg != null) { + final String? revision; + try { + revision = await shorebirdFlutter.getRevisionForVersion( + flutterVersionArg!, + ); + } catch (error) { + logger.err( + ''' +Unable to determine revision for Flutter version: $flutterVersionArg. +$error''', + ); + exit(ExitCode.software.code); + } + + if (revision == null) { + final openIssueLink = link( + uri: Uri.parse( + 'https://github.com/shorebirdtech/shorebird/issues/new?assignees=&labels=feature&projects=&template=feature_request.md&title=feat%3A+', + ), + message: 'open an issue', + ); + logger.err( + ''' +Version $flutterVersionArg not found. Please $openIssueLink to request a new version. +Use `shorebird flutter versions list` to list available versions. +''', + ); + exit(ExitCode.software.code); + } + + return revision; + } + + return shorebirdEnv.flutterRevision; + } + + /// Asserts that a release with version [version] can be released using + /// flutter revision [flutterRevision]. If a release has already been + /// published with the given [version] for the platform [releasePlatform], or + /// if a release already exists with [version] but was compiled with a + /// different Flutter revision, an error will be thrown. + Future ensureVersionIsReleasable({ + required String version, + required String flutterRevision, + required ReleasePlatform releasePlatform, + }) async { + final existingRelease = await codePushClientWrapper.maybeGetRelease( + appId: appId, + releaseVersion: version, + ); + + if (existingRelease != null) { + codePushClientWrapper.ensureReleaseIsNotActive( + release: existingRelease, + platform: releasePlatform, + ); + + // All artifacts associated with a given release must be built + // with the same Flutter revision. + if (existingRelease.flutterRevision != flutterRevision) { + logger + ..err(''' +${styleBold.wrap(lightRed.wrap('A release with version $version already exists but was built using a different Flutter revision.'))} +''') + ..info(''' + + Existing release built with: ${lightCyan.wrap(existingRelease.flutterRevision)} + Current release built with: ${lightCyan.wrap(flutterRevision)} ${styleBold.wrap(lightRed.wrap('All platforms for a given release must be built using the same Flutter revision.'))} To resolve this issue, you can: - * Re-run the release command with "${lightCyan.wrap('--flutter-version=$existingFlutterRevision')}". + * Re-run the release command with "${lightCyan.wrap('--flutter-version=${existingRelease.flutterRevision}')}". * Delete the existing release and re-run the release command with the desired Flutter version. - * Bump the release version and re-run the release command with the desired Flutter version.''', + * Bump the release version and re-run the release command with the desired Flutter version.'''); + exit(ExitCode.software.code); + } + } + } + + /// Prints a confirmation prompt with details about the release to be created. + /// If the user confirms, the release will be created. If the user cancels, + /// the command will exit with a success code. When running in a headless + /// or CI environment, this prompt will print but will not wait for user + /// confirmation. + Future confirmCreateRelease({ + required AppMetadata app, + required String releaseVersion, + required String flutterVersion, + required ReleasePlatform releasePlatform, + }) async { + final flutterVersionString = await shorebirdFlutter.getVersionAndRevision(); + // TODO(bryanoltman): include archs in the summary for android + // (and other platforms?) + final summary = [ + '''šŸ“± App: ${lightCyan.wrap(app.displayName)} ${lightCyan.wrap('(${app.appId})')}''', + if (flavor != null) 'šŸ§ Flavor: ${lightCyan.wrap(flavor)}', + 'šŸ“¦ Release Version: ${lightCyan.wrap(releaseVersion)}', + 'šŸ•¹ļø Platform: ${lightCyan.wrap(releasePlatform.name)}', + '🐦 Flutter Version: ${lightCyan.wrap(flutterVersionString)}', + ]; + + logger.info(''' + +${styleBold.wrap(lightGreen.wrap('šŸš€ Ready to create a new release!'))} + +${summary.join('\n')} +'''); + + if (shorebirdEnv.canAcceptUserInput) { + final confirm = logger.confirm('Would you like to continue?'); + + if (!confirm) { + logger.info('Aborting.'); + exit(ExitCode.success.code); + } + } + } + + /// Fetches the release with version [version] from the server or creates a + /// new release if none exists. + Future getOrCreateRelease({ + required String version, + required ReleasePlatform releasePlatform, + }) async { + return await codePushClientWrapper.maybeGetRelease( + appId: appId, + releaseVersion: version, + ) ?? + await codePushClientWrapper.createRelease( + appId: appId, + version: version, + flutterRevision: shorebirdEnv.flutterRevision, + platform: releasePlatform, + ); + } + + /// Prepares the release by updating the release status to draft. + Future prepareRelease({ + required Release release, + required Releaser releaser, + }) async { + await codePushClientWrapper.updateReleaseStatus( + appId: appId, + releaseId: release.id, + platform: releaser.releaseType.releasePlatform, + status: ReleaseStatus.draft, ); } - static void printPatchInstructions({ - required String name, + /// Finalizes the release by updating the status to active. + Future finalizeRelease({ + required Release release, + required Releaser releaser, + }) async { + await codePushClientWrapper.updateReleaseStatus( + appId: appId, + releaseId: release.id, + platform: releaser.releaseType.releasePlatform, + status: ReleaseStatus.active, + metadata: await releaser.releaseMetadata(), + ); + } + + /// Instructions explaining how to patch the release that was just creatd. + void printPatchInstructions({ + required Releaser releaser, required String releaseVersion, + required ReleaseType releaseType, String? flavor, String? target, - bool requiresReleaseVersion = false, }) { final baseCommand = [ 'shorebird patch', - name, + '--platform=${releaseType.cliName}', if (flavor != null) '--flavor=$flavor', if (target != null) '--target=$target', ].join(' '); @@ -60,7 +442,7 @@ To resolve this issue, you can: '''To create a patch for this release, run ${lightCyan.wrap('$baseCommand --release-version=$releaseVersion')}''', ); - if (!requiresReleaseVersion) { + if (!releaser.requiresReleaseVersionArg) { logger.info( ''' diff --git a/packages/shorebird_cli/lib/src/commands/release/release_ios_command.dart b/packages/shorebird_cli/lib/src/commands/release/release_ios_command.dart deleted file mode 100644 index e1dbcd86..00000000 --- a/packages/shorebird_cli/lib/src/commands/release/release_ios_command.dart +++ /dev/null @@ -1,389 +0,0 @@ -import 'dart:io' hide Platform; - -import 'package:mason_logger/mason_logger.dart'; -import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; -import 'package:pub_semver/pub_semver.dart'; -import 'package:scoped/scoped.dart'; -import 'package:shorebird_cli/src/archive_analysis/archive_analysis.dart'; -import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; -import 'package:shorebird_cli/src/command.dart'; -import 'package:shorebird_cli/src/commands/release/release.dart'; -import 'package:shorebird_cli/src/config/config.dart'; -import 'package:shorebird_cli/src/doctor.dart'; -import 'package:shorebird_cli/src/executables/xcodebuild.dart'; -import 'package:shorebird_cli/src/extensions/arg_results.dart'; -import 'package:shorebird_cli/src/logger.dart'; -import 'package:shorebird_cli/src/platform.dart'; -import 'package:shorebird_cli/src/platform/platform.dart'; -import 'package:shorebird_cli/src/shorebird_artifact_mixin.dart'; -import 'package:shorebird_cli/src/shorebird_build_mixin.dart'; -import 'package:shorebird_cli/src/shorebird_env.dart'; -import 'package:shorebird_cli/src/shorebird_flutter.dart'; -import 'package:shorebird_cli/src/shorebird_validator.dart'; -import 'package:shorebird_cli/src/version.dart'; -import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; - -/// {@template release_ios_command} -/// `shorebird release ios` -/// Create new app releases for iOS. -/// {@endtemplate} -class ReleaseIosCommand extends ShorebirdCommand - with ShorebirdBuildMixin, ShorebirdArtifactMixin { - /// {@macro release_ios_command} - ReleaseIosCommand() { - argParser - ..addOption( - 'target', - abbr: 't', - help: 'The main entrypoint file of the application.', - ) - ..addOption( - 'flavor', - help: 'The product flavor to use when building the app.', - ) - ..addOption( - exportMethodArgName, - defaultsTo: ExportMethod.appStore.argName, - allowed: ExportMethod.values.map((e) => e.argName), - help: 'Specify how the IPA will be distributed.', - allowedHelp: { - for (final method in ExportMethod.values) - method.argName: method.description, - }, - ) - ..addOption( - exportOptionsPlistArgName, - help: - '''Export an IPA with these options. See "xcodebuild -h" for available exportOptionsPlist keys.''', - ) - ..addOption( - 'flutter-version', - help: 'The Flutter version to use when building the app (e.g: 3.19.5).', - ) - ..addFlag( - 'codesign', - help: 'Codesign the application bundle.', - defaultsTo: true, - ); - } - - @override - String get name => 'ios'; - - @override - List get aliases => ['ios-alpha']; - - @override - String get description => ''' -Builds and submits your iOS app to Shorebird. -Shorebird saves the compiled Dart code from your application in order to -make smaller updates to your app. -'''; - - @override - Future run() async { - try { - await shorebirdValidator.validatePreconditions( - checkUserIsAuthenticated: true, - checkShorebirdInitialized: true, - validators: doctor.iosCommandValidators, - supportedOperatingSystems: {Platform.macOS}, - ); - } on PreconditionFailedException catch (e) { - return e.exitCode.code; - } - - if (results.rest.contains('--obfuscate')) { - // Obfuscated releases break patching, so we don't support them. - // See https://github.com/shorebirdtech/shorebird/issues/1619 - logger - ..err('Shorebird does not currently support obfuscation on iOS.') - ..info( - '''We hope to support obfuscation in the future. We are tracking this work at ${link(uri: Uri.parse('https://github.com/shorebirdtech/shorebird/issues/1619'))}.''', - ); - return ExitCode.usage.code; - } - - final codesign = results['codesign'] == true; - if (!codesign) { - logger - ..info( - '''Building for device with codesigning disabled. You will have to manually codesign before deploying to device.''', - ) - ..warn( - '''shorebird preview will not work for releases created with "--no-codesign". However, you can still preview your app by signing the generated .xcarchive in Xcode.''', - ); - } - - final File exportOptionsPlist; - try { - exportOptionsPlist = ios.exportOptionsPlistFromArgs(results); - } catch (error) { - logger.err('$error'); - return ExitCode.usage.code; - } - - const releasePlatform = ReleasePlatform.ios; - final flavor = results.findOption('flavor', argParser: argParser); - final target = results.findOption('target', argParser: argParser); - final flutterVersion = results.findOption( - 'flutter-version', - argParser: argParser, - ); - final shorebirdYaml = shorebirdEnv.getShorebirdYaml()!; - final appId = shorebirdYaml.getAppId(flavor: flavor); - final app = await codePushClientWrapper.getApp(appId: appId); - - var flutterRevisionForRelease = shorebirdEnv.flutterRevision; - if (flutterVersion != null) { - if (Version.parse(flutterVersion) < minimumSupportedIosFlutterVersion) { - logger.err( - '''iOS releases are not supported with Flutter versions older than $minimumSupportedIosFlutterVersion.''', - ); - return ExitCode.usage.code; - } - - final String? revision; - try { - revision = await shorebirdFlutter.getRevisionForVersion( - flutterVersion, - ); - } catch (error) { - logger.err( - ''' -Unable to determine revision for Flutter version: $flutterVersion. -$error''', - ); - return ExitCode.software.code; - } - - if (revision == null) { - final openIssueLink = link( - uri: Uri.parse( - 'https://github.com/shorebirdtech/shorebird/issues/new?assignees=&labels=feature&projects=&template=feature_request.md&title=feat%3A+', - ), - message: 'open an issue', - ); - logger.err(''' -Version $flutterVersion not found. Please $openIssueLink to request a new version. -Use `shorebird flutter versions list` to list available versions. -'''); - return ExitCode.software.code; - } - - flutterRevisionForRelease = revision; - } - - try { - await shorebirdFlutter.installRevision( - revision: flutterRevisionForRelease, - ); - } catch (_) { - return ExitCode.software.code; - } - - final releaseFlutterShorebirdEnv = shorebirdEnv.copyWith( - flutterRevisionOverride: flutterRevisionForRelease, - ); - - return await runScoped( - () async { - final flutterVersionString = - await shorebirdFlutter.getVersionAndRevision(); - - final buildProgress = logger.progress( - 'Building release with Flutter $flutterVersionString', - ); - - try { - await buildIpa( - codesign: codesign, - exportOptionsPlist: exportOptionsPlist, - flavor: flavor, - target: target, - ); - } on ProcessException catch (error) { - buildProgress.fail('Failed to build: ${error.message}'); - return ExitCode.software.code; - } on BuildException catch (error) { - buildProgress.fail('Failed to build'); - logger.err(error.message); - return ExitCode.software.code; - } - - buildProgress.complete(); - - final archiveDirectory = getXcarchiveDirectory(); - if (archiveDirectory == null) { - logger.err('Unable to find .xcarchive directory'); - return ExitCode.software.code; - } - final archivePath = archiveDirectory.path; - - final appDirectory = - getAppDirectory(xcarchiveDirectory: archiveDirectory); - if (appDirectory == null) { - logger.err('Unable to find .app directory'); - return ExitCode.software.code; - } - final runnerPath = appDirectory.path; - - final plistFile = File(p.join(archivePath, 'Info.plist')); - if (!plistFile.existsSync()) { - logger.err('No Info.plist file found at ${plistFile.path}.'); - return ExitCode.software.code; - } - - final plist = Plist(file: plistFile); - final String releaseVersion; - try { - releaseVersion = plist.versionNumber; - } catch (error) { - logger.err( - '''Failed to determine release version from ${plistFile.path}: $error''', - ); - return ExitCode.software.code; - } - - final existingRelease = await codePushClientWrapper.maybeGetRelease( - appId: appId, - releaseVersion: releaseVersion, - ); - if (existingRelease != null) { - codePushClientWrapper.ensureReleaseIsNotActive( - release: existingRelease, - platform: releasePlatform, - ); - - // All artifacts associated with a given release must be built - // with the same Flutter revision. - if (existingRelease.flutterRevision != flutterRevisionForRelease) { - ReleaseCommand.printConflictingFlutterRevisionError( - existingFlutterRevision: existingRelease.flutterRevision, - currentFlutterRevision: flutterRevisionForRelease, - releaseVersion: releaseVersion, - ); - return ExitCode.software.code; - } - } - - final summary = [ - '''šŸ“± App: ${lightCyan.wrap(app.displayName)} ${lightCyan.wrap('($appId)')}''', - if (flavor != null) 'šŸ§ Flavor: ${lightCyan.wrap(flavor)}', - 'šŸ“¦ Release Version: ${lightCyan.wrap(releaseVersion)}', - '''šŸ•¹ļø Platform: ${lightCyan.wrap(releasePlatform.name)}''', - '🐦 Flutter Version: ${lightCyan.wrap(flutterVersionString)}', - ]; - - logger.info(''' - -${styleBold.wrap(lightGreen.wrap('šŸš€ Ready to create a new release!'))} - -${summary.join('\n')} -'''); - - if (shorebirdEnv.canAcceptUserInput) { - final confirm = logger.confirm('Would you like to continue?'); - - if (!confirm) { - logger.info('Aborting.'); - return ExitCode.success.code; - } - } - - final Release release; - if (existingRelease != null) { - release = existingRelease; - await codePushClientWrapper.updateReleaseStatus( - appId: appId, - releaseId: release.id, - platform: releasePlatform, - status: ReleaseStatus.draft, - ); - } else { - release = await codePushClientWrapper.createRelease( - appId: appId, - version: releaseVersion, - flutterRevision: shorebirdEnv.flutterRevision, - platform: releasePlatform, - ); - } - - await codePushClientWrapper.createIosReleaseArtifacts( - appId: app.appId, - releaseId: release.id, - xcarchivePath: archivePath, - runnerPath: runnerPath, - isCodesigned: codesign, - ); - - await codePushClientWrapper.updateReleaseStatus( - appId: app.appId, - releaseId: release.id, - platform: releasePlatform, - status: ReleaseStatus.active, - metadata: UpdateReleaseMetadata( - releasePlatform: releasePlatform, - flutterVersionOverride: flutterVersion, - generatedApks: false, - environment: BuildEnvironmentMetadata( - operatingSystem: platform.operatingSystem, - operatingSystemVersion: platform.operatingSystemVersion, - shorebirdVersion: packageVersion, - xcodeVersion: await xcodeBuild.version(), - ), - ), - ); - - logger.success('\nāœ… Published Release ${release.version}!'); - - final relativeArchivePath = p.relative(archivePath); - if (codesign) { - // Ensure the ipa was built - final String ipaPath; - try { - ipaPath = getIpaPath(); - } catch (error) { - logger.err('Could not find ipa file: $error'); - return ExitCode.software.code; - } - - final relativeIpaPath = p.relative(ipaPath); - logger.info(''' - -Your next step is to upload your app to App Store Connect. - -To upload to the App Store, do one of the following: - 1. Open ${lightCyan.wrap(relativeArchivePath)} in Xcode and use the "Distribute App" flow. - 2. Drag and drop the ${lightCyan.wrap(relativeIpaPath)} bundle into the Apple Transporter macOS app (https://apps.apple.com/us/app/transporter/id1450874784). - 3. Run ${lightCyan.wrap('xcrun altool --upload-app --type ios -f $relativeIpaPath --apiKey your_api_key --apiIssuer your_issuer_id')}. - See "man altool" for details about how to authenticate with the App Store Connect API key. -'''); - } else { - logger.info(''' - -Your next step is to submit the archive at ${lightCyan.wrap(relativeArchivePath)} to the App Store using Xcode. - -You can open the archive in Xcode by running: - ${lightCyan.wrap('open $relativeArchivePath')} - -${styleBold.wrap('Make sure to uncheck "Manage Version and Build Number", or else shorebird will not work.')} -'''); - } - - ReleaseCommand.printPatchInstructions( - name: name, - flavor: flavor, - target: target, - releaseVersion: release.version, - ); - - return ExitCode.success.code; - }, - values: { - shorebirdEnvRef.overrideWith(() => releaseFlutterShorebirdEnv), - }, - ); - } -} diff --git a/packages/shorebird_cli/lib/src/commands/release/release_ios_framework_command.dart b/packages/shorebird_cli/lib/src/commands/release/release_ios_framework_command.dart deleted file mode 100644 index bf38f5b8..00000000 --- a/packages/shorebird_cli/lib/src/commands/release/release_ios_framework_command.dart +++ /dev/null @@ -1,270 +0,0 @@ -import 'dart:io'; - -import 'package:io/io.dart' show copyPath; -import 'package:mason_logger/mason_logger.dart'; -import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; -import 'package:pub_semver/pub_semver.dart'; -import 'package:scoped/scoped.dart'; -import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; -import 'package:shorebird_cli/src/command.dart'; -import 'package:shorebird_cli/src/commands/release/release.dart'; -import 'package:shorebird_cli/src/config/config.dart'; -import 'package:shorebird_cli/src/doctor.dart'; -import 'package:shorebird_cli/src/executables/xcodebuild.dart'; -import 'package:shorebird_cli/src/logger.dart'; -import 'package:shorebird_cli/src/platform.dart'; -import 'package:shorebird_cli/src/platform/ios.dart'; -import 'package:shorebird_cli/src/shorebird_artifact_mixin.dart'; -import 'package:shorebird_cli/src/shorebird_build_mixin.dart'; -import 'package:shorebird_cli/src/shorebird_env.dart'; -import 'package:shorebird_cli/src/shorebird_flutter.dart'; -import 'package:shorebird_cli/src/shorebird_validator.dart'; -import 'package:shorebird_cli/src/version.dart'; -import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; - -class ReleaseIosFrameworkCommand extends ShorebirdCommand - with ShorebirdArtifactMixin, ShorebirdBuildMixin { - ReleaseIosFrameworkCommand() { - argParser - ..addOption( - 'release-version', - help: ''' -The version of the associated release (e.g. "1.0.0"). This should be the version -of the iOS app that is using this module.''', - mandatory: true, - ) - ..addOption( - 'flutter-version', - help: 'The Flutter version to use when building the app (e.g: 3.16.3).', - ); - } - - @override - String get name => 'ios-framework'; - - @override - List get aliases => ['ios-framework-alpha']; - - @override - String get description => - 'Builds and submits your iOS framework to Shorebird.'; - - @override - Future run() async { - try { - await shorebirdValidator.validatePreconditions( - checkUserIsAuthenticated: true, - checkShorebirdInitialized: true, - supportedOperatingSystems: {Platform.macOS}, - validators: doctor.iosCommandValidators, - ); - } on PreconditionFailedException catch (e) { - return e.exitCode.code; - } - - const releasePlatform = ReleasePlatform.ios; - final releaseVersion = results['release-version'] as String; - final flutterVersion = results['flutter-version'] as String?; - final shorebirdYaml = shorebirdEnv.getShorebirdYaml()!; - final appId = shorebirdYaml.getAppId(); - final app = await codePushClientWrapper.getApp(appId: appId); - - final existingRelease = await codePushClientWrapper.maybeGetRelease( - appId: appId, - releaseVersion: releaseVersion, - ); - if (existingRelease != null) { - codePushClientWrapper.ensureReleaseIsNotActive( - release: existingRelease, - platform: releasePlatform, - ); - } - - var flutterRevisionForRelease = shorebirdEnv.flutterRevision; - if (flutterVersion != null) { - if (Version.parse(flutterVersion) < minimumSupportedIosFlutterVersion) { - logger.err( - '''iOS releases are not supported with Flutter versions older than $minimumSupportedIosFlutterVersion.''', - ); - return ExitCode.usage.code; - } - - final String? revision; - try { - revision = await shorebirdFlutter.getRevisionForVersion( - flutterVersion, - ); - } catch (error) { - logger.err( - ''' -Unable to determine revision for Flutter version: $flutterVersion. -$error''', - ); - return ExitCode.software.code; - } - - if (revision == null) { - final openIssueLink = link( - uri: Uri.parse( - 'https://github.com/shorebirdtech/shorebird/issues/new?assignees=&labels=feature&projects=&template=feature_request.md&title=feat%3A+', - ), - message: 'open an issue', - ); - logger.err(''' -Version $flutterVersion not found. Please $openIssueLink to request a new version. -Use `shorebird flutter versions list` to list available versions. -'''); - return ExitCode.software.code; - } - - flutterRevisionForRelease = revision; - } - - try { - await shorebirdFlutter.installRevision( - revision: flutterRevisionForRelease, - ); - } catch (_) { - return ExitCode.software.code; - } - - final releaseFlutterShorebirdEnv = shorebirdEnv.copyWith( - flutterRevisionOverride: flutterRevisionForRelease, - ); - - return await runScoped( - () async { - final flutterVersionString = - await shorebirdFlutter.getVersionAndRevision(); - - final buildProgress = logger.progress( - 'Building iOS framework with Flutter $flutterVersionString', - ); - - try { - await buildIosFramework(); - } catch (error) { - buildProgress.fail('Failed to build iOS framework: $error'); - return ExitCode.software.code; - } - - buildProgress.complete(); - - final summary = [ - '''šŸ“± App: ${lightCyan.wrap(app.displayName)} ${lightCyan.wrap('($appId)')}''', - 'šŸ“¦ Release Version: ${lightCyan.wrap(releaseVersion)}', - '''šŸ•¹ļø Platform: ${lightCyan.wrap(releasePlatform.name)}''', - '🐦 Flutter Version: ${lightCyan.wrap(flutterVersionString)}', - ]; - - logger.info(''' - -${styleBold.wrap(lightGreen.wrap('šŸš€ Ready to create a new release!'))} - -${summary.join('\n')} -'''); - - if (shorebirdEnv.canAcceptUserInput) { - final confirm = logger.confirm('Would you like to continue?'); - - if (!confirm) { - logger.info('Aborting.'); - return ExitCode.success.code; - } - } - - // Copy release xcframework to a new directory to avoid overwriting with - // subsequent patch builds. - final sourceLibraryDirectory = getAppXcframeworkDirectory(); - final targetLibraryDirectory = Directory( - p.join(shorebirdEnv.getShorebirdProjectRoot()!.path, 'release'), - ); - if (targetLibraryDirectory.existsSync()) { - targetLibraryDirectory.deleteSync(recursive: true); - } - await copyPath( - sourceLibraryDirectory.path, - targetLibraryDirectory.path, - ); - - // Rename Flutter.xcframework to ShorebirdFlutter.xcframework to avoid - // Xcode warning users about the .xcframework signature changing. - Directory( - p.join( - targetLibraryDirectory.path, - 'Flutter.xcframework', - ), - ).renameSync( - p.join( - targetLibraryDirectory.path, - 'ShorebirdFlutter.xcframework', - ), - ); - - final Release release; - if (existingRelease != null) { - release = existingRelease; - } else { - release = await codePushClientWrapper.createRelease( - appId: appId, - version: releaseVersion, - flutterRevision: shorebirdEnv.flutterRevision, - platform: releasePlatform, - ); - } - - await codePushClientWrapper.createIosFrameworkReleaseArtifacts( - appId: appId, - releaseId: release.id, - appFrameworkPath: - p.join(targetLibraryDirectory.path, 'App.xcframework'), - ); - - await codePushClientWrapper.updateReleaseStatus( - appId: app.appId, - releaseId: release.id, - platform: releasePlatform, - status: ReleaseStatus.active, - metadata: UpdateReleaseMetadata( - releasePlatform: releasePlatform, - flutterVersionOverride: flutterVersion, - generatedApks: false, - environment: BuildEnvironmentMetadata( - operatingSystem: platform.operatingSystem, - operatingSystemVersion: platform.operatingSystemVersion, - shorebirdVersion: packageVersion, - xcodeVersion: await xcodeBuild.version(), - ), - ), - ); - - final relativeFrameworkDirectoryPath = - p.relative(targetLibraryDirectory.path); - logger - ..success('\nāœ… Published Release ${release.version}!') - ..info(''' - -Your next step is to add the .xcframework files found in the ${lightCyan.wrap(relativeFrameworkDirectoryPath)} directory to your iOS app. - -To do this: - 1. Add the relative path to the ${lightCyan.wrap(relativeFrameworkDirectoryPath)} directory to your app's Framework Search Paths in your Xcode build settings. - 2. Embed the App.xcframework and ShorebirdFlutter.framework in your Xcode project. - -Instructions for these steps can be found at https://docs.flutter.dev/add-to-app/ios/project-setup#option-b---embed-frameworks-in-xcode. -'''); - - ReleaseCommand.printPatchInstructions( - name: name, - releaseVersion: release.version, - requiresReleaseVersion: true, - ); - - return ExitCode.success.code; - }, - values: { - shorebirdEnvRef.overrideWith(() => releaseFlutterShorebirdEnv), - }, - ); - } -} diff --git a/packages/shorebird_cli/lib/src/commands/release_new/releaser.dart b/packages/shorebird_cli/lib/src/commands/release/releaser.dart similarity index 100% rename from packages/shorebird_cli/lib/src/commands/release_new/releaser.dart rename to packages/shorebird_cli/lib/src/commands/release/releaser.dart diff --git a/packages/shorebird_cli/lib/src/commands/release_new/release_new.dart b/packages/shorebird_cli/lib/src/commands/release_new/release_new.dart deleted file mode 100644 index 9871b356..00000000 --- a/packages/shorebird_cli/lib/src/commands/release_new/release_new.dart +++ /dev/null @@ -1,6 +0,0 @@ -export 'aar_releaser.dart'; -export 'android_releaser.dart'; -export 'ios_framework_releaser.dart'; -export 'ios_releaser.dart'; -export 'release_new_command.dart'; -export 'releaser.dart'; diff --git a/packages/shorebird_cli/lib/src/commands/release_new/release_new_command.dart b/packages/shorebird_cli/lib/src/commands/release_new/release_new_command.dart deleted file mode 100644 index 41a0c36c..00000000 --- a/packages/shorebird_cli/lib/src/commands/release_new/release_new_command.dart +++ /dev/null @@ -1,454 +0,0 @@ -import 'dart:async'; - -import 'package:mason_logger/mason_logger.dart'; -import 'package:meta/meta.dart'; -import 'package:scoped/scoped.dart'; -import 'package:shorebird_cli/src/cache.dart'; -import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; -import 'package:shorebird_cli/src/command.dart'; -import 'package:shorebird_cli/src/commands/release_new/release_new.dart'; -import 'package:shorebird_cli/src/config/config.dart'; -import 'package:shorebird_cli/src/extensions/arg_results.dart'; -import 'package:shorebird_cli/src/logger.dart'; -import 'package:shorebird_cli/src/platform/platform.dart'; -import 'package:shorebird_cli/src/release_type.dart'; -import 'package:shorebird_cli/src/shorebird_env.dart'; -import 'package:shorebird_cli/src/shorebird_flutter.dart'; -import 'package:shorebird_cli/src/third_party/flutter_tools/lib/flutter_tools.dart'; -import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; -import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart'; - -typedef ResolveReleaser = Releaser Function(ReleaseType releaseType); - -/// {@template release_command} -/// Creates a new app release for the specified platform(s). -/// {@endtemplate} -class ReleaseNewCommand extends ShorebirdCommand { - /// {@macro release_command} - ReleaseNewCommand({ResolveReleaser? resolveReleaser}) { - _resolveReleaser = resolveReleaser ?? getReleaser; - argParser - ..addOption( - 'target', - abbr: 't', - help: 'The main entrypoint file of the application.', - ) - ..addOption( - 'flavor', - help: 'The product flavor to use when building the app.', - ) - ..addOption( - 'build-number', - help: ''' -An identifier used as an internal version number. -Each build must have a unique identifier to differentiate it from previous builds. -It is used to determine whether one build is more recent than another, with higher numbers indicating more recent build. -On Android it is used as "versionCode". -On Xcode builds it is used as "CFBundleVersion".''', - defaultsTo: '1.0', - ) - ..addFlag( - 'codesign', - help: 'Codesign the application bundle.', - defaultsTo: true, - ) - ..addFlag( - 'dry-run', - abbr: 'n', - negatable: false, - help: 'Validate but do not upload the release.', - ) - ..addOption( - exportOptionsPlistArgName, - help: - '''Export an IPA with these options. See "xcodebuild -h" for available exportOptionsPlist keys.''', - ) - ..addOption( - 'flutter-version', - help: 'The Flutter version to use when building the app (e.g: 3.16.3).', - ) - ..addOption( - 'android-artifact', - help: - '''The type of artifact to generate. Only relevant for Android releases.''', - allowed: ['aab', 'apk'], - defaultsTo: 'aab', - allowedHelp: { - 'aab': 'Android App Bundle', - 'apk': 'Android Package Kit', - }, - ) - ..addMultiOption( - 'platform', - abbr: 'p', - help: 'The platform(s) to to build this release for.', - allowed: ReleaseType.values.map((e) => e.cliName).toList(), - // TODO(bryanoltman): uncomment this once https://github.com/dart-lang/args/pull/273 lands - // mandatory: true. - ) - ..addOption( - 'release-version', - help: ''' -The version of the associated release (e.g. "1.0.0"). This should be the version -of the iOS app that is using this module.''', - ) - ..addMultiOption( - 'target-platform', - help: 'The target platform(s) for which the app is compiled.', - defaultsTo: Arch.values.map((arch) => arch.targetPlatformCliArg), - allowed: Arch.values.map((arch) => arch.targetPlatformCliArg), - ); - } - - late final ResolveReleaser _resolveReleaser; - - @override - bool get hidden => true; - - @override - String get description => - 'Creates a shorebird release for the provided target platforms'; - - @override - String get name => 'release-new'; - - @override - Future run() async { - final releaserFutures = - results.releaseTypes.map(_resolveReleaser).map(createRelease); - - for (final future in releaserFutures) { - await future; - } - - return ExitCode.success.code; - } - - @visibleForTesting - Releaser getReleaser(ReleaseType releaseType) { - switch (releaseType) { - case ReleaseType.android: - return AndroidReleaser( - argResults: results, - flavor: flavor, - target: target, - ); - case ReleaseType.ios: - return IosReleaser( - argResults: results, - flavor: flavor, - target: target, - ); - case ReleaseType.iosFramework: - return IosFrameworkReleaser( - argResults: results, - flavor: flavor, - target: target, - ); - case ReleaseType.aar: - return AarReleaser( - argResults: results, - flavor: flavor, - target: target, - ); - } - } - - /// The shorebird app ID for the current project. - String get appId => shorebirdEnv.getShorebirdYaml()!.getAppId(flavor: flavor); - - /// The build flavor, if provided. - late String? flavor = results.findOption('flavor', argParser: argParser); - - /// The target script, if provided. - late String? target = results.findOption('target', argParser: argParser); - - /// The flutter version specified by the user, if any. - late String? flutterVersionArg = results['flutter-version'] as String?; - - /// The workflow to create a new release for a Shorebird app. - /// - /// Expectations for methods invoked by this command: - /// - They perform their own logging. If an error occurs, they are - /// responsible for properly logging the error, cleaning up running - /// [Progress]es, etc. - /// - They handle their own exceptions and exit with a non-zero exit code if - /// an error occurs *instead of* throwing an exception. - @visibleForTesting - Future createRelease(Releaser releaser) async { - await releaser.assertPreconditions(); - await releaser.assertArgsAreValid(); - - await cache.updateAll(); - - // This command handles logging, we don't need to provide our own - // progress, error logs, etc. - final app = await codePushClientWrapper.getApp(appId: appId); - final targetFlutterRevision = await resolveTargetFlutterRevision(); - try { - await shorebirdFlutter.installRevision(revision: targetFlutterRevision); - } catch (_) { - exit(ExitCode.software.code); - } - - final releaseFlutterShorebirdEnv = shorebirdEnv.copyWith( - flutterRevisionOverride: targetFlutterRevision, - ); - return await runScoped( - () async { - await cache.updateAll(); - - final releaseArtifact = await releaser.buildReleaseArtifacts(); - final releaseVersion = await releaser.getReleaseVersion( - releaseArtifactRoot: releaseArtifact, - ); - - // Ensure we can create a release from what we've built. - await ensureVersionIsReleasable( - version: releaseVersion, - flutterRevision: targetFlutterRevision, - releasePlatform: releaser.releaseType.releasePlatform, - ); - - final dryRun = results['dry-run'] == true; - if (dryRun) { - logger - ..info('No issues detected.') - ..info('The server may enforce additional checks.'); - exit(ExitCode.success.code); - } - - // Ask the user to proceed (this is skipped when running via CI). - await confirmCreateRelease( - app: app, - releaseVersion: releaseVersion, - flutterVersion: targetFlutterRevision, - releasePlatform: releaser.releaseType.releasePlatform, - ); - - final release = await getOrCreateRelease( - version: releaseVersion, - releasePlatform: releaser.releaseType.releasePlatform, - ); - await prepareRelease(release: release, releaser: releaser); - await releaser.uploadReleaseArtifacts(release: release, appId: appId); - await finalizeRelease(release: release, releaser: releaser); - - logger - ..success(''' - -āœ… Published Release ${release.version}!''') - ..info(releaser.postReleaseInstructions); - - printPatchInstructions( - releaser: releaser, - releaseVersion: releaseVersion, - releaseType: releaser.releaseType, - flavor: flavor, - target: target, - ); - }, - values: { - shorebirdEnvRef.overrideWith(() => releaseFlutterShorebirdEnv), - }, - ); - } - - /// Determines which Flutter version to use for the release. This will be - /// either the version specified by the user or the version provided by - /// [shorebirdEnv]. Will exit with [ExitCode.software] if the version - /// specified by the user is not found/supported. - Future resolveTargetFlutterRevision() async { - if (flutterVersionArg != null) { - final String? revision; - try { - revision = await shorebirdFlutter.getRevisionForVersion( - flutterVersionArg!, - ); - } catch (error) { - logger.err( - ''' -Unable to determine revision for Flutter version: $flutterVersionArg. -$error''', - ); - exit(ExitCode.software.code); - } - - if (revision == null) { - final openIssueLink = link( - uri: Uri.parse( - 'https://github.com/shorebirdtech/shorebird/issues/new?assignees=&labels=feature&projects=&template=feature_request.md&title=feat%3A+', - ), - message: 'open an issue', - ); - logger.err( - ''' -Version $flutterVersionArg not found. Please $openIssueLink to request a new version. -Use `shorebird flutter versions list` to list available versions. -''', - ); - exit(ExitCode.software.code); - } - - return revision; - } - - return shorebirdEnv.flutterRevision; - } - - /// Asserts that a release with version [version] can be released using - /// flutter revision [flutterRevision]. If a release has already been - /// published with the given [version] for the platform [releasePlatform], or - /// if a release already exists with [version] but was compiled with a - /// different Flutter revision, an error will be thrown. - Future ensureVersionIsReleasable({ - required String version, - required String flutterRevision, - required ReleasePlatform releasePlatform, - }) async { - final existingRelease = await codePushClientWrapper.maybeGetRelease( - appId: appId, - releaseVersion: version, - ); - - if (existingRelease != null) { - codePushClientWrapper.ensureReleaseIsNotActive( - release: existingRelease, - platform: releasePlatform, - ); - - // All artifacts associated with a given release must be built - // with the same Flutter revision. - if (existingRelease.flutterRevision != flutterRevision) { - logger - ..err(''' -${styleBold.wrap(lightRed.wrap('A release with version $version already exists but was built using a different Flutter revision.'))} -''') - ..info(''' - - Existing release built with: ${lightCyan.wrap(existingRelease.flutterRevision)} - Current release built with: ${lightCyan.wrap(flutterRevision)} - -${styleBold.wrap(lightRed.wrap('All platforms for a given release must be built using the same Flutter revision.'))} - -To resolve this issue, you can: - * Re-run the release command with "${lightCyan.wrap('--flutter-version=${existingRelease.flutterRevision}')}". - * Delete the existing release and re-run the release command with the desired Flutter version. - * Bump the release version and re-run the release command with the desired Flutter version.'''); - exit(ExitCode.software.code); - } - } - } - - /// Prints a confirmation prompt with details about the release to be created. - /// If the user confirms, the release will be created. If the user cancels, - /// the command will exit with a success code. When running in a headless - /// or CI environment, this prompt will print but will not wait for user - /// confirmation. - Future confirmCreateRelease({ - required AppMetadata app, - required String releaseVersion, - required String flutterVersion, - required ReleasePlatform releasePlatform, - }) async { - final flutterVersionString = await shorebirdFlutter.getVersionAndRevision(); - // TODO(bryanoltman): include archs in the summary for android - // (and other platforms?) - final summary = [ - '''šŸ“± App: ${lightCyan.wrap(app.displayName)} ${lightCyan.wrap('(${app.appId})')}''', - if (flavor != null) 'šŸ§ Flavor: ${lightCyan.wrap(flavor)}', - 'šŸ“¦ Release Version: ${lightCyan.wrap(releaseVersion)}', - 'šŸ•¹ļø Platform: ${lightCyan.wrap(releasePlatform.name)}', - '🐦 Flutter Version: ${lightCyan.wrap(flutterVersionString)}', - ]; - - logger.info(''' - -${styleBold.wrap(lightGreen.wrap('šŸš€ Ready to create a new release!'))} - -${summary.join('\n')} -'''); - - if (shorebirdEnv.canAcceptUserInput) { - final confirm = logger.confirm('Would you like to continue?'); - - if (!confirm) { - logger.info('Aborting.'); - exit(ExitCode.success.code); - } - } - } - - /// Fetches the release with version [version] from the server or creates a - /// new release if none exists. - Future getOrCreateRelease({ - required String version, - required ReleasePlatform releasePlatform, - }) async { - return await codePushClientWrapper.maybeGetRelease( - appId: appId, - releaseVersion: version, - ) ?? - await codePushClientWrapper.createRelease( - appId: appId, - version: version, - flutterRevision: shorebirdEnv.flutterRevision, - platform: releasePlatform, - ); - } - - /// Prepares the release by updating the release status to draft. - Future prepareRelease({ - required Release release, - required Releaser releaser, - }) async { - await codePushClientWrapper.updateReleaseStatus( - appId: appId, - releaseId: release.id, - platform: releaser.releaseType.releasePlatform, - status: ReleaseStatus.draft, - ); - } - - /// Finalizes the release by updating the status to active. - Future finalizeRelease({ - required Release release, - required Releaser releaser, - }) async { - await codePushClientWrapper.updateReleaseStatus( - appId: appId, - releaseId: release.id, - platform: releaser.releaseType.releasePlatform, - status: ReleaseStatus.active, - metadata: await releaser.releaseMetadata(), - ); - } - - /// Instructions explaining how to patch the release that was just creatd. - void printPatchInstructions({ - required Releaser releaser, - required String releaseVersion, - required ReleaseType releaseType, - String? flavor, - String? target, - }) { - final baseCommand = [ - 'shorebird patch', - '--platform=${releaseType.cliName}', - if (flavor != null) '--flavor=$flavor', - if (target != null) '--target=$target', - ].join(' '); - logger.info( - '''To create a patch for this release, run ${lightCyan.wrap('$baseCommand --release-version=$releaseVersion')}''', - ); - - if (!releaser.requiresReleaseVersionArg) { - logger.info( - ''' - -Note: ${lightCyan.wrap(baseCommand)} without the --release-version option will patch the current version of the app. -''', - ); - } - } -} diff --git a/packages/shorebird_cli/lib/src/shorebird_artifact_mixin.dart b/packages/shorebird_cli/lib/src/shorebird_artifact_mixin.dart index 360f2044..97fb2ddc 100644 --- a/packages/shorebird_cli/lib/src/shorebird_artifact_mixin.dart +++ b/packages/shorebird_cli/lib/src/shorebird_artifact_mixin.dart @@ -112,42 +112,6 @@ mixin ShorebirdArtifactMixin on ShorebirdCommand { .firstWhereOrNull((directory) => directory.path.endsWith('.app')); } - /// Returns the path to the .ipa file generated by `flutter build ipa`. Throws - /// an exception if there is not exactly one .ipa file in the build directory, - /// or if there is no build directory. - String getIpaPath() { - final projectRoot = shorebirdEnv.getShorebirdProjectRoot()!; - final ipaBuildDirectory = Directory( - p.join( - projectRoot.path, - 'build', - 'ios', - 'ipa', - ), - ); - - if (!ipaBuildDirectory.existsSync()) { - throw Exception('No directory found at ${ipaBuildDirectory.path}'); - } - - final ipaFiles = ipaBuildDirectory - .listSync(recursive: true) - .whereType() - .where((f) => p.extension(f.path) == '.ipa'); - - if (ipaFiles.isEmpty) { - throw Exception('No .ipa files found in ${ipaBuildDirectory.path}'); - } - - if (ipaFiles.length > 1) { - throw Exception( - 'More than one .ipa file found in ${ipaBuildDirectory.path}', - ); - } - - return ipaFiles.single.path; - } - static const String appXcframeworkName = 'App.xcframework'; /// Returns the path to the App.xcframework generated by diff --git a/packages/shorebird_cli/lib/src/shorebird_build_mixin.dart b/packages/shorebird_cli/lib/src/shorebird_build_mixin.dart index ddac7e0f..428b5949 100644 --- a/packages/shorebird_cli/lib/src/shorebird_build_mixin.dart +++ b/packages/shorebird_cli/lib/src/shorebird_build_mixin.dart @@ -10,6 +10,7 @@ import 'package:shorebird_cli/src/shorebird_artifacts.dart'; import 'package:shorebird_cli/src/shorebird_env.dart'; import 'package:shorebird_cli/src/shorebird_process.dart'; +// coverage:ignore-start /// Used to wrap code that invokes `flutter build` with Shorebird's fork of /// Flutter. typedef ShorebirdBuildCommand = Future Function(); @@ -128,9 +129,7 @@ mixin ShorebirdBuildMixin on ShorebirdCommand { if (targetPlatformArgs != null) '--target-platform=$targetPlatformArgs', // TODO(bryanoltman): reintroduce coverage when we can support this. // See https://github.com/shorebirdtech/shorebird/issues/1141. - // coverage:ignore-start if (splitPerAbi) '--split-per-abi', - // coverage:ignore-end ...results.rest, ]; @@ -345,3 +344,5 @@ Either run `flutter pub get` manually, or follow the steps in ${link(uri: Uri.pa return File(outFilePath); } } + +// coverage:ignore-end diff --git a/packages/shorebird_cli/test/src/archive_analysis/ios_archive_differ_test.dart b/packages/shorebird_cli/test/src/archive_analysis/ios_archive_differ_test.dart index 3298ce2b..42dfea81 100644 --- a/packages/shorebird_cli/test/src/archive_analysis/ios_archive_differ_test.dart +++ b/packages/shorebird_cli/test/src/archive_analysis/ios_archive_differ_test.dart @@ -272,5 +272,6 @@ void main() { }); }); }, + testOn: 'mac-os', ); } diff --git a/packages/shorebird_cli/test/src/artifact_manager_test.dart b/packages/shorebird_cli/test/src/artifact_manager_test.dart index 2216e2ee..e2ca9016 100644 --- a/packages/shorebird_cli/test/src/artifact_manager_test.dart +++ b/packages/shorebird_cli/test/src/artifact_manager_test.dart @@ -604,10 +604,11 @@ void main() { )..createSync(recursive: true); File(p.join(flutterBuildDir.path, 'app1', 'app.dill')) ..createSync(recursive: true) - ..setLastModified(DateTime.now().subtract(const Duration(days: 1))); + ..setLastModifiedSync( + DateTime.now().subtract(const Duration(days: 1)), + ); appDill2 = File(p.join(flutterBuildDir.path, 'app2', 'app.dill')) - ..createSync(recursive: true) - ..setLastModified(DateTime.now()); + ..createSync(recursive: true); }); test('selects the most recently edited .app.dill file', () { diff --git a/packages/shorebird_cli/test/src/commands/release_new/aar_releaser_test.dart b/packages/shorebird_cli/test/src/commands/release/aar_releaser_test.dart similarity index 99% rename from packages/shorebird_cli/test/src/commands/release_new/aar_releaser_test.dart rename to packages/shorebird_cli/test/src/commands/release/aar_releaser_test.dart index 78ebb867..4ed824e3 100644 --- a/packages/shorebird_cli/test/src/commands/release_new/aar_releaser_test.dart +++ b/packages/shorebird_cli/test/src/commands/release/aar_releaser_test.dart @@ -6,7 +6,7 @@ import 'package:platform/platform.dart'; import 'package:scoped/scoped.dart'; import 'package:shorebird_cli/src/artifact_builder.dart'; import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; -import 'package:shorebird_cli/src/commands/release_new/aar_releaser.dart'; +import 'package:shorebird_cli/src/commands/release/aar_releaser.dart'; import 'package:shorebird_cli/src/engine_config.dart'; import 'package:shorebird_cli/src/logger.dart'; import 'package:shorebird_cli/src/os/operating_system_interface.dart'; diff --git a/packages/shorebird_cli/test/src/commands/release_new/android_releaser_test.dart b/packages/shorebird_cli/test/src/commands/release/android_releaser_test.dart similarity index 99% rename from packages/shorebird_cli/test/src/commands/release_new/android_releaser_test.dart rename to packages/shorebird_cli/test/src/commands/release/android_releaser_test.dart index f4422835..2755fc36 100644 --- a/packages/shorebird_cli/test/src/commands/release_new/android_releaser_test.dart +++ b/packages/shorebird_cli/test/src/commands/release/android_releaser_test.dart @@ -6,7 +6,7 @@ import 'package:platform/platform.dart'; import 'package:scoped/scoped.dart'; import 'package:shorebird_cli/src/artifact_builder.dart'; import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; -import 'package:shorebird_cli/src/commands/release_new/android_releaser.dart'; +import 'package:shorebird_cli/src/commands/release/android_releaser.dart'; import 'package:shorebird_cli/src/doctor.dart'; import 'package:shorebird_cli/src/engine_config.dart'; import 'package:shorebird_cli/src/logger.dart'; diff --git a/packages/shorebird_cli/test/src/commands/release_new/ios_framework_releaser_test.dart b/packages/shorebird_cli/test/src/commands/release/ios_framework_releaser_test.dart similarity index 99% rename from packages/shorebird_cli/test/src/commands/release_new/ios_framework_releaser_test.dart rename to packages/shorebird_cli/test/src/commands/release/ios_framework_releaser_test.dart index 622f366c..f2b56c49 100644 --- a/packages/shorebird_cli/test/src/commands/release_new/ios_framework_releaser_test.dart +++ b/packages/shorebird_cli/test/src/commands/release/ios_framework_releaser_test.dart @@ -7,7 +7,7 @@ import 'package:scoped/scoped.dart'; import 'package:shorebird_cli/src/artifact_builder.dart'; import 'package:shorebird_cli/src/artifact_manager.dart'; import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; -import 'package:shorebird_cli/src/commands/release_new/ios_framework_releaser.dart'; +import 'package:shorebird_cli/src/commands/release/ios_framework_releaser.dart'; import 'package:shorebird_cli/src/doctor.dart'; import 'package:shorebird_cli/src/executables/executables.dart'; import 'package:shorebird_cli/src/logger.dart'; diff --git a/packages/shorebird_cli/test/src/commands/release_new/ios_releaser_test.dart b/packages/shorebird_cli/test/src/commands/release/ios_releaser_test.dart similarity index 94% rename from packages/shorebird_cli/test/src/commands/release_new/ios_releaser_test.dart rename to packages/shorebird_cli/test/src/commands/release/ios_releaser_test.dart index 91b9fa4a..c7cf92e4 100644 --- a/packages/shorebird_cli/test/src/commands/release_new/ios_releaser_test.dart +++ b/packages/shorebird_cli/test/src/commands/release/ios_releaser_test.dart @@ -7,7 +7,7 @@ import 'package:scoped/scoped.dart'; import 'package:shorebird_cli/src/artifact_builder.dart'; import 'package:shorebird_cli/src/artifact_manager.dart'; import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; -import 'package:shorebird_cli/src/commands/release_new/ios_releaser.dart'; +import 'package:shorebird_cli/src/commands/release/ios_releaser.dart'; import 'package:shorebird_cli/src/doctor.dart'; import 'package:shorebird_cli/src/executables/xcodebuild.dart'; import 'package:shorebird_cli/src/logger.dart'; @@ -178,6 +178,36 @@ void main() { ).called(1); }); }); + + group('when specified flutter version is less than minimum', () { + setUp(() { + when( + () => shorebirdValidator.validatePreconditions( + checkUserIsAuthenticated: + any(named: 'checkUserIsAuthenticated'), + checkShorebirdInitialized: + any(named: 'checkShorebirdInitialized'), + validators: any(named: 'validators'), + supportedOperatingSystems: + any(named: 'supportedOperatingSystems'), + ), + ).thenAnswer((_) async {}); + when(() => argResults['flutter-version']).thenReturn('3.0.0'); + }); + + test('logs error and exits with code 64', () async { + await expectLater( + () => runWithOverrides(iosReleaser.assertPreconditions), + exitsWithCode(ExitCode.usage), + ); + + verify( + () => logger.err( + '''iOS releases are not supported with Flutter versions older than $minimumSupportedIosFlutterVersion.''', + ), + ).called(1); + }); + }); }); group('assertArgsAreValid', () { diff --git a/packages/shorebird_cli/test/src/commands/release/release_aar_command_test.dart b/packages/shorebird_cli/test/src/commands/release/release_aar_command_test.dart deleted file mode 100644 index 2089f922..00000000 --- a/packages/shorebird_cli/test/src/commands/release/release_aar_command_test.dart +++ /dev/null @@ -1,638 +0,0 @@ -import 'dart:io'; - -import 'package:args/args.dart'; -import 'package:http/http.dart' as http; -import 'package:mason_logger/mason_logger.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; -import 'package:scoped/scoped.dart'; -import 'package:shorebird_cli/src/auth/auth.dart'; -import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; -import 'package:shorebird_cli/src/commands/commands.dart'; -import 'package:shorebird_cli/src/config/config.dart'; -import 'package:shorebird_cli/src/engine_config.dart'; -import 'package:shorebird_cli/src/executables/executables.dart'; -import 'package:shorebird_cli/src/logger.dart'; -import 'package:shorebird_cli/src/os/operating_system_interface.dart'; -import 'package:shorebird_cli/src/platform.dart'; -import 'package:shorebird_cli/src/platform/platform.dart'; -import 'package:shorebird_cli/src/shorebird_env.dart'; -import 'package:shorebird_cli/src/shorebird_flutter.dart'; -import 'package:shorebird_cli/src/shorebird_process.dart'; -import 'package:shorebird_cli/src/shorebird_validator.dart'; -import 'package:shorebird_cli/src/version.dart'; -import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; -import 'package:test/test.dart'; - -import '../../fakes.dart'; -import '../../mocks.dart'; - -void main() { - group(ReleaseAarCommand, () { - const appDisplayName = 'Test App'; - const appId = 'test-app-id'; - const shorebirdYaml = ShorebirdYaml(appId: appId); - const androidPackageName = 'com.example.my_flutter_module'; - final appMetadata = AppMetadata( - appId: appId, - displayName: appDisplayName, - createdAt: DateTime(2023), - updatedAt: DateTime(2023), - ); - const flutterRevision = '83305b5088e6fe327fb3334a73ff190828d85713'; - const flutterVersionAndRevision = '3.10.6 (83305b5088)'; - const versionName = '1.2.3'; - const versionCode = '1'; - const version = '$versionName+$versionCode'; - const operatingSystem = 'macOS'; - const operatingSystemVersion = '11.0.0'; - final release = Release( - id: 0, - appId: appId, - version: version, - flutterRevision: flutterRevision, - displayName: '1.2.3+1', - platformStatuses: {}, - createdAt: DateTime(2023), - updatedAt: DateTime(2023), - ); - const releasePlatform = ReleasePlatform.android; - const buildNumber = '1.0'; - - late ArgResults argResults; - late http.Client httpClient; - late Auth auth; - late CodePushClientWrapper codePushClientWrapper; - late Directory shorebirdRoot; - late Directory projectRoot; - late Java java; - late OperatingSystemInterface operatingSystemInterface; - late Platform platform; - late Progress progress; - late ShorebirdLogger logger; - late ShorebirdProcessResult flutterBuildProcessResult; - late ShorebirdProcessResult flutterPubGetProcessResult; - late ShorebirdEnv shorebirdEnv; - late ShorebirdFlutter shorebirdFlutter; - late ShorebirdProcess shorebirdProcess; - late ShorebirdValidator shorebirdValidator; - late ReleaseAarCommand command; - - R runWithOverrides(R Function() body) { - return runScoped( - body, - values: { - authRef.overrideWith(() => auth), - codePushClientWrapperRef.overrideWith(() => codePushClientWrapper), - engineConfigRef.overrideWith(() => const EngineConfig.empty()), - javaRef.overrideWith(() => java), - loggerRef.overrideWith(() => logger), - osInterfaceRef.overrideWith(() => operatingSystemInterface), - platformRef.overrideWith(() => platform), - processRef.overrideWith(() => shorebirdProcess), - shorebirdEnvRef.overrideWith(() => shorebirdEnv), - shorebirdFlutterRef.overrideWith(() => shorebirdFlutter), - shorebirdValidatorRef.overrideWith(() => shorebirdValidator), - }, - ); - } - - void setUpProjectRootArtifacts() { - final aarDir = p.join( - projectRoot.path, - 'build', - 'host', - 'outputs', - 'repo', - 'com', - 'example', - 'my_flutter_module', - 'flutter_release', - buildNumber, - ); - final aarPath = p.join(aarDir, 'flutter_release-$buildNumber.aar'); - for (final archMetadata in Arch.values) { - final artifactPath = p.join( - aarDir, - 'flutter_release-$buildNumber', - 'jni', - archMetadata.androidBuildPath, - 'libapp.so', - ); - File(artifactPath).createSync(recursive: true); - } - File(aarPath).createSync(recursive: true); - } - - setUpAll(() { - registerFallbackValue(ReleasePlatform.android); - registerFallbackValue(ReleaseStatus.draft); - registerFallbackValue(FakeRelease()); - registerFallbackValue(FakeShorebirdProcess()); - }); - - setUp(() { - argResults = MockArgResults(); - httpClient = MockHttpClient(); - auth = MockAuth(); - codePushClientWrapper = MockCodePushClientWrapper(); - java = MockJava(); - operatingSystemInterface = MockOperatingSystemInterface(); - platform = MockPlatform(); - progress = MockProgress(); - logger = MockShorebirdLogger(); - flutterBuildProcessResult = MockProcessResult(); - flutterPubGetProcessResult = MockProcessResult(); - shorebirdProcess = MockShorebirdProcess(); - shorebirdRoot = Directory.systemTemp.createTempSync(); - projectRoot = Directory.systemTemp.createTempSync(); - shorebirdEnv = MockShorebirdEnv(); - shorebirdFlutter = MockShorebirdFlutter(); - shorebirdValidator = MockShorebirdValidator(); - - when(() => auth.client).thenReturn(httpClient); - when(() => argResults['build-number']).thenReturn(buildNumber); - when(() => argResults['release-version']).thenReturn(version); - when(() => argResults['target-platform']) - .thenReturn(Arch.values.map((a) => a.targetPlatformCliArg).toList()); - when(() => argResults.rest).thenReturn([]); - when(() => auth.isAuthenticated).thenReturn(true); - when(() => logger.confirm(any())).thenReturn(true); - when(() => logger.progress(any())).thenReturn(progress); - when( - () => operatingSystemInterface.which('flutter'), - ).thenReturn('/path/to/flutter'); - when(() => platform.operatingSystem).thenReturn(operatingSystem); - when(() => platform.operatingSystemVersion) - .thenReturn(operatingSystemVersion); - - when(() => shorebirdEnv.getShorebirdYaml()).thenReturn(shorebirdYaml); - when( - () => shorebirdEnv.copyWith( - flutterRevisionOverride: any(named: 'flutterRevisionOverride'), - ), - ).thenAnswer((invocation) { - when(() => shorebirdEnv.flutterRevision).thenReturn( - invocation.namedArguments[#flutterRevisionOverride] as String, - ); - return shorebirdEnv; - }); - when(() => shorebirdEnv.shorebirdRoot).thenReturn(shorebirdRoot); - when( - () => shorebirdEnv.getShorebirdProjectRoot(), - ).thenReturn(projectRoot); - when( - () => shorebirdEnv.androidPackageName, - ).thenReturn(androidPackageName); - when(() => shorebirdEnv.flutterRevision).thenReturn(flutterRevision); - when(() => shorebirdEnv.canAcceptUserInput).thenReturn(true); - - when( - () => shorebirdFlutter.getVersionAndRevision(), - ).thenAnswer((_) async => flutterVersionAndRevision); - when( - () => shorebirdFlutter.installRevision( - revision: any(named: 'revision'), - ), - ).thenAnswer((_) async => {}); - - when( - () => flutterBuildProcessResult.exitCode, - ).thenReturn(ExitCode.success.code); - when( - () => flutterPubGetProcessResult.exitCode, - ).thenReturn(ExitCode.success.code); - - when( - () => shorebirdProcess.run( - 'flutter', - ['--no-version-check', 'pub', 'get', '--offline'], - runInShell: any(named: 'runInShell'), - useVendedFlutter: false, - ), - ).thenAnswer((_) async => flutterPubGetProcessResult); - when( - () => shorebirdProcess.run( - any(), - any(that: containsAll(['build', 'aar'])), - runInShell: any(named: 'runInShell'), - ), - ).thenAnswer((invocation) async { - return flutterBuildProcessResult; - }); - - when( - () => codePushClientWrapper.getApp(appId: any(named: 'appId')), - ).thenAnswer((_) async => appMetadata); - when( - () => codePushClientWrapper.maybeGetRelease( - appId: any(named: 'appId'), - releaseVersion: any(named: 'releaseVersion'), - ), - ).thenAnswer((_) async => null); - when( - () => codePushClientWrapper.ensureReleaseIsNotActive( - release: any(named: 'release'), - platform: any(named: 'platform'), - ), - ).thenAnswer((_) async => {}); - when( - () => codePushClientWrapper.createRelease( - appId: any(named: 'appId'), - version: any(named: 'version'), - flutterRevision: any(named: 'flutterRevision'), - platform: any(named: 'platform'), - ), - ).thenAnswer((_) async => release); - when( - () => codePushClientWrapper.createAndroidArchiveReleaseArtifacts( - appId: any(named: 'appId'), - releaseId: any(named: 'releaseId'), - platform: any(named: 'platform'), - aarPath: any(named: 'aarPath'), - extractedAarDir: any(named: 'extractedAarDir'), - architectures: any(named: 'architectures'), - ), - ).thenAnswer((_) async => {}); - when( - () => codePushClientWrapper.updateReleaseStatus( - appId: any(named: 'appId'), - releaseId: any(named: 'releaseId'), - platform: any(named: 'platform'), - status: any(named: 'status'), - metadata: any(named: 'metadata'), - ), - ).thenAnswer((_) async => {}); - - when( - () => shorebirdValidator.validatePreconditions( - checkUserIsAuthenticated: any(named: 'checkUserIsAuthenticated'), - checkShorebirdInitialized: any(named: 'checkShorebirdInitialized'), - ), - ).thenAnswer((_) async {}); - - command = runWithOverrides( - () => ReleaseAarCommand(unzipFn: (_, __) async {}), - )..testArgResults = argResults; - }); - - test('has correct description', () { - expect(command.description, isNotEmpty); - }); - - test('exits when validation fails', () async { - final exception = ValidationFailedException(); - when( - () => shorebirdValidator.validatePreconditions( - checkUserIsAuthenticated: any(named: 'checkUserIsAuthenticated'), - checkShorebirdInitialized: any(named: 'checkShorebirdInitialized'), - ), - ).thenThrow(exception); - await expectLater( - runWithOverrides(command.run), - completion(equals(exception.exitCode.code)), - ); - verify( - () => shorebirdValidator.validatePreconditions( - checkUserIsAuthenticated: true, - checkShorebirdInitialized: true, - ), - ).called(1); - }); - - test('exits with 78 if no module entry exists in pubspec.yaml', () async { - when(() => shorebirdEnv.androidPackageName).thenReturn(null); - - final result = await runWithOverrides(command.run); - - expect(result, ExitCode.config.code); - }); - - group('when target-platforms is specified', () { - setUp(() { - when(() => argResults['target-platform']).thenReturn(['android-arm']); - }); - - test('only creates artifcats for the specified archs', () async { - setUpProjectRootArtifacts(); - final exitCode = await runWithOverrides(command.run); - expect(exitCode, equals(ExitCode.success.code)); - verify( - () => logger.info( - any( - that: contains( - ''' -šŸ•¹ļø Platform: ${lightCyan.wrap('android')} ${lightCyan.wrap('(arm32)')}''', - ), - ), - ), - ).called(1); - verify( - () => shorebirdProcess.run( - 'flutter', - [ - 'build', - 'aar', - '--no-debug', - '--no-profile', - '--build-number=$buildNumber', - '--target-platform=android-arm', - ], - runInShell: any(named: 'runInShell'), - ), - ).called(1); - }); - }); - - group('when flutter-version is provided', () { - const flutterVersion = '3.16.3'; - setUp(() { - when(() => argResults['flutter-version']).thenReturn(flutterVersion); - }); - - group('when unable to determine flutter revision', () { - final exception = Exception('oops'); - setUp(() { - when( - () => shorebirdFlutter.getRevisionForVersion(any()), - ).thenThrow(exception); - }); - - test('exits with code 70', () async { - final exitCode = await runWithOverrides(command.run); - expect(exitCode, equals(ExitCode.software.code)); - verify( - () => logger.err( - ''' -Unable to determine revision for Flutter version: $flutterVersion. -$exception''', - ), - ).called(1); - }); - }); - - group('when flutter version is not supported', () { - setUp(() { - when( - () => shorebirdFlutter.getRevisionForVersion(any()), - ).thenAnswer((_) async => null); - }); - - test('exits with code 70', () async { - final exitCode = await runWithOverrides(command.run); - expect(exitCode, equals(ExitCode.software.code)); - verify( - () => logger.err( - any(that: contains('Version $flutterVersion not found.')), - ), - ).called(1); - }); - }); - - group('when flutter version is supported', () { - const revision = '771d07b2cf'; - setUp(() { - when( - () => shorebirdFlutter.getRevisionForVersion(any()), - ).thenAnswer((_) async => revision); - }); - - test('uses specified flutter version to build', () async { - setUpProjectRootArtifacts(); - when( - () => shorebirdProcess.run( - any(), - any(that: containsAll(['build', 'aar'])), - runInShell: any(named: 'runInShell'), - ), - ).thenAnswer((_) async { - // Ensure we're using the correct flutter revision. - expect(shorebirdEnv.flutterRevision, equals(revision)); - return flutterBuildProcessResult; - }); - - await runWithOverrides(command.run); - - verify(() => shorebirdFlutter.installRevision(revision: revision)) - .called(1); - verify( - () => codePushClientWrapper.createRelease( - appId: appId, - version: version, - flutterRevision: revision, - platform: releasePlatform, - ), - ).called(1); - }); - - group('when flutter version install fails', () { - setUp(() { - when( - () => shorebirdFlutter.installRevision( - revision: any(named: 'revision'), - ), - ).thenThrow(Exception('oops')); - }); - - test('exits with code 70', () async { - setUpProjectRootArtifacts(); - final result = await runWithOverrides(command.run); - - expect(result, equals(ExitCode.software.code)); - verify( - () => shorebirdFlutter.installRevision(revision: revision), - ).called(1); - }); - }); - }); - }); - - test('exits with code 70 when building aar fails', () async { - when(() => flutterBuildProcessResult.exitCode).thenReturn(1); - when(() => flutterBuildProcessResult.stderr).thenReturn('oops'); - final result = await runWithOverrides(command.run); - expect(result, equals(ExitCode.software.code)); - verify( - () => shorebirdProcess.run( - 'flutter', - [ - 'build', - 'aar', - '--no-debug', - '--no-profile', - '--build-number=$buildNumber', - '--target-platform=android-arm,android-arm64,android-x64', - ], - runInShell: any(named: 'runInShell'), - ), - ).called(1); - verify( - () => progress.fail(any(that: contains('Failed to build'))), - ).called(1); - }); - - test('aborts when user opts out', () async { - when(() => logger.confirm(any())).thenReturn(false); - when( - () => logger.prompt( - 'What is the version of this release?', - defaultValue: any(named: 'defaultValue'), - ), - ).thenAnswer((_) => '1.0.0'); - setUpProjectRootArtifacts(); - final exitCode = await runWithOverrides(command.run); - expect(exitCode, ExitCode.success.code); - verify(() => logger.info('Aborting.')).called(1); - }); - - test('does not prompt for confirmation if unable to accpet user input', - () async { - when(() => shorebirdEnv.canAcceptUserInput).thenReturn(false); - setUpProjectRootArtifacts(); - final exitCode = await runWithOverrides(command.run); - expect(exitCode, ExitCode.success.code); - verify(() => logger.success('\nāœ… Published Release $version!')).called(1); - verifyNever( - () => logger.prompt(any(), defaultValue: any(named: 'defaultValue')), - ); - }); - - test('succeeds when release is successful', () async { - setUpProjectRootArtifacts(); - final exitCode = await runWithOverrides(command.run); - expect(exitCode, ExitCode.success.code); - verify(() => logger.success('\nāœ… Published Release $version!')).called(1); - verify( - () => codePushClientWrapper.createAndroidArchiveReleaseArtifacts( - appId: appId, - releaseId: release.id, - platform: releasePlatform, - aarPath: any( - named: 'aarPath', - that: endsWith( - p.join( - 'build', - 'host', - 'outputs', - 'repo', - 'com', - 'example', - 'my_flutter_module', - 'flutter_release', - '1.0', - 'flutter_release-1.0.aar', - ), - ), - ), - extractedAarDir: any( - named: 'extractedAarDir', - that: endsWith( - p.join( - 'build', - 'host', - 'outputs', - 'repo', - 'com', - 'example', - 'my_flutter_module', - 'flutter_release', - '1.0', - 'flutter_release-1.0', - ), - ), - ), - architectures: any(named: 'architectures'), - ), - ).called(1); - verify( - () => codePushClientWrapper.updateReleaseStatus( - appId: appId, - releaseId: release.id, - platform: releasePlatform, - status: ReleaseStatus.active, - metadata: const UpdateReleaseMetadata( - releasePlatform: releasePlatform, - flutterVersionOverride: null, - generatedApks: false, - environment: BuildEnvironmentMetadata( - operatingSystem: operatingSystem, - operatingSystemVersion: operatingSystemVersion, - shorebirdVersion: packageVersion, - xcodeVersion: null, - ), - ), - ), - ).called(1); - verify( - () => logger.info( - '''To create a patch for this release, run ${lightCyan.wrap('shorebird patch aar --release-version=${release.version}')}''', - ), - ).called(1); - verifyNever( - () => logger.info( - ''' - -Note: ${lightCyan.wrap('shorebird patch aar')} without the --release-version option will patch the current version of the app. -''', - ), - ); - }); - - test('copies aar library to a releases folder', () async { - setUpProjectRootArtifacts(); - final exitCode = await runWithOverrides(command.run); - expect(exitCode, ExitCode.success.code); - expect( - Directory(p.join(projectRoot.path, 'release')).existsSync(), - isTrue, - ); - }); - - test('runs flutter pub get with system flutter after successful build', - () async { - setUpProjectRootArtifacts(); - - await runWithOverrides(command.run); - - verify( - () => shorebirdProcess.run( - 'flutter', - ['--no-version-check', 'pub', 'get', '--offline'], - runInShell: any(named: 'runInShell'), - useVendedFlutter: false, - ), - ).called(1); - }); - - test('does not create new release if existing release is present', - () async { - when( - () => codePushClientWrapper.maybeGetRelease( - appId: any(named: 'appId'), - releaseVersion: any(named: 'releaseVersion'), - ), - ).thenAnswer((_) async => release); - setUpProjectRootArtifacts(); - final exitCode = await runWithOverrides(command.run); - expect(exitCode, ExitCode.success.code); - verifyNever( - () => codePushClientWrapper.createRelease( - appId: any(named: 'appId'), - version: any(named: 'version'), - flutterRevision: any(named: 'flutterRevision'), - platform: any(named: 'platform'), - ), - ); - verify( - () => codePushClientWrapper.updateReleaseStatus( - appId: appId, - releaseId: release.id, - platform: releasePlatform, - status: ReleaseStatus.active, - metadata: any(named: 'metadata'), - ), - ).called(1); - }); - }); -} diff --git a/packages/shorebird_cli/test/src/commands/release/release_android_command_test.dart b/packages/shorebird_cli/test/src/commands/release/release_android_command_test.dart deleted file mode 100644 index 2807d131..00000000 --- a/packages/shorebird_cli/test/src/commands/release/release_android_command_test.dart +++ /dev/null @@ -1,1059 +0,0 @@ -import 'dart:io' hide Platform; - -import 'package:args/args.dart'; -import 'package:http/http.dart' as http; -import 'package:mason_logger/mason_logger.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; -import 'package:scoped/scoped.dart'; -import 'package:shorebird_cli/src/auth/auth.dart'; -import 'package:shorebird_cli/src/cache.dart'; -import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; -import 'package:shorebird_cli/src/commands/commands.dart'; -import 'package:shorebird_cli/src/config/config.dart'; -import 'package:shorebird_cli/src/doctor.dart'; -import 'package:shorebird_cli/src/engine_config.dart'; -import 'package:shorebird_cli/src/executables/executables.dart'; -import 'package:shorebird_cli/src/logger.dart'; -import 'package:shorebird_cli/src/os/operating_system_interface.dart'; -import 'package:shorebird_cli/src/platform.dart'; -import 'package:shorebird_cli/src/platform/platform.dart'; -import 'package:shorebird_cli/src/shorebird_android_artifacts.dart'; -import 'package:shorebird_cli/src/shorebird_env.dart'; -import 'package:shorebird_cli/src/shorebird_flutter.dart'; -import 'package:shorebird_cli/src/shorebird_process.dart'; -import 'package:shorebird_cli/src/shorebird_validator.dart'; -import 'package:shorebird_cli/src/validators/validators.dart'; -import 'package:shorebird_cli/src/version.dart'; -import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; -import 'package:test/test.dart'; - -import '../../fakes.dart'; -import '../../mocks.dart'; - -void main() { - group(ReleaseAndroidCommand, () { - const appId = 'test-app-id'; - const shorebirdYaml = ShorebirdYaml(appId: appId); - const flutterRevision = '83305b5088e6fe327fb3334a73ff190828d85713'; - const flutterVersionAndRevision = '3.10.6 (83305b5088)'; - const versionName = '1.2.3'; - const versionCode = '1'; - const version = '$versionName+$versionCode'; - const operatingSystem = 'macOS'; - const operatingSystemVersion = '11.0.0'; - const appDisplayName = 'Test App'; - const arch = 'aarch64'; - const releasePlatform = ReleasePlatform.android; - final appMetadata = AppMetadata( - appId: appId, - displayName: appDisplayName, - createdAt: DateTime(2023), - updatedAt: DateTime(2023), - ); - final release = Release( - id: 0, - appId: appId, - version: version, - flutterRevision: flutterRevision, - displayName: '1.2.3+1', - platformStatuses: {}, - createdAt: DateTime(2023), - updatedAt: DateTime(2023), - ); - - const javaHome = 'test-java-home'; - - late ArgResults argResults; - late Bundletool bundletool; - late http.Client httpClient; - late CodePushClientWrapper codePushClientWrapper; - late Directory shorebirdRoot; - late Directory projectRoot; - late Doctor doctor; - late Platform platform; - late Auth auth; - late Cache cache; - late Java java; - late ShorebirdLogger logger; - late OperatingSystemInterface operatingSystemInterface; - late Progress progress; - late ShorebirdProcessResult flutterAppbundleBuildProcessResult; - late ShorebirdProcessResult flutterApkBuildProcessResult; - late ShorebirdProcessResult flutterPubGetProcessResult; - late ShorebirdFlutterValidator flutterValidator; - late ShorebirdProcess shorebirdProcess; - late ShorebirdEnv shorebirdEnv; - late ShorebirdFlutter shorebirdFlutter; - late ShorebirdValidator shorebirdValidator; - late ShorebirdAndroidArtifacts shorebirdAndroidArtifacts; - late ReleaseAndroidCommand command; - - late String aabPath; - late String apkPath; - - R runWithOverrides(R Function() body) { - return runScoped( - body, - values: { - authRef.overrideWith(() => auth), - bundletoolRef.overrideWith(() => bundletool), - cacheRef.overrideWith(() => cache), - codePushClientWrapperRef.overrideWith(() => codePushClientWrapper), - doctorRef.overrideWith(() => doctor), - engineConfigRef.overrideWith(() => const EngineConfig.empty()), - javaRef.overrideWith(() => java), - loggerRef.overrideWith(() => logger), - osInterfaceRef.overrideWith(() => operatingSystemInterface), - platformRef.overrideWith(() => platform), - processRef.overrideWith(() => shorebirdProcess), - shorebirdEnvRef.overrideWith(() => shorebirdEnv), - shorebirdFlutterRef.overrideWith(() => shorebirdFlutter), - shorebirdValidatorRef.overrideWith(() => shorebirdValidator), - shorebirdAndroidArtifactsRef - .overrideWith(() => shorebirdAndroidArtifacts), - }, - ); - } - - setUpAll(() { - registerFallbackValue(ReleasePlatform.android); - registerFallbackValue(ReleaseStatus.draft); - registerFallbackValue(FakeRelease()); - registerFallbackValue(FakeShorebirdProcess()); - registerFallbackValue(Directory('')); - }); - - setUp(() { - argResults = MockArgResults(); - bundletool = MockBundleTool(); - codePushClientWrapper = MockCodePushClientWrapper(); - doctor = MockDoctor(); - httpClient = MockHttpClient(); - operatingSystemInterface = MockOperatingSystemInterface(); - platform = MockPlatform(); - shorebirdRoot = Directory.systemTemp.createTempSync(); - projectRoot = Directory.systemTemp.createTempSync(); - auth = MockAuth(); - cache = MockCache(); - java = MockJava(); - progress = MockProgress(); - logger = MockShorebirdLogger(); - flutterAppbundleBuildProcessResult = MockProcessResult(); - flutterApkBuildProcessResult = MockProcessResult(); - flutterPubGetProcessResult = MockProcessResult(); - flutterValidator = MockShorebirdFlutterValidator(); - shorebirdProcess = MockShorebirdProcess(); - shorebirdEnv = MockShorebirdEnv(); - shorebirdFlutter = MockShorebirdFlutter(); - shorebirdValidator = MockShorebirdValidator(); - shorebirdAndroidArtifacts = ShorebirdAndroidArtifacts(); - - aabPath = p.join( - projectRoot.path, - 'build', - 'app', - 'outputs', - 'bundle', - 'release', - 'app-release.aab', - ); - apkPath = p.join( - projectRoot.path, - 'build', - 'app', - 'outputs', - 'flutter-apk', - 'app-release.apk', - ); - - File(apkPath).createSync(recursive: true); - File(aabPath).createSync(recursive: true); - - when(() => shorebirdEnv.getShorebirdYaml()).thenReturn(shorebirdYaml); - when( - () => shorebirdEnv.copyWith( - flutterRevisionOverride: any(named: 'flutterRevisionOverride'), - ), - ).thenAnswer((invocation) { - when(() => shorebirdEnv.flutterRevision).thenReturn( - invocation.namedArguments[#flutterRevisionOverride] as String, - ); - return shorebirdEnv; - }); - when(() => shorebirdEnv.shorebirdRoot).thenReturn(shorebirdRoot); - when( - () => shorebirdEnv.getShorebirdProjectRoot(), - ).thenReturn(projectRoot); - when(() => shorebirdEnv.flutterRevision).thenReturn(flutterRevision); - when(() => shorebirdEnv.canAcceptUserInput).thenReturn(true); - - when( - () => shorebirdFlutter.getVersionAndRevision(), - ).thenAnswer((_) async => flutterVersionAndRevision); - when( - () => shorebirdFlutter.installRevision( - revision: any(named: 'revision'), - ), - ).thenAnswer((_) async => {}); - - when( - () => shorebirdProcess.run( - 'flutter', - ['--no-version-check', 'pub', 'get', '--offline'], - runInShell: any(named: 'runInShell'), - useVendedFlutter: false, - ), - ).thenAnswer((_) async => flutterPubGetProcessResult); - when( - () => shorebirdProcess.run( - 'flutter', - any(that: contains('appbundle')), - runInShell: any(named: 'runInShell'), - ), - ).thenAnswer((_) async => flutterAppbundleBuildProcessResult); - when( - () => shorebirdProcess.run( - 'flutter', - any(that: contains('apk')), - runInShell: any(named: 'runInShell'), - ), - ).thenAnswer((_) async => flutterApkBuildProcessResult); - - when(() => argResults['arch']).thenReturn(arch); - when(() => argResults['platform']).thenReturn(releasePlatform); - when(() => argResults['artifact']).thenReturn('aab'); - when(() => argResults['target-platform']) - .thenReturn(Arch.values.map((a) => a.targetPlatformCliArg).toList()); - when(() => argResults.rest).thenReturn([]); - when(() => argResults.wasParsed(any())).thenReturn(true); - when(() => auth.isAuthenticated).thenReturn(true); - when(() => auth.client).thenReturn(httpClient); - when(() => cache.updateAll()).thenAnswer((_) async => {}); - when( - () => cache.getArtifactDirectory(any()), - ).thenReturn(Directory.systemTemp.createTempSync()); - when(() => logger.progress(any())).thenReturn(progress); - when(() => logger.confirm(any())).thenReturn(true); - when( - () => logger.prompt(any(), defaultValue: any(named: 'defaultValue')), - ).thenReturn(version); - when(() => platform.operatingSystem).thenReturn(operatingSystem); - when(() => platform.operatingSystemVersion) - .thenReturn(operatingSystemVersion); - when( - () => flutterAppbundleBuildProcessResult.exitCode, - ).thenReturn(ExitCode.success.code); - when( - () => flutterApkBuildProcessResult.exitCode, - ).thenReturn(ExitCode.success.code); - when(() => flutterPubGetProcessResult.exitCode) - .thenReturn(ExitCode.success.code); - when( - () => codePushClientWrapper.getApp(appId: any(named: 'appId')), - ).thenAnswer((_) async => appMetadata); - when( - () => codePushClientWrapper.maybeGetRelease( - appId: any(named: 'appId'), - releaseVersion: any(named: 'releaseVersion'), - ), - ).thenAnswer((_) async => null); - when( - () => codePushClientWrapper.ensureReleaseIsNotActive( - release: any(named: 'release'), - platform: any(named: 'platform'), - ), - ).thenAnswer((_) async => {}); - when( - () => codePushClientWrapper.createRelease( - appId: any(named: 'appId'), - version: any(named: 'version'), - flutterRevision: any(named: 'flutterRevision'), - platform: any(named: 'platform'), - ), - ).thenAnswer((_) async => release); - when( - () => codePushClientWrapper.createAndroidReleaseArtifacts( - appId: any(named: 'appId'), - releaseId: any(named: 'releaseId'), - projectRoot: any(named: 'projectRoot'), - aabPath: any(named: 'aabPath'), - platform: any(named: 'platform'), - architectures: any(named: 'architectures'), - flavor: any(named: 'flavor'), - ), - ).thenAnswer((_) async {}); - when( - () => codePushClientWrapper.updateReleaseStatus( - appId: any(named: 'appId'), - releaseId: any(named: 'releaseId'), - platform: any(named: 'platform'), - status: any(named: 'status'), - metadata: any(named: 'metadata'), - ), - ).thenAnswer((_) async {}); - when(() => doctor.androidCommandValidators) - .thenReturn([flutterValidator]); - when(flutterValidator.validate).thenAnswer((_) async => []); - when(() => java.home).thenReturn(javaHome); - when( - () => bundletool.getVersionName(any()), - ).thenAnswer((_) async => versionName); - when( - () => bundletool.getVersionCode(any()), - ).thenAnswer((_) async => versionCode); - when(() => operatingSystemInterface.which('flutter')) - .thenReturn('/path/to/flutter'); - when( - () => shorebirdValidator.validatePreconditions( - checkUserIsAuthenticated: any(named: 'checkUserIsAuthenticated'), - checkShorebirdInitialized: any(named: 'checkShorebirdInitialized'), - validators: any(named: 'validators'), - supportedOperatingSystems: any(named: 'supportedOperatingSystems'), - ), - ).thenAnswer((_) async {}); - - command = runWithOverrides(ReleaseAndroidCommand.new) - ..testArgResults = argResults; - }); - - test('has a description', () { - expect(command.description, isNotEmpty); - }); - - test('exits when validation fails', () async { - final exception = ValidationFailedException(); - when( - () => shorebirdValidator.validatePreconditions( - checkUserIsAuthenticated: any(named: 'checkUserIsAuthenticated'), - checkShorebirdInitialized: any(named: 'checkShorebirdInitialized'), - validators: any(named: 'validators'), - ), - ).thenThrow(exception); - await expectLater( - runWithOverrides(command.run), - completion(equals(exception.exitCode.code)), - ); - verify( - () => shorebirdValidator.validatePreconditions( - checkUserIsAuthenticated: true, - checkShorebirdInitialized: true, - validators: [flutterValidator], - ), - ).called(1); - }); - - test('exits with code unavailable when --split-per-abi is provided', - () async { - when(() => argResults['artifact']).thenReturn('apk'); - when(() => argResults['split-per-abi']).thenReturn(true); - - final exitCode = await runWithOverrides(command.run); - - expect(exitCode, ExitCode.unavailable.code); - }); - - group('when target-platforms is specified', () { - setUp(() { - when(() => argResults['target-platform']).thenReturn(['android-arm']); - }); - - test('only creates artifcats for the specified archs', () async { - final exitCode = await runWithOverrides(command.run); - expect(exitCode, equals(ExitCode.success.code)); - verify( - () => logger.info( - any( - that: contains( - ''' -šŸ•¹ļø Platform: ${lightCyan.wrap('android')} ${lightCyan.wrap('(arm32)')}''', - ), - ), - ), - ).called(1); - }); - }); - - group('when flutter-version is provided', () { - const flutterVersion = '3.16.3'; - setUp(() { - when(() => argResults['flutter-version']).thenReturn(flutterVersion); - }); - - group('when unable to determine flutter revision', () { - final exception = Exception('oops'); - setUp(() { - when( - () => shorebirdFlutter.getRevisionForVersion(any()), - ).thenThrow(exception); - }); - - test('exits with code 70', () async { - final exitCode = await runWithOverrides(command.run); - expect(exitCode, equals(ExitCode.software.code)); - verify( - () => logger.err( - ''' -Unable to determine revision for Flutter version: $flutterVersion. -$exception''', - ), - ).called(1); - }); - }); - - group('when flutter version is not supported', () { - setUp(() { - when( - () => shorebirdFlutter.getRevisionForVersion(any()), - ).thenAnswer((_) async => null); - }); - - test('exits with code 70', () async { - final exitCode = await runWithOverrides(command.run); - expect(exitCode, equals(ExitCode.software.code)); - verify( - () => logger.err( - any(that: contains('Version $flutterVersion not found.')), - ), - ).called(1); - }); - }); - - group('when flutter version is supported', () { - const revision = '771d07b2cf'; - setUp(() { - when( - () => shorebirdFlutter.getRevisionForVersion(any()), - ).thenAnswer((_) async => revision); - }); - - test( - 'uses specified flutter version to build ' - 'and reverts to original flutter version', () async { - when( - () => shorebirdProcess.run( - 'flutter', - any(), - runInShell: any(named: 'runInShell'), - ), - ).thenAnswer((_) async { - // Ensure we're using the correct flutter version. - expect(shorebirdEnv.flutterRevision, equals(revision)); - return flutterAppbundleBuildProcessResult; - }); - - await runWithOverrides(command.run); - - verify(() => shorebirdFlutter.installRevision(revision: revision)) - .called(1); - verify( - () => codePushClientWrapper.createRelease( - appId: appId, - version: version, - flutterRevision: revision, - platform: releasePlatform, - ), - ).called(1); - verify( - () => codePushClientWrapper.updateReleaseStatus( - appId: appId, - releaseId: release.id, - platform: releasePlatform, - status: ReleaseStatus.active, - metadata: const UpdateReleaseMetadata( - releasePlatform: releasePlatform, - flutterVersionOverride: flutterVersion, - generatedApks: false, - environment: BuildEnvironmentMetadata( - operatingSystem: operatingSystem, - operatingSystemVersion: operatingSystemVersion, - shorebirdVersion: packageVersion, - xcodeVersion: null, - ), - ), - ), - ).called(1); - }); - - group('when flutter version install fails', () { - setUp(() { - when( - () => shorebirdFlutter.installRevision( - revision: any(named: 'revision'), - ), - ).thenThrow(Exception('oops')); - }); - - test('exits with code 70', () async { - final result = await runWithOverrides(command.run); - - expect(result, equals(ExitCode.software.code)); - verify( - () => shorebirdFlutter.installRevision(revision: revision), - ).called(1); - }); - }); - }); - }); - - test('exits with code 70 when building appbundle fails', () async { - when(() => argResults['artifact']).thenReturn('apk'); - when(() => flutterAppbundleBuildProcessResult.exitCode).thenReturn(1); - when(() => flutterAppbundleBuildProcessResult.stderr).thenReturn('oops'); - final exitCode = await runWithOverrides(command.run); - expect(exitCode, equals(ExitCode.software.code)); - }); - - test('exits with code 70 when building apk fails', () async { - when(() => argResults['artifact']).thenReturn('apk'); - when(() => flutterApkBuildProcessResult.exitCode).thenReturn(1); - when(() => flutterApkBuildProcessResult.stderr).thenReturn('oops'); - final exitCode = await runWithOverrides(command.run); - expect(exitCode, equals(ExitCode.software.code)); - }); - - test('errors when detecting release version name fails', () async { - final exception = Exception( - 'Failed to extract version name from app bundle: oops', - ); - when(() => bundletool.getVersionName(any())).thenThrow(exception); - final exitCode = await runWithOverrides(command.run); - expect(exitCode, ExitCode.software.code); - verify(() => progress.fail('$exception')).called(1); - }); - - test('errors when detecting release version code fails', () async { - final exception = Exception( - 'Failed to extract version code from app bundle: oops', - ); - when(() => bundletool.getVersionCode(any())).thenThrow(exception); - final exitCode = await runWithOverrides(command.run); - expect(exitCode, ExitCode.software.code); - verify(() => progress.fail('$exception')).called(1); - }); - - test('aborts when user opts out', () async { - when(() => logger.confirm(any())).thenReturn(false); - when( - () => logger.prompt( - 'What is the version of this release?', - defaultValue: any(named: 'defaultValue'), - ), - ).thenAnswer((_) => '1.0.0'); - final exitCode = await runWithOverrides(command.run); - expect(exitCode, ExitCode.success.code); - verify(() => logger.info('Aborting.')).called(1); - verifyNever( - () => codePushClientWrapper.updateReleaseStatus( - appId: any(named: 'appId'), - releaseId: any(named: 'releaseId'), - platform: any(named: 'platform'), - status: any(named: 'status'), - ), - ); - }); - - test('succeeds when release is successful', () async { - final exitCode = await runWithOverrides(command.run); - verify(() => logger.success('\nāœ… Published Release $version!')).called(1); - final aabPath = p.join( - projectRoot.path, - 'build', - 'app', - 'outputs', - 'bundle', - 'release', - 'app-release.aab', - ); - // Verify info message does not include apk instructions. - verify( - () => logger.info(''' - -Your next step is to upload the app bundle to the Play Store: -${lightCyan.wrap(aabPath)} - -For information on uploading to the Play Store, see: -${link(uri: Uri.parse('https://support.google.com/googleplay/android-developer/answer/9859152?hl=en'))} -'''), - ).called(1); - verify( - () => codePushClientWrapper.createAndroidReleaseArtifacts( - appId: appId, - releaseId: release.id, - platform: releasePlatform, - projectRoot: any(named: 'projectRoot'), - aabPath: any(named: 'aabPath'), - architectures: any(named: 'architectures'), - ), - ).called(1); - verify( - () => codePushClientWrapper.updateReleaseStatus( - appId: appId, - releaseId: release.id, - platform: releasePlatform, - status: ReleaseStatus.active, - metadata: const UpdateReleaseMetadata( - releasePlatform: releasePlatform, - flutterVersionOverride: null, - generatedApks: false, - environment: BuildEnvironmentMetadata( - operatingSystem: operatingSystem, - operatingSystemVersion: operatingSystemVersion, - shorebirdVersion: packageVersion, - xcodeVersion: null, - ), - ), - ), - ).called(1); - expect(exitCode, ExitCode.success.code); - }); - - test('succeeds when release is successful (with apk)', () async { - when(() => argResults['artifact']).thenReturn('apk'); - final exitCode = await runWithOverrides(command.run); - verify(() => logger.success('\nāœ… Published Release $version!')).called(1); - // Verify info message does include apk instructions. - verify( - () => logger.info(''' - -Your next step is to upload the app bundle to the Play Store: -${lightCyan.wrap(aabPath)} - -Or distribute the apk: -${lightCyan.wrap(apkPath)} - -For information on uploading to the Play Store, see: -${link(uri: Uri.parse('https://support.google.com/googleplay/android-developer/answer/9859152?hl=en'))} -'''), - ).called(1); - verify( - () => codePushClientWrapper.createAndroidReleaseArtifacts( - appId: appId, - releaseId: release.id, - platform: releasePlatform, - projectRoot: any(named: 'projectRoot'), - aabPath: any(named: 'aabPath'), - architectures: any(named: 'architectures'), - ), - ).called(1); - verify( - () => codePushClientWrapper.updateReleaseStatus( - appId: appId, - releaseId: release.id, - platform: releasePlatform, - status: ReleaseStatus.active, - metadata: const UpdateReleaseMetadata( - releasePlatform: releasePlatform, - flutterVersionOverride: null, - generatedApks: true, - environment: BuildEnvironmentMetadata( - operatingSystem: operatingSystem, - operatingSystemVersion: operatingSystemVersion, - shorebirdVersion: packageVersion, - xcodeVersion: null, - ), - ), - ), - ).called(1); - const buildApkArguments = [ - 'build', - 'apk', - '--release', - '--target-platform=android-arm,android-arm64,android-x64', - ]; - verify( - () => shorebirdProcess.run( - 'flutter', - buildApkArguments, - runInShell: true, - ), - ).called(1); - verify( - () => logger.info( - '''To create a patch for this release, run ${lightCyan.wrap('shorebird patch android --release-version=${release.version}')}''', - ), - ).called(1); - verify( - () => logger.info( - ''' - -Note: ${lightCyan.wrap('shorebird patch android')} without the --release-version option will patch the current version of the app. -''', - ), - ).called(1); - expect(exitCode, ExitCode.success.code); - }); - - group('after a build', () { - group('when the build is successful', () { - setUp(() { - when(() => flutterAppbundleBuildProcessResult.exitCode) - .thenReturn(ExitCode.success.code); - }); - - group('when flutter is installed', () { - setUp(() { - when(() => operatingSystemInterface.which('flutter')) - .thenReturn('/path/to/flutter'); - }); - - test('runs flutter pub get with system flutter', () async { - await runWithOverrides(command.run); - - verify( - () => shorebirdProcess.run( - 'flutter', - ['--no-version-check', 'pub', 'get', '--offline'], - runInShell: any(named: 'runInShell'), - useVendedFlutter: false, - ), - ).called(1); - }); - }); - - group('when flutter is not installed', () { - setUp(() { - when(() => operatingSystemInterface.which('flutter')) - .thenReturn(null); - }); - - test('does not attempt to run flutter pub get', () async { - await runWithOverrides(command.run); - - verifyNever( - () => shorebirdProcess.run( - 'flutter', - ['--no-version-check', 'pub', 'get', '--offline'], - runInShell: any(named: 'runInShell'), - useVendedFlutter: false, - ), - ); - }); - }); - }); - - group('when the build fails', () { - setUp(() { - when(() => flutterAppbundleBuildProcessResult.exitCode) - .thenReturn(ExitCode.software.code); - }); - - group('when flutter is installed', () { - setUp(() { - when(() => operatingSystemInterface.which('flutter')) - .thenReturn('/path/to/flutter'); - }); - - test('runs flutter pub get with system flutter', () async { - await runWithOverrides(command.run); - - verify( - () => shorebirdProcess.run( - 'flutter', - ['--no-version-check', 'pub', 'get', '--offline'], - runInShell: any(named: 'runInShell'), - useVendedFlutter: false, - ), - ).called(1); - }); - - test('prints error message if system flutter pub get fails', - () async { - when(() => flutterPubGetProcessResult.exitCode).thenReturn(1); - - await runWithOverrides(command.run); - - verify( - () => logger.warn( - ''' -Build was successful, but `flutter pub get` failed to run after the build completed. You may see unexpected behavior in VS Code. - -Either run `flutter pub get` manually, or follow the steps in ${link(uri: Uri.parse('https://docs.shorebird.dev/troubleshooting#i-installed-shorebird-and-now-i-cant-run-my-app-in-vs-code'))}. -''', - ), - ).called(1); - }); - }); - - group('when flutter is not installed', () { - setUp(() { - when(() => operatingSystemInterface.which('flutter')) - .thenReturn(null); - }); - - test('does not attempt to run flutter pub get', () async { - await runWithOverrides(command.run); - - verifyNever( - () => shorebirdProcess.run( - 'flutter', - ['--no-version-check', 'pub', 'get', '--offline'], - runInShell: any(named: 'runInShell'), - useVendedFlutter: false, - ), - ); - }); - }); - }); - }); - - test( - 'succeeds when release is successful ' - 'with flavors and target', () async { - aabPath = p.join( - projectRoot.path, - 'build', - 'app', - 'outputs', - 'bundle', - 'developmentRelease', - 'app-development-release.aab', - ); - apkPath = p.join( - projectRoot.path, - 'build', - 'app', - 'outputs', - 'flutter-apk', - 'app-development-release.apk', - ); - - File(apkPath).createSync(recursive: true); - File(aabPath).createSync(recursive: true); - const flavor = 'development'; - final target = p.join('lib', 'main_development.dart'); - when(() => argResults['flavor']).thenReturn(flavor); - when(() => argResults['target']).thenReturn(target); - when(() => argResults['artifact']).thenReturn('apk'); - const shorebirdYaml = ShorebirdYaml( - appId: 'productionAppId', - flavors: {flavor: appId}, - ); - when(() => shorebirdEnv.getShorebirdYaml()).thenReturn(shorebirdYaml); - final exitCode = await runWithOverrides(command.run); - - verify(() => logger.success('\nāœ… Published Release $version!')).called(1); - verify( - () => codePushClientWrapper.createAndroidReleaseArtifacts( - appId: appId, - releaseId: release.id, - platform: releasePlatform, - projectRoot: any(named: 'projectRoot'), - aabPath: any(named: 'aabPath'), - architectures: any(named: 'architectures'), - flavor: flavor, - ), - ).called(1); - verify( - () => codePushClientWrapper.updateReleaseStatus( - appId: appId, - releaseId: release.id, - platform: releasePlatform, - status: ReleaseStatus.active, - metadata: any(named: 'metadata'), - ), - ).called(1); - verify( - () => logger.info( - '''To create a patch for this release, run ${lightCyan.wrap('shorebird patch android --flavor=$flavor --target=$target --release-version=${release.version}')}''', - ), - ).called(1); - verify( - () => logger.info( - ''' - -Note: ${lightCyan.wrap('shorebird patch android --flavor=$flavor --target=$target')} without the --release-version option will patch the current version of the app. -''', - ), - ).called(1); - expect(exitCode, ExitCode.success.code); - }); - - test('does not create new release if existing release is present', - () async { - when( - () => codePushClientWrapper.maybeGetRelease( - appId: any(named: 'appId'), - releaseVersion: any(named: 'releaseVersion'), - ), - ).thenAnswer((_) async => release); - final exitCode = await runWithOverrides(command.run); - expect(exitCode, ExitCode.success.code); - verifyNever( - () => codePushClientWrapper.createRelease( - appId: any(named: 'appId'), - version: any(named: 'version'), - flutterRevision: any(named: 'flutterRevision'), - platform: any(named: 'platform'), - ), - ); - }); - - test( - 'does not upload artifacts if an existing release is present ' - 'with a different flutter revision', () async { - when( - () => codePushClientWrapper.maybeGetRelease( - appId: any(named: 'appId'), - releaseVersion: any(named: 'releaseVersion'), - ), - ).thenAnswer( - (_) async => Release( - id: release.id, - appId: release.appId, - version: release.version, - flutterRevision: 'different-revision', - displayName: release.displayName, - createdAt: release.createdAt.add(const Duration(minutes: 1)), - updatedAt: release.createdAt.add(const Duration(minutes: 1)), - platformStatuses: release.platformStatuses, - ), - ); - final exitCode = await runWithOverrides(command.run); - expect(exitCode, ExitCode.software.code); - verify( - () => logger.err( - any( - that: contains( - '''All platforms for a given release must be built using the same Flutter revision.''', - ), - ), - ), - ).called(1); - verifyNever( - () => codePushClientWrapper.createRelease( - appId: any(named: 'appId'), - version: any(named: 'version'), - flutterRevision: any(named: 'flutterRevision'), - platform: any(named: 'platform'), - ), - ); - verifyNever( - () => codePushClientWrapper.createAndroidReleaseArtifacts( - appId: any(named: 'appId'), - releaseId: any(named: 'releaseId'), - projectRoot: any(named: 'projectRoot'), - aabPath: any(named: 'aabPath'), - platform: any(named: 'platform'), - architectures: any(named: 'architectures'), - flavor: any(named: 'flavor'), - ), - ); - }); - - test('does not prompt if unable to accept user input', () async { - when(() => shorebirdEnv.canAcceptUserInput).thenReturn(false); - - final exitCode = await runWithOverrides(command.run); - - expect(exitCode, equals(ExitCode.success.code)); - verifyNever(() => logger.confirm(any())); - }); - - test('errors when multiple apks are found', () async { - shorebirdAndroidArtifacts = MockShorebirdAndroidArtifacts(); - when(() => argResults['artifact']).thenReturn('apk'); - when( - () => shorebirdAndroidArtifacts.findAab( - project: any(named: 'project'), - flavor: any(named: 'flavor'), - ), - ).thenReturn(File('app-release.aab')); - when( - () => shorebirdAndroidArtifacts.findApk( - project: any(named: 'project'), - flavor: any(named: 'flavor'), - ), - ).thenThrow( - MultipleArtifactsFoundException( - foundArtifacts: [File('a'), File('b')], - buildDir: 'buildDir', - ), - ); - final exitCode = await runWithOverrides(command.run); - verify( - () => progress.fail( - 'Build succeeded, but it generated multiple APKs in the build ' - 'directory. (a, b)', - ), - ).called(1); - expect(exitCode, ExitCode.software.code); - }); - - test('errors when the apk cannot be found', () async { - shorebirdAndroidArtifacts = MockShorebirdAndroidArtifacts(); - when(() => argResults['artifact']).thenReturn('apk'); - when( - () => shorebirdAndroidArtifacts.findAab( - project: any(named: 'project'), - flavor: any(named: 'flavor'), - ), - ).thenReturn(File('app-release.aab')); - when( - () => shorebirdAndroidArtifacts.findApk( - project: any(named: 'project'), - flavor: any(named: 'flavor'), - ), - ).thenThrow( - ArtifactNotFoundException( - artifactName: 'app-release.apk', - buildDir: 'buildDir', - ), - ); - final exitCode = await runWithOverrides(command.run); - verify( - () => progress.fail( - 'Build succeeded, but could not find the APK in the build ' - 'directory. Expected to find app-release.apk', - ), - ).called(1); - expect(exitCode, ExitCode.software.code); - }); - - test('errors when the app bundle cannot be found', () async { - shorebirdAndroidArtifacts = MockShorebirdAndroidArtifacts(); - when( - () => shorebirdAndroidArtifacts.findAab( - project: any(named: 'project'), - flavor: any(named: 'flavor'), - ), - ).thenThrow( - ArtifactNotFoundException( - artifactName: 'app-release.aab', - buildDir: 'buildDir', - ), - ); - final exitCode = await runWithOverrides(command.run); - verify( - () => progress - .fail('Build succeeded, but could not find the AAB in the build ' - 'directory. Expected to find app-release.aab'), - ).called(1); - expect(exitCode, ExitCode.software.code); - }); - - test('errors when multiple aabs are found', () async { - shorebirdAndroidArtifacts = MockShorebirdAndroidArtifacts(); - when( - () => shorebirdAndroidArtifacts.findAab( - project: any(named: 'project'), - flavor: any(named: 'flavor'), - ), - ).thenThrow( - MultipleArtifactsFoundException( - foundArtifacts: [File('a'), File('b')], - buildDir: 'buildDir', - ), - ); - final exitCode = await runWithOverrides(command.run); - verify( - () => progress.fail( - 'Build succeeded, but it generated multiple AABs in the build ' - 'directory. (a, b)', - ), - ).called(1); - expect(exitCode, ExitCode.software.code); - }); - }); -} diff --git a/packages/shorebird_cli/test/src/commands/release_new/release_new_command_test.dart b/packages/shorebird_cli/test/src/commands/release/release_command_test.dart similarity index 98% rename from packages/shorebird_cli/test/src/commands/release_new/release_new_command_test.dart rename to packages/shorebird_cli/test/src/commands/release/release_command_test.dart index 5f766c7f..208830fa 100644 --- a/packages/shorebird_cli/test/src/commands/release_new/release_new_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/release/release_command_test.dart @@ -4,7 +4,7 @@ import 'package:mocktail/mocktail.dart'; import 'package:scoped/scoped.dart'; import 'package:shorebird_cli/src/cache.dart'; import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; -import 'package:shorebird_cli/src/commands/release_new/release_new.dart'; +import 'package:shorebird_cli/src/commands/release/release.dart'; import 'package:shorebird_cli/src/config/config.dart'; import 'package:shorebird_cli/src/logger.dart'; import 'package:shorebird_cli/src/release_type.dart'; @@ -18,7 +18,7 @@ import '../../matchers.dart'; import '../../mocks.dart'; void main() { - group(ReleaseNewCommand, () { + group(ReleaseCommand, () { const appId = 'test-app-id'; const appDisplayName = 'Test App'; const flutterRevision = '83305b5088e6fe327fb3334a73ff190828d85713'; @@ -53,7 +53,7 @@ void main() { late ShorebirdEnv shorebirdEnv; late ShorebirdFlutter shorebirdFlutter; - late ReleaseNewCommand command; + late ReleaseCommand command; R runWithOverrides(R Function() body) { return runScoped( @@ -174,7 +174,7 @@ void main() { ), ).thenAnswer((_) async => {}); - command = ReleaseNewCommand(resolveReleaser: (_) => releaser) + command = ReleaseCommand(resolveReleaser: (_) => releaser) ..testArgResults = argResults; }); diff --git a/packages/shorebird_cli/test/src/commands/release/release_ios_command_test.dart b/packages/shorebird_cli/test/src/commands/release/release_ios_command_test.dart deleted file mode 100644 index d1477596..00000000 --- a/packages/shorebird_cli/test/src/commands/release/release_ios_command_test.dart +++ /dev/null @@ -1,1182 +0,0 @@ -import 'dart:io' hide Platform; - -import 'package:args/args.dart'; -import 'package:collection/collection.dart'; -import 'package:mason_logger/mason_logger.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; -import 'package:scoped/scoped.dart'; -import 'package:shorebird_cli/src/auth/auth.dart'; -import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; -import 'package:shorebird_cli/src/commands/commands.dart'; -import 'package:shorebird_cli/src/config/config.dart'; -import 'package:shorebird_cli/src/doctor.dart'; -import 'package:shorebird_cli/src/executables/xcodebuild.dart'; -import 'package:shorebird_cli/src/logger.dart'; -import 'package:shorebird_cli/src/os/operating_system_interface.dart'; -import 'package:shorebird_cli/src/platform.dart'; -import 'package:shorebird_cli/src/platform/platform.dart'; -import 'package:shorebird_cli/src/shorebird_env.dart'; -import 'package:shorebird_cli/src/shorebird_flutter.dart'; -import 'package:shorebird_cli/src/shorebird_process.dart'; -import 'package:shorebird_cli/src/shorebird_validator.dart'; -import 'package:shorebird_cli/src/validators/validators.dart'; -import 'package:shorebird_cli/src/version.dart'; -import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; -import 'package:test/test.dart'; - -import '../../fakes.dart'; -import '../../mocks.dart'; - -void main() { - group( - ReleaseIosCommand, - () { - const appId = 'test-app-id'; - const shorebirdYaml = ShorebirdYaml(appId: appId); - const flutterRevision = '83305b5088e6fe327fb3334a73ff190828d85713'; - const flutterVersionAndRevision = '3.10.6 (83305b5088)'; - const versionName = '1.2.3'; - const versionCode = '1'; - const version = '$versionName+$versionCode'; - const operatingSystem = 'macOS'; - const operatingSystemVersion = '11.0.0'; - const xcodeVersion = '12.0'; - const appDisplayName = 'Test App'; - const arch = 'armv7'; - const releasePlatform = ReleasePlatform.ios; - const ipaPath = 'build/ios/ipa/Runner.ipa'; - final appMetadata = AppMetadata( - appId: appId, - displayName: appDisplayName, - createdAt: DateTime(2023), - updatedAt: DateTime(2023), - ); - final release = Release( - id: 0, - appId: appId, - version: version, - flutterRevision: flutterRevision, - displayName: '1.2.3+1', - platformStatuses: {}, - createdAt: DateTime(2023), - updatedAt: DateTime(2023), - ); - const infoPlistContent = ''' - - - - - ApplicationProperties - - ApplicationPath - Applications/Runner.app - Architectures - - arm64 - - CFBundleIdentifier - com.shorebird.timeShift - CFBundleShortVersionString - 1.2.3 - CFBundleVersion - 1 - - ArchiveVersion - 2 - Name - Runner - SchemeName - Runner - -'''; - const emptyPlistContent = ''' - - - - - ApplicationProperties - - - -' -'''; - const pubspecYamlContent = ''' -name: example -version: $version -environment: - sdk: ">=2.19.0 <3.0.0" - -flutter: - assets: - - shorebird.yaml'''; - - late ArgResults argResults; - late CodePushClientWrapper codePushClientWrapper; - late Directory shorebirdRoot; - late Directory projectRoot; - late Doctor doctor; - late Platform platform; - late Auth auth; - late Progress progress; - late ShorebirdLogger logger; - late Ios ios; - late OperatingSystemInterface operatingSystemInterface; - late ShorebirdProcessResult flutterBuildProcessResult; - late ShorebirdProcessResult flutterPubGetProcessResult; - late ShorebirdFlutterValidator flutterValidator; - late ShorebirdProcess shorebirdProcess; - late ShorebirdEnv shorebirdEnv; - late ShorebirdFlutter shorebirdFlutter; - late ShorebirdValidator shorebirdValidator; - late XcodeBuild xcodeBuild; - late ReleaseIosCommand command; - - R runWithOverrides(R Function() body) { - return runScoped( - body, - values: { - authRef.overrideWith(() => auth), - codePushClientWrapperRef.overrideWith(() => codePushClientWrapper), - doctorRef.overrideWith(() => doctor), - iosRef.overrideWith(() => ios), - loggerRef.overrideWith(() => logger), - osInterfaceRef.overrideWith(() => operatingSystemInterface), - platformRef.overrideWith(() => platform), - processRef.overrideWith(() => shorebirdProcess), - shorebirdEnvRef.overrideWith(() => shorebirdEnv), - shorebirdFlutterRef.overrideWith(() => shorebirdFlutter), - shorebirdValidatorRef.overrideWith(() => shorebirdValidator), - xcodeBuildRef.overrideWith(() => xcodeBuild), - }, - ); - } - - void setUpProjectRoot() { - File( - p.join(projectRoot.path, 'pubspec.yaml'), - ).writeAsStringSync(pubspecYamlContent); - File( - p.join(projectRoot.path, 'shorebird.yaml'), - ).writeAsStringSync('app_id: $appId'); - File( - p.join( - projectRoot.path, - 'build', - 'ios', - 'archive', - 'Runner.xcarchive', - 'Info.plist', - ), - ) - ..createSync(recursive: true) - ..writeAsStringSync(infoPlistContent); - Directory( - p.join( - projectRoot.path, - 'build', - 'ios', - 'archive', - 'Runner.xcarchive', - 'Products', - 'Applications', - 'Runner.app', - ), - ).createSync(recursive: true); - File(p.join(projectRoot.path, ipaPath)).createSync(recursive: true); - } - - setUpAll(() { - registerFallbackValue(File('')); - registerFallbackValue(ReleasePlatform.ios); - registerFallbackValue(ReleaseStatus.draft); - registerFallbackValue(FakeRelease()); - registerFallbackValue(FakeShorebirdProcess()); - }); - - setUp(() { - argResults = MockArgResults(); - codePushClientWrapper = MockCodePushClientWrapper(); - doctor = MockDoctor(); - platform = MockPlatform(); - shorebirdRoot = Directory.systemTemp.createTempSync(); - projectRoot = Directory.systemTemp.createTempSync(); - auth = MockAuth(); - operatingSystemInterface = MockOperatingSystemInterface(); - progress = MockProgress(); - logger = MockShorebirdLogger(); - ios = MockIos(); - flutterBuildProcessResult = MockProcessResult(); - flutterPubGetProcessResult = MockProcessResult(); - flutterValidator = MockShorebirdFlutterValidator(); - shorebirdProcess = MockShorebirdProcess(); - shorebirdEnv = MockShorebirdEnv(); - shorebirdFlutter = MockShorebirdFlutter(); - shorebirdValidator = MockShorebirdValidator(); - xcodeBuild = MockXcodeBuild(); - - when(() => shorebirdEnv.getShorebirdYaml()).thenReturn(shorebirdYaml); - when(() => shorebirdEnv.shorebirdRoot).thenReturn(shorebirdRoot); - when( - () => shorebirdEnv.getShorebirdProjectRoot(), - ).thenReturn(projectRoot); - when(() => shorebirdEnv.flutterRevision).thenReturn(flutterRevision); - when(() => shorebirdEnv.canAcceptUserInput).thenReturn(true); - when( - () => shorebirdEnv.copyWith( - flutterRevisionOverride: any(named: 'flutterRevisionOverride'), - ), - ).thenAnswer((invocation) { - when(() => shorebirdEnv.flutterRevision).thenReturn( - invocation.namedArguments[#flutterRevisionOverride] as String, - ); - return shorebirdEnv; - }); - when( - () => shorebirdFlutter.getVersionAndRevision(), - ).thenAnswer((_) async => flutterVersionAndRevision); - when( - () => shorebirdFlutter.installRevision( - revision: any(named: 'revision'), - ), - ).thenAnswer((_) async => {}); - when( - () => shorebirdProcess.run( - 'flutter', - ['--no-version-check', 'pub', 'get', '--offline'], - runInShell: any(named: 'runInShell'), - useVendedFlutter: false, - ), - ).thenAnswer((_) async => flutterPubGetProcessResult); - when( - () => shorebirdProcess.run( - 'flutter', - any(), - runInShell: any(named: 'runInShell'), - ), - ).thenAnswer((_) async => flutterBuildProcessResult); - when(() => argResults['arch']).thenReturn(arch); - when(() => argResults['codesign']).thenReturn(true); - when(() => argResults['platform']).thenReturn(releasePlatform); - when(() => argResults.rest).thenReturn([]); - when(() => argResults.wasParsed(any())).thenReturn(true); - when(() => auth.isAuthenticated).thenReturn(true); - when(() => logger.progress(any())).thenReturn(progress); - when(() => logger.confirm(any())).thenReturn(true); - when( - () => logger.prompt(any(), defaultValue: any(named: 'defaultValue')), - ).thenReturn(version); - when(() => ios.exportOptionsPlistFromArgs(argResults)).thenReturn( - File('.'), - ); - when( - () => operatingSystemInterface.which('flutter'), - ).thenReturn('/path/to/flutter'); - when(() => platform.operatingSystem).thenReturn(operatingSystem); - when(() => platform.operatingSystemVersion) - .thenReturn(operatingSystemVersion); - when( - () => flutterBuildProcessResult.exitCode, - ).thenReturn(ExitCode.success.code); - when( - () => flutterPubGetProcessResult.exitCode, - ).thenReturn(ExitCode.success.code); - when( - () => codePushClientWrapper.getApp(appId: any(named: 'appId')), - ).thenAnswer((_) async => appMetadata); - when( - () => codePushClientWrapper.maybeGetRelease( - appId: any(named: 'appId'), - releaseVersion: any(named: 'releaseVersion'), - ), - ).thenAnswer((_) async => null); - when( - () => codePushClientWrapper.ensureReleaseIsNotActive( - release: any(named: 'release'), - platform: any(named: 'platform'), - ), - ).thenAnswer((_) async => {}); - when( - () => codePushClientWrapper.createRelease( - appId: any(named: 'appId'), - version: any(named: 'version'), - flutterRevision: any(named: 'flutterRevision'), - platform: any(named: 'platform'), - ), - ).thenAnswer((_) async => release); - when( - () => codePushClientWrapper.createIosReleaseArtifacts( - appId: any(named: 'appId'), - releaseId: any(named: 'releaseId'), - xcarchivePath: any(named: 'xcarchivePath'), - runnerPath: any(named: 'runnerPath'), - isCodesigned: any(named: 'isCodesigned'), - ), - ).thenAnswer((_) async => release); - when( - () => codePushClientWrapper.updateReleaseStatus( - appId: any(named: 'appId'), - releaseId: any(named: 'releaseId'), - platform: any(named: 'platform'), - status: any(named: 'status'), - metadata: any(named: 'metadata'), - ), - ).thenAnswer((_) async => {}); - - when(() => doctor.iosCommandValidators).thenReturn([flutterValidator]); - when( - () => shorebirdValidator.validatePreconditions( - checkUserIsAuthenticated: any(named: 'checkUserIsAuthenticated'), - checkShorebirdInitialized: any(named: 'checkShorebirdInitialized'), - validators: any(named: 'validators'), - supportedOperatingSystems: any(named: 'supportedOperatingSystems'), - ), - ).thenAnswer((_) async {}); - when(() => xcodeBuild.version()).thenAnswer((_) async => xcodeVersion); - - command = runWithOverrides(ReleaseIosCommand.new) - ..testArgResults = argResults; - }); - - test('supports alpha alias', () { - expect(command.aliases, contains('ios-alpha')); - }); - - test('has a description', () { - expect(command.description, isNotEmpty); - }); - - test('exits when validation fails', () async { - final exception = ValidationFailedException(); - when( - () => shorebirdValidator.validatePreconditions( - checkUserIsAuthenticated: any(named: 'checkUserIsAuthenticated'), - checkShorebirdInitialized: any(named: 'checkShorebirdInitialized'), - validators: any(named: 'validators'), - supportedOperatingSystems: any(named: 'supportedOperatingSystems'), - ), - ).thenThrow(exception); - await expectLater( - runWithOverrides(command.run), - completion(equals(exception.exitCode.code)), - ); - verify( - () => shorebirdValidator.validatePreconditions( - checkUserIsAuthenticated: true, - checkShorebirdInitialized: true, - validators: any(named: 'validators'), - supportedOperatingSystems: {Platform.macOS}, - ), - ).called(1); - }); - - group('when obfuscate flag is passed', () { - setUp(() { - when(() => argResults.rest).thenReturn(['--obfuscate']); - }); - - test('prints error and exits with usage code', () async { - final exitCode = await runWithOverrides(command.run); - - expect(exitCode, equals(ExitCode.usage.code)); - verify( - () => logger.err( - 'Shorebird does not currently support obfuscation on iOS.', - ), - ).called(1); - }); - }); - - group('when codesign is disabled', () { - setUp(() { - when(() => argResults['codesign']).thenReturn(false); - }); - - test('prints instructions to manually codesign', () async { - setUpProjectRoot(); - await runWithOverrides(command.run); - - verify( - () => logger.info( - '''Building for device with codesigning disabled. You will have to manually codesign before deploying to device.''', - ), - ).called(1); - }); - - test('builds without codesigning', () async { - setUpProjectRoot(); - await runWithOverrides(command.run); - - verify( - () => shorebirdProcess.run( - 'flutter', - any( - that: containsAllInOrder( - [ - 'build', - 'ipa', - '--release', - '--no-codesign', - ], - ), - ), - runInShell: true, - ), - ).called(1); - }); - - group('when build directory has non-default structure', () { - test( - 'prints error and exits with code 70 if xcarchive does not exist', - () async { - setUpProjectRoot(); - Directory( - p.join( - projectRoot.path, - 'build', - 'ios', - 'archive', - 'Runner.xcarchive', - ), - ).deleteSync(recursive: true); - - final exitCode = await runWithOverrides(command.run); - - expect(exitCode, equals(ExitCode.software.code)); - verify( - () => logger.err('Unable to find .xcarchive directory'), - ).called(1); - }); - - test( - '''prints error and exits with code 70 if .app directory does not exist''', - () async { - setUpProjectRoot(); - Directory( - p.join( - projectRoot.path, - 'build', - 'ios', - 'archive', - 'Runner.xcarchive', - 'Products', - 'Applications', - ), - ).deleteSync(recursive: true); - - final exitCode = await runWithOverrides(command.run); - - expect(exitCode, equals(ExitCode.software.code)); - verify(() => logger.err('Unable to find .app directory')).called(1); - }); - - test( - '''finds .xcarchive and .app when they do not have the default "Runner" name''', - () async { - setUpProjectRoot(); - final archivePath = p.join( - projectRoot.path, - 'build', - 'ios', - 'archive', - ); - final applicationsPath = p.join( - archivePath, - 'Runner.xcarchive', - 'Products', - 'Applications', - ); - Directory(p.join(applicationsPath, 'Runner.app')).renameSync( - p.join( - applicationsPath, - 'ؓوربيرد | Shorebird.app', - ), - ); - Directory(p.join(archivePath, 'Runner.xcarchive')).renameSync( - p.join( - archivePath, - 'ؓوربيرد | Shorebird.xcarchive', - ), - ); - - final exitCode = await runWithOverrides(command.run); - - expect(exitCode, equals(ExitCode.success.code)); - }); - }); - - test('prints archive upload instructions on success', () async { - setUpProjectRoot(); - final exitCode = await runWithOverrides(command.run); - - expect(exitCode, equals(ExitCode.success.code)); - final archivePath = p.join( - projectRoot.path, - 'build', - 'ios', - 'archive', - 'Runner.xcarchive', - ); - verify( - () => logger.info( - any( - that: stringContainsInOrder( - [ - 'Your next step is to submit the archive', - p.relative(archivePath), - 'to the App Store using Xcode.', - 'You can open the archive in Xcode by running', - 'open ${p.relative(archivePath)}', - '''Make sure to uncheck "Manage Version and Build Number", or else shorebird will not work.''', - ], - ), - ), - ), - ).called(1); - }); - - test('creates unsigned release artifacts', () async { - setUpProjectRoot(); - final exitCode = await runWithOverrides(command.run); - - expect(exitCode, equals(ExitCode.success.code)); - - verify( - () => codePushClientWrapper.createIosReleaseArtifacts( - appId: appId, - releaseId: release.id, - xcarchivePath: any( - named: 'xcarchivePath', - that: endsWith('.xcarchive'), - ), - runnerPath: - any(named: 'runnerPath', that: endsWith('Runner.app')), - isCodesigned: false, - ), - ).called(1); - }); - }); - - group('when exportOptionsPlistFromArgs throws exception', () { - setUp(() { - when(() => ios.exportOptionsPlistFromArgs(argResults)) - .thenThrow(ArgumentError('bad args')); - }); - - test('logs error and exits with usage code', () async { - setUpProjectRoot(); - final exitCode = await runWithOverrides(command.run); - - expect(exitCode, equals(ExitCode.usage.code)); - verify(() => logger.err('Invalid argument(s): bad args')).called(1); - }); - }); - - group('when flutter-version is provided', () { - const flutterVersion = '3.19.5'; - setUp(() { - when(() => argResults['flutter-version']).thenReturn(flutterVersion); - }); - - group('when unable to determine flutter revision', () { - final exception = Exception('oops'); - setUp(() { - when( - () => shorebirdFlutter.getRevisionForVersion(any()), - ).thenThrow(exception); - }); - - test('exits with code 70', () async { - final exitCode = await runWithOverrides(command.run); - expect(exitCode, equals(ExitCode.software.code)); - verify( - () => logger.err( - ''' -Unable to determine revision for Flutter version: $flutterVersion. -$exception''', - ), - ).called(1); - }); - }); - - group('when flutter version is too old', () { - setUp(() { - when(() => argResults['flutter-version']).thenReturn('3.16.3'); - }); - - test('prints error log and exits with code 64 (usage)', () async { - final exitCode = await runWithOverrides(command.run); - expect(exitCode, equals(ExitCode.usage.code)); - verify( - () => logger.err( - '''iOS releases are not supported with Flutter versions older than 3.19.5.''', - ), - ).called(1); - }); - }); - - group('when flutter version is not supported', () { - setUp(() { - when( - () => shorebirdFlutter.getRevisionForVersion(any()), - ).thenAnswer((_) async => null); - }); - - test('exits with code 70', () async { - final exitCode = await runWithOverrides(command.run); - expect(exitCode, equals(ExitCode.software.code)); - verify( - () => logger.err( - any(that: contains('Version $flutterVersion not found.')), - ), - ).called(1); - }); - }); - - group('when flutter version is supported', () { - const revision = '771d07b2cf'; - setUp(() { - setUpProjectRoot(); - when( - () => shorebirdFlutter.getRevisionForVersion(any()), - ).thenAnswer((_) async => revision); - }); - - test('uses specified flutter version to build', () async { - when( - () => shorebirdProcess.run( - 'flutter', - any(), - runInShell: any(named: 'runInShell'), - ), - ).thenAnswer((_) async { - // Ensure we're using the correct flutter version. - expect(shorebirdEnv.flutterRevision, equals(revision)); - return flutterBuildProcessResult; - }); - - await runWithOverrides(command.run); - - verify(() => shorebirdFlutter.installRevision(revision: revision)) - .called(1); - verify( - () => codePushClientWrapper.createRelease( - appId: appId, - version: version, - flutterRevision: revision, - platform: releasePlatform, - ), - ).called(1); - verify( - () => codePushClientWrapper.updateReleaseStatus( - appId: appId, - releaseId: release.id, - platform: releasePlatform, - status: ReleaseStatus.active, - metadata: const UpdateReleaseMetadata( - releasePlatform: releasePlatform, - flutterVersionOverride: flutterVersion, - generatedApks: false, - environment: BuildEnvironmentMetadata( - operatingSystem: operatingSystem, - operatingSystemVersion: operatingSystemVersion, - shorebirdVersion: packageVersion, - xcodeVersion: xcodeVersion, - ), - ), - ), - ).called(1); - }); - - group('when flutter version install fails', () { - setUp(() { - when( - () => shorebirdFlutter.installRevision( - revision: any(named: 'revision'), - ), - ).thenThrow(Exception('oops')); - }); - - test('exits with code 70', () async { - final result = await runWithOverrides(command.run); - - expect(result, equals(ExitCode.software.code)); - verify( - () => shorebirdFlutter.installRevision(revision: revision), - ).called(1); - }); - }); - }); - }); - - test('exits with code 70 when build fails with non-zero exit code', - () async { - when(() => flutterBuildProcessResult.exitCode).thenReturn(1); - when(() => flutterBuildProcessResult.stderr).thenReturn('oops'); - - setUpProjectRoot(); - final exitCode = await runWithOverrides(command.run); - - expect(exitCode, equals(ExitCode.software.code)); - verify( - () => progress.fail(any(that: contains('Failed to build'))), - ).called(1); - }); - - test('exits with code 70 when building fails with 0 exit code', () async { - when(() => flutterBuildProcessResult.exitCode).thenReturn(0); - when(() => flutterBuildProcessResult.stderr).thenReturn(''' -Encountered error while creating the IPA: -error: exportArchive: Communication with Apple failed -error: exportArchive: No signing certificate "iOS Distribution" found -error: exportArchive: Communication with Apple failed -error: exportArchive: No signing certificate "iOS Distribution" found -error: exportArchive: Team "My Team" does not have permission to create "iOS App Store" provisioning profiles. -error: exportArchive: No profiles for 'com.example.co' were found -error: exportArchive: Communication with Apple failed -error: exportArchive: No signing certificate "iOS Distribution" found -error: exportArchive: Communication with Apple failed -error: exportArchive: No signing certificate "iOS Distribution" found -error: exportArchive: Communication with Apple failed -error: exportArchive: No signing certificate "iOS Distribution" found -'''); - - setUpProjectRoot(); - final exitCode = await runWithOverrides(command.run); - - expect(exitCode, equals(ExitCode.software.code)); - verify( - () => progress.fail(any(that: contains('Failed to build'))), - ).called(1); - verify( - () => logger.err(''' - Communication with Apple failed - No signing certificate "iOS Distribution" found - Team "My Team" does not have permission to create "iOS App Store" provisioning profiles. - No profiles for 'com.example.co' were found'''), - ).called(1); - }); - - test('exits with code 70 when release version cannot be determined', - () async { - setUpProjectRoot(); - final file = File( - p.join( - projectRoot.path, - 'build', - 'ios', - 'archive', - 'Runner.xcarchive', - 'Info.plist', - ), - ) - ..createSync(recursive: true) - ..writeAsStringSync(emptyPlistContent); - final exitCode = await runWithOverrides(command.run); - - expect(exitCode, equals(ExitCode.software.code)); - verify( - () => logger.err( - 'Failed to determine release version from ${file.path}: ' - 'Exception: Could not determine release version', - ), - ).called(1); - }); - - test('aborts when user opts out', () async { - when(() => logger.confirm(any())).thenReturn(false); - setUpProjectRoot(); - - final exitCode = await runWithOverrides(command.run); - - expect(exitCode, ExitCode.success.code); - verify(() => logger.info('Aborting.')).called(1); - verifyNever( - () => codePushClientWrapper.createIosReleaseArtifacts( - appId: appId, - releaseId: release.id, - xcarchivePath: - any(named: 'xcarchivePath', that: endsWith('.xcarchive')), - runnerPath: any(named: 'runnerPath', that: endsWith('Runner.app')), - isCodesigned: any(named: 'isCodesigned'), - ), - ); - }); - - test('exits with code 70 if Info.plist does not exist', () async { - setUpProjectRoot(); - final infoPlistFile = File( - p.join( - projectRoot.path, - 'build', - 'ios', - 'archive', - 'Runner.xcarchive', - 'Info.plist', - ), - )..deleteSync(recursive: true); - - final exitCode = await runWithOverrides(command.run); - - expect(exitCode, equals(ExitCode.software.code)); - verify( - () => - logger.err('No Info.plist file found at ${infoPlistFile.path}.'), - ).called(1); - }); - - test('exits with code 70 if build directory does not exist', () async { - setUpProjectRoot(); - Directory(p.join(projectRoot.path, 'build')) - .deleteSync(recursive: true); - - final exitCode = await runWithOverrides(command.run); - - expect(exitCode, equals(ExitCode.software.code)); - verify(() => logger.err('Unable to find .xcarchive directory')) - .called(1); - }); - - test('exits with code 70 if ipa build directory does not exist', - () async { - setUpProjectRoot(); - final ipaDirectory = Directory( - p.join(projectRoot.path, 'build', 'ios', 'ipa'), - )..deleteSync(recursive: true); - - final exitCode = await runWithOverrides(command.run); - - expect(exitCode, equals(ExitCode.software.code)); - verify( - () => logger.err( - any( - that: stringContainsInOrder( - [ - 'Could not find ipa file', - 'No directory found at ${ipaDirectory.path}', - ], - ), - ), - ), - ).called(1); - }); - - test('exits with code 70 if ipa file does not exist', () async { - setUpProjectRoot(); - File(p.join(projectRoot.path, ipaPath)).deleteSync(recursive: true); - - final exitCode = await runWithOverrides(command.run); - - expect(exitCode, equals(ExitCode.software.code)); - verify( - () => logger.err( - any( - that: stringContainsInOrder([ - 'Could not find ipa file', - 'No .ipa files found in', - p.join('build', 'ios', 'ipa'), - ]), - ), - ), - ).called(1); - }); - - test('exits with code 70 if more than one ipa file is found', () async { - setUpProjectRoot(); - File( - p.join(projectRoot.path, 'build/ios/ipa/Runner2.ipa'), - ).createSync(recursive: true); - - final exitCode = await runWithOverrides(command.run); - - expect(exitCode, equals(ExitCode.software.code)); - verify( - () => logger.err( - any( - that: stringContainsInOrder([ - 'Could not find ipa file', - 'More than one .ipa file found in', - p.join('build', 'ios', 'ipa'), - ]), - ), - ), - ).called(1); - }); - - test('succeeds when release is successful', () async { - setUpProjectRoot(); - - final exitCode = await runWithOverrides(command.run); - - verify(() => logger.success('\nāœ… Published Release $version!')) - .called(1); - verify( - () => logger.info( - any( - that: stringContainsInOrder( - [ - 'Your next step is to upload your app to App Store Connect.', - p.join('build', 'ios', 'ipa', 'Runner.ipa'), - ], - ), - ), - ), - ).called(1); - verify( - () => codePushClientWrapper.createIosReleaseArtifacts( - appId: appId, - releaseId: release.id, - xcarchivePath: - any(named: 'xcarchivePath', that: endsWith('.xcarchive')), - runnerPath: any(named: 'runnerPath', that: endsWith('Runner.app')), - isCodesigned: true, - ), - ).called(1); - verify( - () => codePushClientWrapper.updateReleaseStatus( - appId: appId, - releaseId: release.id, - platform: releasePlatform, - status: ReleaseStatus.active, - metadata: const UpdateReleaseMetadata( - releasePlatform: releasePlatform, - flutterVersionOverride: null, - generatedApks: false, - environment: BuildEnvironmentMetadata( - operatingSystem: operatingSystem, - operatingSystemVersion: operatingSystemVersion, - shorebirdVersion: packageVersion, - xcodeVersion: xcodeVersion, - ), - ), - ), - ).called(1); - verify( - () => logger.info( - '''To create a patch for this release, run ${lightCyan.wrap('shorebird patch ios --release-version=${release.version}')}''', - ), - ).called(1); - verify( - () => logger.info( - ''' - -Note: ${lightCyan.wrap('shorebird patch ios')} without the --release-version option will patch the current version of the app. -''', - ), - ).called(1); - expect(exitCode, ExitCode.success.code); - }); - - test('runs flutter pub get with system flutter after successful build', - () async { - setUpProjectRoot(); - - await runWithOverrides(command.run); - - verify( - () => shorebirdProcess.run( - 'flutter', - ['--no-version-check', 'pub', 'get', '--offline'], - runInShell: any(named: 'runInShell'), - useVendedFlutter: false, - ), - ).called(1); - }); - - test( - 'succeeds when release is successful ' - 'with flavors and target', () async { - const flavor = 'development'; - final target = p.join('lib', 'main_development.dart'); - when(() => argResults['flavor']).thenReturn(flavor); - when(() => argResults['target']).thenReturn(target); - setUpProjectRoot(); - File( - p.join(projectRoot.path, 'shorebird.yaml'), - ).writeAsStringSync(''' -app_id: productionAppId -flavors: - development: $appId'''); - - final exitCode = await runWithOverrides(command.run); - - verify(() => logger.success('\nāœ… Published Release $version!')) - .called(1); - verify( - () => logger.info( - any( - that: stringContainsInOrder( - [ - 'Your next step is to upload your app to App Store Connect.', - p.join('build', 'ios', 'ipa', 'Runner.ipa'), - ], - ), - ), - ), - ).called(1); - verify( - () => codePushClientWrapper.createIosReleaseArtifacts( - appId: appId, - releaseId: release.id, - xcarchivePath: - any(named: 'xcarchivePath', that: endsWith('.xcarchive')), - runnerPath: any(named: 'runnerPath', that: endsWith('Runner.app')), - isCodesigned: true, - ), - ).called(1); - verify( - () => logger.info( - '''To create a patch for this release, run ${lightCyan.wrap('shorebird patch ios --flavor=$flavor --target=$target --release-version=${release.version}')}''', - ), - ).called(1); - verify( - () => logger.info( - ''' - -Note: ${lightCyan.wrap('shorebird patch ios --flavor=$flavor --target=$target')} without the --release-version option will patch the current version of the app. -''', - ), - ).called(1); - expect(exitCode, ExitCode.success.code); - }); - - test('does not create new release if existing release is present', - () async { - when( - () => codePushClientWrapper.maybeGetRelease( - appId: any(named: 'appId'), - releaseVersion: any(named: 'releaseVersion'), - ), - ).thenAnswer((_) async => release); - setUpProjectRoot(); - - final exitCode = await runWithOverrides(command.run); - - expect(exitCode, ExitCode.success.code); - verifyNever( - () => codePushClientWrapper.createRelease( - appId: any(named: 'appId'), - version: any(named: 'version'), - flutterRevision: any(named: 'flutterRevision'), - platform: any(named: 'platform'), - ), - ); - verify( - () => codePushClientWrapper.createIosReleaseArtifacts( - appId: appId, - releaseId: release.id, - xcarchivePath: - any(named: 'xcarchivePath', that: endsWith('.xcarchive')), - runnerPath: any(named: 'runnerPath', that: endsWith('Runner.app')), - isCodesigned: true, - ), - ).called(1); - verify( - () => codePushClientWrapper.updateReleaseStatus( - appId: appId, - releaseId: release.id, - platform: releasePlatform, - status: ReleaseStatus.active, - metadata: any(named: 'metadata'), - ), - ).called(1); - }); - - test( - 'does not upload artifacts if an existing release is present ' - 'with a different flutter revision', () async { - when( - () => codePushClientWrapper.maybeGetRelease( - appId: any(named: 'appId'), - releaseVersion: any(named: 'releaseVersion'), - ), - ).thenAnswer( - (_) async => Release( - id: release.id, - appId: release.appId, - version: release.version, - flutterRevision: 'different-revision', - displayName: release.displayName, - createdAt: release.createdAt.add(const Duration(minutes: 1)), - updatedAt: release.createdAt.add(const Duration(minutes: 1)), - platformStatuses: release.platformStatuses, - ), - ); - setUpProjectRoot(); - - final exitCode = await runWithOverrides(command.run); - expect(exitCode, ExitCode.software.code); - - verify( - () => logger.err( - any( - that: contains( - '''All platforms for a given release must be built using the same Flutter revision.''', - ), - ), - ), - ).called(1); - verifyNever( - () => codePushClientWrapper.createRelease( - appId: any(named: 'appId'), - version: any(named: 'version'), - flutterRevision: any(named: 'flutterRevision'), - platform: any(named: 'platform'), - ), - ); - verifyNever( - () => codePushClientWrapper.createIosReleaseArtifacts( - appId: any(named: 'appId'), - releaseId: any(named: 'releaseId'), - xcarchivePath: - any(named: 'xcarchivePath', that: endsWith('.xcarchive')), - runnerPath: any(named: 'runnerPath', that: endsWith('Runner.app')), - isCodesigned: any(named: 'isCodesigned'), - ), - ); - verifyNever( - () => codePushClientWrapper.updateReleaseStatus( - appId: any(named: 'appId'), - releaseId: any(named: 'releaseId'), - platform: any(named: 'platform'), - status: any(named: 'status'), - metadata: any(named: 'metadata'), - ), - ); - }); - - test('does not provide export options when codesign is false', () async { - when(() => argResults['codesign']).thenReturn(false); - setUpProjectRoot(); - - await runWithOverrides(command.run); - - final capturedArgs = verify( - () => shorebirdProcess.run( - 'flutter', - captureAny(), - runInShell: any(named: 'runInShell'), - ), - ).captured.first as List; - expect( - capturedArgs - .whereType() - .firstWhereOrNull((arg) => arg.contains('export-options-plist')), - isNull, - ); - }); - - test('does not prompt if unable to accept user input', () async { - when(() => shorebirdEnv.canAcceptUserInput).thenReturn(false); - setUpProjectRoot(); - - final exitCode = await runWithOverrides(command.run); - - expect(exitCode, equals(ExitCode.success.code)); - verifyNever(() => logger.confirm(any())); - }); - }, - testOn: 'mac-os', - ); -} diff --git a/packages/shorebird_cli/test/src/commands/release/release_ios_framework_command_test.dart b/packages/shorebird_cli/test/src/commands/release/release_ios_framework_command_test.dart deleted file mode 100644 index ae691d64..00000000 --- a/packages/shorebird_cli/test/src/commands/release/release_ios_framework_command_test.dart +++ /dev/null @@ -1,608 +0,0 @@ -import 'dart:io' hide Platform; - -import 'package:args/args.dart'; -import 'package:mason_logger/mason_logger.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; -import 'package:scoped/scoped.dart'; -import 'package:shorebird_cli/src/auth/auth.dart'; -import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; -import 'package:shorebird_cli/src/commands/release/release.dart'; -import 'package:shorebird_cli/src/config/config.dart'; -import 'package:shorebird_cli/src/doctor.dart'; -import 'package:shorebird_cli/src/executables/xcodebuild.dart'; -import 'package:shorebird_cli/src/logger.dart'; -import 'package:shorebird_cli/src/os/operating_system_interface.dart'; -import 'package:shorebird_cli/src/platform.dart'; -import 'package:shorebird_cli/src/shorebird_env.dart'; -import 'package:shorebird_cli/src/shorebird_flutter.dart'; -import 'package:shorebird_cli/src/shorebird_process.dart'; -import 'package:shorebird_cli/src/shorebird_validator.dart'; -import 'package:shorebird_cli/src/validators/validators.dart'; -import 'package:shorebird_cli/src/version.dart'; -import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; -import 'package:test/test.dart'; - -import '../../fakes.dart'; -import '../../mocks.dart'; - -void main() { - group( - ReleaseIosFrameworkCommand, - () { - const appId = 'test-app-id'; - const shorebirdYaml = ShorebirdYaml(appId: appId); - const flutterRevision = '83305b5088e6fe327fb3334a73ff190828d85713'; - const flutterVersionAndRevision = '3.10.6 (83305b5088)'; - const versionName = '1.2.3'; - const versionCode = '1'; - const version = '$versionName+$versionCode'; - const operatingSystem = 'macOS'; - const operatingSystemVersion = '11.0.0'; - const xcodeVersion = '12.0'; - const appDisplayName = 'Test App'; - const releasePlatform = ReleasePlatform.ios; - final appMetadata = AppMetadata( - appId: appId, - displayName: appDisplayName, - createdAt: DateTime(2023), - updatedAt: DateTime(2023), - ); - final release = Release( - id: 0, - appId: appId, - version: version, - flutterRevision: flutterRevision, - displayName: '1.2.3+1', - platformStatuses: {}, - createdAt: DateTime(2023), - updatedAt: DateTime(2023), - ); - const pubspecYamlContent = ''' -name: example -version: $version -environment: - sdk: ">=2.19.0 <3.0.0" - -flutter: - assets: - - shorebird.yaml'''; - - late ArgResults argResults; - late CodePushClientWrapper codePushClientWrapper; - late Directory shorebirdRoot; - late Directory projectRoot; - late Doctor doctor; - late Platform platform; - late Auth auth; - late Progress progress; - late ShorebirdLogger logger; - late OperatingSystemInterface operatingSystemInterface; - late ShorebirdProcessResult flutterBuildProcessResult; - late ShorebirdProcessResult flutterPubGetProcessResult; - late ShorebirdFlutterValidator flutterValidator; - late ShorebirdProcess shorebirdProcess; - late ShorebirdEnv shorebirdEnv; - late ShorebirdFlutter shorebirdFlutter; - late ShorebirdValidator shorebirdValidator; - late XcodeBuild xcodeBuild; - late ReleaseIosFrameworkCommand command; - - R runWithOverrides(R Function() body) { - return runScoped( - body, - values: { - authRef.overrideWith(() => auth), - codePushClientWrapperRef.overrideWith(() => codePushClientWrapper), - doctorRef.overrideWith(() => doctor), - loggerRef.overrideWith(() => logger), - osInterfaceRef.overrideWith(() => operatingSystemInterface), - platformRef.overrideWith(() => platform), - processRef.overrideWith(() => shorebirdProcess), - shorebirdEnvRef.overrideWith(() => shorebirdEnv), - shorebirdFlutterRef.overrideWith(() => shorebirdFlutter), - shorebirdValidatorRef.overrideWith(() => shorebirdValidator), - xcodeBuildRef.overrideWith(() => xcodeBuild), - }, - ); - } - - void setUpProjectRoot() { - File( - p.join(projectRoot.path, 'pubspec.yaml'), - ).writeAsStringSync(pubspecYamlContent); - File( - p.join(projectRoot.path, 'shorebird.yaml'), - ).writeAsStringSync('app_id: $appId'); - // Create an xcframework in the release directory to simulate running - // this command a subsequent time. - Directory(p.join(projectRoot.path, 'release', 'Flutter.xcframework')) - .createSync(recursive: true); - Directory( - p.join( - projectRoot.path, - 'build', - 'ios', - 'framework', - 'Release', - 'Flutter.xcframework', - ), - ).createSync(recursive: true); - } - - setUpAll(() { - registerFallbackValue(Directory('')); - registerFallbackValue(ReleasePlatform.ios); - registerFallbackValue(ReleaseStatus.draft); - registerFallbackValue(FakeRelease()); - registerFallbackValue(FakeShorebirdProcess()); - }); - - setUp(() { - argResults = MockArgResults(); - codePushClientWrapper = MockCodePushClientWrapper(); - doctor = MockDoctor(); - platform = MockPlatform(); - shorebirdRoot = Directory.systemTemp.createTempSync(); - projectRoot = Directory.systemTemp.createTempSync(); - auth = MockAuth(); - progress = MockProgress(); - logger = MockShorebirdLogger(); - operatingSystemInterface = MockOperatingSystemInterface(); - flutterBuildProcessResult = MockProcessResult(); - flutterPubGetProcessResult = MockProcessResult(); - flutterValidator = MockShorebirdFlutterValidator(); - shorebirdProcess = MockShorebirdProcess(); - shorebirdEnv = MockShorebirdEnv(); - shorebirdFlutter = MockShorebirdFlutter(); - shorebirdValidator = MockShorebirdValidator(); - xcodeBuild = MockXcodeBuild(); - - when(() => shorebirdEnv.getShorebirdYaml()).thenReturn(shorebirdYaml); - when(() => shorebirdEnv.shorebirdRoot).thenReturn(shorebirdRoot); - when( - () => shorebirdEnv.copyWith( - flutterRevisionOverride: any(named: 'flutterRevisionOverride'), - ), - ).thenAnswer((invocation) { - when(() => shorebirdEnv.flutterRevision).thenReturn( - invocation.namedArguments[#flutterRevisionOverride] as String, - ); - return shorebirdEnv; - }); - when( - () => shorebirdEnv.getShorebirdProjectRoot(), - ).thenReturn(projectRoot); - when(() => shorebirdEnv.canAcceptUserInput).thenReturn(true); - when(() => shorebirdEnv.flutterRevision).thenReturn(flutterRevision); - when( - () => shorebirdFlutter.getVersionAndRevision(), - ).thenAnswer((_) async => flutterVersionAndRevision); - when( - () => shorebirdFlutter.installRevision( - revision: any(named: 'revision'), - ), - ).thenAnswer((_) async => {}); - when( - () => shorebirdProcess.run( - 'flutter', - ['--no-version-check', 'pub', 'get', '--offline'], - runInShell: any(named: 'runInShell'), - useVendedFlutter: false, - ), - ).thenAnswer((_) async => flutterPubGetProcessResult); - when( - () => shorebirdProcess.run( - 'flutter', - any(), - runInShell: any(named: 'runInShell'), - ), - ).thenAnswer((_) async => flutterBuildProcessResult); - when(() => argResults['release-version']).thenReturn(version); - when(() => argResults.rest).thenReturn([]); - when(() => auth.isAuthenticated).thenReturn(true); - when(() => doctor.iosCommandValidators).thenReturn([flutterValidator]); - when( - () => flutterBuildProcessResult.exitCode, - ).thenReturn(ExitCode.success.code); - when(() => flutterPubGetProcessResult.exitCode) - .thenReturn(ExitCode.success.code); - when(() => logger.progress(any())).thenReturn(progress); - when(() => logger.confirm(any())).thenReturn(true); - when(() => operatingSystemInterface.which('flutter')) - .thenReturn('/path/to/flutter'); - when(() => platform.operatingSystem).thenReturn(operatingSystem); - when(() => platform.operatingSystemVersion) - .thenReturn(operatingSystemVersion); - when( - () => codePushClientWrapper.getApp(appId: any(named: 'appId')), - ).thenAnswer((_) async => appMetadata); - when( - () => codePushClientWrapper.createIosFrameworkReleaseArtifacts( - appId: any(named: 'appId'), - releaseId: any(named: 'releaseId'), - appFrameworkPath: any(named: 'appFrameworkPath'), - ), - ).thenAnswer((_) async => {}); - when( - () => codePushClientWrapper.createRelease( - appId: any(named: 'appId'), - version: any(named: 'version'), - flutterRevision: any(named: 'flutterRevision'), - platform: any(named: 'platform'), - ), - ).thenAnswer((_) async => release); - when( - () => codePushClientWrapper.maybeGetRelease( - appId: any(named: 'appId'), - releaseVersion: any(named: 'releaseVersion'), - ), - ).thenAnswer((_) async => null); - when( - () => codePushClientWrapper.ensureReleaseIsNotActive( - release: any(named: 'release'), - platform: any(named: 'platform'), - ), - ).thenAnswer((_) async => {}); - when( - () => codePushClientWrapper.updateReleaseStatus( - appId: any(named: 'appId'), - releaseId: any(named: 'releaseId'), - platform: any(named: 'platform'), - status: any(named: 'status'), - metadata: any(named: 'metadata'), - ), - ).thenAnswer((_) async => {}); - when( - () => shorebirdValidator.validatePreconditions( - checkUserIsAuthenticated: any(named: 'checkUserIsAuthenticated'), - checkShorebirdInitialized: any(named: 'checkShorebirdInitialized'), - validators: any(named: 'validators'), - supportedOperatingSystems: any(named: 'supportedOperatingSystems'), - ), - ).thenAnswer((_) async {}); - when(() => xcodeBuild.version()).thenAnswer((_) async => xcodeVersion); - - command = runWithOverrides(ReleaseIosFrameworkCommand.new) - ..testArgResults = argResults; - }); - - test('supports alpha alias', () { - expect(command.aliases, contains('ios-framework-alpha')); - }); - - test('has a description', () { - expect(command.description, isNotEmpty); - }); - - test('exits when validation fails', () async { - final exception = ValidationFailedException(); - when( - () => shorebirdValidator.validatePreconditions( - checkUserIsAuthenticated: any(named: 'checkUserIsAuthenticated'), - checkShorebirdInitialized: any(named: 'checkShorebirdInitialized'), - validators: any(named: 'validators'), - supportedOperatingSystems: any(named: 'supportedOperatingSystems'), - ), - ).thenThrow(exception); - await expectLater( - runWithOverrides(command.run), - completion(equals(exception.exitCode.code)), - ); - verify( - () => shorebirdValidator.validatePreconditions( - checkUserIsAuthenticated: true, - checkShorebirdInitialized: true, - validators: any(named: 'validators'), - supportedOperatingSystems: {Platform.macOS}, - ), - ).called(1); - }); - - group('when flutter-version is provided', () { - const flutterVersion = '3.19.5'; - setUp(() { - when(() => argResults['flutter-version']).thenReturn(flutterVersion); - }); - - group('when unable to determine flutter revision', () { - final exception = Exception('oops'); - setUp(() { - when( - () => shorebirdFlutter.getRevisionForVersion(any()), - ).thenThrow(exception); - }); - - test('exits with code 70', () async { - final exitCode = await runWithOverrides(command.run); - expect(exitCode, equals(ExitCode.software.code)); - verify( - () => logger.err( - ''' -Unable to determine revision for Flutter version: $flutterVersion. -$exception''', - ), - ).called(1); - }); - }); - - group('when flutter version is too old', () { - setUp(() { - when(() => argResults['flutter-version']).thenReturn('3.16.3'); - }); - - test('prints error log and exits with code 64 (usage)', () async { - final exitCode = await runWithOverrides(command.run); - expect(exitCode, equals(ExitCode.usage.code)); - verify( - () => logger.err( - '''iOS releases are not supported with Flutter versions older than 3.19.5.''', - ), - ).called(1); - }); - }); - - group('when flutter version is not supported', () { - setUp(() { - when( - () => shorebirdFlutter.getRevisionForVersion(any()), - ).thenAnswer((_) async => null); - }); - - test('exits with code 70', () async { - final exitCode = await runWithOverrides(command.run); - expect(exitCode, equals(ExitCode.software.code)); - verify( - () => logger.err( - any(that: contains('Version $flutterVersion not found.')), - ), - ).called(1); - }); - }); - - group('when flutter version is supported', () { - const revision = '771d07b2cf'; - setUp(() { - when( - () => shorebirdFlutter.getRevisionForVersion(any()), - ).thenAnswer((_) async => revision); - }); - - test('uses specified flutter version build', () async { - when( - () => shorebirdProcess.run( - 'flutter', - any(), - runInShell: any(named: 'runInShell'), - ), - ).thenAnswer((_) async { - // Ensure we're using the correct flutter version. - expect(shorebirdEnv.flutterRevision, equals(revision)); - return flutterBuildProcessResult; - }); - - setUpProjectRoot(); - - await runWithOverrides(command.run); - - verify(() => shorebirdFlutter.installRevision(revision: revision)) - .called(1); - verify( - () => codePushClientWrapper.createRelease( - appId: appId, - version: version, - flutterRevision: revision, - platform: releasePlatform, - ), - ).called(1); - verify( - () => codePushClientWrapper.updateReleaseStatus( - appId: appId, - releaseId: release.id, - platform: releasePlatform, - status: ReleaseStatus.active, - metadata: const UpdateReleaseMetadata( - releasePlatform: releasePlatform, - flutterVersionOverride: flutterVersion, - generatedApks: false, - environment: BuildEnvironmentMetadata( - operatingSystem: operatingSystem, - operatingSystemVersion: operatingSystemVersion, - shorebirdVersion: packageVersion, - xcodeVersion: xcodeVersion, - ), - ), - ), - ).called(1); - }); - - group('when flutter version install fails', () { - setUp(() { - when( - () => shorebirdFlutter.installRevision( - revision: any(named: 'revision'), - ), - ).thenThrow(Exception('oops')); - }); - - test('exits with code 70', () async { - setUpProjectRoot(); - final result = await runWithOverrides(command.run); - - expect(result, equals(ExitCode.software.code)); - verify( - () => shorebirdFlutter.installRevision(revision: revision), - ).called(1); - }); - }); - }); - }); - - test('exits with code 70 when build fails with non-zero exit code', - () async { - when(() => flutterBuildProcessResult.exitCode).thenReturn(1); - when(() => flutterBuildProcessResult.stderr).thenReturn('oops'); - - setUpProjectRoot(); - final exitCode = await runWithOverrides(command.run); - - expect(exitCode, equals(ExitCode.software.code)); - verify( - () => progress.fail(any(that: contains('Failed to build'))), - ).called(1); - }); - - test('checks that release is not active if release exists', () async { - when( - () => codePushClientWrapper.maybeGetRelease( - appId: any(named: 'appId'), - releaseVersion: any(named: 'releaseVersion'), - ), - ).thenAnswer((_) async => release); - setUpProjectRoot(); - - await runWithOverrides(command.run); - - verify( - () => codePushClientWrapper.ensureReleaseIsNotActive( - release: release, - platform: releasePlatform, - ), - ).called(1); - }); - - test('aborts when user opts out', () async { - when(() => logger.confirm(any())).thenReturn(false); - setUpProjectRoot(); - - final exitCode = await runWithOverrides(command.run); - - expect(exitCode, ExitCode.success.code); - verify(() => logger.info('Aborting.')).called(1); - verifyNever( - () => codePushClientWrapper.createIosReleaseArtifacts( - appId: appId, - releaseId: release.id, - xcarchivePath: any( - named: 'xcarchivePath', - that: endsWith('.xcarchive'), - ), - runnerPath: any(named: 'runnerPath', that: endsWith('Runner.app')), - isCodesigned: any(named: 'isCodesigned'), - ), - ); - }); - - test('does not prompt for confirmation if unable to accept user input', - () async { - when(() => shorebirdEnv.canAcceptUserInput).thenReturn(false); - when(() => argResults['release-version']).thenReturn(version); - setUpProjectRoot(); - - final exitCode = await runWithOverrides(command.run); - - verify(() => logger.success('\nāœ… Published Release $version!')) - .called(1); - expect(exitCode, ExitCode.success.code); - verifyNever( - () => logger.prompt(any(), defaultValue: any(named: 'defaultValue')), - ); - verify( - () => codePushClientWrapper.updateReleaseStatus( - appId: appId, - releaseId: release.id, - platform: releasePlatform, - status: ReleaseStatus.active, - metadata: any(named: 'metadata'), - ), - ).called(1); - }); - - test('succeeds when release is successful', () async { - setUpProjectRoot(); - - final exitCode = await runWithOverrides(command.run); - - verify(() => logger.success('\nāœ… Published Release $version!')) - .called(1); - verify( - () => logger.info( - any( - that: stringContainsInOrder( - [ - 'Your next step is to add the .xcframework files found in', - 'release', - 'to your iOS app.', - '''Embed the App.xcframework and ShorebirdFlutter.framework in your Xcode project''', - ], - ), - ), - ), - ).called(1); - verify( - () => codePushClientWrapper.createIosFrameworkReleaseArtifacts( - appId: appId, - releaseId: release.id, - appFrameworkPath: any( - named: 'appFrameworkPath', - that: endsWith( - p.join('release', 'App.xcframework'), - ), - ), - ), - ).called(1); - verify( - () => codePushClientWrapper.updateReleaseStatus( - appId: appId, - releaseId: release.id, - platform: releasePlatform, - status: ReleaseStatus.active, - metadata: const UpdateReleaseMetadata( - releasePlatform: releasePlatform, - flutterVersionOverride: null, - generatedApks: false, - environment: BuildEnvironmentMetadata( - operatingSystem: operatingSystem, - operatingSystemVersion: operatingSystemVersion, - shorebirdVersion: packageVersion, - xcodeVersion: xcodeVersion, - ), - ), - ), - ).called(1); - verify( - () => logger.info( - '''To create a patch for this release, run ${lightCyan.wrap('shorebird patch ios-framework --release-version=${release.version}')}''', - ), - ).called(1); - verifyNever( - () => logger.info( - ''' - -Note: ${lightCyan.wrap('shorebird patch ios-framework')} without the --release-version option will patch the current version of the app. -''', - ), - ); - expect(exitCode, ExitCode.success.code); - }); - - test('runs flutter pub get with system flutter after successful build', - () async { - setUpProjectRoot(); - - await runWithOverrides(command.run); - - verify( - () => shorebirdProcess.run( - 'flutter', - ['--no-version-check', 'pub', 'get', '--offline'], - runInShell: any(named: 'runInShell'), - useVendedFlutter: false, - ), - ).called(1); - }); - }, - testOn: 'mac-os', - ); -} diff --git a/packages/shorebird_cli/test/src/commands/release_new/releaser_test.dart b/packages/shorebird_cli/test/src/commands/release/releaser_test.dart similarity index 95% rename from packages/shorebird_cli/test/src/commands/release_new/releaser_test.dart rename to packages/shorebird_cli/test/src/commands/release/releaser_test.dart index 8b59c682..45680a3b 100644 --- a/packages/shorebird_cli/test/src/commands/release_new/releaser_test.dart +++ b/packages/shorebird_cli/test/src/commands/release/releaser_test.dart @@ -1,6 +1,6 @@ import 'dart:io'; -import 'package:shorebird_cli/src/commands/release_new/releaser.dart'; +import 'package:shorebird_cli/src/commands/release/releaser.dart'; import 'package:shorebird_cli/src/release_type.dart'; import 'package:shorebird_code_push_protocol/src/models/release.dart'; import 'package:shorebird_code_push_protocol/src/models/update_release_metadata.dart'; diff --git a/packages/shorebird_cli/test/src/config/shorebird_yaml_test.dart b/packages/shorebird_cli/test/src/config/shorebird_yaml_test.dart index 75bec973..0d56e354 100644 --- a/packages/shorebird_cli/test/src/config/shorebird_yaml_test.dart +++ b/packages/shorebird_cli/test/src/config/shorebird_yaml_test.dart @@ -66,5 +66,37 @@ auto_update: true expect(shorebirdYaml.baseUrl, isNull); expect(shorebirdYaml.autoUpdate, isTrue); }); + + group('AppIdExtension', () { + test('getAppId returns base app id when no flavor is provided', () { + const shorebirdYaml = ShorebirdYaml( + appId: 'test_app_id', + ); + expect(shorebirdYaml.getAppId(), 'test_app_id'); + }); + + test('getAppId returns base app id when flavor is not found', () { + const shorebirdYaml = ShorebirdYaml( + appId: 'test_app_id', + flavors: { + 'development': 'test_app_id1', + 'production': 'test_app_id2', + }, + ); + expect(shorebirdYaml.getAppId(flavor: 'staging'), 'test_app_id'); + }); + + test('getAppId returns app id for flavor', () { + const shorebirdYaml = ShorebirdYaml( + appId: 'test_app_id', + flavors: { + 'development': 'test_app_id1', + 'production': 'test_app_id2', + }, + ); + expect(shorebirdYaml.getAppId(flavor: 'development'), 'test_app_id1'); + expect(shorebirdYaml.getAppId(flavor: 'production'), 'test_app_id2'); + }); + }); }); } diff --git a/packages/shorebird_cli/test/src/mocks.dart b/packages/shorebird_cli/test/src/mocks.dart index 495b9285..b4864a1d 100644 --- a/packages/shorebird_cli/test/src/mocks.dart +++ b/packages/shorebird_cli/test/src/mocks.dart @@ -17,7 +17,7 @@ import 'package:shorebird_cli/src/auth/auth.dart'; import 'package:shorebird_cli/src/cache.dart' show Cache; import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; import 'package:shorebird_cli/src/commands/patch_new/patch_new.dart'; -import 'package:shorebird_cli/src/commands/release_new/releaser.dart'; +import 'package:shorebird_cli/src/commands/release/releaser.dart'; import 'package:shorebird_cli/src/config/config.dart'; import 'package:shorebird_cli/src/doctor.dart'; import 'package:shorebird_cli/src/engine_config.dart';