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 2e2e101 Add an API endpoint to delete a draft release
2e2e101 is described below
commit 2e2e101b6248c50e55e845d0502ee72e7a15e732
Author: Sean B. Palmer <[email protected]>
AuthorDate: Thu Jul 10 19:01:33 2025 +0100
Add an API endpoint to delete a draft release
---
atr/blueprints/api/__init__.py | 11 ++++++++++-
atr/blueprints/api/api.py | 31 +++++++++++++++++++++++++++++++
atr/routes/draft.py | 30 +++++++++++++++---------------
3 files changed, 56 insertions(+), 16 deletions(-)
diff --git a/atr/blueprints/api/__init__.py b/atr/blueprints/api/__init__.py
index 3c34d20..c08aa9d 100644
--- a/atr/blueprints/api/__init__.py
+++ b/atr/blueprints/api/__init__.py
@@ -15,6 +15,8 @@
# specific language governing permissions and limitations
# under the License.
+import sys
+
import asfquart.base as base
import quart
import quart.blueprints as blueprints
@@ -51,7 +53,14 @@ async def _handle_not_found(err: exceptions.NotFound) ->
tuple[quart.Response, i
def _json_error(message: str, status_code: int | None) ->
tuple[quart.Response, int]:
- return quart.jsonify({"error": message}), status_code or 500
+ payload = {"error": message}
+ show_traceback = False
+ if show_traceback:
+ import traceback
+
+ traceback_str = "".join(traceback.format_exception(*sys.exc_info()))
+ payload["traceback"] = traceback_str
+ return quart.jsonify(payload), status_code or 500
@BLUEPRINT.record_once
diff --git a/atr/blueprints/api/api.py b/atr/blueprints/api/api.py
index fa2c3ce..2be17c3 100644
--- a/atr/blueprints/api/api.py
+++ b/atr/blueprints/api/api.py
@@ -38,6 +38,7 @@ import atr.db.models as models
import atr.jwtoken as jwtoken
import atr.revision as revision
import atr.routes as routes
+import atr.routes.draft as draft
import atr.routes.start as start
import atr.routes.voting as voting
import atr.schema as schema
@@ -94,6 +95,11 @@ class VoteStartRequest(schema.Strict):
body: str
+class DraftDeleteRequest(schema.Strict):
+ project_name: str
+ version: str
+
+
# We implicitly have /api/openapi.json
@@ -165,6 +171,31 @@ async def committees_name_projects(name: str) ->
tuple[list[Mapping], int]:
return [project.model_dump() for project in committee.projects], 200
[email protected]("/draft/delete", methods=["POST"])
[email protected]
+@quart_schema.security_scheme([{"BearerAuth": []}])
+@quart_schema.validate_response(dict[str, str], 200)
+async def draft_delete_project_version() -> tuple[dict[str, str], int]:
+ payload = await _payload_get()
+ req = DraftDeleteRequest.model_validate(payload)
+ asf_uid = _jwt_asf_uid()
+
+ async with db.session() as data:
+ release_name = models.release_name(req.project_name, req.version)
+ release = await data.release(
+ name=release_name,
phase=models.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")
+
+ # TODO: This causes "A transaction is already begun on this Session"
+ # async with data.begin():
+ # Probably due to autobegin in data.release above
+ await draft.delete_candidate_draft(data, release_name)
+ await data.commit()
+ return {"deleted": release_name}, 200
+
+
@api.BLUEPRINT.route("/list/<project>/<version>")
@api.BLUEPRINT.route("/list/<project>/<version>/<revision>")
@quart_schema.validate_response(dict[str, list[str]], 200)
diff --git a/atr/routes/draft.py b/atr/routes/draft.py
index 76e5a96..d3567f4 100644
--- a/atr/routes/draft.py
+++ b/atr/routes/draft.py
@@ -113,7 +113,7 @@ async def delete(session: routes.CommitterSession) ->
response.Response:
async with db.session() as data:
async with data.begin():
try:
- await _delete_candidate_draft(data, release_name)
+ await delete_candidate_draft(data, release_name)
except Exception as e:
logging.exception("Error deleting candidate draft:")
return await session.redirect(root.index, error=f"Error
deleting candidate draft: {e!s}")
@@ -131,6 +131,20 @@ async def delete(session: routes.CommitterSession) ->
response.Response:
return await session.redirect(root.index, success="Candidate draft deleted
successfully")
+async def delete_candidate_draft(data: db.Session, candidate_draft_name: str)
-> None:
+ """Delete a candidate draft and all its associated files."""
+ # Check that the release exists
+ # TODO: Use session.release here
+ release = await data.release(name=candidate_draft_name,
_project=True).get()
+ if not release:
+ raise routes.FlashError("Candidate draft not found")
+ if release.phase != models.ReleasePhase.RELEASE_CANDIDATE_DRAFT:
+ raise routes.FlashError("Candidate draft is not in the release
candidate draft phase")
+
+ # Delete the release record
+ await data.delete(release)
+
+
@routes.committer("/draft/delete-file/<project_name>/<version_name>",
methods=["POST"])
async def delete_file(session: routes.CommitterSession, project_name: str,
version_name: str) -> response.Response:
"""Delete a specific file from the release candidate, creating a new
revision."""
@@ -507,17 +521,3 @@ async def vote_preview(
),
)
return quart.Response(body, mimetype="text/plain")
-
-
-async def _delete_candidate_draft(data: db.Session, candidate_draft_name: str)
-> None:
- """Delete a candidate draft and all its associated files."""
- # Check that the release exists
- # TODO: Use session.release here
- release = await data.release(name=candidate_draft_name,
_project=True).get()
- if not release:
- raise routes.FlashError("Candidate draft not found")
- if release.phase != models.ReleasePhase.RELEASE_CANDIDATE_DRAFT:
- raise routes.FlashError("Candidate draft is not in the release
candidate draft phase")
-
- # Delete the release record
- await data.delete(release)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]