feat(shorebird_cli): shorebird patch <platform> --release-version=latest (#2782)

This commit is contained in:
Felix Angelov
2025-01-17 17:15:14 -06:00
committed by GitHub
parent 3c21810340
commit 7f195df3a2
2 changed files with 161 additions and 11 deletions
@@ -77,8 +77,9 @@ class PatchCommand extends ShorebirdCommand {
..addOption(
'release-version',
help: '''
The version of the associated release (e.g. "1.0.0"). This should be the version
of the iOS app that is using this module.''',
The version of the associated release (e.g. "1.0.0").
If you are building an xcframework or aar, this number needs to match the host app's release version.
To target the latest release (e.g. the release that was most recently updated) use --release-version=latest.''',
)
..addFlag(
'allow-native-diffs',
@@ -186,6 +187,10 @@ NOTE: this is ${styleBold.wrap('not')} recommended. Asset changes cannot be incl
/// Whether the patch is for the staging environment.
bool get isStaging => track == DeploymentTrack.staging;
/// Whether the patch is targeting the latest release version
/// (--release-version=latest).
bool get useLatestRelease => results['release-version'] == 'latest';
/// The deployment track to publish the patch to.
DeploymentTrack get track {
final channel = results['track'] as String;
@@ -285,14 +290,30 @@ NOTE: this is ${styleBold.wrap('not')} recommended. Asset changes cannot be incl
File? patchArtifactFile;
final Release release;
if (results.wasParsed('release-version')) {
final releasePlatform = patcher.releaseType.releasePlatform;
if (useLatestRelease) {
final releases = await codePushClientWrapper.getReleases(appId: appId);
releases
..removeWhere(
(release) => !release.platformStatuses.keys.contains(releasePlatform),
)
..sortByUpdatedAt();
if (releases.isEmpty) {
logger.warn(
'''No ${releasePlatform.displayName} releases found for app $appId. You must first create a release before you can create a patch.''',
);
throw ProcessExit(ExitCode.usage.code);
}
// Use the most recently updated release for the specified platform.
release = releases.last;
} else if (results.wasParsed('release-version')) {
final releaseVersion = results['release-version'] as String;
release = await codePushClientWrapper.getRelease(
appId: appId,
releaseVersion: releaseVersion,
);
} else if (shorebirdEnv.canAcceptUserInput) {
release = await promptForRelease(patcher.releaseType.releasePlatform);
release = await promptForRelease(releasePlatform);
} else {
logger.info(
'''Tip: make your patches build faster by specifying --release-version''',
@@ -321,7 +342,7 @@ NOTE: this is ${styleBold.wrap('not')} recommended. Asset changes cannot be incl
appId: appId,
releaseId: release.id,
arch: patcher.primaryReleaseArtifactArch,
platform: patcher.releaseType.releasePlatform,
platform: releasePlatform,
);
final supplementalArtifact =
@@ -330,7 +351,7 @@ NOTE: this is ${styleBold.wrap('not')} recommended. Asset changes cannot be incl
appId: appId,
releaseId: release.id,
arch: patcher.supplementaryReleaseArtifactArch!,
platform: patcher.releaseType.releasePlatform,
platform: releasePlatform,
)
: null;
@@ -516,7 +537,7 @@ Please re-run the release command for this version or create a new release.''');
'''📱 App: ${lightCyan.wrap(app.displayName)} ${lightCyan.wrap('(${app.appId})')}''',
if (flavor != null) '🍧 Flavor: ${lightCyan.wrap(flavor)}',
'📦 Release Version: ${lightCyan.wrap(releaseVersion)}',
'''🕹️ Platform: ${lightCyan.wrap(patcher.releaseType.releasePlatform.name)} ${lightCyan.wrap('[${archMetadata.join(', ')}]')}''',
'''🕹️ Platform: ${lightCyan.wrap(patcher.releaseType.releasePlatform.displayName)} ${lightCyan.wrap('[${archMetadata.join(', ')}]')}''',
trackSummary,
if (patcher.linkPercentage != null &&
patcher.linkPercentage! < Patcher.minLinkPercentage)
@@ -559,3 +580,9 @@ ${summary.join('\n')}
return artifactFile;
}
}
/// Extension on list of releases for sorting the releases.
extension SortReleases on List<Release> {
/// Sort the list of releases by when they were last updated ascending.
void sortByUpdatedAt() => sort((a, b) => a.updatedAt.compareTo(b.updatedAt));
}
@@ -2,6 +2,7 @@ import 'dart:io';
import 'package:args/args.dart';
import 'package:collection/collection.dart';
import 'package:equatable/equatable.dart';
import 'package:mason_logger/mason_logger.dart';
import 'package:mocktail/mocktail.dart';
import 'package:scoped_deps/scoped_deps.dart';
@@ -31,6 +32,16 @@ import '../../helpers.dart';
import '../../matchers.dart';
import '../../mocks.dart';
class _FakeRelease extends Fake with EquatableMixin implements Release {
_FakeRelease({required this.updatedAt});
@override
final DateTime updatedAt;
@override
List<Object?> get props => [updatedAt];
}
void main() {
group(PatchCommand, () {
const appId = 'test-app-id';
@@ -590,7 +601,7 @@ void main() {
'''📱 App: ${lightCyan.wrap(appDisplayName)} ${lightCyan.wrap('($appId)')}''',
'🍧 Flavor: ${lightCyan.wrap(flavor)}',
'📦 Release Version: ${lightCyan.wrap(releaseVersion)}',
'''🕹️ Platform: ${lightCyan.wrap(patcher.releaseType.releasePlatform.name)} ${lightCyan.wrap('[arm32 (42 B)]')}''',
'''🕹️ Platform: ${lightCyan.wrap(patcher.releaseType.releasePlatform.displayName)} ${lightCyan.wrap('[arm32 (42 B)]')}''',
'🟢 Track: ${lightCyan.wrap('Stable')}',
];
await expectLater(
@@ -627,7 +638,7 @@ void main() {
final expectedSummary = [
'''📱 App: ${lightCyan.wrap(appDisplayName)} ${lightCyan.wrap('($appId)')}''',
'📦 Release Version: ${lightCyan.wrap(releaseVersion)}',
'''🕹️ Platform: ${lightCyan.wrap(patcher.releaseType.releasePlatform.name)} ${lightCyan.wrap('[arm32 (42 B)]')}''',
'''🕹️ Platform: ${lightCyan.wrap(patcher.releaseType.releasePlatform.displayName)} ${lightCyan.wrap('[arm32 (42 B)]')}''',
'🟠 Track: ${lightCyan.wrap('Staging')}',
];
await expectLater(
@@ -660,7 +671,7 @@ void main() {
final expectedSummary = [
'''📱 App: ${lightCyan.wrap(appDisplayName)} ${lightCyan.wrap('($appId)')}''',
'📦 Release Version: ${lightCyan.wrap(releaseVersion)}',
'''🕹️ Platform: ${lightCyan.wrap(patcher.releaseType.releasePlatform.name)} ${lightCyan.wrap('[arm32 (42 B)]')}''',
'''🕹️ Platform: ${lightCyan.wrap(patcher.releaseType.releasePlatform.displayName)} ${lightCyan.wrap('[arm32 (42 B)]')}''',
'🔵 Track: ${lightCyan.wrap('Beta')}',
];
await expectLater(
@@ -695,7 +706,7 @@ void main() {
final expectedSummary = [
'''📱 App: ${lightCyan.wrap(appDisplayName)} ${lightCyan.wrap('($appId)')}''',
'📦 Release Version: ${lightCyan.wrap(releaseVersion)}',
'''🕹️ Platform: ${lightCyan.wrap(patcher.releaseType.releasePlatform.name)} ${lightCyan.wrap('[arm32 (42 B)]')}''',
'''🕹️ Platform: ${lightCyan.wrap(patcher.releaseType.releasePlatform.displayName)} ${lightCyan.wrap('[arm32 (42 B)]')}''',
'🟢 Track: ${lightCyan.wrap('Stable')}',
'''🔍 Debug Info: ${lightCyan.wrap(patcher.debugInfoFile.path)}''',
];
@@ -787,6 +798,99 @@ void main() {
});
});
group('when release version is latest', () {
setUp(() {
when(() => argResults['release-version']).thenReturn('latest');
});
group('when no releases for the target platform exist', () {
setUp(() {
when(
() => codePushClientWrapper.getReleases(appId: any(named: 'appId')),
).thenAnswer(
(_) async => [
Release(
id: 0,
appId: appId,
version: releaseVersion,
flutterRevision: flutterRevision,
flutterVersion: flutterVersion,
displayName: '1.0.0+1',
platformStatuses: {
ReleasePlatform.windows: ReleaseStatus.active,
},
createdAt: DateTime(2023),
updatedAt: DateTime(2023),
)
],
);
});
test('warns and exits', () async {
await expectLater(
() => runWithOverrides(command.run),
exitsWithCode(ExitCode.usage),
);
verify(
() => codePushClientWrapper.getReleases(appId: appId),
).called(1);
verify(
() => logger.warn(
'''No ${releasePlatform.displayName} releases found for app $appId. You must first create a release before you can create a patch.''',
),
).called(1);
});
});
group('when multiple releases for the target platform exist', () {
setUp(() {
when(
() => codePushClientWrapper.getReleases(appId: any(named: 'appId')),
).thenAnswer(
(_) async => [
Release(
id: 0,
appId: appId,
version: releaseVersion,
flutterRevision: flutterRevision,
flutterVersion: flutterVersion,
displayName: releaseVersion,
platformStatuses: {
releasePlatform: ReleaseStatus.active,
},
createdAt: DateTime(2024),
updatedAt: DateTime(2024),
),
Release(
id: 1,
appId: appId,
version: '99.99.99+99',
flutterRevision: flutterRevision,
flutterVersion: flutterVersion,
displayName: '99.99.99+99',
platformStatuses: {
releasePlatform: ReleaseStatus.active,
},
createdAt: DateTime(2023),
updatedAt: DateTime(2023),
),
],
);
});
test('uses the latest version', () async {
await expectLater(runWithOverrides(command.run), completes);
verify(
() => codePushClientWrapper.getReleases(appId: appId),
).called(1);
verify(
() => patcher.buildPatchArtifact(releaseVersion: releaseVersion),
).called(1);
});
});
});
group('when release version is not specified', () {
setUp(() {
when(() => argResults.wasParsed('release-version')).thenReturn(false);
@@ -1211,4 +1315,23 @@ Please re-run the release command for this version or create a new release.''',
});
});
});
group('sortByUpdatedAt', () {
test('sorts versions correctly', () {
expect(
[
_FakeRelease(updatedAt: DateTime(2025, 05, 15)),
_FakeRelease(updatedAt: DateTime(2025, 04, 15)),
_FakeRelease(updatedAt: DateTime(2021, 09, 25)),
_FakeRelease(updatedAt: DateTime(2024)),
]..sortByUpdatedAt(),
equals([
_FakeRelease(updatedAt: DateTime(2021, 09, 25)),
_FakeRelease(updatedAt: DateTime(2024)),
_FakeRelease(updatedAt: DateTime(2025, 04, 15)),
_FakeRelease(updatedAt: DateTime(2025, 05, 15)),
]),
);
});
});
}