From 7da0b6910612fa5aede94e513ec4773386046ec7 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Mon, 20 Apr 2026 17:55:33 +0200 Subject: [PATCH 1/3] feat(preprod): Add granular installable app error codes (EME-883) Extend InstallableAppErrorCode with specific values so the backend can express the precise reason a build isn't installable without stuffing semantics into the free-form error_message string: - DISTRIBUTION_DISABLED (4): project has build distribution turned off - DISTRIBUTION_FILTERED (5): build doesn't match project filter rules - INVALID_CODE_SIGNATURE (6): code signature could not be verified - SIMULATOR_BUILD (7): simulator builds cannot be distributed - UNSUPPORTED_ARTIFACT_TYPE (8): artifact type not supported Update the two Sentry-side emission sites in project_preprod_artifact_update to use the new specific codes for disabled/filtered cases instead of SKIPPED. For the launchpad-facing distribution PUT endpoint, add a legacy compatibility shim that translates the old payload shape (error_code=SKIPPED + error_message="invalid_signature", etc.) to the new granular codes, so current launchpad deployments keep working. Widen the pydantic validator to accept the new codes. Frontend can now switch on error_code alone to render titles, links, and other affordances without pattern-matching on backend strings. A matching launchpad change will follow to emit the new codes directly, after which the compat shim can be removed. --- .../project_preprod_artifact_update.py | 9 +-- .../endpoints/project_preprod_distribution.py | 45 ++++++++++- src/sentry/preprod/models.py | 19 ++++- .../test_project_preprod_artifact_update.py | 24 ++++-- .../test_project_preprod_distribution.py | 80 +++++++++++++++++-- 5 files changed, 155 insertions(+), 22 deletions(-) diff --git a/src/sentry/preprod/api/endpoints/project_preprod_artifact_update.py b/src/sentry/preprod/api/endpoints/project_preprod_artifact_update.py index 1fda7dde0c79..cec38f608971 100644 --- a/src/sentry/preprod/api/endpoints/project_preprod_artifact_update.py +++ b/src/sentry/preprod/api/endpoints/project_preprod_artifact_update.py @@ -437,16 +437,13 @@ def put( else: if distro_skip_reason == "quota": distro_error_code = PreprodArtifact.InstallableAppErrorCode.NO_QUOTA - distro_error_message = "Distribution quota exceeded" elif distro_skip_reason == "disabled": - distro_error_code = PreprodArtifact.InstallableAppErrorCode.SKIPPED - distro_error_message = "Distribution disabled for this project" + distro_error_code = PreprodArtifact.InstallableAppErrorCode.DISTRIBUTION_DISABLED else: - distro_error_code = PreprodArtifact.InstallableAppErrorCode.SKIPPED - distro_error_message = "Distribution filtered out by project settings" + distro_error_code = PreprodArtifact.InstallableAppErrorCode.DISTRIBUTION_FILTERED head_artifact.installable_app_error_code = distro_error_code - head_artifact.installable_app_error_message = distro_error_message + head_artifact.installable_app_error_message = None head_artifact.save( update_fields=[ "installable_app_error_code", diff --git a/src/sentry/preprod/api/endpoints/project_preprod_distribution.py b/src/sentry/preprod/api/endpoints/project_preprod_distribution.py index 3ae3c1882165..7757bd623489 100644 --- a/src/sentry/preprod/api/endpoints/project_preprod_distribution.py +++ b/src/sentry/preprod/api/endpoints/project_preprod_distribution.py @@ -23,11 +23,49 @@ logger = logging.getLogger(__name__) +_MAX_ERROR_CODE = max(int(c) for c in PreprodArtifact.InstallableAppErrorCode) + + class PutDistribution(BaseModel): - error_code: int = Field(ge=0, le=3) + error_code: int = Field(ge=0, le=_MAX_ERROR_CODE) error_message: str +# Launchpad historically encoded specific reasons inside the free-form +# error_message field (e.g. error_code=SKIPPED + error_message="invalid_signature"). +# Translate those legacy payloads to the new granular enum values so the frontend +# can work purely off error_code. Remove once all launchpad deployments emit the +# new codes directly. +_LEGACY_MESSAGE_TO_CODE: dict[ + tuple[PreprodArtifact.InstallableAppErrorCode, str], + PreprodArtifact.InstallableAppErrorCode, +] = { + ( + PreprodArtifact.InstallableAppErrorCode.SKIPPED, + "invalid_signature", + ): PreprodArtifact.InstallableAppErrorCode.INVALID_CODE_SIGNATURE, + ( + PreprodArtifact.InstallableAppErrorCode.SKIPPED, + "simulator", + ): PreprodArtifact.InstallableAppErrorCode.SIMULATOR_BUILD, + ( + PreprodArtifact.InstallableAppErrorCode.PROCESSING_ERROR, + "Unsupported artifact type", + ): PreprodArtifact.InstallableAppErrorCode.UNSUPPORTED_ARTIFACT_TYPE, +} + + +def _translate_legacy_payload(error_code: int, error_message: str) -> tuple[int, str | None]: + try: + code = PreprodArtifact.InstallableAppErrorCode(error_code) + except ValueError: + return error_code, error_message + translated = _LEGACY_MESSAGE_TO_CODE.get((code, error_message)) + if translated is None: + return error_code, error_message + return int(translated), None + + @internal_cell_silo_endpoint class ProjectPreprodDistributionEndpoint(PreprodArtifactEndpoint): owner = ApiOwner.EMERGE_TOOLS @@ -46,8 +84,9 @@ def put( ) -> Response: put: PutDistribution = parse_request_with_pydantic(request, cast(Any, PutDistribution)) - head_artifact.installable_app_error_code = put.error_code - head_artifact.installable_app_error_message = put.error_message + error_code, error_message = _translate_legacy_payload(put.error_code, put.error_message) + head_artifact.installable_app_error_code = error_code + head_artifact.installable_app_error_message = error_message head_artifact.save( update_fields=[ "installable_app_error_code", diff --git a/src/sentry/preprod/models.py b/src/sentry/preprod/models.py index 9f172db9d9dd..28ceea73dcc8 100644 --- a/src/sentry/preprod/models.py +++ b/src/sentry/preprod/models.py @@ -159,9 +159,19 @@ class InstallableAppErrorCode(IntEnum): NO_QUOTA = 1 """No quota available for distribution.""" SKIPPED = 2 - """Distribution was not requested on this build.""" + """Generic skip; a more specific code should be used when available.""" PROCESSING_ERROR = 3 - """Distribution failed due to a processing error.""" + """Generic distribution failure; a more specific code should be used when available.""" + DISTRIBUTION_DISABLED = 4 + """Build distribution is disabled in the project's settings.""" + DISTRIBUTION_FILTERED = 5 + """Build does not match the project's distribution filter rules.""" + INVALID_CODE_SIGNATURE = 6 + """The build's code signature could not be verified.""" + SIMULATOR_BUILD = 7 + """Simulator builds cannot be distributed.""" + UNSUPPORTED_ARTIFACT_TYPE = 8 + """The artifact type is not supported for distribution.""" @classmethod def as_choices(cls) -> tuple[tuple[int, str], ...]: @@ -170,6 +180,11 @@ def as_choices(cls) -> tuple[tuple[int, str], ...]: (cls.NO_QUOTA, "no_quota"), (cls.SKIPPED, "skipped"), (cls.PROCESSING_ERROR, "processing_error"), + (cls.DISTRIBUTION_DISABLED, "distribution_disabled"), + (cls.DISTRIBUTION_FILTERED, "distribution_filtered"), + (cls.INVALID_CODE_SIGNATURE, "invalid_code_signature"), + (cls.SIMULATOR_BUILD, "simulator_build"), + (cls.UNSUPPORTED_ARTIFACT_TYPE, "unsupported_artifact_type"), ) __relocation_scope__ = RelocationScope.Excluded diff --git a/tests/sentry/preprod/api/endpoints/test_project_preprod_artifact_update.py b/tests/sentry/preprod/api/endpoints/test_project_preprod_artifact_update.py index f4db03426398..2a302fd9f7a3 100644 --- a/tests/sentry/preprod/api/endpoints/test_project_preprod_artifact_update.py +++ b/tests/sentry/preprod/api/endpoints/test_project_preprod_artifact_update.py @@ -648,10 +648,10 @@ def test_update_sets_error_code_no_quota(self, mock_has_installable_quota) -> No self.preprod_artifact.installable_app_error_code == PreprodArtifact.InstallableAppErrorCode.NO_QUOTA ) - assert self.preprod_artifact.installable_app_error_message == "Distribution quota exceeded" + assert self.preprod_artifact.installable_app_error_message is None @override_settings(LAUNCHPAD_RPC_SHARED_SECRET=["test-secret-key"]) - def test_update_sets_error_code_skipped_when_filtered(self) -> None: + def test_update_sets_error_code_distribution_filtered(self) -> None: self.preprod_artifact.app_id = "com.my.app" self.preprod_artifact.save() @@ -664,12 +664,26 @@ def test_update_sets_error_code_skipped_when_filtered(self) -> None: self.preprod_artifact.refresh_from_db() assert ( self.preprod_artifact.installable_app_error_code - == PreprodArtifact.InstallableAppErrorCode.SKIPPED + == PreprodArtifact.InstallableAppErrorCode.DISTRIBUTION_FILTERED ) + assert self.preprod_artifact.installable_app_error_message is None + + @override_settings(LAUNCHPAD_RPC_SHARED_SECRET=["test-secret-key"]) + def test_update_sets_error_code_distribution_disabled(self) -> None: + from sentry.preprod.quotas import DISTRIBUTION_ENABLED_KEY + + self.project.update_option(DISTRIBUTION_ENABLED_KEY, False) + + data = {"artifact_type": 1} + response = self._make_request(data) + + assert response.status_code == 200 + self.preprod_artifact.refresh_from_db() assert ( - self.preprod_artifact.installable_app_error_message - == "Distribution filtered out by project settings" + self.preprod_artifact.installable_app_error_code + == PreprodArtifact.InstallableAppErrorCode.DISTRIBUTION_DISABLED ) + assert self.preprod_artifact.installable_app_error_message is None class FindOrCreateReleaseTest(TestCase): diff --git a/tests/sentry/preprod/api/endpoints/test_project_preprod_distribution.py b/tests/sentry/preprod/api/endpoints/test_project_preprod_distribution.py index a5bd25de0184..f4f4271334a9 100644 --- a/tests/sentry/preprod/api/endpoints/test_project_preprod_distribution.py +++ b/tests/sentry/preprod/api/endpoints/test_project_preprod_distribution.py @@ -61,10 +61,8 @@ def test_bad_json(self, mock_send_webhook) -> None: @patch( "sentry.preprod.api.endpoints.project_preprod_distribution.send_build_distribution_webhook" ) - def test_set_error(self, mock_send_webhook) -> None: - response = self._put( - orjson.dumps({"error_code": 3, "error_message": "Unsupported artifact type"}) - ) + def test_accepts_generic_processing_error(self, mock_send_webhook) -> None: + response = self._put(orjson.dumps({"error_code": 3, "error_message": "some novel failure"})) assert response.status_code == 200 self.artifact.refresh_from_db() @@ -72,13 +70,83 @@ def test_set_error(self, mock_send_webhook) -> None: self.artifact.installable_app_error_code == PreprodArtifact.InstallableAppErrorCode.PROCESSING_ERROR ) - assert self.artifact.installable_app_error_message == "Unsupported artifact type" + assert self.artifact.installable_app_error_message == "some novel failure" - # Verify webhook was sent mock_send_webhook.assert_called_once() call_kwargs = mock_send_webhook.call_args assert call_kwargs.kwargs["organization_id"] == self.project.organization_id + @override_settings(LAUNCHPAD_RPC_SHARED_SECRET=[SHARED_SECRET_FOR_TESTS]) + @patch( + "sentry.preprod.api.endpoints.project_preprod_distribution.send_build_distribution_webhook" + ) + def test_translates_legacy_invalid_signature(self, mock_send_webhook) -> None: + response = self._put(orjson.dumps({"error_code": 2, "error_message": "invalid_signature"})) + + assert response.status_code == 200 + self.artifact.refresh_from_db() + assert ( + self.artifact.installable_app_error_code + == PreprodArtifact.InstallableAppErrorCode.INVALID_CODE_SIGNATURE + ) + assert self.artifact.installable_app_error_message is None + + @override_settings(LAUNCHPAD_RPC_SHARED_SECRET=[SHARED_SECRET_FOR_TESTS]) + @patch( + "sentry.preprod.api.endpoints.project_preprod_distribution.send_build_distribution_webhook" + ) + def test_translates_legacy_simulator(self, mock_send_webhook) -> None: + response = self._put(orjson.dumps({"error_code": 2, "error_message": "simulator"})) + + assert response.status_code == 200 + self.artifact.refresh_from_db() + assert ( + self.artifact.installable_app_error_code + == PreprodArtifact.InstallableAppErrorCode.SIMULATOR_BUILD + ) + assert self.artifact.installable_app_error_message is None + + @override_settings(LAUNCHPAD_RPC_SHARED_SECRET=[SHARED_SECRET_FOR_TESTS]) + @patch( + "sentry.preprod.api.endpoints.project_preprod_distribution.send_build_distribution_webhook" + ) + def test_translates_legacy_unsupported_artifact_type(self, mock_send_webhook) -> None: + response = self._put( + orjson.dumps({"error_code": 3, "error_message": "Unsupported artifact type"}) + ) + + assert response.status_code == 200 + self.artifact.refresh_from_db() + assert ( + self.artifact.installable_app_error_code + == PreprodArtifact.InstallableAppErrorCode.UNSUPPORTED_ARTIFACT_TYPE + ) + assert self.artifact.installable_app_error_message is None + + @override_settings(LAUNCHPAD_RPC_SHARED_SECRET=[SHARED_SECRET_FOR_TESTS]) + @patch( + "sentry.preprod.api.endpoints.project_preprod_distribution.send_build_distribution_webhook" + ) + def test_accepts_new_granular_code(self, mock_send_webhook) -> None: + response = self._put( + orjson.dumps( + { + "error_code": int( + PreprodArtifact.InstallableAppErrorCode.INVALID_CODE_SIGNATURE + ), + "error_message": "", + } + ) + ) + + assert response.status_code == 200 + self.artifact.refresh_from_db() + assert ( + self.artifact.installable_app_error_code + == PreprodArtifact.InstallableAppErrorCode.INVALID_CODE_SIGNATURE + ) + assert self.artifact.installable_app_error_message == "" + @override_settings(LAUNCHPAD_RPC_SHARED_SECRET=[SHARED_SECRET_FOR_TESTS]) def test_invalid_error_code(self) -> None: response = self._put(orjson.dumps({"error_code": 99, "error_message": "bad"})) From 156e0af4f1a458fad0ebc286d038dc6ff1de92a6 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Thu, 23 Apr 2026 15:25:22 +0200 Subject: [PATCH 2/3] ref(preprod): Simplify distribution PUT endpoint validation (EME-883) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Type the PutDistribution.error_code field as PreprodArtifact.InstallableAppErrorCode so pydantic validates against the enum directly, instead of hand-maintaining an integer range. Drop the legacy error_code=SKIPPED+message="invalid_signature" shim in favour of updating launchpad to emit the new granular codes directly. Legacy payloads still pass validation (SKIPPED and PROCESSING_ERROR remain valid enum members) and write to the DB exactly as they did before — no regression during the rollout window. --- .../endpoints/project_preprod_distribution.py | 47 ++----------------- .../test_project_preprod_distribution.py | 42 +++-------------- 2 files changed, 11 insertions(+), 78 deletions(-) diff --git a/src/sentry/preprod/api/endpoints/project_preprod_distribution.py b/src/sentry/preprod/api/endpoints/project_preprod_distribution.py index 7757bd623489..33f89694ae27 100644 --- a/src/sentry/preprod/api/endpoints/project_preprod_distribution.py +++ b/src/sentry/preprod/api/endpoints/project_preprod_distribution.py @@ -3,7 +3,7 @@ import logging from typing import Any, cast -from pydantic import BaseModel, Field +from pydantic import BaseModel from rest_framework.request import Request from rest_framework.response import Response @@ -23,49 +23,11 @@ logger = logging.getLogger(__name__) -_MAX_ERROR_CODE = max(int(c) for c in PreprodArtifact.InstallableAppErrorCode) - - class PutDistribution(BaseModel): - error_code: int = Field(ge=0, le=_MAX_ERROR_CODE) + error_code: PreprodArtifact.InstallableAppErrorCode error_message: str -# Launchpad historically encoded specific reasons inside the free-form -# error_message field (e.g. error_code=SKIPPED + error_message="invalid_signature"). -# Translate those legacy payloads to the new granular enum values so the frontend -# can work purely off error_code. Remove once all launchpad deployments emit the -# new codes directly. -_LEGACY_MESSAGE_TO_CODE: dict[ - tuple[PreprodArtifact.InstallableAppErrorCode, str], - PreprodArtifact.InstallableAppErrorCode, -] = { - ( - PreprodArtifact.InstallableAppErrorCode.SKIPPED, - "invalid_signature", - ): PreprodArtifact.InstallableAppErrorCode.INVALID_CODE_SIGNATURE, - ( - PreprodArtifact.InstallableAppErrorCode.SKIPPED, - "simulator", - ): PreprodArtifact.InstallableAppErrorCode.SIMULATOR_BUILD, - ( - PreprodArtifact.InstallableAppErrorCode.PROCESSING_ERROR, - "Unsupported artifact type", - ): PreprodArtifact.InstallableAppErrorCode.UNSUPPORTED_ARTIFACT_TYPE, -} - - -def _translate_legacy_payload(error_code: int, error_message: str) -> tuple[int, str | None]: - try: - code = PreprodArtifact.InstallableAppErrorCode(error_code) - except ValueError: - return error_code, error_message - translated = _LEGACY_MESSAGE_TO_CODE.get((code, error_message)) - if translated is None: - return error_code, error_message - return int(translated), None - - @internal_cell_silo_endpoint class ProjectPreprodDistributionEndpoint(PreprodArtifactEndpoint): owner = ApiOwner.EMERGE_TOOLS @@ -84,9 +46,8 @@ def put( ) -> Response: put: PutDistribution = parse_request_with_pydantic(request, cast(Any, PutDistribution)) - error_code, error_message = _translate_legacy_payload(put.error_code, put.error_message) - head_artifact.installable_app_error_code = error_code - head_artifact.installable_app_error_message = error_message + head_artifact.installable_app_error_code = put.error_code + head_artifact.installable_app_error_message = put.error_message head_artifact.save( update_fields=[ "installable_app_error_code", diff --git a/tests/sentry/preprod/api/endpoints/test_project_preprod_distribution.py b/tests/sentry/preprod/api/endpoints/test_project_preprod_distribution.py index f4f4271334a9..c09ca9b98fbf 100644 --- a/tests/sentry/preprod/api/endpoints/test_project_preprod_distribution.py +++ b/tests/sentry/preprod/api/endpoints/test_project_preprod_distribution.py @@ -80,48 +80,20 @@ def test_accepts_generic_processing_error(self, mock_send_webhook) -> None: @patch( "sentry.preprod.api.endpoints.project_preprod_distribution.send_build_distribution_webhook" ) - def test_translates_legacy_invalid_signature(self, mock_send_webhook) -> None: + def test_legacy_payload_stored_verbatim(self, mock_send_webhook) -> None: + # Launchpad deployments that still send the legacy shape + # (error_code=SKIPPED + error_message="invalid_signature", etc.) + # should continue to write unchanged until launchpad emits the + # new granular codes directly. response = self._put(orjson.dumps({"error_code": 2, "error_message": "invalid_signature"})) assert response.status_code == 200 self.artifact.refresh_from_db() assert ( self.artifact.installable_app_error_code - == PreprodArtifact.InstallableAppErrorCode.INVALID_CODE_SIGNATURE - ) - assert self.artifact.installable_app_error_message is None - - @override_settings(LAUNCHPAD_RPC_SHARED_SECRET=[SHARED_SECRET_FOR_TESTS]) - @patch( - "sentry.preprod.api.endpoints.project_preprod_distribution.send_build_distribution_webhook" - ) - def test_translates_legacy_simulator(self, mock_send_webhook) -> None: - response = self._put(orjson.dumps({"error_code": 2, "error_message": "simulator"})) - - assert response.status_code == 200 - self.artifact.refresh_from_db() - assert ( - self.artifact.installable_app_error_code - == PreprodArtifact.InstallableAppErrorCode.SIMULATOR_BUILD - ) - assert self.artifact.installable_app_error_message is None - - @override_settings(LAUNCHPAD_RPC_SHARED_SECRET=[SHARED_SECRET_FOR_TESTS]) - @patch( - "sentry.preprod.api.endpoints.project_preprod_distribution.send_build_distribution_webhook" - ) - def test_translates_legacy_unsupported_artifact_type(self, mock_send_webhook) -> None: - response = self._put( - orjson.dumps({"error_code": 3, "error_message": "Unsupported artifact type"}) - ) - - assert response.status_code == 200 - self.artifact.refresh_from_db() - assert ( - self.artifact.installable_app_error_code - == PreprodArtifact.InstallableAppErrorCode.UNSUPPORTED_ARTIFACT_TYPE + == PreprodArtifact.InstallableAppErrorCode.SKIPPED ) - assert self.artifact.installable_app_error_message is None + assert self.artifact.installable_app_error_message == "invalid_signature" @override_settings(LAUNCHPAD_RPC_SHARED_SECRET=[SHARED_SECRET_FOR_TESTS]) @patch( From 8315dc97d2638b98451349e04d0219de61043eab Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Thu, 23 Apr 2026 17:10:20 +0200 Subject: [PATCH 3/3] fix(preprod): Set error message for Sentry-skipped distributions (EME-883) When distribution is skipped for quota/disabled/filtered reasons, launchpad never runs, so it cannot populate installable_app_error_message. Set the message in Sentry alongside the error code so the install page has human text to render until the frontend switches to rendering from error_code. Refs EME-883 Co-Authored-By: Claude --- .../api/endpoints/project_preprod_artifact_update.py | 5 ++++- .../test_project_preprod_artifact_update.py | 12 +++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/sentry/preprod/api/endpoints/project_preprod_artifact_update.py b/src/sentry/preprod/api/endpoints/project_preprod_artifact_update.py index cec38f608971..f67c1c99a809 100644 --- a/src/sentry/preprod/api/endpoints/project_preprod_artifact_update.py +++ b/src/sentry/preprod/api/endpoints/project_preprod_artifact_update.py @@ -437,13 +437,16 @@ def put( else: if distro_skip_reason == "quota": distro_error_code = PreprodArtifact.InstallableAppErrorCode.NO_QUOTA + distro_error_message = "Distribution quota exceeded" elif distro_skip_reason == "disabled": distro_error_code = PreprodArtifact.InstallableAppErrorCode.DISTRIBUTION_DISABLED + distro_error_message = "Distribution disabled for this project" else: distro_error_code = PreprodArtifact.InstallableAppErrorCode.DISTRIBUTION_FILTERED + distro_error_message = "Build filtered out by project settings" head_artifact.installable_app_error_code = distro_error_code - head_artifact.installable_app_error_message = None + head_artifact.installable_app_error_message = distro_error_message head_artifact.save( update_fields=[ "installable_app_error_code", diff --git a/tests/sentry/preprod/api/endpoints/test_project_preprod_artifact_update.py b/tests/sentry/preprod/api/endpoints/test_project_preprod_artifact_update.py index 2a302fd9f7a3..331f3c5c8e28 100644 --- a/tests/sentry/preprod/api/endpoints/test_project_preprod_artifact_update.py +++ b/tests/sentry/preprod/api/endpoints/test_project_preprod_artifact_update.py @@ -648,7 +648,7 @@ def test_update_sets_error_code_no_quota(self, mock_has_installable_quota) -> No self.preprod_artifact.installable_app_error_code == PreprodArtifact.InstallableAppErrorCode.NO_QUOTA ) - assert self.preprod_artifact.installable_app_error_message is None + assert self.preprod_artifact.installable_app_error_message == "Distribution quota exceeded" @override_settings(LAUNCHPAD_RPC_SHARED_SECRET=["test-secret-key"]) def test_update_sets_error_code_distribution_filtered(self) -> None: @@ -666,7 +666,10 @@ def test_update_sets_error_code_distribution_filtered(self) -> None: self.preprod_artifact.installable_app_error_code == PreprodArtifact.InstallableAppErrorCode.DISTRIBUTION_FILTERED ) - assert self.preprod_artifact.installable_app_error_message is None + assert ( + self.preprod_artifact.installable_app_error_message + == "Build filtered out by project settings" + ) @override_settings(LAUNCHPAD_RPC_SHARED_SECRET=["test-secret-key"]) def test_update_sets_error_code_distribution_disabled(self) -> None: @@ -683,7 +686,10 @@ def test_update_sets_error_code_distribution_disabled(self) -> None: self.preprod_artifact.installable_app_error_code == PreprodArtifact.InstallableAppErrorCode.DISTRIBUTION_DISABLED ) - assert self.preprod_artifact.installable_app_error_message is None + assert ( + self.preprod_artifact.installable_app_error_message + == "Distribution disabled for this project" + ) class FindOrCreateReleaseTest(TestCase):