Skip to content

Commit f12c060

Browse files
committed
feat: Extract and send tooling versions to Sentry (EME-606)
Extract sentry-cli, fastlane, and gradle plugin versions from artifact metadata files and send them to Sentry's preprod artifact update API. Changes: - Add sentry_cli_version, fastlane_version, and gradle_plugin_version fields to UpdateData model - Add _extract_tooling_versions method to parse .sentry-cli-metadata.txt - Update _prepare_update_data to extract and include version information - Add comprehensive tests for version extraction Refs: EME-606
1 parent 7551f63 commit f12c060

File tree

3 files changed

+145
-0
lines changed

3 files changed

+145
-0
lines changed

src/launchpad/api/update_api_models.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ class UpdateData(BaseModel):
6363
apple_app_info: Optional[AppleAppInfo] = None
6464
android_app_info: Optional[AndroidAppInfo] = None
6565
dequeued_at: Optional[datetime] = Field(None, description="Timestamp when message was dequeued from Kafka")
66+
sentry_cli_version: Optional[str] = Field(None, description="Version of sentry-cli used to upload the artifact")
67+
fastlane_version: Optional[str] = Field(None, description="Version of fastlane plugin used")
68+
gradle_plugin_version: Optional[str] = Field(None, description="Version of gradle plugin used")
6669

6770
@field_serializer("dequeued_at")
6871
def serialize_datetime(self, dt: datetime | None) -> str | None:

src/launchpad/artifact_processor.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,41 @@ def _update_size_error(
411411
except SentryClientError:
412412
logger.exception(f"Failed to update artifact with error {message}")
413413

414+
def _extract_tooling_versions(self, artifact: Artifact) -> Dict[str, str | None]:
415+
"""Extract tooling version information from the artifact zip file.
416+
417+
Looks for metadata files like .sentry-cli-metadata.txt in the zip
418+
and extracts version information.
419+
420+
Returns:
421+
Dict with keys: sentry_cli_version, fastlane_version, gradle_plugin_version
422+
"""
423+
import zipfile
424+
425+
versions = {
426+
"sentry_cli_version": None,
427+
"fastlane_version": None,
428+
"gradle_plugin_version": None,
429+
}
430+
431+
try:
432+
with zipfile.ZipFile(artifact.path, "r") as zf:
433+
# Look for .sentry-cli-metadata.txt in the root of the zip
434+
if ".sentry-cli-metadata.txt" in zf.namelist():
435+
with zf.open(".sentry-cli-metadata.txt") as f:
436+
content = f.read().decode("utf-8")
437+
# Parse format: sentry-cli-version: {version}
438+
for line in content.strip().split("\n"):
439+
if line.startswith("sentry-cli-version:"):
440+
version = line.split(":", 1)[1].strip()
441+
versions["sentry_cli_version"] = version
442+
logger.debug(f"Extracted sentry-cli version: {version}")
443+
break
444+
except Exception as e:
445+
logger.debug(f"Could not extract tooling versions: {e}")
446+
447+
return versions
448+
414449
def _prepare_update_data(
415450
self,
416451
app_info: AppleAppInfo | BaseAppInfo,
@@ -450,6 +485,9 @@ def _get_artifact_type(artifact: Artifact) -> ArtifactType:
450485
has_proguard_mapping=app_info.has_proguard_mapping,
451486
)
452487

488+
# Extract tooling versions from the artifact metadata
489+
tooling_versions = self._extract_tooling_versions(artifact)
490+
453491
update_data = UpdateData(
454492
app_name=app_info.name,
455493
app_id=app_info.app_id,
@@ -459,6 +497,9 @@ def _get_artifact_type(artifact: Artifact) -> ArtifactType:
459497
apple_app_info=apple_app_info,
460498
android_app_info=android_app_info,
461499
dequeued_at=dequeued_at,
500+
sentry_cli_version=tooling_versions["sentry_cli_version"],
501+
fastlane_version=tooling_versions["fastlane_version"],
502+
gradle_plugin_version=tooling_versions["gradle_plugin_version"],
462503
)
463504

464505
return update_data.model_dump(exclude_none=True)

tests/unit/artifacts/test_artifact_processor.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,3 +291,104 @@ def test_process_message_project_not_skipped(self, mock_process, mock_sentry_cli
291291
calls = fake_statsd.calls
292292
assert ("increment", {"metric": "artifact.processing.started", "value": 1, "tags": None}) in calls
293293
assert ("increment", {"metric": "artifact.processing.completed", "value": 1, "tags": None}) in calls
294+
295+
296+
class TestToolingVersionExtraction:
297+
"""Test extraction of tooling version information from artifacts."""
298+
299+
def setup_method(self):
300+
"""Set up test fixtures."""
301+
mock_sentry_client = Mock(spec=SentryClient)
302+
mock_statsd = Mock()
303+
self.processor = ArtifactProcessor(mock_sentry_client, mock_statsd)
304+
305+
def test_extract_tooling_versions_with_sentry_cli_version(self, tmp_path):
306+
"""Test extracting sentry-cli version from artifact zip."""
307+
import zipfile
308+
309+
from launchpad.artifacts.artifact import Artifact
310+
311+
# Create a test zip file with .sentry-cli-metadata.txt
312+
zip_path = tmp_path / "test_artifact.zip"
313+
with zipfile.ZipFile(zip_path, "w") as zf:
314+
zf.writestr(".sentry-cli-metadata.txt", "sentry-cli-version: 2.39.1\n")
315+
316+
# Create an artifact pointing to the zip
317+
artifact = Artifact(path=zip_path)
318+
319+
# Extract versions
320+
versions = self.processor._extract_tooling_versions(artifact)
321+
322+
# Verify sentry-cli version was extracted
323+
assert versions["sentry_cli_version"] == "2.39.1"
324+
assert versions["fastlane_version"] is None
325+
assert versions["gradle_plugin_version"] is None
326+
327+
def test_extract_tooling_versions_no_metadata_file(self, tmp_path):
328+
"""Test extracting versions when no metadata file exists."""
329+
import zipfile
330+
331+
from launchpad.artifacts.artifact import Artifact
332+
333+
# Create a test zip file without metadata
334+
zip_path = tmp_path / "test_artifact.zip"
335+
with zipfile.ZipFile(zip_path, "w") as zf:
336+
zf.writestr("some_file.txt", "content")
337+
338+
artifact = Artifact(path=zip_path)
339+
versions = self.processor._extract_tooling_versions(artifact)
340+
341+
# All versions should be None
342+
assert versions["sentry_cli_version"] is None
343+
assert versions["fastlane_version"] is None
344+
assert versions["gradle_plugin_version"] is None
345+
346+
def test_extract_tooling_versions_multiline_metadata(self, tmp_path):
347+
"""Test extracting version from metadata file with multiple lines."""
348+
import zipfile
349+
350+
from launchpad.artifacts.artifact import Artifact
351+
352+
zip_path = tmp_path / "test_artifact.zip"
353+
with zipfile.ZipFile(zip_path, "w") as zf:
354+
metadata_content = """# Sentry CLI Metadata
355+
sentry-cli-version: 2.40.0
356+
upload-date: 2025-11-10
357+
"""
358+
zf.writestr(".sentry-cli-metadata.txt", metadata_content)
359+
360+
artifact = Artifact(path=zip_path)
361+
versions = self.processor._extract_tooling_versions(artifact)
362+
363+
assert versions["sentry_cli_version"] == "2.40.0"
364+
365+
def test_extract_tooling_versions_with_spaces(self, tmp_path):
366+
"""Test extracting version with extra whitespace."""
367+
import zipfile
368+
369+
from launchpad.artifacts.artifact import Artifact
370+
371+
zip_path = tmp_path / "test_artifact.zip"
372+
with zipfile.ZipFile(zip_path, "w") as zf:
373+
zf.writestr(".sentry-cli-metadata.txt", "sentry-cli-version: 2.39.1 \n")
374+
375+
artifact = Artifact(path=zip_path)
376+
versions = self.processor._extract_tooling_versions(artifact)
377+
378+
assert versions["sentry_cli_version"] == "2.39.1"
379+
380+
def test_extract_tooling_versions_invalid_zip(self, tmp_path):
381+
"""Test graceful handling of invalid/corrupted zip file."""
382+
from launchpad.artifacts.artifact import Artifact
383+
384+
# Create a non-zip file
385+
invalid_zip = tmp_path / "invalid.zip"
386+
invalid_zip.write_text("not a zip file")
387+
388+
artifact = Artifact(path=invalid_zip)
389+
versions = self.processor._extract_tooling_versions(artifact)
390+
391+
# Should return all None values without crashing
392+
assert versions["sentry_cli_version"] is None
393+
assert versions["fastlane_version"] is None
394+
assert versions["gradle_plugin_version"] is None

0 commit comments

Comments
 (0)