fix(shorebird_cli): don't fail if a release artifact has already been uploaded (#554)

Co-authored-by: Felix Angelov <felix@shorebird.dev>
This commit is contained in:
Bryan Oltman
2023-05-30 11:37:25 -04:00
committed by GitHub
parent 21c095ebaf
commit 6abe32b956
4 changed files with 141 additions and 23 deletions
@@ -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;
@@ -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(
@@ -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<String, dynamic>;
@@ -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<String, dynamic>);
}
@@ -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<String, dynamic>);
}
@@ -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<String, dynamic>;
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<String, dynamic>;
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<String, dynamic>;
@@ -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<String, dynamic>;
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<String, dynamic>;
@@ -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<String, dynamic>;
@@ -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<String, dynamic>;
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);
}
}
@@ -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<CodePushConflictException>()),
);
});
test('throws an exception if the http request fails', () async {
when(() => httpClient.send(any())).thenAnswer((_) async {
return http.StreamedResponse(