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 1c78ad7 Add an API endpoint to resolve a vote
1c78ad7 is described below
commit 1c78ad7773a7f6285000cc70bb453430ead8f68d
Author: Sean B. Palmer <[email protected]>
AuthorDate: Mon Jul 14 20:05:53 2025 +0100
Add an API endpoint to resolve a vote
---
atr/blueprints/api/api.py | 79 ++++++++++++++++++++++++++++++++---------------
atr/models/api.py | 6 ++++
2 files changed, 60 insertions(+), 25 deletions(-)
diff --git a/atr/blueprints/api/api.py b/atr/blueprints/api/api.py
index d59236c..fce5a3c 100644
--- a/atr/blueprints/api/api.py
+++ b/atr/blueprints/api/api.py
@@ -168,8 +168,9 @@ async def draft_delete_project_version(data:
models.api.ProjectVersion) -> tuple
release = await db_data.release(
name=release_name, phase=sql.ReleasePhase.RELEASE_CANDIDATE_DRAFT,
_committee=True
).demand(exceptions.NotFound())
- if not (user.is_committee_member(release.project.committee, asf_uid)
or user.is_admin(asf_uid)):
- raise exceptions.Forbidden("You do not have permission to delete
this draft")
+ if release.project.committee is None:
+ raise exceptions.NotFound("Project has no committee")
+ _committee_member_or_admin(release.project.committee, asf_uid)
# TODO: This causes "A transaction is already begun on this Session"
# async with data.begin():
@@ -460,10 +461,33 @@ async def tasks(query_args: models.api.Task) ->
quart.Response:
@api.BLUEPRINT.route("/vote/resolve", methods=["POST"])
@jwtoken.require
@quart_schema.security_scheme([{"BearerAuth": []}])
-@quart_schema.validate_request(models.api.VoteStart)
-@quart_schema.validate_response(sql.Task, 201)
-async def vote_resolve(req: models.api.VoteStart) -> tuple[Mapping, int]:
- return {}, 200
+@quart_schema.validate_request(models.api.ProjectVersionResolution)
+@quart_schema.validate_response(dict[str, str], 200)
+async def vote_resolve(data: models.api.ProjectVersionResolution) ->
tuple[Mapping, int]:
+ asf_uid = _jwt_asf_uid()
+
+ async with db.session() as db_data:
+ release_name = sql.release_name(data.project, data.version)
+ release = await db_data.release(name=release_name, _project=True,
_committee=True).demand(exceptions.NotFound())
+ if release.project.committee is None:
+ raise exceptions.NotFound("Project has no committee")
+ _committee_member_or_admin(release.project.committee, asf_uid)
+
+ release = await db_data.merge(release)
+ match data.resolution:
+ case "passed":
+ release.phase = sql.ReleasePhase.RELEASE_PREVIEW
+ success_message = "Vote marked as passed"
+ description = "Create a preview revision from the last
candidate draft"
+ async with revision.create_and_manage(
+ data.project, release.version, asf_uid,
description=description
+ ) as _creating:
+ pass
+ case "failed":
+ release.phase = sql.ReleasePhase.RELEASE_CANDIDATE_DRAFT
+ success_message = "Vote marked as failed"
+ await db_data.commit()
+ return {"success": success_message}, 200
@api.BLUEPRINT.route("/vote/start", methods=["POST"])
@@ -471,25 +495,25 @@ async def vote_resolve(req: models.api.VoteStart) ->
tuple[Mapping, int]:
@quart_schema.security_scheme([{"BearerAuth": []}])
@quart_schema.validate_request(models.api.VoteStart)
@quart_schema.validate_response(sql.Task, 201)
-async def vote_start(req: models.api.VoteStart) -> tuple[Mapping, int]:
+async def vote_start(data: models.api.VoteStart) -> tuple[Mapping, int]:
asf_uid = _jwt_asf_uid()
permitted_recipients = util.permitted_recipients(asf_uid)
- if req.email_to not in permitted_recipients:
+ if data.email_to not in permitted_recipients:
raise exceptions.Forbidden("Invalid mailing list choice")
- async with db.session() as data:
- release_name = sql.release_name(req.project, req.version)
- release = await data.release(name=release_name, _project=True,
_committee=True).demand(exceptions.NotFound())
-
- if not (user.is_committee_member(release.committee, asf_uid) or
user.is_admin(asf_uid)):
- raise exceptions.Forbidden("You do not have permission to start a
vote for this project")
+ async with db.session() as db_data:
+ release_name = sql.release_name(data.project, data.version)
+ release = await db_data.release(name=release_name, _project=True,
_committee=True).demand(exceptions.NotFound())
+ if release.project.committee is None:
+ raise exceptions.NotFound("Project has no committee")
+ _committee_member_or_admin(release.project.committee, asf_uid)
- revision_exists = await data.revision(release_name=release_name,
number=req.revision).get()
+ revision_exists = await db_data.revision(release_name=release_name,
number=data.revision).get()
if revision_exists is None:
- raise exceptions.NotFound(f"Revision '{req.revision}' does not
exist")
+ raise exceptions.NotFound(f"Revision '{data.revision}' does not
exist")
- error = await voting.promote_release(data, release_name, req.revision,
vote_manual=False)
+ error = await voting.promote_release(db_data, release_name,
data.revision, vote_manual=False)
if error:
raise exceptions.BadRequest(error)
@@ -499,18 +523,18 @@ async def vote_start(req: models.api.VoteStart) ->
tuple[Mapping, int]:
task_type=sql.TaskType.VOTE_INITIATE,
task_args=tasks_vote.Initiate(
release_name=release_name,
- email_to=req.email_to,
- vote_duration=req.vote_duration,
+ email_to=data.email_to,
+ vote_duration=data.vote_duration,
initiator_id=asf_uid,
initiator_fullname=asf_uid,
- subject=req.subject,
- body=req.body,
+ subject=data.subject,
+ body=data.body,
).model_dump(),
- project_name=req.project,
- version_name=req.version,
+ project_name=data.project,
+ version_name=data.version,
)
- data.add(task)
- await data.commit()
+ db_data.add(task)
+ await db_data.commit()
return task.model_dump(exclude={"result"}), 201
@@ -532,6 +556,11 @@ async def upload(data:
models.api.ProjectVersionRelpathContent) -> tuple[Mapping
return revision.model_dump(), 201
+def _committee_member_or_admin(committee: sql.Committee, asf_uid: str) -> None:
+ if not (user.is_committee_member(committee, asf_uid) or
user.is_admin(asf_uid)):
+ raise exceptions.Forbidden("You do not have permission to perform this
action")
+
+
@db.session_function
async def _get_pat(data: db.Session, uid: str, token_hash: str) ->
sql.PersonalAccessToken | None:
return await data.query_one_or_none(
diff --git a/atr/models/api.py b/atr/models/api.py
index 1662806..2a41445 100644
--- a/atr/models/api.py
+++ b/atr/models/api.py
@@ -67,6 +67,12 @@ class ProjectVersionRelpathContent(schema.Strict):
content: str
+class ProjectVersionResolution(schema.Strict):
+ project: str
+ version: str
+ resolution: Literal["passed", "failed"]
+
+
class VoteStart(schema.Strict):
project: str
version: str
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]