feat(shorebird_cli): upload ipa as part of shorebird release ios (#705)

This commit is contained in:
Bryan Oltman
2023-06-23 17:24:18 -04:00
committed by GitHub
parent 5d46d3fe36
commit f28fb358ed
4 changed files with 171 additions and 7 deletions
@@ -442,6 +442,29 @@ aar artifact already exists, continuing...''',
createArtifactProgress.complete();
}
/// Uploads a release ipa to the Shorebird server.
Future<void> 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<Patch> createPatch({required int releaseId}) async {
final createPatchProgress = logger.progress('Creating patch');
@@ -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('''
@@ -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 {
@@ -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',