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

arm pushed a commit to branch gha-distributions
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git

commit d284a336f55c4dc4ce5d049ba2ae2a124fab6de5
Author: Alastair McFarlane <[email protected]>
AuthorDate: Thu Jan 8 16:04:46 2026 +0000

    Add new SSH register endpoint for distributions
---
 atr/api/__init__.py                  | 26 ++++++++++++++++++++++++++
 atr/db/interaction.py                | 18 ++++++++++++++++++
 atr/models/api.py                    | 15 +++++++++++++++
 atr/storage/writers/distributions.py |  2 +-
 atr/tasks/gha.py                     |  3 ++-
 5 files changed, 62 insertions(+), 2 deletions(-)

diff --git a/atr/api/__init__.py b/atr/api/__init__.py
index eb3b6cf..87e641c 100644
--- a/atr/api/__init__.py
+++ b/atr/api/__init__.py
@@ -251,6 +251,32 @@ async def committees_list() -> DictResponse:
     ).model_dump(), 200
 
 
[email protected]("/distribute/ssh/register", methods=["POST"])
+@quart_schema.validate_request(models.api.DistributeSshRegisterArgs)
+async def distribute_ssh_register(data: models.api.DistributeSshRegisterArgs) 
-> DictResponse:
+    """
+    Register an SSH key sent with a corroborating Trusted Publisher JWT,
+    validating the requested version is in the correct phase.
+    """
+    payload, asf_uid, project = await interaction.trusted_jwt_for_version(
+        data.publisher, data.jwt, interaction.TrustedProjectPhase(data.phase), 
data.version
+    )
+    async with 
storage.write_as_committee_member(util.unwrap(project.committee).name, asf_uid) 
as wacm:
+        fingerprint, expires = await wacm.ssh.add_workflow_key(
+            payload["actor"],
+            payload["actor_id"],
+            project.name,
+            data.ssh_key,
+        )
+
+    return models.api.DistributeSshRegisterResults(
+        endpoint="/distribute/ssh/register",
+        fingerprint=fingerprint,
+        project=project.name,
+        expires=expires,
+    ).model_dump(), 200
+
+
 @api.route("/distribution/record", methods=["POST"])
 @jwtoken.require
 @quart_schema.security_scheme([{"BearerAuth": []}])
diff --git a/atr/db/interaction.py b/atr/db/interaction.py
index f8026f8..1bd097c 100644
--- a/atr/db/interaction.py
+++ b/atr/db/interaction.py
@@ -179,6 +179,24 @@ async def trusted_jwt(publisher: str, jwt: str, phase: 
TrustedProjectPhase) -> t
     return payload, asf_uid, project
 
 
+async def trusted_jwt_for_version(
+    publisher: str, jwt: str, phase: TrustedProjectPhase, version_name: str
+) -> tuple[dict[str, Any], str, sql.Project]:
+    payload, asf_uid, project = await trusted_jwt(publisher, jwt, phase)
+    async with db.session() as db_data:
+        release = await db_data.release(project_name=project.name, 
version=version_name).get()
+        if not release:
+            raise InteractionError(f"Release {version} does not exist in 
project {project.name}")
+        if phase == TrustedProjectPhase.COMPOSE and release.phase != 
sql.ReleasePhase.RELEASE_CANDIDATE_DRAFT:
+            raise InteractionError(f"Release {version} is not in compose 
phase")
+        if phase == TrustedProjectPhase.VOTE and release.phase != 
sql.ReleasePhase.RELEASE_CANDIDATE:
+            raise InteractionError(f"Release {version} is not in vote phase")
+        if phase == TrustedProjectPhase.FINISH and release.phase != 
sql.ReleasePhase.RELEASE_PREVIEW:
+            raise InteractionError(f"Release {version} is not in finish phase")
+
+    return payload, asf_uid, project
+
+
 async def has_failing_checks(release: sql.Release, revision_number: str, 
caller_data: db.Session | None = None) -> bool:
     async with db.ensure_session(caller_data) as data:
         query = (
diff --git a/atr/models/api.py b/atr/models/api.py
index 716fbce..06e5122 100644
--- a/atr/models/api.py
+++ b/atr/models/api.py
@@ -67,6 +67,21 @@ class CommitteesListResults(schema.Strict):
     committees: Sequence[sql.Committee]
 
 
+class DistributeSshRegisterArgs(schema.Strict):
+    publisher: str = schema.example("user")
+    jwt: str = schema.example("eyJhbGciOiJIUzI1[...]mMjLiuyu5CSpyHI=")
+    ssh_key: str = schema.example("ssh-ed25519 
AAAAC3NzaC1lZDI1NTEgH5C9okWi0dh25AAAAIOMqqnkVzrm0SdG6UOoqKLsabl9GKJl")
+    phase: str = schema.Field(strict=False, default="compose", 
json_schema_extra={"examples": ["compose", "finish"]})
+    version: str = schema.example("0.0.1")
+
+
+class DistributeSshRegisterResults(schema.Strict):
+    endpoint: Literal["/distribute/ssh/register"] = schema.alias("endpoint")
+    fingerprint: str = 
schema.example("SHA256:0123456789abcdef0123456789abcdef01234567")
+    project: str = schema.example("example")
+    expires: int = schema.example(1713547200)
+
+
 class DistributionRecordArgs(schema.Strict):
     project: str = schema.example("example")
     version: str = schema.example("0.0.1")
diff --git a/atr/storage/writers/distributions.py 
b/atr/storage/writers/distributions.py
index a0748d4..91839f8 100644
--- a/atr/storage/writers/distributions.py
+++ b/atr/storage/writers/distributions.py
@@ -115,7 +115,7 @@ class CommitteeMember(CommitteeParticipant):
             task_type=sql.TaskType.DISTRIBUTION_WORKFLOW,
             task_args=gha.DistributionWorkflow(
                 name=release_name,
-                # "distribution-owner-namespace": owner_namespace, # TODO: Put 
into workflow
+                namespace=owner_namespace or "",
                 package=package,
                 version=version,
                 project_name=project_name,
diff --git a/atr/tasks/gha.py b/atr/tasks/gha.py
index 669d469..cd9a458 100644
--- a/atr/tasks/gha.py
+++ b/atr/tasks/gha.py
@@ -46,6 +46,7 @@ class DistributionWorkflow(schema.Strict):
     owner: str = schema.description("Github owner of the repository")
     repo: str = schema.description("Repository in which to start the workflow")
     ref: str = schema.description("Git ref to trigger the workflow")
+    namespace: str = schema.description("Namespace to distribute to")
     package: str = schema.description("Package to distribute")
     version: str = schema.description("Version to distribute")
     project_name: str = schema.description("Project name in ATR")
@@ -108,7 +109,7 @@ async def trigger_workflow(args: DistributionWorkflow) -> 
results.Results | None
                 "committee.name",
                 "release",
                 sql_platform,
-                "",  # TODO: Needs set in args
+                namespace=args.namespace,
                 package=args.package,
                 version=args.version,
                 staging=True,


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

Reply via email to