From 3dc991cc910277f0c2b7ac33f301534d7fa765a2 Mon Sep 17 00:00:00 2001 From: Felix Angelov Date: Tue, 16 Apr 2024 14:03:04 -0500 Subject: [PATCH] fix(shorebird_cli): ensure all release artifacts are built using the same flutter revision (#1911) --- .../release/release_android_command.dart | 11 ++++ .../src/commands/release/release_command.dart | 21 +++++++ .../commands/release/release_ios_command.dart | 11 ++++ .../release/release_android_command_test.dart | 52 +++++++++++++++ .../release/release_ios_command_test.dart | 63 +++++++++++++++++++ 5 files changed, 158 insertions(+) diff --git a/packages/shorebird_cli/lib/src/commands/release/release_android_command.dart b/packages/shorebird_cli/lib/src/commands/release/release_android_command.dart index 41ef5468..850ad6c8 100644 --- a/packages/shorebird_cli/lib/src/commands/release/release_android_command.dart +++ b/packages/shorebird_cli/lib/src/commands/release/release_android_command.dart @@ -245,6 +245,17 @@ Use `shorebird flutter versions list` to list available versions. release: existingRelease, platform: releasePlatform, ); + + // All artifacts associated with a given release must be built + // with the same Flutter revision. + if (existingRelease.flutterRevision != flutterRevisionForRelease) { + ReleaseCommand.printConflictingFlutterRevisionError( + existingFlutterRevision: existingRelease.flutterRevision, + currentFlutterRevision: flutterRevisionForRelease, + releaseVersion: releaseVersion, + ); + return ExitCode.software.code; + } } final archNames = architectures.map((a) => a.name); diff --git a/packages/shorebird_cli/lib/src/commands/release/release_command.dart b/packages/shorebird_cli/lib/src/commands/release/release_command.dart index ae452646..3165f463 100644 --- a/packages/shorebird_cli/lib/src/commands/release/release_command.dart +++ b/packages/shorebird_cli/lib/src/commands/release/release_command.dart @@ -22,6 +22,27 @@ class ReleaseCommand extends ShorebirdCommand { @override String get name => 'release'; + static void printConflictingFlutterRevisionError({ + required String existingFlutterRevision, + required String currentFlutterRevision, + required String releaseVersion, + }) { + logger.err( + ''' +${styleBold.wrap(lightRed.wrap('A release with version $releaseVersion already exists but was built using a different Flutter revision.'))} + + Existing release built with: ${lightCyan.wrap(existingFlutterRevision)} + Current release built with: ${lightCyan.wrap(currentFlutterRevision)} + +${styleBold.wrap(lightRed.wrap('All platforms for a given release must be built using the same Flutter revision.'))} + +To resolve this issue, you can: + * Re-run the release command with "${lightCyan.wrap('--flutter-version=$existingFlutterRevision')}". + * Delete the existing release and re-run the release command with the desired Flutter version. + * Bump the release version and re-run the release command with the desired Flutter version.''', + ); + } + static void printPatchInstructions({ required String name, required String releaseVersion, diff --git a/packages/shorebird_cli/lib/src/commands/release/release_ios_command.dart b/packages/shorebird_cli/lib/src/commands/release/release_ios_command.dart index 4dec9e0c..e1dbcd86 100644 --- a/packages/shorebird_cli/lib/src/commands/release/release_ios_command.dart +++ b/packages/shorebird_cli/lib/src/commands/release/release_ios_command.dart @@ -255,6 +255,17 @@ Use `shorebird flutter versions list` to list available versions. release: existingRelease, platform: releasePlatform, ); + + // All artifacts associated with a given release must be built + // with the same Flutter revision. + if (existingRelease.flutterRevision != flutterRevisionForRelease) { + ReleaseCommand.printConflictingFlutterRevisionError( + existingFlutterRevision: existingRelease.flutterRevision, + currentFlutterRevision: flutterRevisionForRelease, + releaseVersion: releaseVersion, + ); + return ExitCode.software.code; + } } final summary = [ diff --git a/packages/shorebird_cli/test/src/commands/release/release_android_command_test.dart b/packages/shorebird_cli/test/src/commands/release/release_android_command_test.dart index 9b774b68..8a601793 100644 --- a/packages/shorebird_cli/test/src/commands/release/release_android_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/release/release_android_command_test.dart @@ -836,6 +836,58 @@ Note: ${lightCyan.wrap('shorebird patch android --flavor=$flavor --target=$targe ); }); + test( + 'does not upload artifacts if an existing release is present ' + 'with a different flutter revision', () async { + when( + () => codePushClientWrapper.maybeGetRelease( + appId: any(named: 'appId'), + releaseVersion: any(named: 'releaseVersion'), + ), + ).thenAnswer( + (_) async => Release( + id: release.id, + appId: release.appId, + version: release.version, + flutterRevision: 'different-revision', + displayName: release.displayName, + createdAt: release.createdAt.add(const Duration(minutes: 1)), + updatedAt: release.createdAt.add(const Duration(minutes: 1)), + platformStatuses: release.platformStatuses, + ), + ); + final exitCode = await runWithOverrides(command.run); + expect(exitCode, ExitCode.software.code); + verify( + () => logger.err( + any( + that: contains( + '''All platforms for a given release must be built using the same Flutter revision.''', + ), + ), + ), + ).called(1); + verifyNever( + () => codePushClientWrapper.createRelease( + appId: any(named: 'appId'), + version: any(named: 'version'), + flutterRevision: any(named: 'flutterRevision'), + platform: any(named: 'platform'), + ), + ); + verifyNever( + () => codePushClientWrapper.createAndroidReleaseArtifacts( + appId: any(named: 'appId'), + releaseId: any(named: 'releaseId'), + projectRoot: any(named: 'projectRoot'), + aabPath: any(named: 'aabPath'), + platform: any(named: 'platform'), + architectures: any(named: 'architectures'), + flavor: any(named: 'flavor'), + ), + ); + }); + test('does not prompt if unable to accept user input', () async { when(() => shorebirdEnv.canAcceptUserInput).thenReturn(false); diff --git a/packages/shorebird_cli/test/src/commands/release/release_ios_command_test.dart b/packages/shorebird_cli/test/src/commands/release/release_ios_command_test.dart index 32ac0e02..cf208a7c 100644 --- a/packages/shorebird_cli/test/src/commands/release/release_ios_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/release/release_ios_command_test.dart @@ -1083,6 +1083,69 @@ Note: ${lightCyan.wrap('shorebird patch ios --flavor=$flavor --target=$target')} ).called(1); }); + test( + 'does not upload artifacts if an existing release is present ' + 'with a different flutter revision', () async { + when( + () => codePushClientWrapper.maybeGetRelease( + appId: any(named: 'appId'), + releaseVersion: any(named: 'releaseVersion'), + ), + ).thenAnswer( + (_) async => Release( + id: release.id, + appId: release.appId, + version: release.version, + flutterRevision: 'different-revision', + displayName: release.displayName, + createdAt: release.createdAt.add(const Duration(minutes: 1)), + updatedAt: release.createdAt.add(const Duration(minutes: 1)), + platformStatuses: release.platformStatuses, + ), + ); + setUpProjectRoot(); + + final exitCode = await runWithOverrides(command.run); + expect(exitCode, ExitCode.software.code); + + verify( + () => logger.err( + any( + that: contains( + '''All platforms for a given release must be built using the same Flutter revision.''', + ), + ), + ), + ).called(1); + verifyNever( + () => codePushClientWrapper.createRelease( + appId: any(named: 'appId'), + version: any(named: 'version'), + flutterRevision: any(named: 'flutterRevision'), + platform: any(named: 'platform'), + ), + ); + verifyNever( + () => codePushClientWrapper.createIosReleaseArtifacts( + appId: any(named: 'appId'), + releaseId: any(named: 'releaseId'), + xcarchivePath: + any(named: 'xcarchivePath', that: endsWith('.xcarchive')), + runnerPath: any(named: 'runnerPath', that: endsWith('Runner.app')), + isCodesigned: any(named: 'isCodesigned'), + ), + ); + verifyNever( + () => codePushClientWrapper.updateReleaseStatus( + appId: any(named: 'appId'), + releaseId: any(named: 'releaseId'), + platform: any(named: 'platform'), + status: any(named: 'status'), + metadata: any(named: 'metadata'), + ), + ); + }); + test('does not provide export options when codesign is false', () async { when(() => argResults['codesign']).thenReturn(false); setUpProjectRoot();