feat(shorebird_cli): add shorebird release (#188)

This commit is contained in:
Felix Angelov
2023-03-28 15:58:29 -05:00
committed by GitHub
parent d3db325d9c
commit c137744d28
7 changed files with 715 additions and 66 deletions
@@ -8,6 +8,9 @@ import 'package:shorebird_cli/src/auth/auth.dart';
import 'package:shorebird_cli/src/command_runner.dart';
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
/// Signature for a function which takes a list of bytes and returns a hash.
typedef HashFunction = String Function(List<int> bytes);
typedef CodePushClientBuilder = CodePushClient Function({
required String apiKey,
Uri? hostedUri,
@@ -52,6 +52,7 @@ class ShorebirdCliCommandRunner extends CompletionCommandRunner<int> {
addCommand(LoginCommand(logger: _logger));
addCommand(LogoutCommand(logger: _logger));
addCommand(PublishCommand(logger: _logger));
addCommand(ReleaseCommand(logger: _logger));
addCommand(RunCommand(logger: _logger));
addCommand(UpgradeCommand(logger: _logger));
}
@@ -4,5 +4,6 @@ export 'init_command.dart';
export 'login_command.dart';
export 'logout_command.dart';
export 'publish_command.dart';
export 'release_command.dart';
export 'run_command.dart';
export 'upgrade_command.dart';
@@ -11,9 +11,6 @@ import 'package:shorebird_cli/src/shorebird_create_app_mixin.dart';
import 'package:shorebird_cli/src/shorebird_engine_mixin.dart';
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
/// Signature for a function which takes a list of bytes and returns a hash.
typedef HashFunction = String Function(List<int> bytes);
/// {@template publish_command}
///
/// `shorebird publish <path/to/artifact>`
@@ -120,26 +117,14 @@ class PublishCommand extends ShorebirdCommand
return ExitCode.software.code;
}
var app = apps.firstWhereOrNull((a) => a.id == shorebirdYaml.appId);
final app = apps.firstWhereOrNull((a) => a.id == shorebirdYaml.appId);
if (app == null) {
logger.info(
lightCyan.wrap("\nIt looks like this is a new app. Let's get started!"),
logger.err(
'''
Could not find app with id: "${shorebirdYaml.appId}".
Did you forget to run "shorebird init"?''',
);
try {
app = await createApp();
addShorebirdYamlToProject(app.id);
addShorebirdYamlToPubspecAssets();
logger.info('''
${lightGreen.wrap('🐦 Shorebird initialized successfully!')}
✅ A shorebird app has been created.
✅ A "shorebird.yaml" has been created.
✅ The "pubspec.yaml" has been updated to include "shorebird.yaml" as an asset.''');
} catch (error) {
logger.err('$error');
return ExitCode.software.code;
}
return ExitCode.software.code;
}
logger.info(
@@ -172,22 +157,20 @@ ${styleBold.wrap(lightGreen.wrap('🚀 Ready to publish a new patch!'))}
return ExitCode.software.code;
}
var release = releases.firstWhereOrNull(
final release = releases.firstWhereOrNull(
(r) => r.version == versionString,
);
if (release == null) {
final createReleaseProgress = logger.progress('Creating release');
try {
release = await codePushClient.createRelease(
appId: app.id,
version: versionString,
);
createReleaseProgress.complete();
} catch (error) {
createReleaseProgress.fail('$error');
return ExitCode.software.code;
}
logger.err(
'''
Release not found: "$versionString"
Patches can only be published for existing releases.
Please create a release using "shorebird release" and try again.
''',
);
return ExitCode.software.code;
}
late final Patch patch;
@@ -0,0 +1,229 @@
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:crypto/crypto.dart';
import 'package:mason_logger/mason_logger.dart';
import 'package:path/path.dart' as p;
import 'package:shorebird_cli/src/command.dart';
import 'package:shorebird_cli/src/shorebird_build_mixin.dart';
import 'package:shorebird_cli/src/shorebird_config_mixin.dart';
import 'package:shorebird_cli/src/shorebird_create_app_mixin.dart';
import 'package:shorebird_cli/src/shorebird_engine_mixin.dart';
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
/// {@template release_command}
/// `shorebird release`
/// Create new app releases.
/// {@endtemplate}
class ReleaseCommand extends ShorebirdCommand
with
ShorebirdConfigMixin,
ShorebirdEngineMixin,
ShorebirdBuildMixin,
ShorebirdCreateAppMixin {
/// {@macro release_command}
ReleaseCommand({
required super.logger,
super.auth,
super.buildCodePushClient,
super.runProcess,
HashFunction? hashFn,
}) : _hashFn = hashFn ?? ((m) => sha256.convert(m).toString()) {
argParser
..addOption(
'release-version',
help: 'The version of the release (e.g. "1.0.0").',
)
..addOption(
'platform',
help: 'The platform of the release (e.g. "android").',
allowed: ['android'],
allowedHelp: {'android': 'The Android platform.'},
defaultsTo: 'android',
)
..addOption(
'arch',
help: 'The architecture of the release (e.g. "aarch64").',
allowed: ['aarch64'],
allowedHelp: {'aarch64': 'The 64-bit ARM architecture.'},
defaultsTo: 'aarch64',
);
}
@override
String get description => '''
Builds and submits your 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 => 'release';
final HashFunction _hashFn;
@override
Future<int> run() async {
if (!isShorebirdInitialized) {
logger.err(
'Shorebird is not initialized. Did you run "shorebird init"?',
);
return ExitCode.config.code;
}
final session = auth.currentSession;
if (session == null) {
logger.err('You must be logged in to release.');
return ExitCode.noUser.code;
}
try {
await ensureEngineExists();
} catch (error) {
logger.err(error.toString());
return ExitCode.software.code;
}
final buildProgress = logger.progress('Building release');
try {
await buildRelease();
buildProgress.complete();
} on ProcessException catch (error) {
buildProgress.fail('Failed to build: ${error.message}');
return ExitCode.software.code;
}
final artifactPath = p.join(
Directory.current.path,
'build',
'app',
'intermediates',
'stripped_native_libs',
'release',
'out',
'lib',
'arm64-v8a',
'libapp.so',
);
final artifact = File(artifactPath);
if (!artifact.existsSync()) {
logger.err('Artifact not found: "${artifact.path}"');
return ExitCode.software.code;
}
final hash = _hashFn(await artifact.readAsBytes());
final pubspecYaml = getPubspecYaml()!;
final shorebirdYaml = getShorebirdYaml()!;
final codePushClient = buildCodePushClient(
apiKey: session.apiKey,
hostedUri: hostedUri,
);
final version = pubspecYaml.version!;
final versionString = '${version.major}.${version.minor}.${version.patch}';
late final List<App> apps;
final fetchAppsProgress = logger.progress('Fetching apps');
try {
apps = (await codePushClient.getApps())
.map((a) => App(id: a.appId, displayName: a.displayName))
.toList();
fetchAppsProgress.complete();
} catch (error) {
fetchAppsProgress.fail('$error');
return ExitCode.software.code;
}
final app = apps.firstWhereOrNull((a) => a.id == shorebirdYaml.appId);
if (app == null) {
logger.err(
'''
Could not find app with id: "${shorebirdYaml.appId}".
Did you forget to run "shorebird init"?''',
);
return ExitCode.software.code;
}
final releaseVersionArg = results['release-version'] as String?;
final pubspecVersion = pubspecYaml.version!;
final pubspecVersionString =
'''${pubspecVersion.major}.${pubspecVersion.minor}.${pubspecVersion.patch}''';
final releaseVersion = releaseVersionArg ??
logger.prompt(
'\nWhat is the version of this release?',
defaultValue: pubspecVersionString,
);
final arch = results['arch'] as String;
final platform = results['platform'] as String;
logger.info(
'''
${styleBold.wrap(lightGreen.wrap('🚀 Ready to create a new release!'))}
📱 App: ${lightCyan.wrap(app.displayName)} ${lightCyan.wrap('(${app.id})')}
📦 Release Version: ${lightCyan.wrap(releaseVersion)}
⚙️ Architecture: ${lightCyan.wrap(arch)}
🕹️ Platform: ${lightCyan.wrap(platform)}
#️⃣ Hash: ${lightCyan.wrap(hash)}
Your next step is to upload the release artifact to the Play Store.
${lightCyan.wrap(artifactPath)}
See the following link for more information:
${link(uri: Uri.parse('https://support.google.com/googleplay/android-developer/answer/9859152?hl=en'))}
''',
);
final confirm = logger.confirm('Would you like to continue?');
if (!confirm) {
logger.info('Aborting.');
return ExitCode.success.code;
}
late final List<Release> releases;
final fetchReleasesProgress = logger.progress('Fetching releases');
try {
releases = await codePushClient.getReleases(appId: app.id);
fetchReleasesProgress.complete();
} catch (error) {
fetchReleasesProgress.fail('$error');
return ExitCode.software.code;
}
var release = releases.firstWhereOrNull((r) => r.version == versionString);
if (release == null) {
final createReleaseProgress = logger.progress('Creating release');
try {
release = await codePushClient.createRelease(
appId: app.id,
version: versionString,
);
createReleaseProgress.complete();
} catch (error) {
createReleaseProgress.fail('$error');
return ExitCode.software.code;
}
}
final createArtifactProgress = logger.progress('Creating artifact');
try {
await codePushClient.createReleaseArtifact(
releaseId: release.id,
artifactPath: artifact.path,
arch: arch,
platform: platform,
hash: hash,
);
createArtifactProgress.complete();
} catch (error) {
createArtifactProgress.fail('$error');
return ExitCode.software.code;
}
logger.success('\n✅ Released Successfully!');
return ExitCode.success.code;
}
}
@@ -31,7 +31,6 @@ void main() {
const appId = 'test-app-id';
const version = '1.2.3';
const appDisplayName = 'Test App';
const app = App(id: appId, displayName: appDisplayName);
const appMetadata = AppMetadata(appId: appId, displayName: appDisplayName);
const patchArtifact = PatchArtifact(
id: 0,
@@ -124,21 +123,12 @@ flutter:
when(
() => codePushClient.getReleases(appId: any(named: 'appId')),
).thenAnswer((_) async => [release]);
when(
() => codePushClient.createApp(displayName: any(named: 'displayName')),
).thenAnswer((_) async => app);
when(
() => codePushClient.createChannel(
appId: any(named: 'appId'),
channel: any(named: 'channel'),
),
).thenAnswer((_) async => channel);
when(
() => codePushClient.createRelease(
appId: any(named: 'appId'),
version: any(named: 'version'),
),
).thenAnswer((_) async => release);
when(
() => codePushClient.createPatch(releaseId: any(named: 'releaseId')),
).thenAnswer((_) async => patch);
@@ -260,15 +250,11 @@ flutter:
expect(exitCode, ExitCode.software.code);
});
test('throws error when creating apps fails.', () async {
const error = 'something went wrong';
test('throws error when app does not exist fails.', () async {
when(
() => logger.prompt(any(), defaultValue: any(named: 'defaultValue')),
).thenReturn(appDisplayName);
when(() => codePushClient.getApps()).thenAnswer((_) async => []);
when(
() => codePushClient.createApp(displayName: any(named: 'displayName')),
).thenThrow(error);
final tempDir = setUpTempDir();
Directory(
p.join(command.shorebirdEnginePath, 'engine'),
@@ -290,7 +276,13 @@ flutter:
command.run,
getCurrentDirectory: () => tempDir,
);
verify(() => logger.err(error)).called(1);
verify(
() => logger.err(
'''
Could not find app with id: "$appId".
Did you forget to run "shorebird init"?''',
),
).called(1);
expect(exitCode, ExitCode.software.code);
});
@@ -299,7 +291,6 @@ flutter:
when(
() => logger.prompt(any(), defaultValue: any(named: 'defaultValue')),
).thenReturn(appDisplayName);
when(() => codePushClient.getApps()).thenAnswer((_) async => []);
final tempDir = setUpTempDir();
Directory(
p.join(command.shorebirdEnginePath, 'engine'),
@@ -355,18 +346,10 @@ flutter:
expect(exitCode, ExitCode.software.code);
});
test('throws error when creating release fails.', () async {
const error = 'something went wrong';
test('throws error when release does not exist.', () async {
when(
() => codePushClient.getReleases(appId: any(named: 'appId')),
).thenAnswer((_) async => []);
when(
() => codePushClient.createRelease(
appId: any(named: 'appId'),
version: any(named: 'version'),
displayName: any(named: 'displayName'),
),
).thenThrow(error);
final tempDir = setUpTempDir();
Directory(
p.join(command.shorebirdEnginePath, 'engine'),
@@ -388,15 +371,21 @@ flutter:
command.run,
getCurrentDirectory: () => tempDir,
);
verify(() => progress.fail(error)).called(1);
verify(
() => logger.err(
'''
Release not found: "$version"
Patches can only be published for existing releases.
Please create a release using "shorebird release" and try again.
''',
),
).called(1);
expect(exitCode, ExitCode.software.code);
});
test('throws error when creating patch fails.', () async {
const error = 'something went wrong';
when(
() => codePushClient.getReleases(appId: any(named: 'appId')),
).thenAnswer((_) async => []);
when(
() => codePushClient.createPatch(releaseId: any(named: 'releaseId')),
).thenThrow(error);
@@ -427,9 +416,6 @@ flutter:
test('throws error when uploading patch artifact fails.', () async {
const error = 'something went wrong';
when(
() => codePushClient.getReleases(appId: any(named: 'appId')),
).thenAnswer((_) async => []);
when(
() => codePushClient.createPatchArtifact(
artifactPath: any(named: 'artifactPath'),
@@ -0,0 +1,446 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:archive/archive.dart';
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:shorebird_cli/src/auth/auth.dart';
import 'package:shorebird_cli/src/auth/session.dart';
import 'package:shorebird_cli/src/commands/commands.dart';
import 'package:shorebird_cli/src/config/config.dart';
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
import 'package:test/test.dart';
class _MockArgResults extends Mock implements ArgResults {}
class _MockAuth extends Mock implements Auth {}
class _MockLogger extends Mock implements Logger {}
class _MockProgress extends Mock implements Progress {}
class _MockProcessResult extends Mock implements ProcessResult {}
class _MockCodePushClient extends Mock implements CodePushClient {}
void main() {
group('release', () {
const session = Session(apiKey: 'test-api-key');
const appId = 'test-app-id';
const version = '1.2.3';
const appDisplayName = 'Test App';
const arch = 'aarch64';
const platform = 'android';
const appMetadata = AppMetadata(appId: appId, displayName: appDisplayName);
const release = Release(
id: 0,
appId: appId,
version: version,
displayName: '1.2.3',
);
const releaseArtifact = ReleaseArtifact(
id: 0,
releaseId: 0,
arch: arch,
platform: platform,
hash: '#',
size: 42,
url: 'https://example.com',
);
const pubspecYamlContent = '''
name: example
version: $version
environment:
sdk: ">=2.19.0 <3.0.0"
flutter:
assets:
- shorebird.yaml''';
late ArgResults argResults;
late Directory applicationConfigHome;
late Auth auth;
late Progress progress;
late Logger logger;
late ProcessResult processResult;
late CodePushClient codePushClient;
late ReleaseCommand command;
late Uri? capturedHostedUri;
Directory setUpTempDir() {
final tempDir = Directory.systemTemp.createTempSync();
File(
p.join(tempDir.path, 'pubspec.yaml'),
).writeAsStringSync(pubspecYamlContent);
File(
p.join(tempDir.path, 'shorebird.yaml'),
).writeAsStringSync('app_id: $appId');
return tempDir;
}
setUp(() {
argResults = _MockArgResults();
applicationConfigHome = Directory.systemTemp.createTempSync();
auth = _MockAuth();
progress = _MockProgress();
logger = _MockLogger();
processResult = _MockProcessResult();
codePushClient = _MockCodePushClient();
command = ReleaseCommand(
auth: auth,
buildCodePushClient: ({required String apiKey, Uri? hostedUri}) {
capturedHostedUri = hostedUri;
return codePushClient;
},
runProcess: (
executable,
arguments, {
bool runInShell = false,
String? workingDirectory,
}) async {
return processResult;
},
logger: logger,
)..testArgResults = argResults;
testApplicationConfigHome = (_) => applicationConfigHome.path;
when(() => argResults.rest).thenReturn([]);
when(() => argResults['arch']).thenReturn(arch);
when(() => argResults['platform']).thenReturn(platform);
when(() => auth.currentSession).thenReturn(session);
when(() => logger.progress(any())).thenReturn(progress);
when(() => logger.confirm(any())).thenReturn(true);
when(
() => logger.prompt(any(), defaultValue: any(named: 'defaultValue')),
).thenReturn(version);
when(() => processResult.exitCode).thenReturn(ExitCode.success.code);
when(
() => codePushClient.downloadEngine(revision: any(named: 'revision')),
).thenAnswer((_) async => Uint8List.fromList([]));
when(
() => codePushClient.getApps(),
).thenAnswer((_) async => [appMetadata]);
when(
() => codePushClient.getReleases(appId: any(named: 'appId')),
).thenAnswer((_) async => [release]);
when(
() => codePushClient.createRelease(
appId: any(named: 'appId'),
version: any(named: 'version'),
),
).thenAnswer((_) async => release);
when(
() => codePushClient.createReleaseArtifact(
artifactPath: any(named: 'artifactPath'),
releaseId: any(named: 'releaseId'),
arch: any(named: 'arch'),
platform: any(named: 'platform'),
hash: any(named: 'hash'),
),
).thenAnswer((_) async => releaseArtifact);
});
test('throws config error when shorebird is not initialized', () async {
final tempDir = Directory.systemTemp.createTempSync();
final exitCode = await IOOverrides.runZoned(
command.run,
getCurrentDirectory: () => tempDir,
);
verify(
() => logger.err(
'Shorebird is not initialized. Did you run "shorebird init"?',
),
).called(1);
expect(exitCode, ExitCode.config.code);
});
test('throws no user error when session does not exist', () async {
when(() => auth.currentSession).thenReturn(null);
final tempDir = setUpTempDir();
final exitCode = await IOOverrides.runZoned(
() => command.run(),
getCurrentDirectory: () => tempDir,
);
expect(exitCode, equals(ExitCode.noUser.code));
});
test('exits with code 70 when pulling engine fails', () async {
when(
() => codePushClient.downloadEngine(revision: any(named: 'revision')),
).thenThrow(Exception('oops'));
when(() => auth.currentSession).thenReturn(session);
final tempDir = setUpTempDir();
final exitCode = await IOOverrides.runZoned(
command.run,
getCurrentDirectory: () => tempDir,
);
expect(exitCode, equals(ExitCode.software.code));
});
test('exits with code 70 when building fails', () async {
when(() => processResult.exitCode).thenReturn(1);
when(() => processResult.stderr).thenReturn('oops');
when(() => auth.currentSession).thenReturn(session);
when(
() => codePushClient.downloadEngine(revision: any(named: 'revision')),
).thenAnswer(
(_) async => Uint8List.fromList(ZipEncoder().encode(Archive())!),
);
final tempDir = setUpTempDir();
final exitCode = await IOOverrides.runZoned(
() async => command.run(),
getCurrentDirectory: () => tempDir,
);
expect(exitCode, equals(ExitCode.software.code));
});
test('throws software error when artifact is not found (default).',
() async {
final tempDir = setUpTempDir();
Directory(
p.join(command.shorebirdEnginePath, 'engine'),
).createSync(recursive: true);
final exitCode = await IOOverrides.runZoned(
command.run,
getCurrentDirectory: () => tempDir,
);
verify(
() => logger.err(any(that: contains('Artifact not found:'))),
).called(1);
expect(exitCode, ExitCode.software.code);
});
test('throws error when fetching apps fails.', () async {
const error = 'something went wrong';
when(() => codePushClient.getApps()).thenThrow(error);
final tempDir = setUpTempDir();
Directory(
p.join(command.shorebirdEnginePath, 'engine'),
).createSync(recursive: true);
final artifactPath = p.join(
tempDir.path,
'build',
'app',
'intermediates',
'stripped_native_libs',
'release',
'out',
'lib',
'arm64-v8a',
'libapp.so',
);
File(artifactPath).createSync(recursive: true);
final exitCode = await IOOverrides.runZoned(
command.run,
getCurrentDirectory: () => tempDir,
);
verify(() => progress.fail(error)).called(1);
expect(exitCode, ExitCode.software.code);
});
test('throws error when app does not exist.', () async {
when(
() => logger.prompt(any(), defaultValue: any(named: 'defaultValue')),
).thenReturn(appDisplayName);
when(() => codePushClient.getApps()).thenAnswer((_) async => []);
final tempDir = setUpTempDir();
Directory(
p.join(command.shorebirdEnginePath, 'engine'),
).createSync(recursive: true);
final artifactPath = p.join(
tempDir.path,
'build',
'app',
'intermediates',
'stripped_native_libs',
'release',
'out',
'lib',
'arm64-v8a',
'libapp.so',
);
File(artifactPath).createSync(recursive: true);
final exitCode = await IOOverrides.runZoned(
command.run,
getCurrentDirectory: () => tempDir,
);
verify(
() => logger.err(
'''
Could not find app with id: "$appId".
Did you forget to run "shorebird init"?''',
),
).called(1);
expect(exitCode, ExitCode.software.code);
});
test('aborts when user opts out', () async {
when(() => logger.confirm(any())).thenReturn(false);
when(
() => logger.prompt(any(), defaultValue: any(named: 'defaultValue')),
).thenReturn(appDisplayName);
final tempDir = setUpTempDir();
Directory(
p.join(command.shorebirdEnginePath, 'engine'),
).createSync(recursive: true);
final artifactPath = p.join(
tempDir.path,
'build',
'app',
'intermediates',
'stripped_native_libs',
'release',
'out',
'lib',
'arm64-v8a',
'libapp.so',
);
File(artifactPath).createSync(recursive: true);
final exitCode = await IOOverrides.runZoned(
command.run,
getCurrentDirectory: () => tempDir,
);
expect(exitCode, ExitCode.success.code);
verify(() => logger.info('Aborting.')).called(1);
});
test('throws error when fetching releases fails.', () async {
const error = 'something went wrong';
when(
() => codePushClient.getReleases(appId: any(named: 'appId')),
).thenThrow(error);
final tempDir = setUpTempDir();
Directory(
p.join(command.shorebirdEnginePath, 'engine'),
).createSync(recursive: true);
final artifactPath = p.join(
tempDir.path,
'build',
'app',
'intermediates',
'stripped_native_libs',
'release',
'out',
'lib',
'arm64-v8a',
'libapp.so',
);
File(artifactPath).createSync(recursive: true);
final exitCode = await IOOverrides.runZoned(
command.run,
getCurrentDirectory: () => tempDir,
);
verify(() => progress.fail(error)).called(1);
expect(exitCode, ExitCode.software.code);
});
test('throws error when creating release fails.', () async {
const error = 'something went wrong';
when(
() => codePushClient.getReleases(appId: any(named: 'appId')),
).thenAnswer((_) async => []);
when(
() => codePushClient.createRelease(
appId: any(named: 'appId'),
version: any(named: 'version'),
displayName: any(named: 'displayName'),
),
).thenThrow(error);
final tempDir = setUpTempDir();
Directory(
p.join(command.shorebirdEnginePath, 'engine'),
).createSync(recursive: true);
final artifactPath = p.join(
tempDir.path,
'build',
'app',
'intermediates',
'stripped_native_libs',
'release',
'out',
'lib',
'arm64-v8a',
'libapp.so',
);
File(artifactPath).createSync(recursive: true);
final exitCode = await IOOverrides.runZoned(
command.run,
getCurrentDirectory: () => tempDir,
);
verify(() => progress.fail(error)).called(1);
expect(exitCode, ExitCode.software.code);
});
test('throws error when uploading release artifact fails.', () async {
const error = 'something went wrong';
when(
() => codePushClient.getReleases(appId: any(named: 'appId')),
).thenAnswer((_) async => []);
when(
() => codePushClient.createReleaseArtifact(
artifactPath: any(named: 'artifactPath'),
releaseId: any(named: 'releaseId'),
arch: any(named: 'arch'),
platform: any(named: 'platform'),
hash: any(named: 'hash'),
),
).thenThrow(error);
final tempDir = setUpTempDir();
Directory(
p.join(command.shorebirdEnginePath, 'engine'),
).createSync(recursive: true);
final artifactPath = p.join(
tempDir.path,
'build',
'app',
'intermediates',
'stripped_native_libs',
'release',
'out',
'lib',
'arm64-v8a',
'libapp.so',
);
File(artifactPath).createSync(recursive: true);
final exitCode = await IOOverrides.runZoned(
command.run,
getCurrentDirectory: () => tempDir,
);
verify(() => progress.fail(error)).called(1);
expect(exitCode, ExitCode.software.code);
});
test('succeeds when release is successful', () async {
final tempDir = setUpTempDir();
Directory(
p.join(command.shorebirdEnginePath, 'engine'),
).createSync(recursive: true);
final artifactPath = p.join(
tempDir.path,
'build',
'app',
'intermediates',
'stripped_native_libs',
'release',
'out',
'lib',
'arm64-v8a',
'libapp.so',
);
File(artifactPath).createSync(recursive: true);
final exitCode = await IOOverrides.runZoned(
command.run,
getCurrentDirectory: () => tempDir,
);
verify(() => logger.success('\n✅ Released Successfully!')).called(1);
expect(exitCode, ExitCode.success.code);
expect(capturedHostedUri, isNull);
});
});
}