feat(shorebird_cli): shorebird patch aar optimizations (#1026)

This commit is contained in:
Felix Angelov
2023-08-04 12:11:48 -05:00
committed by GitHub
parent 1554cf2232
commit 0984683a05
2 changed files with 206 additions and 166 deletions
@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:io';
import 'package:archive/archive_io.dart';
import 'package:collection/collection.dart';
import 'package:crypto/crypto.dart';
import 'package:http/http.dart' as http;
import 'package:mason_logger/mason_logger.dart';
@@ -51,15 +52,6 @@ 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,
)
..addOption(
'channel',
help: 'The channel the patch should be promoted to (e.g. "stable").',
allowed: ['stable'],
allowedHelp: {
'stable': 'The stable channel which is consumed by production apps.'
},
defaultsTo: 'stable',
)
..addFlag(
'force',
abbr: 'f',
@@ -112,16 +104,30 @@ of the Android app that is using this module.''',
return ExitCode.config.code;
}
final buildNumber = results['build-number'] as String;
final releaseVersion = results['release-version'] as String;
final shorebirdYaml = shorebirdEnv.getShorebirdYaml()!;
final appId = shorebirdYaml.getAppId();
final app = await codePushClientWrapper.getApp(appId: appId);
final release = await codePushClientWrapper.getRelease(
appId: appId,
releaseVersion: releaseVersion,
final releases = await codePushClientWrapper.getReleases(appId: appId);
final releaseVersion = results['release-version'] as String? ??
await _promptForReleaseVersion(releases);
final release = releases.firstWhereOrNull(
(r) => r.version == releaseVersion,
);
if (releaseVersion == null || release == null) {
logger.info('No releases found');
return ExitCode.success.code;
}
if (release.platformStatuses[ReleasePlatform.android] ==
ReleaseStatus.draft) {
logger.err('''
Release $releaseVersion is in an incomplete state. It's possible that the original release was terminated or failed to complete.
Please re-run the release command for this version or create a new release.''');
return ExitCode.software.code;
}
final shorebirdFlutterRevision = shorebirdEnv.flutterRevision;
if (release.flutterRevision != shorebirdFlutterRevision) {
final installFlutterRevisionProgress = logger.progress(
@@ -138,6 +144,32 @@ of the Android app that is using this module.''',
}
}
const platform = ReleasePlatform.android;
final releaseArtifacts = await codePushClientWrapper.getReleaseArtifacts(
appId: appId,
releaseId: release.id,
architectures: architectures,
platform: platform,
);
final releaseAarArtifact = await codePushClientWrapper.getReleaseArtifact(
appId: appId,
releaseId: release.id,
arch: 'aar',
platform: platform,
);
final Map<Arch, String> releaseArtifactPaths;
try {
releaseArtifactPaths = await _downloadReleaseArtifacts(
releaseArtifacts: releaseArtifacts,
httpClient: _httpClient,
);
} catch (_) {
return ExitCode.software.code;
}
final buildNumber = results['build-number'] as String;
final buildProgress = logger.progress('Building patch');
try {
await runScoped(
@@ -156,30 +188,10 @@ of the Android app that is using this module.''',
return ExitCode.software.code;
}
const platform = ReleasePlatform.android;
final channelName = results['channel'] as String;
if (release.platformStatuses[ReleasePlatform.android] ==
ReleaseStatus.draft) {
logger.err('''
Release $releaseVersion is in an incomplete state. It's possible that the original release was terminated or failed to complete.
Please re-run the release command for this version or create a new release.''');
return ExitCode.software.code;
}
final releaseArtifacts = await codePushClientWrapper.getReleaseArtifacts(
appId: appId,
releaseId: release.id,
architectures: architectures,
platform: platform,
);
final releaseAarArtifact = await codePushClientWrapper.getReleaseArtifact(
appId: appId,
releaseId: release.id,
arch: 'aar',
platform: platform,
final extractedAarDir = await extractAar(
packageName: shorebirdEnv.androidPackageName!,
buildNumber: buildNumber,
unzipFn: _unzipFn,
);
final shouldContinue =
@@ -194,25 +206,8 @@ Please re-run the release command for this version or create a new release.''');
archiveDiffer: _archiveDiffer,
force: force,
);
if (!shouldContinue) {
return ExitCode.success.code;
}
final Map<Arch, String> releaseArtifactPaths;
try {
releaseArtifactPaths = await _downloadReleaseArtifacts(
releaseArtifacts: releaseArtifacts,
httpClient: _httpClient,
);
} catch (_) {
return ExitCode.software.code;
}
final extractedAarDir = await extractAar(
packageName: shorebirdEnv.androidPackageName!,
buildNumber: buildNumber,
unzipFn: _unzipFn,
);
if (!shouldContinue) return ExitCode.success.code;
final patchArtifactBundles = await _createPatchArtifacts(
releaseArtifactPaths: releaseArtifactPaths,
@@ -235,6 +230,7 @@ Please re-run the release command for this version or create a new release.''');
return ExitCode.success.code;
}
const channelName = 'stable';
final summary = [
'''📱 App: ${lightCyan.wrap(app.displayName)} ${lightCyan.wrap('(${app.appId})')}''',
'📦 Release Version: ${lightCyan.wrap(releaseVersion)}',
@@ -273,6 +269,16 @@ ${summary.join('\n')}
return ExitCode.success.code;
}
Future<String?> _promptForReleaseVersion(List<Release> releases) async {
if (releases.isEmpty) return null;
final release = logger.chooseOne(
'Which release would you like to patch?',
choices: releases,
display: (release) => release.version,
);
return release.version;
}
Future<Map<Arch, PatchArtifactBundle>?> _createPatchArtifacts({
required Map<Arch, String> releaseArtifactPaths,
required String extractedAarDirectory,
@@ -267,11 +267,8 @@ void main() {
() => codePushClientWrapper.getApp(appId: any(named: 'appId')),
).thenAnswer((_) async => appMetadata);
when(
() => codePushClientWrapper.getRelease(
appId: any(named: 'appId'),
releaseVersion: any(named: 'releaseVersion'),
),
).thenAnswer((_) async => release);
() => codePushClientWrapper.getReleases(appId: any(named: 'appId')),
).thenAnswer((_) async => [release]);
when(
() => codePushClientWrapper.getReleaseArtifacts(
appId: any(named: 'appId'),
@@ -360,12 +357,6 @@ void main() {
).called(1);
});
test('exits with 78 if no module entry exists in pubspec.yaml', () async {
when(() => shorebirdEnv.androidPackageName).thenReturn(null);
final exitCode = await runWithOverrides(command.run);
expect(exitCode, ExitCode.config.code);
});
test(
'exits with usage code when '
'both --dry-run and --force are specified', () async {
@@ -375,6 +366,146 @@ void main() {
expect(exitCode, equals(ExitCode.usage.code));
});
test('exits with 78 if no module entry exists in pubspec.yaml', () async {
when(() => shorebirdEnv.androidPackageName).thenReturn(null);
final exitCode = await runWithOverrides(command.run);
expect(exitCode, ExitCode.config.code);
});
test('prompts for release when release-version is not specified', () async {
when(() => argResults['release-version']).thenReturn(null);
when(
() => logger.chooseOne<Release>(
any(),
choices: any(named: 'choices'),
display: any(named: 'display'),
),
).thenReturn(release);
try {
await runWithOverrides(command.run);
} catch (_) {}
await untilCalled(
() => logger.chooseOne<Release>(
any(),
choices: any(named: 'choices'),
display: any(named: 'display'),
),
);
final display = verify(
() => logger.chooseOne<Release>(
any(),
choices: any(named: 'choices'),
display: captureAny(named: 'display'),
),
).captured.single as String Function(Release);
expect(display(release), equals(release.version));
});
test('exits early when no releases are found', () async {
when(() => argResults['release-version']).thenReturn(null);
when(
() => codePushClientWrapper.getReleases(appId: any(named: 'appId')),
).thenAnswer((_) async => []);
try {
await runWithOverrides(command.run);
} catch (_) {}
verifyNever(
() => logger.chooseOne<Release>(
any(),
choices: any(named: 'choices'),
display: captureAny(named: 'display'),
),
);
verify(() => codePushClientWrapper.getReleases(appId: appId)).called(1);
verify(() => logger.info('No releases found')).called(1);
});
test(
'''exits with code 70 if release is in draft state for the android platform''',
() async {
when(
() => codePushClientWrapper.getReleases(appId: any(named: 'appId')),
).thenAnswer(
(_) async => const [
Release(
id: 0,
appId: appId,
version: version,
flutterRevision: flutterRevision,
displayName: '1.2.3+1',
platformStatuses: {ReleasePlatform.android: ReleaseStatus.draft},
),
],
);
final tempDir = setUpTempDir();
setUpTempArtifacts(tempDir);
final exitCode = await IOOverrides.runZoned(
() => runWithOverrides(command.run),
getCurrentDirectory: () => tempDir,
);
expect(exitCode, ExitCode.software.code);
verify(
() => logger.err('''
Release 1.2.3+1 is in an incomplete state. It's possible that the original release was terminated or failed to complete.
Please re-run the release command for this version or create a new release.'''),
).called(1);
});
test('proceeds if release is in draft state for non-android platform',
() async {
when(
() => codePushClientWrapper.getReleases(appId: any(named: 'appId')),
).thenAnswer(
(_) async => const [
Release(
id: 0,
appId: appId,
version: version,
flutterRevision: flutterRevision,
displayName: '1.2.3+1',
platformStatuses: {ReleasePlatform.ios: ReleaseStatus.draft},
),
],
);
final tempDir = setUpTempDir();
setUpTempArtifacts(tempDir);
final exitCode = await IOOverrides.runZoned(
() => runWithOverrides(command.run),
getCurrentDirectory: () => tempDir,
);
expect(exitCode, ExitCode.success.code);
});
test('throws error when release artifact does not exist.', () async {
when(
() => httpClient.send(
any(
that: isA<http.Request>().having(
(req) => req.url.toString(),
'url',
endsWith('so'),
),
),
),
).thenAnswer(
(_) async => http.StreamedResponse(
const Stream.empty(),
HttpStatus.notFound,
reasonPhrase: 'Not Found',
),
);
final tempDir = setUpTempDir();
setUpTempArtifacts(tempDir);
final exitCode = await IOOverrides.runZoned(
() => runWithOverrides(command.run),
getCurrentDirectory: () => tempDir,
);
verify(
() => progress.fail(any(that: contains('404 Not Found'))),
).called(1);
expect(exitCode, ExitCode.software.code);
});
test(
'installs correct flutter revision '
'when release flutter revision differs', () async {
@@ -486,98 +617,6 @@ void main() {
expect(exitCode, equals(ExitCode.software.code));
});
test(
'''exits with code 70 if release is in draft state for the android platform''',
() async {
when(
() => codePushClientWrapper.getRelease(
appId: any(named: 'appId'),
releaseVersion: any(named: 'releaseVersion'),
),
).thenAnswer(
(_) async => const Release(
id: 0,
appId: appId,
version: version,
flutterRevision: flutterRevision,
displayName: '1.2.3+1',
platformStatuses: {ReleasePlatform.android: ReleaseStatus.draft},
),
);
final tempDir = setUpTempDir();
setUpTempArtifacts(tempDir);
final exitCode = await IOOverrides.runZoned(
() => runWithOverrides(command.run),
getCurrentDirectory: () => tempDir,
);
expect(exitCode, ExitCode.software.code);
verify(
() => logger.err('''
Release 1.2.3+1 is in an incomplete state. It's possible that the original release was terminated or failed to complete.
Please re-run the release command for this version or create a new release.'''),
).called(1);
},
);
test(
'proceeds if release is in draft state for non-android platform',
() async {
when(
() => codePushClientWrapper.getRelease(
appId: any(named: 'appId'),
releaseVersion: any(named: 'releaseVersion'),
),
).thenAnswer(
(_) async => const Release(
id: 0,
appId: appId,
version: version,
flutterRevision: flutterRevision,
displayName: '1.2.3+1',
platformStatuses: {ReleasePlatform.ios: ReleaseStatus.draft},
),
);
final tempDir = setUpTempDir();
setUpTempArtifacts(tempDir);
final exitCode = await IOOverrides.runZoned(
() => runWithOverrides(command.run),
getCurrentDirectory: () => tempDir,
);
expect(exitCode, ExitCode.success.code);
},
);
test('throws error when release artifact does not exist.', () async {
when(
() => httpClient.send(
any(
that: isA<http.Request>().having(
(req) => req.url.toString(),
'url',
endsWith('so'),
),
),
),
).thenAnswer(
(_) async => http.StreamedResponse(
const Stream.empty(),
HttpStatus.notFound,
reasonPhrase: 'Not Found',
),
);
final tempDir = setUpTempDir();
setUpTempArtifacts(tempDir);
final exitCode = await IOOverrides.runZoned(
() => runWithOverrides(command.run),
getCurrentDirectory: () => tempDir,
);
verify(
() => progress.fail(any(that: contains('404 Not Found'))),
).called(1);
expect(exitCode, ExitCode.software.code);
});
test('exits if confirmUnpatchableDiffsIfNecessary returns false', () async {
when(() => argResults['force']).thenReturn(false);
when(
@@ -710,12 +749,7 @@ Please re-run the release command for this version or create a new release.'''),
verify(() => logger.success('\n✅ Published Patch!')).called(1);
verify(() => codePushClientWrapper.getApp(appId: appId)).called(1);
verify(
() => codePushClientWrapper.getRelease(
appId: appId,
releaseVersion: version,
),
).called(1);
verify(() => codePushClientWrapper.getReleases(appId: appId)).called(1);
verify(
() => codePushClientWrapper.getReleaseArtifacts(
appId: appId,