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]