Skip to content

Commit ecb26de

Browse files
add transaction + update tests
1 parent f3ce4f3 commit ecb26de

File tree

4 files changed

+356
-77
lines changed

4 files changed

+356
-77
lines changed

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

Lines changed: 48 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,60 @@
1010
from sentry.api.bases.project import ProjectEndpoint, ProjectReleasePermission
1111
from sentry.debug_files.upload import find_missing_chunks
1212
from sentry.models.orgauthtoken import is_org_auth_token_auth, update_org_auth_token_last_used
13+
from sentry.preprod.tasks import assemble_preprod_artifact
1314
from sentry.tasks.assemble import (
1415
AssembleTask,
1516
ChunkFileState,
16-
assemble_preprod_artifact,
1717
get_assemble_status,
1818
set_assemble_status,
1919
)
2020

2121

22+
def validate_preprod_artifact_schema(request_body: bytes) -> tuple[dict, str | None]:
23+
"""
24+
Validate the JSON schema for preprod artifact assembly requests.
25+
26+
Returns:
27+
tuple: (parsed_data, error_message) where error_message is None if validation succeeds
28+
"""
29+
schema = {
30+
"type": "object",
31+
"properties": {
32+
"checksum": {"type": "string", "pattern": "^[0-9a-f]{40}$"},
33+
"chunks": {
34+
"type": "array",
35+
"items": {"type": "string", "pattern": "^[0-9a-f]{40}$"},
36+
},
37+
# Optional metadata
38+
"git_sha": {"type": "string", "pattern": "^[0-9a-f]{40}$"},
39+
"build_configuration": {"type": "string"},
40+
},
41+
"required": ["checksum", "chunks"],
42+
"additionalProperties": False,
43+
}
44+
45+
error_messages = {
46+
"checksum": "The checksum field is required and must be a 40-character hexadecimal string.",
47+
"chunks": "The chunks field is required and must be provided as an array of 40-character hexadecimal strings.",
48+
"git_sha": "The git_sha field must be a 40-character hexadecimal string.",
49+
"build_configuration": "The build_configuration field must be a string.",
50+
}
51+
52+
try:
53+
data = orjson.loads(request_body)
54+
jsonschema.validate(data, schema)
55+
return data, None
56+
except jsonschema.ValidationError as e:
57+
error_message = e.message
58+
# Get the field from the path if available
59+
if e.path:
60+
if field := e.path[0]:
61+
error_message = error_messages.get(str(field), error_message)
62+
return {}, error_message
63+
except (orjson.JSONDecodeError, TypeError):
64+
return {}, "Invalid json body"
65+
66+
2267
@region_silo_endpoint
2368
class ProjectPreprodArtifactAssembleEndpoint(ProjectEndpoint):
2469
owner = ApiOwner.EMERGE_TOOLS
@@ -32,42 +77,9 @@ def post(self, request: Request, project) -> Response:
3277
Assembles a preprod artifact (mobile build, etc.) and stores it in the database.
3378
"""
3479
with sentry_sdk.start_span(op="preprod_artifact.assemble"):
35-
schema = {
36-
"type": "object",
37-
"properties": {
38-
"checksum": {"type": "string", "pattern": "^[0-9a-f]{40}$"},
39-
"chunks": {
40-
"type": "array",
41-
"items": {"type": "string", "pattern": "^[0-9a-f]{40}$"},
42-
},
43-
# Optional metadata
44-
"git_sha": {"type": "string", "pattern": "^[0-9a-f]{40}$"},
45-
"build_configuration": {"type": "string"},
46-
},
47-
"required": ["checksum", "chunks"],
48-
"additionalProperties": False,
49-
}
50-
51-
error_messages = {
52-
"checksum": "The checksum field is required and must be a 40-character hexadecimal string.",
53-
"chunks": "The chunks field is required and must be provided as an array of 40-character hexadecimal strings.",
54-
"git_sha": "The git_sha field must be a string.",
55-
"build_configuration": "The build_configuration field must be a string.",
56-
}
57-
58-
try:
59-
data = orjson.loads(request.body)
60-
jsonschema.validate(data, schema)
61-
except jsonschema.ValidationError as e:
62-
error_message = e.message
63-
# Get the field from the path if available
64-
if e.path:
65-
if field := e.path[0]:
66-
error_message = error_messages.get(str(field), error_message)
67-
80+
data, error_message = validate_preprod_artifact_schema(request.body)
81+
if error_message:
6882
return Response({"error": error_message}, status=400)
69-
except (orjson.JSONDecodeError, TypeError):
70-
return Response({"error": "Invalid json body"}, status=400)
7183

7284
checksum = data.get("checksum")
7385
chunks = data.get("chunks", [])

src/sentry/preprod/tasks.py

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
from __future__ import annotations
22

3+
import datetime
34
import logging
45
import uuid
56

7+
from django.db import router, transaction
8+
69
from sentry.models.organization import Organization
710
from sentry.models.project import Project
811
from sentry.silo.base import SiloMode
@@ -16,7 +19,7 @@
1619

1720

1821
@instrumented_task(
19-
name="sentry.tasks.assemble.assemble_preprod_artifact",
22+
name="sentry.preprod.tasks.assemble_preprod_artifact",
2023
queue="assemble",
2124
silo_mode=SiloMode.REGION,
2225
taskworker_config=TaskworkerConfig(
@@ -38,6 +41,15 @@ def assemble_preprod_artifact(
3841
"""
3942
from sentry.preprod.models import PreprodArtifact, PreprodBuildConfiguration
4043

44+
logger.info(
45+
"Starting preprod artifact assembly",
46+
extra={
47+
"timestamp": datetime.datetime.now().isoformat(),
48+
"project_id": project_id,
49+
"organization_id": org_id,
50+
},
51+
)
52+
4153
try:
4254
organization = Organization.objects.get_from_cache(pk=org_id)
4355
project = Project.objects.get(id=project_id, organization=organization)
@@ -59,22 +71,22 @@ def assemble_preprod_artifact(
5971
if assemble_result is None:
6072
return
6173

62-
# Handle build configuration
63-
build_config = None
64-
if build_configuration:
65-
build_config, _ = PreprodBuildConfiguration.objects.get_or_create(
74+
with transaction.atomic(router.db_for_write(PreprodArtifact)):
75+
build_config = None
76+
if build_configuration:
77+
build_config, _ = PreprodBuildConfiguration.objects.get_or_create(
78+
project=project,
79+
name=build_configuration,
80+
)
81+
82+
# Create PreprodArtifact record
83+
preprod_artifact = PreprodArtifact.objects.create(
6684
project=project,
67-
name=build_configuration,
85+
file_id=assemble_result.bundle.id,
86+
build_configuration=build_config,
87+
state=PreprodArtifact.ArtifactState.UPLOADED,
6888
)
6989

70-
# Create PreprodArtifact record
71-
preprod_artifact = PreprodArtifact.objects.create(
72-
project=project,
73-
file_id=assemble_result.bundle.id,
74-
build_configuration=build_config,
75-
state=PreprodArtifact.ArtifactState.UPLOADED,
76-
)
77-
7890
logger.info(
7991
"Created preprod artifact",
8092
extra={
@@ -84,6 +96,15 @@ def assemble_preprod_artifact(
8496
},
8597
)
8698

99+
logger.info(
100+
"Finished preprod artifact assembly",
101+
extra={
102+
"timestamp": datetime.datetime.now().isoformat(),
103+
"project_id": project_id,
104+
"organization_id": org_id,
105+
},
106+
)
107+
87108
# where next set of changes will happen
88109
# TODO: Trigger artifact processing (size analysis, etc.)
89110
# This is where you'd add logic to:

0 commit comments

Comments
 (0)