Skip to content

Commit 83f4055

Browse files

File tree

4 files changed

+409
-0
lines changed

4 files changed

+409
-0
lines changed

src/sentry/preprod/analytics.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,15 @@ class PreprodArtifactApiAssembleEvent(analytics.Event):
1111
)
1212

1313

14+
class PreprodArtifactApiUpdateEvent(analytics.Event):
15+
type = "preprod_artifact.api.update"
16+
17+
attributes = (
18+
analytics.Attribute("organization_id"),
19+
analytics.Attribute("project_id"),
20+
analytics.Attribute("user_id", required=False),
21+
)
22+
23+
1424
analytics.register(PreprodArtifactApiAssembleEvent)
25+
analytics.register(PreprodArtifactApiUpdateEvent)
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import jsonschema
2+
import orjson
3+
from rest_framework.exceptions import PermissionDenied
4+
from rest_framework.request import Request
5+
from rest_framework.response import Response
6+
7+
from sentry import analytics
8+
from sentry.api.api_owners import ApiOwner
9+
from sentry.api.api_publish_status import ApiPublishStatus
10+
from sentry.api.base import region_silo_endpoint
11+
from sentry.api.bases.project import ProjectEndpoint
12+
from sentry.preprod.authentication import LaunchpadRpcSignatureAuthentication
13+
from sentry.preprod.models import PreprodArtifact
14+
from sentry.utils import json
15+
16+
17+
def validate_preprod_artifact_update_schema(request_body: bytes) -> tuple[dict, str | None]:
18+
"""
19+
Validate the JSON schema for preprod artifact update requests.
20+
21+
Returns:
22+
tuple: (parsed_data, error_message) where error_message is None if validation succeeds
23+
"""
24+
schema = {
25+
"type": "object",
26+
"properties": {
27+
# Optional metadata
28+
"date_built": {"type": "string"},
29+
"artifact_type": {"type": "integer", "minimum": 0, "maximum": 2},
30+
"build_version": {"type": "string", "maxLength": 255},
31+
"build_number": {"type": "integer"},
32+
"error_code": {"type": "integer", "minimum": 0, "maximum": 3},
33+
"error_message": {"type": "string"},
34+
"apple_app_info": {
35+
"type": "object",
36+
"properties": {
37+
"is_simulator": {"type": "boolean"},
38+
"codesigning_type": {"type": "string"},
39+
"profile_name": {"type": "string"},
40+
"is_code_signature_valid": {"type": "boolean"},
41+
"code_signature_errors": {"type": "array", "items": {"type": "string"}},
42+
},
43+
},
44+
},
45+
"additionalProperties": True,
46+
}
47+
48+
error_messages = {
49+
"date_built": "The date_built field must be a string.",
50+
"artifact_type": "The artifact_type field must be an integer between 0 and 2.",
51+
"error_code": "The error_code field must be an integer between 0 and 3.",
52+
"error_message": "The error_message field must be a string.",
53+
"build_version": "The build_version field must be a string with a maximum length of 255 characters.",
54+
"build_number": "The build_number field must be an integer.",
55+
"apple_app_info": "The apple_app_info field must be an object.",
56+
"apple_app_info.is_simulator": "The is_simulator field must be a boolean.",
57+
"apple_app_info.codesigning_type": "The codesigning_type field must be a string.",
58+
"apple_app_info.profile_name": "The profile_name field must be a string.",
59+
"apple_app_info.is_code_signature_valid": "The is_code_signature_valid field must be a boolean.",
60+
"apple_app_info.code_signature_errors": "The code_signature_errors field must be an array of strings.",
61+
}
62+
63+
try:
64+
data = orjson.loads(request_body)
65+
jsonschema.validate(data, schema)
66+
return data, None
67+
except jsonschema.ValidationError as e:
68+
validation_error_message = e.message
69+
# Get the field from the path if available
70+
if e.path:
71+
if field := e.path[0]:
72+
validation_error_message = error_messages.get(str(field), validation_error_message)
73+
return {}, validation_error_message
74+
except (orjson.JSONDecodeError, TypeError):
75+
return {}, "Invalid json body"
76+
77+
78+
@region_silo_endpoint
79+
class ProjectPreprodArtifactUpdateEndpoint(ProjectEndpoint):
80+
owner = ApiOwner.EMERGE_TOOLS
81+
publish_status = {
82+
"PUT": ApiPublishStatus.PRIVATE,
83+
}
84+
authentication_classes = (LaunchpadRpcSignatureAuthentication,)
85+
permission_classes = ()
86+
87+
def _is_authorized(self, request: Request) -> bool:
88+
if request.auth and isinstance(
89+
request.successful_authenticator, LaunchpadRpcSignatureAuthentication
90+
):
91+
return True
92+
return False
93+
94+
def put(self, request: Request, project, artifact_id) -> Response:
95+
"""
96+
Update a preprod artifact with preprocessed data
97+
```````````````````````````````````````````````
98+
99+
Update the preprod artifact with data from preprocessing, such as
100+
artifact type, build information, and processing status.
101+
102+
:pparam string organization_id_or_slug: the id or slug of the organization the
103+
artifact belongs to.
104+
:pparam string project_id_or_slug: the id or slug of the project the artifact
105+
belongs to.
106+
:pparam string artifact_id: the ID of the preprod artifact to update.
107+
:auth: required
108+
"""
109+
if not self._is_authorized(request):
110+
raise PermissionDenied
111+
112+
analytics.record(
113+
"preprod_artifact.api.update",
114+
organization_id=project.organization_id,
115+
project_id=project.id,
116+
user_id=request.user.id,
117+
)
118+
119+
# Validate request data
120+
data, error_message = validate_preprod_artifact_update_schema(request.body)
121+
if error_message:
122+
return Response({"error": error_message}, status=400)
123+
124+
# Get the artifact
125+
try:
126+
preprod_artifact = PreprodArtifact.objects.get(
127+
project=project,
128+
id=artifact_id,
129+
)
130+
except PreprodArtifact.DoesNotExist:
131+
return Response({"error": f"Preprod artifact {artifact_id} not found"}, status=404)
132+
133+
updated_fields = []
134+
135+
if "date_built" in data:
136+
preprod_artifact.date_built = data["date_built"]
137+
updated_fields.append("date_built")
138+
139+
if "artifact_type" in data:
140+
preprod_artifact.artifact_type = data["artifact_type"]
141+
updated_fields.append("artifact_type")
142+
143+
if "error_code" in data:
144+
preprod_artifact.error_code = data["error_code"]
145+
updated_fields.append("error_code")
146+
147+
if "error_message" in data:
148+
preprod_artifact.error_message = data["error_message"]
149+
updated_fields.append("error_message")
150+
151+
if "error_code" in data or "error_message" in data:
152+
preprod_artifact.state = PreprodArtifact.ArtifactState.FAILED
153+
updated_fields.append("state")
154+
155+
if "build_version" in data:
156+
preprod_artifact.build_version = data["build_version"]
157+
updated_fields.append("build_version")
158+
159+
if "build_number" in data:
160+
preprod_artifact.build_number = data["build_number"]
161+
updated_fields.append("build_number")
162+
163+
if "apple_app_info" in data:
164+
apple_info = data["apple_app_info"]
165+
parsed_apple_info = {}
166+
for field in [
167+
"is_simulator",
168+
"codesigning_type",
169+
"profile_name",
170+
"is_code_signature_valid",
171+
"code_signature_errors",
172+
]:
173+
if field in apple_info:
174+
parsed_apple_info[field] = apple_info[field]
175+
176+
if parsed_apple_info:
177+
preprod_artifact.extras = json.dumps(parsed_apple_info)
178+
updated_fields.append("extras")
179+
180+
# Save the artifact if any fields were updated
181+
if updated_fields:
182+
preprod_artifact.save(update_fields=updated_fields + ["date_updated"])
183+
184+
return Response(
185+
{
186+
"success": True,
187+
"artifact_id": artifact_id,
188+
"updated_fields": updated_fields,
189+
}
190+
)

src/sentry/preprod/api/endpoints/urls.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from .organization_preprod_artifact_assemble import ProjectPreprodArtifactAssembleEndpoint
44
from .project_preprod_artifact_download import ProjectPreprodArtifactDownloadEndpoint
5+
from .project_preprod_artifact_update import ProjectPreprodArtifactUpdateEndpoint
56

67
preprod_urlpatterns = [
78
re_path(
@@ -17,4 +18,9 @@
1718
ProjectPreprodArtifactDownloadEndpoint.as_view(),
1819
name="sentry-api-0-project-preprod-artifact-download",
1920
),
21+
re_path(
22+
r"^(?P<organization_id_or_slug>[^/]+)/(?P<project_id_or_slug>[^/]+)/files/preprodartifacts/(?P<artifact_id>[^/]+)/update/$",
23+
ProjectPreprodArtifactUpdateEndpoint.as_view(),
24+
name="sentry-api-0-project-preprod-artifact-update",
25+
),
2026
]

0 commit comments

Comments
 (0)