From f28fb358ed2506631ce34cc1394afb3a6447ff37 Mon Sep 17 00:00:00 2001 From: Bryan Oltman Date: Fri, 23 Jun 2023 17:24:18 -0400 Subject: [PATCH] feat(shorebird_cli): upload ipa as part of shorebird release ios (#705) --- .../lib/src/code_push_client_wrapper.dart | 23 ++++ .../commands/release/release_ios_command.dart | 18 +-- .../src/code_push_client_wrapper_test.dart | 107 ++++++++++++++++++ .../release/release_ios_command_test.dart | 30 +++++ 4 files changed, 171 insertions(+), 7 deletions(-) diff --git a/packages/shorebird_cli/lib/src/code_push_client_wrapper.dart b/packages/shorebird_cli/lib/src/code_push_client_wrapper.dart index f43ba9e5..4b17430f 100644 --- a/packages/shorebird_cli/lib/src/code_push_client_wrapper.dart +++ b/packages/shorebird_cli/lib/src/code_push_client_wrapper.dart @@ -442,6 +442,29 @@ aar artifact already exists, continuing...''', createArtifactProgress.complete(); } + /// Uploads a release ipa to the Shorebird server. + Future createIosReleaseArtifact({ + required int releaseId, + required String ipaPath, + }) async { + final createArtifactProgress = logger.progress('Creating artifacts'); + final ipaFile = File(ipaPath); + try { + await codePushClient.createReleaseArtifact( + releaseId: releaseId, + artifactPath: ipaPath, + arch: 'ipa', + platform: 'ios', + hash: sha256.convert(await ipaFile.readAsBytes()).toString(), + ); + } catch (error) { + createArtifactProgress.fail('Error uploading ipa: $error'); + exit(ExitCode.software.code); + } + + createArtifactProgress.complete(); + } + @visibleForTesting Future createPatch({required int releaseId}) async { final createPatchProgress = logger.progress('Creating patch'); 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 ddff83dc..5142121a 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 @@ -160,16 +160,20 @@ ${summary.join('\n')} return ExitCode.software.code; } - if (existingRelease == null) { - await codePushClientWrapper.createRelease( - appId: appId, - version: releaseVersion, - flutterRevision: shorebirdFlutterRevision, - ); - } + final release = existingRelease ?? + await codePushClientWrapper.createRelease( + appId: appId, + version: releaseVersion, + flutterRevision: shorebirdFlutterRevision, + ); final relativeIpaPath = p.relative(ipaPath); + await codePushClientWrapper.createIosReleaseArtifact( + releaseId: release.id, + ipaPath: ipaPath, + ); + logger ..success('\n✅ Published Release!') ..info(''' diff --git a/packages/shorebird_cli/test/src/code_push_client_wrapper_test.dart b/packages/shorebird_cli/test/src/code_push_client_wrapper_test.dart index 2a28ee16..00969b8e 100644 --- a/packages/shorebird_cli/test/src/code_push_client_wrapper_test.dart +++ b/packages/shorebird_cli/test/src/code_push_client_wrapper_test.dart @@ -1308,6 +1308,113 @@ Please bump your version number and try again.''', }); }); + group('createIosReleaseArtifact', () { + final ipaPath = p.join('path', 'to', 'app.ipa'); + + Directory setUpTempDir({String? flavor}) { + final tempDir = Directory.systemTemp.createTempSync(); + File(p.join(tempDir.path, ipaPath)).createSync(recursive: true); + return tempDir; + } + + setUp(() { + when( + () => codePushClient.createReleaseArtifact( + artifactPath: any(named: 'artifactPath'), + releaseId: any(named: 'releaseId'), + arch: any(named: 'arch'), + platform: any(named: 'platform'), + hash: any(named: 'hash'), + ), + ).thenAnswer((_) async => {}); + }); + + test('exits with code 70 when artifact creation fails', () async { + const error = 'something went wrong'; + when( + () => codePushClient.createReleaseArtifact( + artifactPath: any(named: 'artifactPath'), + releaseId: any(named: 'releaseId'), + arch: any(named: 'arch'), + platform: any(named: 'platform'), + hash: any(named: 'hash'), + ), + ).thenThrow(error); + final tempDir = setUpTempDir(); + + await IOOverrides.runZoned( + () async => expectLater( + () async => runWithOverrides( + () async => codePushClientWrapper.createIosReleaseArtifact( + releaseId: releaseId, + ipaPath: p.join(tempDir.path, ipaPath), + ), + ), + exitsWithCode(ExitCode.software), + ), + getCurrentDirectory: () => tempDir, + ); + + verify(() => progress.fail(any(that: contains(error)))).called(1); + }); + + test('exits with code 70 when uploading ipa that already exists', + () async { + const error = 'something went wrong'; + when( + () => codePushClient.createReleaseArtifact( + artifactPath: any(named: 'artifactPath', that: endsWith('.ipa')), + releaseId: any(named: 'releaseId'), + arch: any(named: 'arch'), + platform: any(named: 'platform'), + hash: any(named: 'hash'), + ), + ).thenThrow(const CodePushConflictException(message: error)); + final tempDir = setUpTempDir(); + + await IOOverrides.runZoned( + () async => expectLater( + () async => runWithOverrides( + () async => codePushClientWrapper.createIosReleaseArtifact( + releaseId: releaseId, + ipaPath: p.join(tempDir.path, ipaPath), + ), + ), + exitsWithCode(ExitCode.software), + ), + getCurrentDirectory: () => tempDir, + ); + + verify(() => progress.fail(any(that: contains(error)))).called(1); + }); + + test('completes successfully when artifact is created', () async { + when( + () => codePushClient.createReleaseArtifact( + artifactPath: any(named: 'artifactPath'), + releaseId: any(named: 'releaseId'), + arch: any(named: 'arch'), + platform: any(named: 'platform'), + hash: any(named: 'hash'), + ), + ).thenAnswer((_) async => {}); + final tempDir = setUpTempDir(); + + await runWithOverrides( + () async => IOOverrides.runZoned( + () async => codePushClientWrapper.createIosReleaseArtifact( + releaseId: releaseId, + ipaPath: p.join(tempDir.path, ipaPath), + ), + getCurrentDirectory: () => tempDir, + ), + ); + + verify(() => progress.complete()).called(1); + verifyNever(() => progress.fail(any())); + }); + }); + group('patch', () { group('createPatch', () { test('exits with code 70 when creating patch fails', () async { 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 f1cdddef..81d52a46 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 @@ -213,6 +213,12 @@ flutter: flutterRevision: any(named: 'flutterRevision'), ), ).thenAnswer((_) async => release); + when( + () => codePushClientWrapper.createIosReleaseArtifact( + releaseId: any(named: 'releaseId'), + ipaPath: any(named: 'ipaPath'), + ), + ).thenAnswer((_) async => release); when(() => flutterValidator.validate(any())).thenAnswer((_) async => []); command = runWithOverrides( @@ -389,6 +395,12 @@ error: exportArchive: No signing certificate "iOS Distribution" found expect(exitCode, ExitCode.success.code); verify(() => logger.info('Aborting.')).called(1); + verifyNever( + () => codePushClientWrapper.createIosReleaseArtifact( + releaseId: release.id, + ipaPath: any(named: 'ipaPath', that: endsWith('.ipa')), + ), + ); }); test('throws error when unable to detect flutter revision', () async { @@ -449,6 +461,12 @@ error: exportArchive: No signing certificate "iOS Distribution" found ), ), ).called(1); + verify( + () => codePushClientWrapper.createIosReleaseArtifact( + releaseId: release.id, + ipaPath: any(named: 'ipaPath', that: endsWith('.ipa')), + ), + ).called(1); expect(exitCode, ExitCode.success.code); }); @@ -485,6 +503,12 @@ flavors: ), ), ).called(1); + verify( + () => codePushClientWrapper.createIosReleaseArtifact( + releaseId: release.id, + ipaPath: any(named: 'ipaPath', that: endsWith('.ipa')), + ), + ).called(1); expect(exitCode, ExitCode.success.code); }); @@ -511,6 +535,12 @@ flavors: flutterRevision: any(named: 'flutterRevision'), ), ); + verify( + () => codePushClientWrapper.createIosReleaseArtifact( + releaseId: release.id, + ipaPath: any(named: 'ipaPath', that: endsWith('.ipa')), + ), + ).called(1); }); test('provides appropriate ExportOptions.plist to build ipa command',