feat(shorebird_cli): shorebird patch <platform> --release-version=latest (#2782)
This commit is contained in:
@@ -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)),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user