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 ca82762d..45c8c02c 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 @@ -211,6 +211,8 @@ ${summary.join('\n')} } } + // TODO(bryanoltman): Consolidate aab and other artifact creation. + // TODO(bryanoltman): Parallelize artifact creation. final createArtifactProgress = logger.progress('Creating artifacts'); for (final archMetadata in architectures.values) { final artifactPath = p.join( @@ -236,6 +238,13 @@ ${summary.join('\n')} platform: platform, hash: hash, ); + } on CodePushConflictException catch (_) { + // Newlines are due to how logger.info interacts with logger.progress. + logger.info( + ''' + +${archMetadata.arch} artifact already exists, continuing...''', + ); } catch (error) { createArtifactProgress.fail('Error uploading ${artifact.path}: $error'); return ExitCode.software.code; @@ -250,6 +259,13 @@ ${summary.join('\n')} platform: platform, hash: _hashFn(await File(bundlePath).readAsBytes()), ); + } on CodePushConflictException catch (_) { + // Newlines are due to how logger.info interacts with logger.progress. + logger.info( + ''' + +aab artifact already exists, continuing...''', + ); } catch (error) { createArtifactProgress.fail('Error uploading $bundlePath: $error'); return ExitCode.software.code; 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 53590f35..6926a386 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 @@ -473,6 +473,66 @@ Did you forget to run "shorebird init"?''', expect(exitCode, ExitCode.software.code); }); + test('logs message when uploading release artifact that already exists.', + () async { + const error = 'something went wrong'; + when( + () => codePushClient.getReleases(appId: any(named: 'appId')), + ).thenAnswer((_) async => []); + when( + () => codePushClient.createReleaseArtifact( + artifactPath: any(named: 'artifactPath'), + releaseId: any(named: 'releaseId'), + arch: any(named: 'arch'), + platform: any(named: 'platform'), + hash: any(named: 'hash'), + ), + ).thenThrow(const CodePushConflictException(message: error)); + final tempDir = setUpTempDir(); + setUpTempArtifacts(tempDir); + final exitCode = await IOOverrides.runZoned( + command.run, + getCurrentDirectory: () => tempDir, + ); + + // 1 for each arch, 1 for the aab + final numArtifactsUploaded = Arch.values.length + 1; + verify( + () => logger.info(any(that: contains('already exists'))), + ).called(numArtifactsUploaded); + verifyNever(() => progress.fail(error)); + expect(exitCode, ExitCode.success.code); + }); + + test('logs message when uploading aab that already exists.', () async { + const error = 'something went wrong'; + when( + () => codePushClient.getReleases(appId: any(named: 'appId')), + ).thenAnswer((_) async => []); + when( + () => codePushClient.createReleaseArtifact( + artifactPath: any(named: 'artifactPath', that: endsWith('.aab')), + releaseId: any(named: 'releaseId'), + arch: any(named: 'arch'), + platform: any(named: 'platform'), + hash: any(named: 'hash'), + ), + ).thenThrow(const CodePushConflictException(message: error)); + final tempDir = setUpTempDir(); + setUpTempArtifacts(tempDir); + final exitCode = await IOOverrides.runZoned( + command.run, + getCurrentDirectory: () => tempDir, + ); + verify( + () => logger.info( + any(that: contains('aab artifact already exists, continuing...')), + ), + ).called(1); + verifyNever(() => progress.fail(error)); + expect(exitCode, ExitCode.success.code); + }); + test('throws error when uploading release artifact fails.', () async { const error = 'something went wrong'; when( diff --git a/packages/shorebird_code_push_client/lib/src/code_push_client.dart b/packages/shorebird_code_push_client/lib/src/code_push_client.dart index 089dc0f1..41e3d356 100644 --- a/packages/shorebird_code_push_client/lib/src/code_push_client.dart +++ b/packages/shorebird_code_push_client/lib/src/code_push_client.dart @@ -21,6 +21,14 @@ class CodePushException implements Exception { String toString() => '$message${details != null ? '\n$details' : ''}'; } +/// {@template code_push_conflict_exception} +/// Exception thrown when a 409 response is received. +/// {@endtemplate} +class CodePushConflictException extends CodePushException { + /// {@macro code_push_conflict_exception} + const CodePushConflictException({required super.message, super.details}); +} + /// {@template code_push_client} /// Dart client for the Shorebird CodePush API. /// {@endtemplate} @@ -54,7 +62,7 @@ class CodePushClient { ); if (response.statusCode != HttpStatus.created) { - throw _parseErrorResponse(response.body); + throw _parseErrorResponse(response.statusCode, response.body); } } @@ -66,7 +74,7 @@ class CodePushClient { if (response.statusCode == HttpStatus.notFound) { return null; } else if (response.statusCode != HttpStatus.ok) { - throw _parseErrorResponse(response.body); + throw _parseErrorResponse(response.statusCode, response.body); } final json = jsonDecode(response.body) as Map; @@ -96,7 +104,9 @@ class CodePushClient { final response = await _httpClient.send(request); final body = await response.stream.bytesToString(); - if (response.statusCode != HttpStatus.ok) throw _parseErrorResponse(body); + if (response.statusCode != HttpStatus.ok) { + throw _parseErrorResponse(response.statusCode, body); + } return PatchArtifact.fromJson(json.decode(body) as Map); } @@ -108,7 +118,7 @@ class CodePushClient { ); if (response.statusCode != HttpStatus.ok) { - throw _parseErrorResponse(response.body); + throw _parseErrorResponse(response.statusCode, response.body); } return CreatePaymentLinkResponse.fromJson( @@ -139,7 +149,9 @@ class CodePushClient { final response = await _httpClient.send(request); final body = await response.stream.bytesToString(); - if (response.statusCode != HttpStatus.ok) throw _parseErrorResponse(body); + if (response.statusCode != HttpStatus.ok) { + throw _parseErrorResponse(response.statusCode, body); + } return ReleaseArtifact.fromJson(json.decode(body) as Map); } @@ -153,7 +165,7 @@ class CodePushClient { ); if (response.statusCode != HttpStatus.ok) { - throw _parseErrorResponse(response.body); + throw _parseErrorResponse(response.statusCode, response.body); } final body = json.decode(response.body) as Map; return App.fromJson(body); @@ -170,7 +182,7 @@ class CodePushClient { ); if (response.statusCode != HttpStatus.ok) { - throw _parseErrorResponse(response.body); + throw _parseErrorResponse(response.statusCode, response.body); } final body = json.decode(response.body) as Map; return Channel.fromJson(body); @@ -184,7 +196,7 @@ class CodePushClient { ); if (response.statusCode != HttpStatus.ok) { - throw _parseErrorResponse(response.body); + throw _parseErrorResponse(response.statusCode, response.body); } final body = json.decode(response.body) as Map; @@ -209,7 +221,7 @@ class CodePushClient { ); if (response.statusCode != HttpStatus.ok) { - throw _parseErrorResponse(response.body); + throw _parseErrorResponse(response.statusCode, response.body); } final body = json.decode(response.body) as Map; return Release.fromJson(body); @@ -225,7 +237,7 @@ class CodePushClient { ); if (response.statusCode != HttpStatus.noContent) { - throw _parseErrorResponse(response.body); + throw _parseErrorResponse(response.statusCode, response.body); } } @@ -236,7 +248,7 @@ class CodePushClient { ); if (response.statusCode != HttpStatus.noContent) { - throw _parseErrorResponse(response.body); + throw _parseErrorResponse(response.statusCode, response.body); } } @@ -252,7 +264,7 @@ class CodePushClient { ); if (response.statusCode != HttpStatus.created) { - throw _parseErrorResponse(response.body); + throw _parseErrorResponse(response.statusCode, response.body); } final body = json.decode(response.body) as Json; @@ -266,7 +278,7 @@ class CodePushClient { ); if (response.statusCode != HttpStatus.noContent) { - throw _parseErrorResponse(response.body); + throw _parseErrorResponse(response.statusCode, response.body); } } @@ -275,7 +287,7 @@ class CodePushClient { final response = await _httpClient.get(Uri.parse('$_v1/apps')); if (response.statusCode != HttpStatus.ok) { - throw _parseErrorResponse(response.body); + throw _parseErrorResponse(response.statusCode, response.body); } final apps = json.decode(response.body) as List; @@ -293,7 +305,7 @@ class CodePushClient { ); if (response.statusCode != HttpStatus.ok) { - throw _parseErrorResponse(response.body); + throw _parseErrorResponse(response.statusCode, response.body); } final channels = json.decode(response.body) as List; @@ -309,7 +321,7 @@ class CodePushClient { ); if (response.statusCode != HttpStatus.ok) { - throw _parseErrorResponse(response.body); + throw _parseErrorResponse(response.statusCode, response.body); } final releases = json.decode(response.body) as List; @@ -329,7 +341,7 @@ class CodePushClient { ); if (response.statusCode != HttpStatus.ok) { - throw _parseErrorResponse(response.body); + throw _parseErrorResponse(response.statusCode, response.body); } final releases = json.decode(response.body) as List; @@ -354,7 +366,7 @@ class CodePushClient { ); if (response.statusCode != HttpStatus.ok) { - throw _parseErrorResponse(response.body); + throw _parseErrorResponse(response.statusCode, response.body); } final body = json.decode(response.body) as Map; @@ -372,7 +384,7 @@ class CodePushClient { ); if (response.statusCode != HttpStatus.created) { - throw _parseErrorResponse(response.body); + throw _parseErrorResponse(response.statusCode, response.body); } } @@ -383,7 +395,7 @@ class CodePushClient { ); if (response.statusCode != HttpStatus.ok) { - throw _parseErrorResponse(response.body); + throw _parseErrorResponse(response.statusCode, response.body); } final json = jsonDecode(response.body) as Map; @@ -394,14 +406,18 @@ class CodePushClient { /// Closes the client. void close() => _httpClient.close(); - CodePushException _parseErrorResponse(String response) { + CodePushException _parseErrorResponse(int statusCode, String response) { + final exceptionBuilder = statusCode == HttpStatus.conflict + ? CodePushConflictException.new + : CodePushException.new; + final ErrorResponse error; try { final body = json.decode(response) as Map; error = ErrorResponse.fromJson(body); } catch (_) { - throw const CodePushException(message: unknownErrorMessage); + throw exceptionBuilder(message: unknownErrorMessage); } - return CodePushException(message: error.message, details: error.details); + return exceptionBuilder(message: error.message, details: error.details); } } diff --git a/packages/shorebird_code_push_client/test/src/code_push_client_test.dart b/packages/shorebird_code_push_client/test/src/code_push_client_test.dart index c7a23859..5bab712c 100644 --- a/packages/shorebird_code_push_client/test/src/code_push_client_test.dart +++ b/packages/shorebird_code_push_client/test/src/code_push_client_test.dart @@ -386,6 +386,32 @@ void main() { ); }); + test( + 'throws a CodePushConflictException if the http response code is 409', + () { + when(() => httpClient.send(any())).thenAnswer((_) async { + return http.StreamedResponse( + Stream.value(utf8.encode(json.encode(errorResponse.toJson()))), + HttpStatus.conflict, + ); + }); + + final tempDir = Directory.systemTemp.createTempSync(); + final fixture = File(path.join(tempDir.path, 'release.txt')) + ..createSync(); + + expect( + codePushClient.createReleaseArtifact( + artifactPath: fixture.path, + releaseId: releaseId, + arch: arch, + platform: platform, + hash: hash, + ), + throwsA(isA()), + ); + }); + test('throws an exception if the http request fails', () async { when(() => httpClient.send(any())).thenAnswer((_) async { return http.StreamedResponse(