feat(shorebird_cli): shorebird patch aar optimizations (#1026)
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user