Skip to content

feat(preprod): Add public build-distribution/latest endpoint#109584

Open
trevor-e wants to merge 25 commits intomasterfrom
telkins/public-bd-apis-latest
Open

feat(preprod): Add public build-distribution/latest endpoint#109584
trevor-e wants to merge 25 commits intomasterfrom
telkins/public-bd-apis-latest

Conversation

@trevor-e
Copy link
Member

@trevor-e trevor-e commented Feb 27, 2026

Summary

Split from #109377, stacked on #109583.

Replaces the paginated list endpoint at GET /api/0/projects/{org}/{project}/preprodartifacts/build-distribution/latest/ with a single-result endpoint that returns the latest installable build, with optional check-for-updates support.

Two modes:

  • Latest mode — caller provides appId + platform, gets back the newest installable build (highest semver version, highest build number as tiebreaker).
  • Check-for-updates mode — caller also provides buildVersion + (buildNumber or mainBinaryIdentifier), gets back both the current and latest builds plus an updateAvailable boolean.

Key behaviors:

  • Filters by buildConfiguration, codesigningType, and installGroup (repeatable).
  • In check-for-updates mode, inherits buildConfiguration, codesigningType, and installGroups from the current artifact when not explicitly provided.
  • Only considers PROCESSED artifacts with an installable app file and build number.
  • Adds installGroups field to the build response shape.

Response shape:

{
  "latest": { <build> } | null,
  "current": { <build> } | null,
  "updateAvailable": true | false | null
}

Extracted shared helpers (find_current_artifact, find_latest_installable_artifact, build_install_groups_q, get_platform_artifact_type_filter) into build_distribution_utils.py for reuse. Added deprecation comment to the experimental check-for-updates endpoint.

Test plan

  • 24 tests covering: feature flag, validation, latest mode, check-for-updates mode (update available / already on latest / current not found / no builds), platform filtering, build config filtering, codesigning type filtering, install group filtering, inheritance from current artifact, semver comparison, build number tiebreaker, binary identifier matching, non-installable exclusion, project scoping, download counts
  • Experimental check-for-updates endpoint tests still pass (29/29)
  • Pre-commit passes on all modified files

- Add organization-scoped public install-details endpoint
- Add platform property to PreprodArtifact model
- Add ArtifactInstallInfo and get_artifact_install_info utility
- Refactor existing install-details endpoint to use shared utility
- Refactor size_analysis tasks to use model platform property
- Add install info response models and OpenAPI examples
@trevor-e trevor-e requested review from a team as code owners February 27, 2026 18:03
@github-actions github-actions bot added Scope: Frontend Automatically applied to PRs that change frontend components Scope: Backend Automatically applied to PRs that change backend components labels Feb 27, 2026
@github-actions
Copy link
Contributor

🚨 Warning: This pull request contains Frontend and Backend changes!

It's discouraged to make changes to Sentry's Frontend and Backend in a single pull request. The Frontend and Backend are not atomically deployed. If the changes are interdependent of each other, they must be separated into two pull requests and be made forward or backwards compatible, such that the Backend or Frontend can be safely deployed independently.

Have questions? Please ask in the #discuss-dev-infra channel.

trevor-e and others added 2 commits February 27, 2026 15:14
Compare against Platform.APPLE and Platform.ANDROID enum members
instead of raw string literals for consistency and type safety.

Co-Authored-By: Claude <noreply@anthropic.com>
Uppercase the platform enum value in public endpoint responses for
consistency with other enum fields like artifactType. Internal
endpoints are unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
Consolidate public build distribution response models into a single
file, consistent with how size_analysis.py is organized.
…tails

get_artifact_install_info() already computes install_url and
download_count. The endpoint was calling the underlying functions a
second time, creating an orphaned InstallablePreprodArtifact DB row
on every GET request.
Add project-scoped endpoint to list the latest installable builds with
filtering by platform, appId, branch, buildVersion, buildConfiguration,
prNumber, and installGroup.
- Rename response fields to `latestArtifact`/`currentArtifact` for clarity
- Rename query param to `installGroups` (plural) and read via getlist
- Remove ListField from validator; read installGroups directly from request
- Add logging when current artifact not found in check-for-updates mode

Co-Authored-By: Claude <noreply@anthropic.com>
Reorganize tests into separate classes by flow:
- LatestBuildValidationTest: feature flags and param validation
- LatestBuildModeTest: latest-only mode behavior
- LatestBuildFilteringTest: explicit filter parameters
- CheckForUpdatesTest: check-for-updates mode with filter inheritance

Adds coverage for combined filter inheritance, multi-group install
groups, and query param arrays to match legacy checkForUpdates endpoint.

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions
Copy link
Contributor

github-actions bot commented Mar 2, 2026

Backend Test Failures

Failures on 1f1f9fe in this run:

tests/sentry/preprod/api/endpoints/test_project_preprod_artifact_install_details.py::ProjectPreprodInstallDetailsEndpointTest::test_install_groups_returnedlog
tests/sentry/preprod/api/endpoints/test_project_preprod_artifact_install_details.py:206: in test_install_groups_returned
    assert response.status_code == 200
E   assert 404 == 200
E    +  where 404 = <Response status_code=404, "application/json">.status_code
tests/sentry/preprod/api/endpoints/test_project_preprod_artifact_install_details.py::ProjectPreprodInstallDetailsEndpointTest::test_ios_artifact_successlog
tests/sentry/preprod/api/endpoints/test_project_preprod_artifact_install_details.py:67: in test_ios_artifact_success
    assert response.status_code == 200
E   assert 404 == 200
E    +  where 404 = <Response status_code=404, "application/json">.status_code
tests/sentry/preprod/api/endpoints/test_project_preprod_artifact_install_details.py::ProjectPreprodInstallDetailsEndpointTest::test_install_groups_null_when_not_setlog
tests/sentry/preprod/api/endpoints/test_project_preprod_artifact_install_details.py:217: in test_install_groups_null_when_not_set
    assert response.status_code == 200
E   assert 404 == 200
E    +  where 404 = <Response status_code=404, "application/json">.status_code
tests/sentry/preprod/api/endpoints/test_project_preprod_artifact_install_details.py::ProjectPreprodInstallDetailsEndpointTest::test_android_artifact_successlog
tests/sentry/preprod/api/endpoints/test_project_preprod_artifact_install_details.py:104: in test_android_artifact_success
    assert response.status_code == 200
E   assert 404 == 200
E    +  where 404 = <Response status_code=404, "application/json">.status_code

Match the legacy checkForUpdates endpoint by requiring
project:distribution scope and adding a 100 req/min org rate limit.

Co-Authored-By: Claude <noreply@anthropic.com>
- Move logger after all imports to avoid splitting import blocks
- Use consistent truthiness check for build_version parameter
- Revert stricter installability check on deprecated endpoint to
  preserve backward compatibility

Co-Authored-By: Claude <noreply@anthropic.com>
Add org-level rate limiting (100 req/min) to the public install details
endpoint to match the latest build endpoint.

Co-Authored-By: Claude <noreply@anthropic.com>
publish_status = {
"GET": ApiPublishStatus.PUBLIC,
}
rate_limits = RateLimitConfig(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forgot to add this

if highest_version is None or parsed_version > parse_version(highest_version):
highest_version = version
except Exception:
continue
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant re-parsing of highest_version each loop iteration

Low Severity

In find_latest_installable_artifact, parse_version(highest_version) is called on every loop iteration when comparing against the current best version. The parsed result of highest_version could be cached in a local variable and updated only when highest_version changes, avoiding redundant reparsing for every version string in the queryset.

Fix in Cursor Fix in Web

The DB column stores an integer, so validate it as IntegerField in the
serializer rather than silently returning None on non-numeric input.
Also updates the function signature and OpenAPI param type to match.

Co-Authored-By: Claude <noreply@anthropic.com>
…test build lookup

find_latest_installable_artifact() could return an XCARCHIVE with an
invalid code signature as the "latest installable build". Now iterates
through candidates and skips Apple artifacts where
is_code_signature_valid is not set in extras.

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions
Copy link
Contributor

github-actions bot commented Mar 2, 2026

Backend Test Failures

Failures on fa1f935 in this run:

tests/sentry/preprod/api/endpoints/test_project_preprod_artifact_install_details.py::ProjectPreprodInstallDetailsEndpointTest::test_android_artifact_successlog
tests/sentry/preprod/api/endpoints/test_project_preprod_artifact_install_details.py:111: in test_android_artifact_success
    assert "?response_format=plist" not in data["install_url"]
E   TypeError: argument of type 'NoneType' is not iterable
tests/sentry/preprod/api/endpoints/test_project_preprod_artifact_install_details.py::ProjectPreprodInstallDetailsEndpointTest::test_ios_artifact_successlog
tests/sentry/preprod/api/endpoints/test_project_preprod_artifact_install_details.py:77: in test_ios_artifact_success
    assert "?response_format=plist" in data["install_url"]
E   TypeError: argument of type 'NoneType' is not iterable

Filter out empty strings from installGroups query param so that
?installGroups= is treated the same as omitting the param.

Use `build_number is None` instead of `not build_number` in the
cross-field validator so that buildNumber=0 is accepted as valid.

Co-Authored-By: Claude <noreply@anthropic.com>
Copy link
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

try:
return (
PreprodArtifact.objects.select_related(
"project", "build_configuration", "commit_comparison", "mobile_app_info"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing project__organization in select_related causes extra queries

Medium Severity

Both find_current_artifact and find_latest_installable_artifact use select_related("project", ...) but omit "project__organization". The returned artifacts are passed to create_install_info_dict, which calls get_download_url_for_artifact, which accesses artifact.project.organization.slug (line 130). This triggers an additional lazy-loaded DB query per installable artifact. The existing OrganizationPreprodArtifactPublicInstallDetailsEndpoint correctly includes "project__organization" in its select_related for the same reason.

Additional Locations (1)

Fix in Cursor Fix in Web

@github-actions
Copy link
Contributor

github-actions bot commented Mar 2, 2026

Backend Test Failures

Failures on 19c1fa1 in this run:

tests/sentry/preprod/api/endpoints/test_project_preprod_artifact_install_details.py::ProjectPreprodInstallDetailsEndpointTest::test_ios_artifact_successlog
tests/sentry/preprod/api/endpoints/test_project_preprod_artifact_install_details.py:77: in test_ios_artifact_success
    assert "?response_format=plist" in data["install_url"]
E   TypeError: argument of type 'NoneType' is not iterable
tests/sentry/preprod/api/endpoints/test_project_preprod_artifact_install_details.py::ProjectPreprodInstallDetailsEndpointTest::test_android_artifact_successlog
tests/sentry/preprod/api/endpoints/test_project_preprod_artifact_install_details.py:111: in test_android_artifact_success
    assert "?response_format=plist" not in data["install_url"]
E   TypeError: argument of type 'NoneType' is not iterable

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Backend Automatically applied to PRs that change backend components Scope: Frontend Automatically applied to PRs that change frontend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant