feat(shorebird_cli): add shorebird release (#188)
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user