refactor(shorebird_cli): remove old release commands (#2051)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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<int> {
|
||||
addCommand(PatchNewCommand());
|
||||
addCommand(PreviewCommand());
|
||||
addCommand(ReleaseCommand());
|
||||
addCommand(ReleaseNewCommand());
|
||||
addCommand(RunCommand());
|
||||
addCommand(UpgradeCommand());
|
||||
}
|
||||
|
||||
+1
-1
@@ -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';
|
||||
+2
-2
@@ -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';
|
||||
+1
-1
@@ -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';
|
||||
+13
-1
@@ -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
|
||||
@@ -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';
|
||||
|
||||
@@ -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<int> 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<String>)
|
||||
.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),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<int> 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<String>)
|
||||
.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),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<int> 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<void> 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<String> 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<void> 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<void> 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<Release> 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<void> 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<void> 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(
|
||||
'''
|
||||
|
||||
|
||||
@@ -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<String> 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<int> 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),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<String> get aliases => ['ios-framework-alpha'];
|
||||
|
||||
@override
|
||||
String get description =>
|
||||
'Builds and submits your iOS framework to Shorebird.';
|
||||
|
||||
@override
|
||||
Future<int> 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),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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<int> 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<void> 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<String> 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<void> 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<void> 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<Release> 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<void> 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<void> 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.
|
||||
''',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<File>()
|
||||
.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
|
||||
|
||||
@@ -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<void> 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
|
||||
|
||||
@@ -272,5 +272,6 @@ void main() {
|
||||
});
|
||||
});
|
||||
},
|
||||
testOn: 'mac-os',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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', () {
|
||||
|
||||
+1
-1
@@ -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';
|
||||
+1
-1
@@ -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';
|
||||
+1
-1
@@ -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';
|
||||
+31
-1
@@ -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', () {
|
||||
@@ -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>(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);
|
||||
});
|
||||
});
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+4
-4
@@ -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>(R Function() body) {
|
||||
return runScoped(
|
||||
@@ -174,7 +174,7 @@ void main() {
|
||||
),
|
||||
).thenAnswer((_) async => {});
|
||||
|
||||
command = ReleaseNewCommand(resolveReleaser: (_) => releaser)
|
||||
command = ReleaseCommand(resolveReleaser: (_) => releaser)
|
||||
..testArgResults = argResults;
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
-608
@@ -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>(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',
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -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';
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user