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:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user