diff --git a/.github/actions/dart_package/action.yaml b/.github/actions/dart_package/action.yaml index 229f91d1..86754f3c 100644 --- a/.github/actions/dart_package/action.yaml +++ b/.github/actions/dart_package/action.yaml @@ -64,10 +64,10 @@ runs: dart pub global activate coverage dart test -j ${{inputs.concurrency}} --coverage=coverage --platform=${{inputs.platform}} && dart pub global run coverage:format_coverage --lcov --in=coverage --out=coverage/lcov.info --packages=.dart_tool/package_config.json --report-on=${{inputs.report_on}} --check-ignore - - name: Upload Coverage - uses: codecov/codecov-action@v3 - with: - token: ${{ secrets.CODECOV_TOKEN }} + # - name: Upload Coverage + # uses: codecov/codecov-action@v3 + # with: + # token: ${{ secrets.CODECOV_TOKEN }} - uses: VeryGoodOpenSource/very_good_coverage@v2 with: diff --git a/packages/shorebird_cli/lib/src/commands/release/release_ios_command.dart b/packages/shorebird_cli/lib/src/commands/release/release_ios_command.dart index d8ebc721..6d3634df 100644 --- a/packages/shorebird_cli/lib/src/commands/release/release_ios_command.dart +++ b/packages/shorebird_cli/lib/src/commands/release/release_ios_command.dart @@ -75,7 +75,11 @@ make smaller updates to your app. try { await buildIpa(flavor: flavor); } on ProcessException catch (error) { - buildProgress.fail('Failed to build: ${error.message}'); + buildProgress.fail('Failed to build IPA: ${error.message}'); + return ExitCode.software.code; + } on BuildException catch (error) { + buildProgress.fail('Failed to build IPA'); + logger.err(error.message); return ExitCode.software.code; } diff --git a/packages/shorebird_cli/lib/src/shorebird_build_mixin.dart b/packages/shorebird_cli/lib/src/shorebird_build_mixin.dart index 456ca4e4..67b4ee99 100644 --- a/packages/shorebird_cli/lib/src/shorebird_build_mixin.dart +++ b/packages/shorebird_cli/lib/src/shorebird_build_mixin.dart @@ -24,6 +24,17 @@ class ArchMetadata { final String enginePath; } +/// {@template build_exception} +/// Thrown when a build fails. +/// {@endtemplate} +class BuildException implements Exception { + /// {@macro build_exception} + BuildException(this.message); + + /// Information about the build failure. + final String message; +} + mixin ShorebirdBuildMixin on ShorebirdCommand { // This exists only so tests can get the full list. static const allAndroidArchitectures = { @@ -182,9 +193,42 @@ mixin ShorebirdBuildMixin on ShorebirdCommand { result.stderr.toString(), result.exitCode, ); + } else if (result.stderr + .toString() + .contains('Encountered error while creating the IPA')) { + final errorMessage = _failedToCreateIpaErrorMessage( + stderr: result.stderr.toString(), + ); + + throw BuildException(errorMessage); } } + String _failedToCreateIpaErrorMessage({required String stderr}) { + // The full error text consists of many repeated lines of the format: + // (newlines added for line length) + // + // error: exportArchive: No signing certificate "iOS Distribution" found + // error: exportArchive: Communication with Apple failed + // error: exportArchive: No signing certificate "iOS Distribution" found + // error: exportArchive: Team "My Team" does not have permission to + // create "iOS App Store" provisioning profiles. + // error: exportArchive: No profiles for 'com.example.demo' were found + // error: exportArchive: Communication with Apple failed + // error: exportArchive: No signing certificate "iOS Distribution" found + // error: exportArchive: Communication with Apple failed + final exportArchiveRegex = RegExp(r'^error: exportArchive: (.+)$'); + + return stderr + .split('\n') + .map((l) => l.trim()) + .toSet() + .map(exportArchiveRegex.firstMatch) + .whereType() + .map((m) => ' ${m.group(1)!}') + .join('\n'); + } + Future createDiff({ required String releaseArtifactPath, required String patchArtifactPath, diff --git a/packages/shorebird_cli/test/src/commands/release/release_ios_command_test.dart b/packages/shorebird_cli/test/src/commands/release/release_ios_command_test.dart index 82bfc4d8..fd82c8e6 100644 --- a/packages/shorebird_cli/test/src/commands/release/release_ios_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/release/release_ios_command_test.dart @@ -227,7 +227,8 @@ flutter: expect(exitCode, equals(ExitCode.noUser.code)); }); - test('exits with code 70 when building fails', () async { + test('exits with code 70 when build fails with non-zero exit code', + () async { when(() => flutterBuildProcessResult.exitCode).thenReturn(1); when(() => flutterBuildProcessResult.stderr).thenReturn('oops'); @@ -243,6 +244,43 @@ flutter: ).called(1); }); + test('exits with code 70 when building fails with 0 exit code', () async { + when(() => flutterBuildProcessResult.exitCode).thenReturn(0); + when(() => flutterBuildProcessResult.stderr).thenReturn(''' +Encountered error while creating the IPA: +error: exportArchive: Communication with Apple failed +error: exportArchive: No signing certificate "iOS Distribution" found +error: exportArchive: Communication with Apple failed +error: exportArchive: No signing certificate "iOS Distribution" found +error: exportArchive: Team "My Team" does not have permission to create "iOS App Store" provisioning profiles. +error: exportArchive: No profiles for 'com.example.co' were found +error: exportArchive: Communication with Apple failed +error: exportArchive: No signing certificate "iOS Distribution" found +error: exportArchive: Communication with Apple failed +error: exportArchive: No signing certificate "iOS Distribution" found +error: exportArchive: Communication with Apple failed +error: exportArchive: No signing certificate "iOS Distribution" found +'''); + + final tempDir = setUpTempDir(); + final exitCode = await IOOverrides.runZoned( + () => runWithOverrides(command.run), + getCurrentDirectory: () => tempDir, + ); + + expect(exitCode, equals(ExitCode.software.code)); + verify( + () => progress.fail(any(that: contains('Failed to build'))), + ).called(1); + verify( + () => logger.err(''' + Communication with Apple failed + No signing certificate "iOS Distribution" found + Team "My Team" does not have permission to create "iOS App Store" provisioning profiles. + No profiles for 'com.example.co' were found'''), + ).called(1); + }); + test('exits with code 70 when release version cannot be determiend', () async { when(() => ipa.versionNumber).thenThrow(Exception('oops'));