refactor(shorebird_cli): migrate patch ios-preview command to use CodePushClientWrapper (#605)

This commit is contained in:
Bryan Oltman
2023-06-08 13:14:41 -04:00
committed by GitHub
parent 303ba6546e
commit d69d63eed9
3 changed files with 52 additions and 334 deletions
@@ -5,17 +5,16 @@ import 'package:crypto/crypto.dart';
import 'package:mason_logger/mason_logger.dart';
import 'package:path/path.dart' as p;
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/config/config.dart';
import 'package:shorebird_cli/src/formatters/file_size_formatter.dart';
import 'package:shorebird_cli/src/logger.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_code_push_client_mixin.dart';
import 'package:shorebird_cli/src/shorebird_config_mixin.dart';
import 'package:shorebird_cli/src/shorebird_environment.dart';
import 'package:shorebird_cli/src/shorebird_validation_mixin.dart';
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
/// {@template patch_ios_command}
/// `shorebird patch ios-preview` command.
@@ -25,12 +24,11 @@ class PatchIosCommand extends ShorebirdCommand
ShorebirdConfigMixin,
ShorebirdBuildMixin,
ShorebirdValidationMixin,
ShorebirdArtifactMixin,
ShorebirdCodePushClientMixin {
ShorebirdArtifactMixin {
/// {@macro patch_ios_command}
PatchIosCommand({
super.auth,
super.buildCodePushClient,
super.codePushClientWrapper,
super.validators,
HashFunction? hashFn,
IpaReader? ipaReader,
@@ -100,21 +98,7 @@ class PatchIosCommand extends ShorebirdCommand
final shorebirdYaml = getShorebirdYaml()!;
final appId = shorebirdYaml.getAppId(flavor: flavor);
final App? app;
try {
app = await getApp(appId: appId, flavor: flavor);
} catch (_) {
return ExitCode.software.code;
}
if (app == null) {
logger.err(
'''
Could not find app with id: "$appId".
Did you forget to run "shorebird init"?''',
);
return ExitCode.software.code;
}
final app = await codePushClientWrapper.getApp(appId: appId);
final buildProgress = logger.progress('Building release');
try {
@@ -160,24 +144,10 @@ Did you forget to run "shorebird init"?''',
return ExitCode.software.code;
}
final Release? release;
try {
release = await getRelease(appId: appId, releaseVersion: releaseVersion);
} catch (_) {
return ExitCode.software.code;
}
if (release == null) {
logger.err(
'''
Release not found: "$releaseVersion"
Patches can only be published for existing releases.
Please create a release using "shorebird release" and try again.
''',
);
return ExitCode.software.code;
}
final release = await codePushClientWrapper.getRelease(
appId: appId,
releaseVersion: releaseVersion,
);
final flutterRevisionProgress = logger.progress(
'Fetching Flutter revision',
@@ -224,14 +194,14 @@ https://github.com/shorebirdtech/shorebird/issues/472
return ExitCode.success.code;
}
final size = formatBytes(aotFile.statSync().size);
final aotFileSize = aotFile.statSync().size;
final summary = [
'''📱 App: ${lightCyan.wrap(app.displayName)} ${lightCyan.wrap('(${app.id})')}''',
'''📱 App: ${lightCyan.wrap(app.displayName)} ${lightCyan.wrap('($appId)')}''',
if (flavor != null) '🍧 Flavor: ${lightCyan.wrap(flavor)}',
'📦 Release Version: ${lightCyan.wrap(releaseVersion)}',
'📺 Channel: ${lightCyan.wrap(channelName)}',
'''🕹️ Platform: ${lightCyan.wrap(platform)} ${lightCyan.wrap('[arm64 ($size)]')}''',
'''🕹️ Platform: ${lightCyan.wrap(platform)} ${lightCyan.wrap('[arm64 (${formatBytes(aotFileSize)})]')}''',
];
logger.info(
@@ -243,6 +213,8 @@ ${summary.join('\n')}
''',
);
// TODO(bryanoltman): check for asset changes
final needsConfirmation = !force;
if (needsConfirmation) {
final confirm = logger.confirm('Would you like to continue?');
@@ -253,64 +225,21 @@ ${summary.join('\n')}
}
}
final Patch patch;
try {
patch = await createPatch(releaseId: release.id);
} catch (e) {
return ExitCode.software.code;
}
final codePushClient = buildCodePushClient(
httpClient: auth.client,
hostedUri: hostedUri,
await codePushClientWrapper.publishPatch(
appId: appId,
releaseId: release.id,
platform: platform,
channelName: channelName,
patchArtifactBundles: {
Arch.arm64: PatchArtifactBundle(
arch: 'arm64',
path: aotFile.path,
hash: _hashFn(aotFile.readAsBytesSync()),
size: aotFileSize,
),
},
);
// TODO(bryanoltman): check for asset changes
final createArtifactProgress = logger.progress('Uploading artifacts');
try {
await codePushClient.createPatchArtifact(
patchId: patch.id,
artifactPath: aotFile.path,
arch: 'arm64',
platform: 'ios',
hash: _hashFn(await aotFile.readAsBytes()),
);
} catch (error) {
createArtifactProgress.fail('$error');
return ExitCode.software.code;
}
createArtifactProgress.complete();
Channel? channel;
try {
channel = await getChannel(appId: appId, name: channelName);
} catch (_) {
return ExitCode.software.code;
}
if (channel == null) {
try {
channel = await createChannel(appId: appId, name: channelName);
} catch (_) {
return ExitCode.software.code;
}
}
final publishPatchProgress = logger.progress(
'Promoting patch to ${channel.name}',
);
try {
await codePushClient.promotePatch(
patchId: patch.id,
channelId: channel.id,
);
publishPatchProgress.complete();
} catch (error) {
publishPatchProgress.fail('$error');
return ExitCode.software.code;
}
logger.success('\n✅ Published Patch!');
return ExitCode.success.code;
}
@@ -124,23 +124,4 @@ mixin ShorebirdCodePushClientMixin on ShorebirdConfigMixin {
fetchReleaseArtifactProgress.complete();
return releaseArtifacts;
}
Future<Patch> createPatch({required int releaseId}) async {
final codePushClient = buildCodePushClient(
httpClient: auth.client,
hostedUri: hostedUri,
);
final Patch patch;
final createPatchProgress = logger.progress('Creating patch');
try {
patch = await codePushClient.createPatch(releaseId: releaseId);
createPatchProgress.complete();
} catch (error) {
createPatchProgress.fail('$error');
rethrow;
}
return patch;
}
}
@@ -8,6 +8,7 @@ import 'package:path/path.dart' as p;
import 'package:scoped/scoped.dart';
import 'package:shorebird_cli/src/archive_analysis/archive_analysis.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/patch/patch.dart';
import 'package:shorebird_cli/src/logger.dart';
import 'package:shorebird_cli/src/shorebird_environment.dart';
@@ -22,6 +23,9 @@ class _MockArgResults extends Mock implements ArgResults {}
class _MockAuth extends Mock implements Auth {}
class _MockCodePushClientWrapper extends Mock
implements CodePushClientWrapper {}
class _MockIpaReader extends Mock implements IpaReader {}
class _MockIpa extends Mock implements Ipa {}
@@ -34,8 +38,6 @@ class _MockProcessResult extends Mock implements ShorebirdProcessResult {}
class _MockHttpClient extends Mock implements http.Client {}
class _MockCodePushClient extends Mock implements CodePushClient {}
class _MockShorebirdFlutterValidator extends Mock
implements ShorebirdFlutterValidator {}
@@ -51,7 +53,6 @@ void main() {
const version = '$versionName+$versionCode';
const arch = 'aarch64';
const appDisplayName = 'Test App';
const channelName = 'stable';
const platform = 'ios';
const pubspecYamlContent = '''
name: example
@@ -64,17 +65,6 @@ flutter:
- shorebird.yaml''';
const appMetadata = AppMetadata(appId: appId, displayName: appDisplayName);
const channel = Channel(id: 0, appId: appId, name: channelName);
const patch = Patch(id: 0, number: 1);
const patchArtifact = PatchArtifact(
id: 0,
patchId: 0,
arch: arch,
platform: platform,
hash: '#',
size: 42,
url: 'https://example.com',
);
const release = Release(
id: 0,
appId: appId,
@@ -86,6 +76,7 @@ flutter:
group(PatchIosCommand, () {
late ArgResults argResults;
late Auth auth;
late CodePushClientWrapper codePushClientWrapper;
late Ipa ipa;
late IpaReader ipaReader;
late Progress progress;
@@ -94,8 +85,6 @@ flutter:
late ShorebirdProcessResult flutterBuildProcessResult;
late ShorebirdProcessResult flutterRevisionProcessResult;
late http.Client httpClient;
late CodePushClient codePushClient;
late Uri? capturedHostedUri;
late ShorebirdFlutterValidator flutterValidator;
late ShorebirdProcess shorebirdProcess;
late PatchIosCommand command;
@@ -140,6 +129,7 @@ flutter:
setUp(() {
argResults = _MockArgResults();
auth = _MockAuth();
codePushClientWrapper = _MockCodePushClientWrapper();
ipaReader = _MockIpaReader();
ipa = _MockIpa();
progress = _MockProgress();
@@ -148,7 +138,6 @@ flutter:
flutterBuildProcessResult = _MockProcessResult();
flutterRevisionProcessResult = _MockProcessResult();
httpClient = _MockHttpClient();
codePushClient = _MockCodePushClient();
flutterValidator = _MockShorebirdFlutterValidator();
shorebirdProcess = _MockShorebirdProcess();
@@ -159,36 +148,21 @@ flutter:
when(() => auth.isAuthenticated).thenReturn(true);
when(() => auth.client).thenReturn(httpClient);
when(
() => codePushClient.getApps(),
).thenAnswer((_) async => [appMetadata]);
() => codePushClientWrapper.getApp(appId: any(named: 'appId')),
).thenAnswer((_) async => appMetadata);
when(
() => codePushClient.getChannels(appId: any(named: 'appId')),
).thenAnswer((_) async => [channel]);
when(
() => codePushClient.getReleases(appId: any(named: 'appId')),
).thenAnswer((_) async => [release]);
when(
() => codePushClient.createChannel(
() => codePushClientWrapper.getRelease(
appId: any(named: 'appId'),
channel: any(named: 'channel'),
releaseVersion: any(named: 'releaseVersion'),
),
).thenAnswer((_) async => channel);
).thenAnswer((_) async => release);
when(
() => codePushClient.createPatch(releaseId: any(named: 'releaseId')),
).thenAnswer((_) async => patch);
when(
() => codePushClient.createPatchArtifact(
artifactPath: any(named: 'artifactPath'),
patchId: any(named: 'patchId'),
arch: any(named: 'arch'),
() => codePushClientWrapper.publishPatch(
appId: any(named: 'appId'),
releaseId: any(named: 'releaseId'),
platform: any(named: 'platform'),
hash: any(named: 'hash'),
),
).thenAnswer((_) async => patchArtifact);
when(
() => codePushClient.promotePatch(
patchId: any(named: 'patchId'),
channelId: any(named: 'channelId'),
channelName: any(named: 'channelName'),
patchArtifactBundles: any(named: 'patchArtifactBundles'),
),
).thenAnswer((_) async {});
when(() => ipa.versionNumber).thenReturn(version);
@@ -230,15 +204,9 @@ flutter:
command = PatchIosCommand(
auth: auth,
codePushClientWrapper: codePushClientWrapper,
ipaReader: ipaReader,
validators: [flutterValidator],
buildCodePushClient: ({
required http.Client httpClient,
Uri? hostedUri,
}) {
capturedHostedUri = hostedUri;
return codePushClient;
},
)
..testArgResults = argResults
..testProcess = shorebirdProcess
@@ -285,40 +253,6 @@ flutter:
expect(exitCode, equals(ExitCode.usage.code));
});
test('throws error when fetching apps fails.', () async {
const error = 'something went wrong';
when(() => codePushClient.getApps()).thenThrow(error);
final tempDir = setUpTempDir();
setUpTempArtifacts(tempDir);
final exitCode = await IOOverrides.runZoned(
() => runWithOverrides(command.run),
getCurrentDirectory: () => tempDir,
);
verify(() => progress.fail(error)).called(1);
expect(exitCode, ExitCode.software.code);
});
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 => []);
final tempDir = setUpTempDir();
setUpTempArtifacts(tempDir);
final exitCode = await IOOverrides.runZoned(
() => runWithOverrides(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);
final tempDir = setUpTempDir();
@@ -387,44 +321,6 @@ https://github.com/shorebirdtech/shorebird/issues/472
).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();
setUpTempArtifacts(tempDir);
final exitCode = await IOOverrides.runZoned(
() => runWithOverrides(command.run),
getCurrentDirectory: () => tempDir,
);
verify(() => progress.fail(error)).called(1);
expect(exitCode, ExitCode.software.code);
});
test('throws error when release does not exist.', () async {
when(
() => codePushClient.getReleases(appId: any(named: 'appId')),
).thenAnswer((_) async => []);
final tempDir = setUpTempDir();
setUpTempArtifacts(tempDir);
final exitCode = await IOOverrides.runZoned(
() => runWithOverrides(command.run),
getCurrentDirectory: () => tempDir,
);
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('exits with code 70 when release version cannot be determiend',
() async {
when(() => ipa.versionNumber).thenThrow(Exception('oops'));
@@ -470,7 +366,9 @@ Please create a release using "shorebird release" and try again.
);
expect(exitCode, equals(ExitCode.success.code));
verifyNever(
() => codePushClient.createPatch(releaseId: any(named: 'releaseId')),
() => codePushClientWrapper.createPatch(
releaseId: any(named: 'releaseId'),
),
);
verify(() => logger.info('No issues detected.')).called(1);
});
@@ -486,101 +384,14 @@ Please create a release using "shorebird release" and try again.
expect(exitCode, equals(ExitCode.success.code));
verifyNever(() => logger.confirm(any()));
verify(
() => codePushClient.createPatch(releaseId: any(named: 'releaseId')),
).called(1);
});
test('throws error when creating patch fails.', () async {
const error = 'something went wrong';
when(
() => codePushClient.createPatch(releaseId: any(named: 'releaseId')),
).thenThrow(error);
final tempDir = setUpTempDir();
setUpTempArtifacts(tempDir);
final exitCode = await IOOverrides.runZoned(
() => runWithOverrides(command.run),
getCurrentDirectory: () => tempDir,
);
verify(() => progress.fail(error)).called(1);
expect(exitCode, ExitCode.software.code);
});
test('throws error when uploading patch artifact fails.', () async {
const error = 'something went wrong';
when(
() => codePushClient.createPatchArtifact(
artifactPath: any(named: 'artifactPath'),
patchId: any(named: 'patchId'),
arch: any(named: 'arch'),
platform: any(named: 'platform'),
hash: any(named: 'hash'),
),
).thenThrow(error);
final tempDir = setUpTempDir();
setUpTempArtifacts(tempDir);
final exitCode = await IOOverrides.runZoned(
() => runWithOverrides(command.run),
getCurrentDirectory: () => tempDir,
);
verify(() => progress.fail(error)).called(1);
expect(exitCode, ExitCode.software.code);
});
test('throws error when fetching channels fails.', () async {
const error = 'something went wrong';
when(
() => codePushClient.getChannels(appId: any(named: 'appId')),
).thenThrow(error);
final tempDir = setUpTempDir();
setUpTempArtifacts(tempDir);
final exitCode = await IOOverrides.runZoned(
() => runWithOverrides(command.run),
getCurrentDirectory: () => tempDir,
);
verify(() => progress.fail(error)).called(1);
expect(exitCode, ExitCode.software.code);
});
test('throws error when creating channel fails.', () async {
const error = 'something went wrong';
when(
() => codePushClient.getChannels(appId: any(named: 'appId')),
).thenAnswer((_) async => []);
when(
() => codePushClient.createChannel(
() => codePushClientWrapper.publishPatch(
appId: any(named: 'appId'),
channel: any(named: 'channel'),
releaseId: any(named: 'releaseId'),
platform: any(named: 'platform'),
channelName: any(named: 'channelName'),
patchArtifactBundles: any(named: 'patchArtifactBundles'),
),
).thenThrow(error);
final tempDir = setUpTempDir();
setUpTempArtifacts(tempDir);
final exitCode = await IOOverrides.runZoned(
() => runWithOverrides(command.run),
getCurrentDirectory: () => tempDir,
);
verify(() => progress.fail(error)).called(1);
expect(exitCode, ExitCode.software.code);
});
test('throws error when promoting patch fails.', () async {
const error = 'something went wrong';
when(
() => codePushClient.getChannels(appId: any(named: 'appId')),
).thenAnswer((_) async => []);
when(
() => codePushClient.promotePatch(
patchId: any(named: 'patchId'),
channelId: any(named: 'channelId'),
),
).thenThrow(error);
final tempDir = setUpTempDir();
setUpTempArtifacts(tempDir);
final exitCode = await IOOverrides.runZoned(
() => runWithOverrides(command.run),
getCurrentDirectory: () => tempDir,
);
verify(() => progress.fail(error)).called(1);
expect(exitCode, ExitCode.software.code);
).called(1);
});
test('succeeds when patch is successful', () async {
@@ -601,7 +412,6 @@ Please create a release using "shorebird release" and try again.
).called(1);
verify(() => logger.success('\n✅ Published Patch!')).called(1);
expect(exitCode, ExitCode.success.code);
expect(capturedHostedUri, isNull);
});
test(
@@ -625,7 +435,6 @@ flavors:
);
verify(() => logger.success('\n✅ Published Patch!')).called(1);
expect(exitCode, ExitCode.success.code);
expect(capturedHostedUri, isNull);
});
test('succeeds when patch is successful using custom base_url', () async {
@@ -643,7 +452,6 @@ base_url: $baseUrl''',
() => runWithOverrides(command.run),
getCurrentDirectory: () => tempDir,
);
expect(capturedHostedUri, equals(Uri.parse(baseUrl)));
});
test('prints flutter validation warnings', () async {