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]