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]

Reply via email to