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 f0af90d  Validate versions when creating new draft releases
f0af90d is described below

commit f0af90dad4faefa8f6ff252b593f16d7ca0a6697
Author: Sean B. Palmer <[email protected]>
AuthorDate: Thu Apr 10 15:04:50 2025 +0100

    Validate versions when creating new draft releases
---
 atr/routes/draft.py |  2 ++
 atr/ssh.py          | 41 ++++++++++++++---------------------------
 atr/util.py         | 18 ++++++++++++++++++
 3 files changed, 34 insertions(+), 27 deletions(-)

diff --git a/atr/routes/draft.py b/atr/routes/draft.py
index b0a41aa..515a95f 100644
--- a/atr/routes/draft.py
+++ b/atr/routes/draft.py
@@ -993,6 +993,8 @@ async def _add(session: routes.CommitterSession, form: 
AddProtocol) -> None:
                 raise routes.FlashError(f"{release.phase.value.upper()} with 
this name already exists")
 
             # Release is now linked to the appropriate project or subproject
+            if version_name_error := util.version_name_error(version):
+                raise routes.FlashError(f'Invalid version name "{version}": 
{version_name_error}')
             release = models.Release(
                 stage=models.ReleaseStage.RELEASE_CANDIDATE,
                 phase=models.ReleasePhase.RELEASE_CANDIDATE_DRAFT,
diff --git a/atr/ssh.py b/atr/ssh.py
index 6e95838..dc007d6 100644
--- a/atr/ssh.py
+++ b/atr/ssh.py
@@ -34,6 +34,7 @@ import atr.db as db
 import atr.db.models as models
 import atr.revision as revision
 import atr.user as user
+import atr.util as util
 
 _LOGGER: Final = logging.getLogger(__name__)
 _CONFIG: Final = config.get()
@@ -240,9 +241,7 @@ async def _command_validate(
     return path_project, path_version
 
 
-async def _ensure_release_object(
-    process: asyncssh.SSHServerProcess, project_name: str, version_name: str, 
new_draft_revision: str
-) -> bool:
+async def _ensure_release_object(process: asyncssh.SSHServerProcess, 
project_name: str, version_name: str) -> bool:
     try:
         async with db.session() as data:
             async with data.begin():
@@ -253,6 +252,8 @@ async def _ensure_release_object(
                     project = await data.project(name=project_name, 
_committee=True).demand(
                         RuntimeError("Project not found")
                     )
+                    if version_name_error := 
util.version_name_error(version_name):
+                        raise RuntimeError(f'Invalid version name 
"{version_name}": {version_name_error}')
                     # Create a new release object
                     release = models.Release(
                         project_id=project.id,
@@ -267,29 +268,13 @@ async def _ensure_release_object(
                     return _fail(
                         process, f"Release {release.name} is no longer in 
draft phase ({release.phase.value})", False
                     )
-
-                # # TODO: We now do this in the context manager, so we can 
delete the rsync task
-                # data.add(
-                #     models.Task(
-                #         status=models.TaskStatus.QUEUED,
-                #         task_type=models.TaskType.RSYNC_ANALYSE,
-                #         task_args=rsync.Analyse(
-                #             project_name=project_name,
-                #             release_version=version_name,
-                #             draft_revision=new_draft_revision,
-                #         ).model_dump(),
-                #         release_name=models.release_name(project_name, 
version_name),
-                #         draft_revision=new_draft_revision,
-                #     )
-                # )
         return True
     except Exception as e:
-        _LOGGER.exception("Error finalising upload in database")
-        return _fail(process, f"Internal error finalising upload: {e}", False)
+        _LOGGER.exception("Error creating release object")
+        return _fail(process, f"Internal error creating release object: {e}", 
False)
 
 
 async def _execute_rsync(process: asyncssh.SSHServerProcess, argv: list[str]) 
-> int:
-    # This is Step 2 of the upload process
     _LOGGER.info(f"Executing modified command: {' '.join(argv)}")
     proc = await asyncio.create_subprocess_shell(
         " ".join(argv),
@@ -324,9 +309,16 @@ async def _process_validated_rsync(
             new_revision_dir,
             new_draft_revision,
         ):
-            # Update the rsync command path to the new temporary revision 
directory
+            # Update the rsync command path to the new revision directory
+            # The revision directory has already been created by the context 
manager
             argv[path_index] = str(new_revision_dir)
 
+            # Ensure that the release object exists
+            # This performs validation, so must be done before the rsync 
command
+            if not await _ensure_release_object(process, project_name, 
version_name):
+                process.exit(1)
+                return
+
             # Execute the rsync command
             exit_status = await _execute_rsync(process, argv)
             if exit_status != 0:
@@ -337,11 +329,6 @@ async def _process_validated_rsync(
                 process.exit(exit_status)
                 return
 
-            # Ensure that the release object exists and is in the correct phase
-            if not await _ensure_release_object(process, project_name, 
version_name, new_draft_revision):
-                process.exit(1)
-                return
-
             # Exit with the rsync exit status
             process.exit(exit_status)
 
diff --git a/atr/util.py b/atr/util.py
index d286871..1c87eca 100644
--- a/atr/util.py
+++ b/atr/util.py
@@ -22,6 +22,7 @@ import dataclasses
 import hashlib
 import logging
 import pathlib
+import re
 import shutil
 import tarfile
 import tempfile
@@ -394,6 +395,23 @@ def validate_as_type(value: Any, t: type[T]) -> T:
     return value
 
 
+def version_name_error(version_name: str) -> str | None:
+    """Check if the given version name is valid."""
+    if version_name == "":
+        return "Must not be empty"
+    if version_name.lower() == "version":
+        return "Must not be 'version'"
+    if not re.match(r"^[a-zA-Z0-9]", version_name):
+        return "Must start with a letter or number"
+    if not re.search(r"[a-zA-Z0-9]$", version_name):
+        return "Must end with a letter or number"
+    if re.search(r"[+.-]{2,}", version_name):
+        return "Must not contain multiple consecutive plus, full stop, or 
hyphen"
+    if not re.match(r"^[a-zA-Z0-9+.-]+$", version_name):
+        return "Must contain only letters, numbers, plus, full stop, or hyphen"
+    return None
+
+
 def _generate_hexdump(data: bytes) -> str:
     """Generate a formatted hexdump string from bytes."""
     hex_lines = []


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

Reply via email to