This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-release.git


The following commit(s) were added to refs/heads/main by this push:
     new 69e565e  Add an endpoint to record distributions from external 
publishing platforms
69e565e is described below

commit 69e565ebd65673adb8023c4d1a8425a27187cc9c
Author: Sean B. Palmer <[email protected]>
AuthorDate: Mon Sep 8 18:35:38 2025 +0100

    Add an endpoint to record distributions from external publishing platforms
---
 atr/blueprints/api/api.py | 52 ++++++++++++++++++++++++++++++++++++++++++-----
 atr/models/api.py         | 50 +++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 97 insertions(+), 5 deletions(-)

diff --git a/atr/blueprints/api/api.py b/atr/blueprints/api/api.py
index ad72a27..b71678c 100644
--- a/atr/blueprints/api/api.py
+++ b/atr/blueprints/api/api.py
@@ -270,9 +270,8 @@ async def distribution_record(data: 
models.api.DistributionRecordArgs) -> DictRe
         ).demand(exceptions.NotFound(f"Release {release_name} not found"))
     if release.committee is None:
         raise exceptions.NotFound(f"Release {release_name} has no committee")
-    platform = data.platform
     dd = models.distribution.Data(
-        platform=platform,
+        platform=data.platform,
         owner_namespace=data.distribution_owner_namespace,
         package=data.distribution_package,
         version=data.distribution_version,
@@ -584,11 +583,54 @@ async def projects_list() -> DictResponse:
     ).model_dump(), 200
 
 
[email protected]("/publisher/distribution/record", methods=["POST"])
[email protected]
+@quart_schema.security_scheme([{"BearerAuth": []}])
+@quart_schema.validate_request(models.api.PublisherDistributionRecordArgs)
+@quart_schema.validate_response(models.api.PublisherDistributionRecordResults, 
200)
+async def publisher_distribution_record(data: 
models.api.PublisherDistributionRecordArgs) -> DictResponse:
+    """
+    Record a distribution with a corroborating Trusted Publisher JWT.
+    """
+    _payload, asf_uid, project = await interaction.trusted_jwt(
+        data.publisher,
+        data.jwt,
+        interaction.TrustedProjectPhase.FINISH,
+    )
+    async with db.session() as db_data:
+        release_name = models.sql.release_name(project.name, data.version)
+        release = await db_data.release(
+            project_name=project.name,
+            version=data.version,
+        ).demand(exceptions.NotFound(f"Release {release_name} not found"))
+    if release.committee is None:
+        raise exceptions.NotFound(f"Release {release_name} has no committee")
+    dd = models.distribution.Data(
+        platform=data.platform,
+        owner_namespace=data.distribution_owner_namespace,
+        package=data.distribution_package,
+        version=data.distribution_version,
+        details=data.details,
+    )
+    async with storage.write(asf_uid) as write:
+        wacm = write.as_committee_member(release.committee.name)
+        await wacm.distributions.record_from_data(
+            release,
+            data.staging,
+            dd,
+        )
+
+    return models.api.PublisherDistributionRecordResults(
+        endpoint="/publisher/distribution/record",
+        success=True,
+    ).model_dump(), 200
+
+
 @api.BLUEPRINT.route("/publisher/release/announce", methods=["POST"])
 @quart_schema.validate_request(models.api.PublisherReleaseAnnounceArgs)
 async def publisher_release_announce(data: 
models.api.PublisherReleaseAnnounceArgs) -> DictResponse:
     """
-    Announce a release with a corroborating GitHub OIDC JWT.
+    Announce a release with a corroborating Trusted Publisher JWT.
     """
     _payload, asf_uid, project = await interaction.trusted_jwt(
         data.publisher,
@@ -623,7 +665,7 @@ async def publisher_release_announce(data: 
models.api.PublisherReleaseAnnounceAr
 @quart_schema.validate_request(models.api.PublisherSshRegisterArgs)
 async def publisher_ssh_register(data: models.api.PublisherSshRegisterArgs) -> 
DictResponse:
     """
-    Register an SSH key sent with a corroborating GitHub OIDC JWT.
+    Register an SSH key sent with a corroborating Trusted Publisher JWT.
     """
     payload, asf_uid, project = await interaction.trusted_jwt(
         data.publisher, data.jwt, interaction.TrustedProjectPhase.COMPOSE
@@ -648,7 +690,7 @@ async def publisher_ssh_register(data: 
models.api.PublisherSshRegisterArgs) -> D
 @quart_schema.validate_request(models.api.PublisherVoteResolveArgs)
 async def publisher_vote_resolve(data: models.api.PublisherVoteResolveArgs) -> 
DictResponse:
     """
-    Resolve a vote with a corroborating GitHub OIDC JWT.
+    Resolve a vote with a corroborating Trusted Publisher JWT.
     """
     # TODO: Need to be able to resolve and make the release immutable
     _payload, asf_uid, project = await interaction.trusted_jwt(
diff --git a/atr/models/api.py b/atr/models/api.py
index eba2c52..adf370b 100644
--- a/atr/models/api.py
+++ b/atr/models/api.py
@@ -81,6 +81,20 @@ class DistributionRecordArgs(schema.Strict):
     staging: bool = schema.Field(..., **example(False))
     details: bool = schema.Field(..., **example(False))
 
+    @pydantic.field_validator("platform", mode="before")
+    @classmethod
+    def platform_to_enum(cls, v):
+        if isinstance(v, str):
+            try:
+                return sql.DistributionPlatform.__members__[v]
+            except KeyError:
+                raise ValueError(f"'{v}' is not a valid DistributionPlatform")
+        return v
+
+    @pydantic.field_serializer("platform")
+    def serialise_platform(self, v):
+        return v.name if isinstance(v, sql.DistributionPlatform) else v
+
 
 class DistributionRecordResults(schema.Strict):
     endpoint: Literal["/distribution/record"] = schema.Field(alias="endpoint")
@@ -213,6 +227,38 @@ class ProjectsListResults(schema.Strict):
     projects: Sequence[sql.Project]
 
 
+class PublisherDistributionRecordArgs(schema.Strict):
+    publisher: str = schema.Field(..., **example("user"))
+    jwt: str = schema.Field(..., 
**example("eyJhbGciOiJIUzI1[...]mMjLiuyu5CSpyHI="))
+    project: str = schema.Field(..., **example("example"))
+    version: str = schema.Field(..., **example("0.0.1"))
+    platform: sql.DistributionPlatform = schema.Field(..., 
**example(sql.DistributionPlatform.ARTIFACT_HUB))
+    distribution_owner_namespace: str | None = schema.Field(default=None, 
**example("example"))
+    distribution_package: str = schema.Field(..., **example("example"))
+    distribution_version: str = schema.Field(..., **example("0.0.1"))
+    staging: bool = schema.Field(..., **example(False))
+    details: bool = schema.Field(..., **example(False))
+
+    @pydantic.field_validator("platform", mode="before")
+    @classmethod
+    def platform_to_enum(cls, v):
+        if isinstance(v, str):
+            try:
+                return sql.DistributionPlatform.__members__[v]
+            except KeyError:
+                raise ValueError(f"'{v}' is not a valid DistributionPlatform")
+        return v
+
+    @pydantic.field_serializer("platform")
+    def serialise_platform(self, v):
+        return v.name if isinstance(v, sql.DistributionPlatform) else v
+
+
+class PublisherDistributionRecordResults(schema.Strict):
+    endpoint: Literal["/publisher/distribution/record"] = 
schema.Field(alias="endpoint")
+    success: Literal[True] = schema.Field(..., **example(True))
+
+
 class PublisherReleaseAnnounceArgs(schema.Strict):
     publisher: str = schema.Field(..., **example("user"))
     jwt: str = schema.Field(..., 
**example("eyJhbGciOiJIUzI1[...]mMjLiuyu5CSpyHI="))
@@ -482,6 +528,7 @@ Results = Annotated[
     | CommitteeKeysResults
     | CommitteeProjectsResults
     | CommitteesListResults
+    | DistributionRecordResults
     | IgnoreAddResults
     | IgnoreDeleteResults
     | IgnoreListResults
@@ -494,6 +541,7 @@ Results = Annotated[
     | ProjectGetResults
     | ProjectReleasesResults
     | ProjectsListResults
+    | PublisherDistributionRecordResults
     | PublisherReleaseAnnounceResults
     | PublisherSshRegisterResults
     | PublisherVoteResolveResults
@@ -537,6 +585,7 @@ validate_committee_get = validator(CommitteeGetResults)
 validate_committee_keys = validator(CommitteeKeysResults)
 validate_committee_projects = validator(CommitteeProjectsResults)
 validate_committees_list = validator(CommitteesListResults)
+validate_distribution_record = validator(DistributionRecordResults)
 validate_ignore_add = validator(IgnoreAddResults)
 validate_ignore_delete = validator(IgnoreDeleteResults)
 validate_ignore_list = validator(IgnoreListResults)
@@ -549,6 +598,7 @@ validate_keys_user = validator(KeysUserResults)
 validate_project_get = validator(ProjectGetResults)
 validate_project_releases = validator(ProjectReleasesResults)
 validate_projects_list = validator(ProjectsListResults)
+validate_publisher_distribution_record = 
validator(PublisherDistributionRecordResults)
 validate_publisher_release_announce = 
validator(PublisherReleaseAnnounceResults)
 validate_publisher_ssh_register = validator(PublisherSshRegisterResults)
 validate_publisher_vote_resolve = validator(PublisherVoteResolveResults)


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to