From a4254dccdd9f13d724739d07fe45e808ac27b38e Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Mon, 23 Feb 2026 10:33:21 +0100 Subject: [PATCH 1/6] 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 423396f2..547c2268 100644 --- a/src/launchpad/artifact_processor.py +++ b/src/launchpad/artifact_processor.py @@ -326,9 +326,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 49aa2b51d288e068c0aa173150f97b22ecfdfeb8 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Mon, 23 Feb 2026 11:25:06 +0100 Subject: [PATCH 2/6] 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 547c2268..4174e9ca 100644 --- a/src/launchpad/artifact_processor.py +++ b/src/launchpad/artifact_processor.py @@ -302,13 +302,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 e003ab17eb52f5f98b5e2424472b134628d5f668 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Mon, 2 Mar 2026 13:25:46 +0100 Subject: [PATCH 3/6] 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 4174e9ca..c5684c9a 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, @@ -304,23 +305,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) @@ -345,14 +334,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, @@ -434,6 +417,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 fe8817ec0a59a568b8fd8f49ac103bffadd2343f Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Fri, 6 Mar 2026 10:25:02 +0100 Subject: [PATCH 4/6] 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 c5684c9a..d2928d7b 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, @@ -424,16 +424,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 e84e225006f71bd72e257787d5220175cc24586b Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Fri, 6 Mar 2026 14:21:41 +0100 Subject: [PATCH 5/6] 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 d2928d7b..ea849d9d 100644 --- a/src/launchpad/artifact_processor.py +++ b/src/launchpad/artifact_processor.py @@ -335,7 +335,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 591113640f91660e8b95d98dc9356c3cd801a96f Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Mon, 9 Mar 2026 10:38:20 +0100 Subject: [PATCH 6/6] 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 ea849d9d..1f6f5ced 100644 --- a/src/launchpad/artifact_processor.py +++ b/src/launchpad/artifact_processor.py @@ -342,7 +342,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( @@ -440,7 +440,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(