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 00e2ebf  Allow the addition of external staging distributions for some 
platforms
00e2ebf is described below

commit 00e2ebf4d8b1a564c690dffb42edc6dde1fb16cf
Author: Sean B. Palmer <[email protected]>
AuthorDate: Fri Aug 8 14:42:17 2025 +0100

    Allow the addition of external staging distributions for some platforms
---
 atr/routes/distribution.py | 160 ++++++++++++++++++++++++++++++++++++++-------
 1 file changed, 135 insertions(+), 25 deletions(-)

diff --git a/atr/routes/distribution.py b/atr/routes/distribution.py
index c3988eb..8da8ae3 100644
--- a/atr/routes/distribution.py
+++ b/atr/routes/distribution.py
@@ -34,6 +34,7 @@ import atr.models.basic as basic
 import atr.models.schema as schema
 import atr.models.sql as sql
 import atr.routes as routes
+import atr.routes.compose as compose
 import atr.routes.finish as finish
 import atr.storage as storage
 import atr.storage.outcome as outcome
@@ -285,28 +286,68 @@ async def record_post(session: routes.CommitterSession, 
project: str, version: s
     return await _distribute_page(fpv)
 
 
[email protected]("/distribution/stage/<project>/<version>", methods=["GET"])
+async def stage(session: routes.CommitterSession, project: str, version: str) 
-> str:
+    form = await DistributeForm.create_form(data={"package": project, 
"version": version})
+    fpv = FormProjectVersion(form=form, project=project, version=version)
+    return await _distribute_page(fpv, staging=True)
+
+
[email protected]("/distribution/stage/<project>/<version>", methods=["POST"])
+async def stage_post(session: routes.CommitterSession, project: str, version: 
str) -> str:
+    form = await DistributeForm.create_form(data=await quart.request.form)
+    fpv = FormProjectVersion(form=form, project=project, version=version)
+    if await form.validate():
+        return await _distribute_post_validated(fpv, staging=True)
+    match len(form.errors):
+        case 0:
+            await quart.flash("Ambiguous submission errors", 
category="warning")
+        case 1:
+            await quart.flash("There was 1 submission error", category="error")
+        case _ as n:
+            await quart.flash(f"There were {n} submission errors", 
category="error")
+    return await _distribute_page(fpv, staging=True)
+
+
 # This function is used in both GET and POST routes
-async def _distribute_page(fpv: FormProjectVersion, *, extra_content: 
htpy.Element | None = None) -> str:
-    # Validate the Release
-    await _release_validated(fpv.project, fpv.version)
+async def _distribute_page(
+    fpv: FormProjectVersion, *, extra_content: htpy.Element | None = None, 
staging: bool = False
+) -> str:
+    if staging:
+        await _release_validated(fpv.project, fpv.version, 
phase=sql.ReleasePhase.RELEASE_CANDIDATE_DRAFT)
+    else:
+        await _release_validated(fpv.project, fpv.version)
 
     # Render the explanation and form
     block = htm.Block()
 
-    back_url = util.as_url(finish.selected, project_name=fpv.project, 
version_name=fpv.version)
-    _nav(block, back_url, back_anchor=f"Finish {fpv.project} {fpv.version}", 
phase="FINISH")
+    if staging:
+        back_url = util.as_url(compose.selected, project_name=fpv.project, 
version_name=fpv.version)
+        _nav(block, back_url, back_anchor=f"Compose {fpv.project} 
{fpv.version}", phase="COMPOSE")
+    else:
+        back_url = util.as_url(finish.selected, project_name=fpv.project, 
version_name=fpv.version)
+        _nav(block, back_url, back_anchor=f"Finish {fpv.project} 
{fpv.version}", phase="FINISH")
 
     # Record a manual distribution
-    block.h1["Record a manual distribution"]
+    block.h1["Record a staging distribution" if staging else "Record a manual 
distribution"]
     if extra_content:
         block.append(extra_content)
-    block.p[
-        "Record a manual distribution of ",
-        htpy.strong[f"{fpv.project}-{fpv.version}"],
-        " during the ",
-        htpy.span(".atr-phase-three.atr-phase-label")["FINISH"],
-        " phase using the form below.",
-    ]
+    if staging:
+        block.p[
+            "Record a staging distribution of ",
+            htpy.strong[f"{fpv.project}-{fpv.version}"],
+            " during the ",
+            htpy.span(".atr-phase-one.atr-phase-label")["COMPOSE"],
+            " phase using the form below.",
+        ]
+    else:
+        block.p[
+            "Record a manual distribution of ",
+            htpy.strong[f"{fpv.project}-{fpv.version}"],
+            " during the ",
+            htpy.span(".atr-phase-three.atr-phase-label")["FINISH"],
+            " phase using the form below.",
+        ]
     block.p[
         "You can also ",
         htpy.a(href=util.as_url(list_get, project=fpv.project, 
version=fpv.version))["view the distribution list"],
@@ -315,7 +356,9 @@ async def _distribute_page(fpv: FormProjectVersion, *, 
extra_content: htpy.Eleme
     block.append(forms.render_columns(fpv.form, action=quart.request.path, 
descriptions=True))
 
     # Render the page
-    return await template.blank("Record a manual distribution", 
content=block.collect())
+    return await template.blank(
+        "Record a staging distribution" if staging else "Record a manual 
distribution", content=block.collect()
+    )
 
 
 async def _distribute_post_api(
@@ -337,10 +380,64 @@ async def _distribute_post_api(
     return outcome.Result(result)
 
 
-async def _distribute_post_validated(fpv: FormProjectVersion, /) -> str:
-    dd = DistributeData.model_validate(fpv.form.data)
+async def _resolve_release_committee_and_template(
+    fpv: FormProjectVersion,
+    dd: DistributeData,
+    staging: bool,
+) -> tuple[sql.Release, sql.Committee, str] | str:
+    if staging:
+        release, committee = await _release_committee_validated(
+            fpv.project, fpv.version, 
phase=sql.ReleasePhase.RELEASE_CANDIDATE_DRAFT
+        )
+        supported = {sql.DistributionPlatform.ARTIFACTHUB, 
sql.DistributionPlatform.PYPI}
+        if dd.platform not in supported:
+            div = htm.Block(htpy.div(".alert.alert-danger"))
+            div.p["Staging is currently supported only for ArtifactHub and 
PyPI."]
+            return await _distribute_page(fpv, extra_content=div.collect(), 
staging=True)
+        template_url = dd.platform.value.template_staging_url
+        if template_url is None:
+            div = htm.Block(htpy.div(".alert.alert-danger"))
+            div.p["This platform does not provide a staging API endpoint."]
+            return await _distribute_page(fpv, extra_content=div.collect(), 
staging=True)
+        return release, committee, template_url
     release, committee = await _release_committee_validated(fpv.project, 
fpv.version)
-    api_url = fpv.form.platform.data.value.template_url.format(
+    return release, committee, dd.platform.value.template_url
+
+
+async def _maybe_upgrade_existing_final(
+    release: sql.Release,
+    dd: DistributeData,
+    upload_date: datetime.datetime,
+    api_url: str,
+    added: bool,
+    staging: bool,
+    distribution: sql.Distribution,
+) -> sql.Distribution:
+    if (not added) and (not staging):
+        async with db.session() as data:
+            existing = await data.distribution(
+                release_name=release.name,
+                platform=dd.platform,
+                owner_namespace=dd.owner_namespace or "",
+                package=dd.package,
+                version=dd.version,
+            ).demand(RuntimeError("Distribution not found"))
+            if existing.staging:
+                existing.staging = False
+                existing.upload_date = upload_date
+                existing.api_url = api_url
+                await data.commit()
+                return existing
+    return distribution
+
+
+async def _distribute_post_validated(fpv: FormProjectVersion, /, staging: bool 
= False) -> str:
+    dd = DistributeData.model_validate(fpv.form.data)
+    resolved = await _resolve_release_committee_and_template(fpv, dd, staging)
+    if isinstance(resolved, str):
+        return resolved
+    release, committee, template_url = resolved
+    api_url = template_url.format(
         owner_namespace=dd.owner_namespace,
         package=dd.package,
         version=dd.version,
@@ -366,7 +463,7 @@ async def _distribute_post_validated(fpv: 
FormProjectVersion, /) -> str:
             pass
         case outcome.Error():
             alert = _alert("package and version", "check the package name and 
version")
-            return await _distribute_page(fpv, extra_content=alert)
+            return await _distribute_page(fpv, extra_content=alert, 
staging=staging)
         # We leak result, usefully, from this scope
 
     # This must come after the api_oc match, as it uses the result
@@ -374,7 +471,7 @@ async def _distribute_post_validated(fpv: 
FormProjectVersion, /) -> str:
     if upload_date is None:
         # TODO: Add a link to an issue tracker
         alert = _alert("upload date", "report this bug to ASF Tooling")
-        return await _distribute_page(fpv, extra_content=alert)
+        return await _distribute_page(fpv, extra_content=alert, 
staging=staging)
 
     async with 
storage.write_as_committee_member(committee_name=committee.name) as w:
         distribution, added = await w.distributions.add_distribution(
@@ -383,10 +480,13 @@ async def _distribute_post_validated(fpv: 
FormProjectVersion, /) -> str:
             owner_namespace=dd.owner_namespace,
             package=dd.package,
             version=dd.version,
-            staging=False,
+            staging=staging,
             upload_date=upload_date,
             api_url=api_url,
         )
+        distribution = await _maybe_upgrade_existing_final(
+            release, dd, upload_date, api_url, added, staging, distribution
+        )
 
     ### Record
     block.h2["Record"]
@@ -516,23 +616,33 @@ def _platform_upload_date(  # noqa: C901
     raise NotImplementedError(f"Platform {platform.name} is not yet supported")
 
 
-async def _release_committee_validated(project: str, version: str) -> 
tuple[sql.Release, sql.Committee]:
-    release = await _release_validated(project, version, committee=True)
+async def _release_committee_validated(
+    project: str,
+    version: str,
+    *,
+    phase: sql.ReleasePhase = sql.ReleasePhase.RELEASE_PREVIEW,
+) -> tuple[sql.Release, sql.Committee]:
+    release = await _release_validated(project, version, committee=True, 
phase=phase)
     committee = release.committee
     if committee is None:
         raise RuntimeError(f"Release {project} {version} has no committee")
     return release, committee
 
 
-async def _release_validated(project: str, version: str, committee: bool = 
False) -> sql.Release:
+async def _release_validated(
+    project: str,
+    version: str,
+    committee: bool = False,
+    phase: sql.ReleasePhase = sql.ReleasePhase.RELEASE_PREVIEW,
+) -> sql.Release:
     async with db.session() as data:
         release = await data.release(
             project_name=project,
             version=version,
             _committee=committee,
         ).demand(RuntimeError(f"Release {project} {version} not found"))
-        if release.phase != sql.ReleasePhase.RELEASE_PREVIEW:
-            raise RuntimeError(f"Release {project} {version} is not a release 
preview")
+        if release.phase != phase:
+            raise RuntimeError(f"Release {project} {version} is not a 
{phase.value.upper()}")
         # if release.project.status != sql.ProjectStatus.ACTIVE:
         #     raise RuntimeError(f"Project {project} is not active")
     return release


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

Reply via email to