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 4da03ed Add a button to import a KEYS file
4da03ed is described below
commit 4da03ed6dfb721df2612ed41c8f37ba2c6d677d5
Author: Sean B. Palmer <[email protected]>
AuthorDate: Mon May 12 17:01:15 2025 +0100
Add a button to import a KEYS file
---
atr/routes/keys.py | 93 +++++++++++++++++++++-------
atr/templates/check-selected-path-table.html | 7 +++
2 files changed, 76 insertions(+), 24 deletions(-)
diff --git a/atr/routes/keys.py b/atr/routes/keys.py
index 4db2936..ed04911 100644
--- a/atr/routes/keys.py
+++ b/atr/routes/keys.py
@@ -29,6 +29,7 @@ import re
import textwrap
from collections.abc import Sequence
+import aiofiles.os
import asfquart as asfquart
import asfquart.base as base
import quart
@@ -42,6 +43,8 @@ import atr.db.interaction as interaction
import atr.db.models as models
import atr.routes as routes
import atr.util as util
+from atr import revision
+from atr.routes import compose
class AddSSHKeyForm(util.QuartFormTyped):
@@ -153,6 +156,39 @@ async def delete(session: routes.CommitterSession) ->
response.Response:
return await session.redirect(keys, error="Key not found or not
owned by you")
[email protected]("/keys/import/<project_name>/<version_name>",
methods=["POST"])
+async def import_selected_revision(
+ session: routes.CommitterSession, project_name: str, version_name: str
+) -> response.Response:
+ await session.check_access(project_name)
+
+ release = await session.release(project_name, version_name,
with_committee=True)
+ keys_path = util.release_directory(release) / "KEYS"
+ async with aiofiles.open(keys_path, encoding="utf-8") as f:
+ keys_text = await f.read()
+ if release.committee is None:
+ raise routes.FlashError("No committee found for release")
+ selected_committees = [release.committee.name]
+ success_count, error_count, submitted_committees = await
_upload_keys(session, keys_text, selected_committees)
+ message = f"Uploaded {success_count} keys,"
+ if error_count > 0:
+ message += f" failed to upload {error_count} keys for {',
'.join(submitted_committees)}"
+ # Remove the KEYS file if 100% imported
+ if (success_count > 0) and (error_count == 0):
+ async with revision.create_and_manage(project_name, version_name,
session.uid) as (
+ new_revision_dir,
+ _new_revision_name,
+ ):
+ path_in_new_revision = new_revision_dir / "KEYS"
+ await aiofiles.os.remove(path_in_new_revision)
+ return await session.redirect(
+ compose.selected,
+ success=message,
+ project_name=project_name,
+ version_name=version_name,
+ )
+
+
async def key_add_post(
session: routes.CommitterSession, request: quart.Request, user_committees:
Sequence[models.Committee]
) -> dict | None:
@@ -416,36 +452,17 @@ async def upload(session: routes.CommitterSession) -> str:
if not isinstance(key_file, datastructures.FileStorage):
return await render(error="Invalid file upload")
- # This is a KEYS file of multiple GPG keys
- # We need to parse it and add each key to the user's account
- keys_content = await asyncio.to_thread(key_file.read)
- keys_text = keys_content.decode("utf-8", errors="replace")
- key_blocks = util.parse_key_blocks(keys_text)
- if not key_blocks:
- return await render(error="No valid GPG keys found in the uploaded
file")
-
# Get selected committee list from the form
selected_committees = form.selected_committees.data
if not selected_committees:
return await render(error="You must select at least one committee")
+ # This is a KEYS file of multiple GPG keys
+ # We need to parse it and add each key to the user's account
+ keys_content = await asyncio.to_thread(key_file.read)
+ keys_text = keys_content.decode("utf-8", errors="replace")
- # Ensure that the selected committees are ones of which the user is
actually a member
- invalid_committees = [
- committee for committee in selected_committees if (committee not
in (session.committees + session.projects))
- ]
- if invalid_committees:
- return await render(error=f"Invalid committee selection: {',
'.join(invalid_committees)}")
-
- # TODO: Do we modify this? Store a copy just in case, for the template
to use
- submitted_committees = selected_committees[:]
-
- # Process each key block
- results = await _upload_process_key_blocks(key_blocks,
selected_committees)
- if not results:
- return await render(error="No keys were added")
+ success_count, error_count, submitted_committees = await
_upload_keys(session, keys_text, selected_committees)
- success_count = sum(1 for result in results if result["status"] ==
"success")
- error_count = len(results) - success_count
await quart.flash(
f"Processed {len(results)} keys: {success_count} successful,
{error_count} failed",
"success" if success_count > 0 else "error",
@@ -455,6 +472,34 @@ async def upload(session: routes.CommitterSession) -> str:
return await render()
+async def _upload_keys(
+ session: routes.CommitterSession, keys_text: str, selected_committees:
list[str]
+) -> tuple[int, int, list[str]]:
+ key_blocks = util.parse_key_blocks(keys_text)
+ if not key_blocks:
+ raise routes.FlashError("No valid GPG keys found in the uploaded file")
+
+ # Ensure that the selected committees are ones of which the user is
actually a member
+ invalid_committees = [
+ committee for committee in selected_committees if (committee not in
(session.committees + session.projects))
+ ]
+ if invalid_committees:
+ raise routes.FlashError(f"Invalid committee selection: {',
'.join(invalid_committees)}")
+
+ # TODO: Do we modify this? Store a copy just in case, for the template to
use
+ submitted_committees = selected_committees[:]
+
+ # Process each key block
+ results = await _upload_process_key_blocks(key_blocks, selected_committees)
+ if not results:
+ raise routes.FlashError("No keys were added")
+
+ success_count = sum(1 for result in results if result["status"] ==
"success")
+ error_count = len(results) - success_count
+
+ return success_count, error_count, submitted_committees
+
+
async def _upload_process_key_blocks(key_blocks: list[str],
selected_committees: list[str]) -> list[dict]:
"""Process GPG key blocks and add them to the user's account."""
results: list[dict] = []
diff --git a/atr/templates/check-selected-path-table.html
b/atr/templates/check-selected-path-table.html
index 693859b..b60ded9 100644
--- a/atr/templates/check-selected-path-table.html
+++ b/atr/templates/check-selected-path-table.html
@@ -56,6 +56,13 @@
</td>
<td class="text-end text-nowrap py-2">
<div class="d-flex justify-content-end align-items-center gap-2">
+ {% if path|string == "KEYS" %}
+ <form method="post"
+ action="{{ as_url(routes.keys.import_selected_revision,
project_name=project_name, version_name=version_name) }}"
+ class="d-inline mb-0">
+ <button type="submit" class="btn btn-sm
btn-outline-primary">Import keys</button>
+ </form>
+ {% endif %}
{% if has_errors %}
<a href="{{ as_url(routes.report.selected_path,
project_name=project_name, version_name=version_name, rel_path=path) }}"
class="btn btn-sm btn-outline-danger"><i class="bi
bi-exclamation-triangle me-1"></i> Show {{ info.errors[path]|length }} {{
"error" if info.errors[path]|length == 1 else "errors" }}</a>
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]