From e5a4018d65b0812138ef76e9e5ca5c189c93a438 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Mon, 23 Feb 2026 10:33:21 +0100 Subject: [PATCH 1/7] fix(artifacts): Report error for unknown artifact types in distribution (EME-422) Replace the TODO in _do_distribution() with a call to _update_artifact_error() so that unsupported artifact types are properly reported back to Sentry instead of silently failing. --- src/launchpad/artifact_processor.py | 9 +++++-- .../unit/artifacts/test_artifact_processor.py | 24 +++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/launchpad/artifact_processor.py b/src/launchpad/artifact_processor.py index 3a1e2384..8144fb04 100644 --- a/src/launchpad/artifact_processor.py +++ b/src/launchpad/artifact_processor.py @@ -331,9 +331,14 @@ def _do_distribution( with apk.raw_file() as f: self._sentry_client.upload_installable_app(organization_id, project_id, artifact_id, f) else: - # TODO(EME-422): Should call _update_artifact_error here once we - # support setting errors just for build. logger.error(f"BUILD_DISTRIBUTION failed for {artifact_id} (project: {project_id}, org: {organization_id})") + self._update_artifact_error( + organization_id, + project_id, + artifact_id, + ProcessingErrorCode.ARTIFACT_PROCESSING_ERROR, + ProcessingErrorMessage.UNSUPPORTED_ARTIFACT_TYPE, + ) def _do_size( self, diff --git a/tests/unit/artifacts/test_artifact_processor.py b/tests/unit/artifacts/test_artifact_processor.py index bdd18b2e..b0e97945 100644 --- a/tests/unit/artifacts/test_artifact_processor.py +++ b/tests/unit/artifacts/test_artifact_processor.py @@ -6,6 +6,7 @@ ) from launchpad.artifact_processor import ArtifactProcessor +from launchpad.artifacts.artifact import Artifact from launchpad.constants import ( ProcessingErrorCode, ProcessingErrorMessage, @@ -137,6 +138,29 @@ def test_processing_error_message_enum_values(self): assert ProcessingErrorMessage.SIZE_ANALYSIS_FAILED.value == "Failed to perform size analysis" assert ProcessingErrorMessage.UNKNOWN_ERROR.value == "An unknown error occurred" + def test_do_distribution_unknown_artifact_type_reports_error(self): + """Test that _do_distribution reports an error for unknown artifact types.""" + mock_sentry_client = Mock(spec=SentryClient) + mock_sentry_client.update_artifact.return_value = None + self.processor._sentry_client = mock_sentry_client + + unknown_artifact = Mock(spec=Artifact) + mock_info = Mock() + + self.processor._do_distribution( + "test-org-id", "test-project-id", "test-artifact-id", unknown_artifact, mock_info + ) + + mock_sentry_client.update_artifact.assert_called_once_with( + org="test-org-id", + project="test-project-id", + artifact_id="test-artifact-id", + data={ + "error_code": ProcessingErrorCode.ARTIFACT_PROCESSING_ERROR.value, + "error_message": ProcessingErrorMessage.UNSUPPORTED_ARTIFACT_TYPE.value, + }, + ) + class TestArtifactProcessorMessageHandling: """Test message processing functionality in ArtifactProcessor.""" From 1fcda90ec43914bddd17521646b3d7cb56a73f51 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Mon, 23 Feb 2026 11:25:06 +0100 Subject: [PATCH 2/7] fix(artifacts): Report error for iOS distribution failures (EME-422) When a ZippedXCArchive has an invalid code signature or is a simulator build, _do_distribution now reports the specific error via _update_artifact_error instead of silently doing nothing. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/launchpad/artifact_processor.py | 33 ++++++++++--- src/launchpad/constants.py | 2 + .../unit/artifacts/test_artifact_processor.py | 47 +++++++++++++++++++ 3 files changed, 75 insertions(+), 7 deletions(-) diff --git a/src/launchpad/artifact_processor.py b/src/launchpad/artifact_processor.py index 8144fb04..88ec5915 100644 --- a/src/launchpad/artifact_processor.py +++ b/src/launchpad/artifact_processor.py @@ -307,13 +307,32 @@ def _do_distribution( logger.info(f"BUILD_DISTRIBUTION for {artifact_id} (project: {project_id}, org: {organization_id})") if isinstance(artifact, ZippedXCArchive): apple_info = cast(AppleAppInfo, info) - if apple_info.is_code_signature_valid and not apple_info.is_simulator: - with tempfile.TemporaryDirectory() as temp_dir_str: - temp_dir = Path(temp_dir_str) - ipa_path = temp_dir / "App.ipa" - artifact.generate_ipa(ipa_path) - with open(ipa_path, "rb") as f: - self._sentry_client.upload_installable_app(organization_id, project_id, artifact_id, f) + if not apple_info.is_code_signature_valid: + logger.warning(f"BUILD_DISTRIBUTION skipped for {artifact_id}: invalid code signature") + self._update_artifact_error( + organization_id, + project_id, + artifact_id, + ProcessingErrorCode.ARTIFACT_PROCESSING_ERROR, + ProcessingErrorMessage.INVALID_CODE_SIGNATURE, + ) + return + if apple_info.is_simulator: + logger.warning(f"BUILD_DISTRIBUTION skipped for {artifact_id}: simulator build") + self._update_artifact_error( + organization_id, + project_id, + artifact_id, + ProcessingErrorCode.ARTIFACT_PROCESSING_ERROR, + ProcessingErrorMessage.SIMULATOR_BUILD, + ) + return + with tempfile.TemporaryDirectory() as temp_dir_str: + temp_dir = Path(temp_dir_str) + ipa_path = temp_dir / "App.ipa" + artifact.generate_ipa(ipa_path) + with open(ipa_path, "rb") as f: + self._sentry_client.upload_installable_app(organization_id, project_id, artifact_id, f) elif isinstance(artifact, (AAB, ZippedAAB)): with tempfile.TemporaryDirectory() as temp_dir_str: temp_dir = Path(temp_dir_str) diff --git a/src/launchpad/constants.py b/src/launchpad/constants.py index 4eb84c0f..93e25d93 100644 --- a/src/launchpad/constants.py +++ b/src/launchpad/constants.py @@ -49,6 +49,8 @@ class ProcessingErrorMessage(Enum): SIZE_ANALYSIS_FAILED = "Failed to perform size analysis" ARTIFACT_PARSING_FAILED = "Failed to parse artifact file" UNSUPPORTED_ARTIFACT_TYPE = "Unsupported artifact type" + INVALID_CODE_SIGNATURE = "Cannot distribute app with invalid code signature" + SIMULATOR_BUILD = "Cannot distribute simulator builds" # System-related errors TEMP_FILE_CREATION_FAILED = "Failed to create temporary file" diff --git a/tests/unit/artifacts/test_artifact_processor.py b/tests/unit/artifacts/test_artifact_processor.py index b0e97945..bb5248c4 100644 --- a/tests/unit/artifacts/test_artifact_processor.py +++ b/tests/unit/artifacts/test_artifact_processor.py @@ -6,6 +6,7 @@ ) from launchpad.artifact_processor import ArtifactProcessor +from launchpad.artifacts.apple.zipped_xcarchive import ZippedXCArchive from launchpad.artifacts.artifact import Artifact from launchpad.constants import ( ProcessingErrorCode, @@ -161,6 +162,52 @@ def test_do_distribution_unknown_artifact_type_reports_error(self): }, ) + def test_do_distribution_invalid_code_signature_reports_error(self): + mock_sentry_client = Mock(spec=SentryClient) + mock_sentry_client.update_artifact.return_value = None + self.processor._sentry_client = mock_sentry_client + + artifact = Mock(spec=ZippedXCArchive) + mock_info = Mock() + mock_info.is_code_signature_valid = False + mock_info.is_simulator = False + + self.processor._do_distribution("test-org-id", "test-project-id", "test-artifact-id", artifact, mock_info) + + mock_sentry_client.update_artifact.assert_called_once_with( + org="test-org-id", + project="test-project-id", + artifact_id="test-artifact-id", + data={ + "error_code": ProcessingErrorCode.ARTIFACT_PROCESSING_ERROR.value, + "error_message": ProcessingErrorMessage.INVALID_CODE_SIGNATURE.value, + }, + ) + mock_sentry_client.upload_installable_app.assert_not_called() + + def test_do_distribution_simulator_build_reports_error(self): + mock_sentry_client = Mock(spec=SentryClient) + mock_sentry_client.update_artifact.return_value = None + self.processor._sentry_client = mock_sentry_client + + artifact = Mock(spec=ZippedXCArchive) + mock_info = Mock() + mock_info.is_code_signature_valid = True + mock_info.is_simulator = True + + self.processor._do_distribution("test-org-id", "test-project-id", "test-artifact-id", artifact, mock_info) + + mock_sentry_client.update_artifact.assert_called_once_with( + org="test-org-id", + project="test-project-id", + artifact_id="test-artifact-id", + data={ + "error_code": ProcessingErrorCode.ARTIFACT_PROCESSING_ERROR.value, + "error_message": ProcessingErrorMessage.SIMULATOR_BUILD.value, + }, + ) + mock_sentry_client.upload_installable_app.assert_not_called() + class TestArtifactProcessorMessageHandling: """Test message processing functionality in ArtifactProcessor.""" From c4003d6d26cfbfee9871865afb0f4da658ed69ff Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Mon, 2 Mar 2026 13:25:46 +0100 Subject: [PATCH 3/7] wip --- src/launchpad/artifact_processor.py | 48 ++++++++++--------- src/launchpad/constants.py | 9 +++- .../unit/artifacts/test_artifact_processor.py | 20 ++++---- 3 files changed, 43 insertions(+), 34 deletions(-) diff --git a/src/launchpad/artifact_processor.py b/src/launchpad/artifact_processor.py index 88ec5915..9d6ddc74 100644 --- a/src/launchpad/artifact_processor.py +++ b/src/launchpad/artifact_processor.py @@ -34,6 +34,7 @@ from launchpad.artifacts.artifact_factory import ArtifactFactory from launchpad.constants import ( ArtifactType, + DistributionState, PreprodFeature, ProcessingErrorCode, ProcessingErrorMessage, @@ -309,23 +310,11 @@ def _do_distribution( apple_info = cast(AppleAppInfo, info) if not apple_info.is_code_signature_valid: logger.warning(f"BUILD_DISTRIBUTION skipped for {artifact_id}: invalid code signature") - self._update_artifact_error( - organization_id, - project_id, - artifact_id, - ProcessingErrorCode.ARTIFACT_PROCESSING_ERROR, - ProcessingErrorMessage.INVALID_CODE_SIGNATURE, - ) + self._update_distribution_skip(organization_id, project_id, artifact_id, "invalid_signature") return if apple_info.is_simulator: logger.warning(f"BUILD_DISTRIBUTION skipped for {artifact_id}: simulator build") - self._update_artifact_error( - organization_id, - project_id, - artifact_id, - ProcessingErrorCode.ARTIFACT_PROCESSING_ERROR, - ProcessingErrorMessage.SIMULATOR_BUILD, - ) + self._update_distribution_skip(organization_id, project_id, artifact_id, "simulator") return with tempfile.TemporaryDirectory() as temp_dir_str: temp_dir = Path(temp_dir_str) @@ -350,14 +339,8 @@ def _do_distribution( with apk.raw_file() as f: self._sentry_client.upload_installable_app(organization_id, project_id, artifact_id, f) else: - logger.error(f"BUILD_DISTRIBUTION failed for {artifact_id} (project: {project_id}, org: {organization_id})") - self._update_artifact_error( - organization_id, - project_id, - artifact_id, - ProcessingErrorCode.ARTIFACT_PROCESSING_ERROR, - ProcessingErrorMessage.UNSUPPORTED_ARTIFACT_TYPE, - ) + logger.error(f"BUILD_DISTRIBUTION failed for {artifact_id}: unsupported artifact type") + self._update_distribution_skip(organization_id, project_id, artifact_id, "unsupported") def _do_size( self, @@ -439,6 +422,27 @@ def _update_artifact_error( else: logger.info(f"Successfully updated artifact {artifact_id} with error information") + def _update_distribution_skip( + self, + organization_id: str, + project_id: str, + artifact_id: str, + skip_reason: str, + ) -> None: + """Update artifact with distribution skip state.""" + try: + self._sentry_client.update_artifact( + org=organization_id, + project=project_id, + artifact_id=artifact_id, + data={ + "distribution_state": DistributionState.NOT_RAN.value, + "distribution_skip_reason": skip_reason, + }, + ) + except SentryClientError: + logger.exception(f"Failed to update distribution skip for artifact {artifact_id}") + def _update_size_error_from_exception( self, organization_id: str, diff --git a/src/launchpad/constants.py b/src/launchpad/constants.py index 93e25d93..9f958d99 100644 --- a/src/launchpad/constants.py +++ b/src/launchpad/constants.py @@ -32,6 +32,13 @@ class PreprodFeature(Enum): BUILD_DISTRIBUTION = "build_distribution" +# Matches PreprodArtifact.DistributionState in sentry +class DistributionState(Enum): + PENDING = 0 + COMPLETED = 1 + NOT_RAN = 2 + + # Health check threshold - consider unhealthy if file not touched in 60 seconds HEALTHCHECK_MAX_AGE_SECONDS = 60.0 @@ -49,8 +56,6 @@ class ProcessingErrorMessage(Enum): SIZE_ANALYSIS_FAILED = "Failed to perform size analysis" ARTIFACT_PARSING_FAILED = "Failed to parse artifact file" UNSUPPORTED_ARTIFACT_TYPE = "Unsupported artifact type" - INVALID_CODE_SIGNATURE = "Cannot distribute app with invalid code signature" - SIMULATOR_BUILD = "Cannot distribute simulator builds" # System-related errors TEMP_FILE_CREATION_FAILED = "Failed to create temporary file" diff --git a/tests/unit/artifacts/test_artifact_processor.py b/tests/unit/artifacts/test_artifact_processor.py index bb5248c4..c7d1fbee 100644 --- a/tests/unit/artifacts/test_artifact_processor.py +++ b/tests/unit/artifacts/test_artifact_processor.py @@ -9,6 +9,7 @@ from launchpad.artifacts.apple.zipped_xcarchive import ZippedXCArchive from launchpad.artifacts.artifact import Artifact from launchpad.constants import ( + DistributionState, ProcessingErrorCode, ProcessingErrorMessage, ) @@ -139,8 +140,7 @@ def test_processing_error_message_enum_values(self): assert ProcessingErrorMessage.SIZE_ANALYSIS_FAILED.value == "Failed to perform size analysis" assert ProcessingErrorMessage.UNKNOWN_ERROR.value == "An unknown error occurred" - def test_do_distribution_unknown_artifact_type_reports_error(self): - """Test that _do_distribution reports an error for unknown artifact types.""" + def test_do_distribution_unknown_artifact_type_skips(self): mock_sentry_client = Mock(spec=SentryClient) mock_sentry_client.update_artifact.return_value = None self.processor._sentry_client = mock_sentry_client @@ -157,12 +157,12 @@ def test_do_distribution_unknown_artifact_type_reports_error(self): project="test-project-id", artifact_id="test-artifact-id", data={ - "error_code": ProcessingErrorCode.ARTIFACT_PROCESSING_ERROR.value, - "error_message": ProcessingErrorMessage.UNSUPPORTED_ARTIFACT_TYPE.value, + "distribution_state": DistributionState.NOT_RAN.value, + "distribution_skip_reason": "unsupported", }, ) - def test_do_distribution_invalid_code_signature_reports_error(self): + def test_do_distribution_invalid_code_signature_skips(self): mock_sentry_client = Mock(spec=SentryClient) mock_sentry_client.update_artifact.return_value = None self.processor._sentry_client = mock_sentry_client @@ -179,13 +179,13 @@ def test_do_distribution_invalid_code_signature_reports_error(self): project="test-project-id", artifact_id="test-artifact-id", data={ - "error_code": ProcessingErrorCode.ARTIFACT_PROCESSING_ERROR.value, - "error_message": ProcessingErrorMessage.INVALID_CODE_SIGNATURE.value, + "distribution_state": DistributionState.NOT_RAN.value, + "distribution_skip_reason": "invalid_signature", }, ) mock_sentry_client.upload_installable_app.assert_not_called() - def test_do_distribution_simulator_build_reports_error(self): + def test_do_distribution_simulator_build_skips(self): mock_sentry_client = Mock(spec=SentryClient) mock_sentry_client.update_artifact.return_value = None self.processor._sentry_client = mock_sentry_client @@ -202,8 +202,8 @@ def test_do_distribution_simulator_build_reports_error(self): project="test-project-id", artifact_id="test-artifact-id", data={ - "error_code": ProcessingErrorCode.ARTIFACT_PROCESSING_ERROR.value, - "error_message": ProcessingErrorMessage.SIMULATOR_BUILD.value, + "distribution_state": DistributionState.NOT_RAN.value, + "distribution_skip_reason": "simulator", }, ) mock_sentry_client.upload_installable_app.assert_not_called() From ba10319e7919982517f20329f055325fc8ce56ee Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Fri, 6 Mar 2026 10:25:02 +0100 Subject: [PATCH 4/7] fix(artifacts): Use distribution endpoint for skip reporting (EME-422) The update_artifact endpoint silently ignored distribution_state and distribution_skip_reason fields. Use the dedicated distribution endpoint that accepts error_code and error_message instead. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/launchpad/artifact_processor.py | 13 +++---- src/launchpad/constants.py | 11 +++--- src/launchpad/sentry_client.py | 18 ++++++++++ .../unit/artifacts/test_artifact_processor.py | 35 +++++++------------ 4 files changed, 42 insertions(+), 35 deletions(-) diff --git a/src/launchpad/artifact_processor.py b/src/launchpad/artifact_processor.py index 9d6ddc74..dddcee44 100644 --- a/src/launchpad/artifact_processor.py +++ b/src/launchpad/artifact_processor.py @@ -34,7 +34,7 @@ from launchpad.artifacts.artifact_factory import ArtifactFactory from launchpad.constants import ( ArtifactType, - DistributionState, + InstallableAppErrorCode, PreprodFeature, ProcessingErrorCode, ProcessingErrorMessage, @@ -429,16 +429,13 @@ def _update_distribution_skip( artifact_id: str, skip_reason: str, ) -> None: - """Update artifact with distribution skip state.""" + """Report distribution skip via the dedicated distribution endpoint.""" try: - self._sentry_client.update_artifact( + self._sentry_client.update_distribution_error( org=organization_id, - project=project_id, artifact_id=artifact_id, - data={ - "distribution_state": DistributionState.NOT_RAN.value, - "distribution_skip_reason": skip_reason, - }, + error_code=InstallableAppErrorCode.SKIPPED.value, + error_message=skip_reason, ) except SentryClientError: logger.exception(f"Failed to update distribution skip for artifact {artifact_id}") diff --git a/src/launchpad/constants.py b/src/launchpad/constants.py index 9f958d99..8f449417 100644 --- a/src/launchpad/constants.py +++ b/src/launchpad/constants.py @@ -32,11 +32,12 @@ class PreprodFeature(Enum): BUILD_DISTRIBUTION = "build_distribution" -# Matches PreprodArtifact.DistributionState in sentry -class DistributionState(Enum): - PENDING = 0 - COMPLETED = 1 - NOT_RAN = 2 +# Matches InstallableApp.ErrorCode in sentry +class InstallableAppErrorCode(Enum): + UNKNOWN = 0 + NO_QUOTA = 1 + SKIPPED = 2 + PROCESSING_ERROR = 3 # Health check threshold - consider unhealthy if file not touched in 60 seconds diff --git a/src/launchpad/sentry_client.py b/src/launchpad/sentry_client.py index e2581ecc..24d03872 100644 --- a/src/launchpad/sentry_client.py +++ b/src/launchpad/sentry_client.py @@ -252,6 +252,24 @@ def update_artifact(self, org: str, project: str, artifact_id: str, data: Dict[s endpoint = f"/api/0/internal/{org}/{project}/files/preprodartifacts/{artifact_id}/update/" return self._make_json_request("PUT", endpoint, UpdateResponse, data=data) + def update_distribution_error(self, org: str, artifact_id: str, error_code: int, error_message: str) -> None: + """Report distribution error via the dedicated distribution endpoint.""" + endpoint = f"/api/0/organizations/{org}/preprodartifacts/{artifact_id}/distribution/" + url = self._build_url(endpoint) + body = json.dumps({"error_code": error_code, "error_message": error_message}).encode("utf-8") + + logger.debug(f"PUT {url}") + response = self.session.request( + method="PUT", + url=url, + data=body, + auth=self.auth, + timeout=30, + ) + + if response.status_code != 200: + raise SentryClientError(response=response) + def upload_size_analysis_file( self, org: str, diff --git a/tests/unit/artifacts/test_artifact_processor.py b/tests/unit/artifacts/test_artifact_processor.py index c7d1fbee..5ecd505d 100644 --- a/tests/unit/artifacts/test_artifact_processor.py +++ b/tests/unit/artifacts/test_artifact_processor.py @@ -9,7 +9,7 @@ from launchpad.artifacts.apple.zipped_xcarchive import ZippedXCArchive from launchpad.artifacts.artifact import Artifact from launchpad.constants import ( - DistributionState, + InstallableAppErrorCode, ProcessingErrorCode, ProcessingErrorMessage, ) @@ -142,7 +142,7 @@ def test_processing_error_message_enum_values(self): def test_do_distribution_unknown_artifact_type_skips(self): mock_sentry_client = Mock(spec=SentryClient) - mock_sentry_client.update_artifact.return_value = None + mock_sentry_client.update_distribution_error.return_value = None self.processor._sentry_client = mock_sentry_client unknown_artifact = Mock(spec=Artifact) @@ -152,19 +152,16 @@ def test_do_distribution_unknown_artifact_type_skips(self): "test-org-id", "test-project-id", "test-artifact-id", unknown_artifact, mock_info ) - mock_sentry_client.update_artifact.assert_called_once_with( + mock_sentry_client.update_distribution_error.assert_called_once_with( org="test-org-id", - project="test-project-id", artifact_id="test-artifact-id", - data={ - "distribution_state": DistributionState.NOT_RAN.value, - "distribution_skip_reason": "unsupported", - }, + error_code=InstallableAppErrorCode.SKIPPED.value, + error_message="unsupported", ) def test_do_distribution_invalid_code_signature_skips(self): mock_sentry_client = Mock(spec=SentryClient) - mock_sentry_client.update_artifact.return_value = None + mock_sentry_client.update_distribution_error.return_value = None self.processor._sentry_client = mock_sentry_client artifact = Mock(spec=ZippedXCArchive) @@ -174,20 +171,17 @@ def test_do_distribution_invalid_code_signature_skips(self): self.processor._do_distribution("test-org-id", "test-project-id", "test-artifact-id", artifact, mock_info) - mock_sentry_client.update_artifact.assert_called_once_with( + mock_sentry_client.update_distribution_error.assert_called_once_with( org="test-org-id", - project="test-project-id", artifact_id="test-artifact-id", - data={ - "distribution_state": DistributionState.NOT_RAN.value, - "distribution_skip_reason": "invalid_signature", - }, + error_code=InstallableAppErrorCode.SKIPPED.value, + error_message="invalid_signature", ) mock_sentry_client.upload_installable_app.assert_not_called() def test_do_distribution_simulator_build_skips(self): mock_sentry_client = Mock(spec=SentryClient) - mock_sentry_client.update_artifact.return_value = None + mock_sentry_client.update_distribution_error.return_value = None self.processor._sentry_client = mock_sentry_client artifact = Mock(spec=ZippedXCArchive) @@ -197,14 +191,11 @@ def test_do_distribution_simulator_build_skips(self): self.processor._do_distribution("test-org-id", "test-project-id", "test-artifact-id", artifact, mock_info) - mock_sentry_client.update_artifact.assert_called_once_with( + mock_sentry_client.update_distribution_error.assert_called_once_with( org="test-org-id", - project="test-project-id", artifact_id="test-artifact-id", - data={ - "distribution_state": DistributionState.NOT_RAN.value, - "distribution_skip_reason": "simulator", - }, + error_code=InstallableAppErrorCode.SKIPPED.value, + error_message="simulator", ) mock_sentry_client.upload_installable_app.assert_not_called() From 19057249eb6872f34f5b8584d22dab04e2c8f445 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Fri, 6 Mar 2026 14:21:41 +0100 Subject: [PATCH 5/7] fix(artifacts): Use PROCESSING_ERROR for unsupported artifact types (EME-422) Unsupported artifact types are genuine errors, not intentional skips. Report them with PROCESSING_ERROR instead of SKIPPED so they surface correctly to users. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/launchpad/artifact_processor.py | 10 +++++++++- tests/unit/artifacts/test_artifact_processor.py | 6 +++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/launchpad/artifact_processor.py b/src/launchpad/artifact_processor.py index dddcee44..fa85926a 100644 --- a/src/launchpad/artifact_processor.py +++ b/src/launchpad/artifact_processor.py @@ -340,7 +340,15 @@ def _do_distribution( self._sentry_client.upload_installable_app(organization_id, project_id, artifact_id, f) else: logger.error(f"BUILD_DISTRIBUTION failed for {artifact_id}: unsupported artifact type") - self._update_distribution_skip(organization_id, project_id, artifact_id, "unsupported") + try: + self._sentry_client.update_distribution_error( + org=organization_id, + artifact_id=artifact_id, + error_code=InstallableAppErrorCode.PROCESSING_ERROR.value, + error_message="unsupported artifact type", + ) + except SentryClientError: + logger.exception(f"Failed to update distribution error for artifact {artifact_id}") def _do_size( self, diff --git a/tests/unit/artifacts/test_artifact_processor.py b/tests/unit/artifacts/test_artifact_processor.py index 5ecd505d..853d055a 100644 --- a/tests/unit/artifacts/test_artifact_processor.py +++ b/tests/unit/artifacts/test_artifact_processor.py @@ -140,7 +140,7 @@ def test_processing_error_message_enum_values(self): assert ProcessingErrorMessage.SIZE_ANALYSIS_FAILED.value == "Failed to perform size analysis" assert ProcessingErrorMessage.UNKNOWN_ERROR.value == "An unknown error occurred" - def test_do_distribution_unknown_artifact_type_skips(self): + def test_do_distribution_unknown_artifact_type_reports_error(self): mock_sentry_client = Mock(spec=SentryClient) mock_sentry_client.update_distribution_error.return_value = None self.processor._sentry_client = mock_sentry_client @@ -155,8 +155,8 @@ def test_do_distribution_unknown_artifact_type_skips(self): mock_sentry_client.update_distribution_error.assert_called_once_with( org="test-org-id", artifact_id="test-artifact-id", - error_code=InstallableAppErrorCode.SKIPPED.value, - error_message="unsupported", + error_code=InstallableAppErrorCode.PROCESSING_ERROR.value, + error_message="unsupported artifact type", ) def test_do_distribution_invalid_code_signature_skips(self): From 4ef03e1f44d30cdbb2bf0d2c851eb9124771f30a Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Mon, 9 Mar 2026 10:38:20 +0100 Subject: [PATCH 6/7] fix(artifacts): Widen exception handling for distribution notifications (EME-422) Catch all exceptions (not just SentryClientError) when reporting distribution errors/skips. Network errors like ConnectionError and Timeout from requests aren't subclasses of SentryClientError, so they would propagate uncaught and crash the pipeline. These are best-effort notifications that should never block artifact processing. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/launchpad/artifact_processor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/launchpad/artifact_processor.py b/src/launchpad/artifact_processor.py index fa85926a..f1651d6b 100644 --- a/src/launchpad/artifact_processor.py +++ b/src/launchpad/artifact_processor.py @@ -347,7 +347,7 @@ def _do_distribution( error_code=InstallableAppErrorCode.PROCESSING_ERROR.value, error_message="unsupported artifact type", ) - except SentryClientError: + except Exception: logger.exception(f"Failed to update distribution error for artifact {artifact_id}") def _do_size( @@ -445,7 +445,7 @@ def _update_distribution_skip( error_code=InstallableAppErrorCode.SKIPPED.value, error_message=skip_reason, ) - except SentryClientError: + except Exception: logger.exception(f"Failed to update distribution skip for artifact {artifact_id}") def _update_size_error_from_exception( From 10c7af71ab627be28ee813f4513ab23fc7adacb3 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Wed, 11 Mar 2026 17:46:56 +0100 Subject: [PATCH 7/7] fix(artifacts): Remove iOS distribution skip reporting (EME-422) Revert the iOS-specific changes that reported skip reasons for invalid code signatures and simulator builds. Keep the unsupported artifact type error reporting via the distribution endpoint. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/launchpad/artifact_processor.py | 39 ++++-------------- .../unit/artifacts/test_artifact_processor.py | 41 ------------------- 2 files changed, 7 insertions(+), 73 deletions(-) diff --git a/src/launchpad/artifact_processor.py b/src/launchpad/artifact_processor.py index f1651d6b..ebbc4e94 100644 --- a/src/launchpad/artifact_processor.py +++ b/src/launchpad/artifact_processor.py @@ -308,20 +308,13 @@ def _do_distribution( logger.info(f"BUILD_DISTRIBUTION for {artifact_id} (project: {project_id}, org: {organization_id})") if isinstance(artifact, ZippedXCArchive): apple_info = cast(AppleAppInfo, info) - if not apple_info.is_code_signature_valid: - logger.warning(f"BUILD_DISTRIBUTION skipped for {artifact_id}: invalid code signature") - self._update_distribution_skip(organization_id, project_id, artifact_id, "invalid_signature") - return - if apple_info.is_simulator: - logger.warning(f"BUILD_DISTRIBUTION skipped for {artifact_id}: simulator build") - self._update_distribution_skip(organization_id, project_id, artifact_id, "simulator") - return - with tempfile.TemporaryDirectory() as temp_dir_str: - temp_dir = Path(temp_dir_str) - ipa_path = temp_dir / "App.ipa" - artifact.generate_ipa(ipa_path) - with open(ipa_path, "rb") as f: - self._sentry_client.upload_installable_app(organization_id, project_id, artifact_id, f) + if apple_info.is_code_signature_valid and not apple_info.is_simulator: + with tempfile.TemporaryDirectory() as temp_dir_str: + temp_dir = Path(temp_dir_str) + ipa_path = temp_dir / "App.ipa" + artifact.generate_ipa(ipa_path) + with open(ipa_path, "rb") as f: + self._sentry_client.upload_installable_app(organization_id, project_id, artifact_id, f) elif isinstance(artifact, (AAB, ZippedAAB)): with tempfile.TemporaryDirectory() as temp_dir_str: temp_dir = Path(temp_dir_str) @@ -430,24 +423,6 @@ def _update_artifact_error( else: logger.info(f"Successfully updated artifact {artifact_id} with error information") - def _update_distribution_skip( - self, - organization_id: str, - project_id: str, - artifact_id: str, - skip_reason: str, - ) -> None: - """Report distribution skip via the dedicated distribution endpoint.""" - try: - self._sentry_client.update_distribution_error( - org=organization_id, - artifact_id=artifact_id, - error_code=InstallableAppErrorCode.SKIPPED.value, - error_message=skip_reason, - ) - except Exception: - logger.exception(f"Failed to update distribution skip for artifact {artifact_id}") - def _update_size_error_from_exception( self, organization_id: str, diff --git a/tests/unit/artifacts/test_artifact_processor.py b/tests/unit/artifacts/test_artifact_processor.py index 853d055a..1d668100 100644 --- a/tests/unit/artifacts/test_artifact_processor.py +++ b/tests/unit/artifacts/test_artifact_processor.py @@ -6,7 +6,6 @@ ) from launchpad.artifact_processor import ArtifactProcessor -from launchpad.artifacts.apple.zipped_xcarchive import ZippedXCArchive from launchpad.artifacts.artifact import Artifact from launchpad.constants import ( InstallableAppErrorCode, @@ -159,46 +158,6 @@ def test_do_distribution_unknown_artifact_type_reports_error(self): error_message="unsupported artifact type", ) - def test_do_distribution_invalid_code_signature_skips(self): - mock_sentry_client = Mock(spec=SentryClient) - mock_sentry_client.update_distribution_error.return_value = None - self.processor._sentry_client = mock_sentry_client - - artifact = Mock(spec=ZippedXCArchive) - mock_info = Mock() - mock_info.is_code_signature_valid = False - mock_info.is_simulator = False - - self.processor._do_distribution("test-org-id", "test-project-id", "test-artifact-id", artifact, mock_info) - - mock_sentry_client.update_distribution_error.assert_called_once_with( - org="test-org-id", - artifact_id="test-artifact-id", - error_code=InstallableAppErrorCode.SKIPPED.value, - error_message="invalid_signature", - ) - mock_sentry_client.upload_installable_app.assert_not_called() - - def test_do_distribution_simulator_build_skips(self): - mock_sentry_client = Mock(spec=SentryClient) - mock_sentry_client.update_distribution_error.return_value = None - self.processor._sentry_client = mock_sentry_client - - artifact = Mock(spec=ZippedXCArchive) - mock_info = Mock() - mock_info.is_code_signature_valid = True - mock_info.is_simulator = True - - self.processor._do_distribution("test-org-id", "test-project-id", "test-artifact-id", artifact, mock_info) - - mock_sentry_client.update_distribution_error.assert_called_once_with( - org="test-org-id", - artifact_id="test-artifact-id", - error_code=InstallableAppErrorCode.SKIPPED.value, - error_message="simulator", - ) - mock_sentry_client.upload_installable_app.assert_not_called() - class TestArtifactProcessorMessageHandling: """Test message processing functionality in ArtifactProcessor."""