fix(shorebird_cli): exit non-zero when iOS IPA export fails (#3819)

Co-authored-by: Eric Seidel <eric@shorebird.dev>
This commit is contained in:
Brandon DeRosier
2026-06-26 13:52:27 -07:00
committed by GitHub
parent 0accf66986
commit b8379117e0
2 changed files with 55 additions and 0 deletions
@@ -129,6 +129,22 @@ If left checked, Xcode will rewrite the build number in the uploaded IPA, so the
throw ProcessExit(ExitCode.software.code);
}
// When code signing is requested (the default), `flutter build ipa` is
// expected to export a signed .ipa. Flutter treats the export step as
// optional and exits 0 even when it fails (e.g. no signing certificate),
// so we must verify the .ipa was actually produced. Otherwise we would
// report a successful release and point the user at an .ipa that does not
// exist. See https://github.com/shorebirdtech/shorebird/issues/3807.
if (codesign && artifactManager.getIpa() == null) {
logger.err(
'''
Unable to find generated IPA. This usually means that the IPA export step of "flutter build ipa" failed (for example, due to a missing or invalid code signing certificate). Review the build output above for the underlying error.
If you do not need a signed IPA (for example, you will sign the .xcarchive in Xcode), re-run this command with --no-codesign.''',
);
throw ProcessExit(ExitCode.software.code);
}
return xcarchiveDirectory;
}
@@ -423,6 +423,9 @@ $body
when(
() => artifactManager.getXcarchiveDirectory(),
).thenReturn(xcarchiveDirectory);
when(
() => artifactManager.getIpa(),
).thenReturn(File(p.join(Directory.systemTemp.path, 'app.ipa')));
when(
() => codeSigner.base64PublicKeyFromPem(any()),
@@ -660,6 +663,42 @@ $body
});
});
group('when codesigning and ipa not found after build', () {
setUp(() {
when(() => argResults['codesign']).thenReturn(true);
when(() => artifactManager.getIpa()).thenReturn(null);
});
test('logs message and exits with code 70', () async {
await expectLater(
() => runWithOverrides(iosReleaser.buildReleaseArtifacts),
exitsWithCode(ExitCode.software),
);
verify(
() => logger.err(
any(that: contains('Unable to find generated IPA')),
),
).called(1);
});
});
group('when not codesigning and ipa not found after build', () {
setUp(() {
when(() => argResults['codesign']).thenReturn(false);
when(() => artifactManager.getIpa()).thenReturn(null);
});
test('does not check for the ipa and returns xcarchive path', () async {
expect(
await runWithOverrides(iosReleaser.buildReleaseArtifacts),
equals(xcarchiveDirectory),
);
verifyNever(() => artifactManager.getIpa());
});
});
group('when --obfuscate is passed', () {
setUp(() {
when(() => argResults['obfuscate']).thenReturn(true);