You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tooling.apache.org by ar...@apache.org on 2026/04/20 09:48:19 UTC
(tooling-trusted-releases) branch project_archival updated: #510 - allow projects to be archived
This is an automated email from the ASF dual-hosted git repository.
alitheg pushed a commit to branch project_archival
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git
The following commit(s) were added to refs/heads/project_archival by this push:
new 9ba6a595 #510 - allow projects to be archived
9ba6a595 is described below
commit 9ba6a5953832949dcd61eb58d78ad7f4d810e6ba
Author: Alastair McFarlane <ar...@apache.org>
AuthorDate: Mon Apr 20 10:48:04 2026 +0100
#510 - allow projects to be archived
---
atr/get/projects.py | 14 +++++++++---
atr/post/projects.py | 22 +++++++++++++++++++
atr/storage/writers/project.py | 50 +++++++++++++++++++++++++++++++++++++-----
3 files changed, 77 insertions(+), 9 deletions(-)
diff --git a/atr/get/projects.py b/atr/get/projects.py
index a717fcc2..fb288726 100644
--- a/atr/get/projects.py
+++ b/atr/get/projects.py
@@ -17,6 +17,7 @@
from __future__ import annotations
+from collections import Counter
from typing import Literal
import asfquart.base as base
@@ -102,21 +103,28 @@ async def projects(session: web.Public, _projects: Literal["projects"]) -> str:
async with db.session() as data:
projects = await data.project(_committee=True, _releases=True).order_by(sql.Project.name).all()
+ committee_project_counts: Counter[str] = Counter(str(p.committee.key) for p in projects if p.committee)
+
action_forms: dict[str, htm.Element] = {}
if session is not None:
for project in projects:
+ if project.status != sql.ProjectStatus.ACTIVE:
+ continue
if not (user.is_committee_member(project.committee, session.uid) or session.is_admin):
continue
- if project.releases:
+ if project.committee and committee_project_counts[str(project.committee.key)] <= 1:
+ continue
+ _finished = {sql.ReleasePhase.RELEASE_PREVIEW, sql.ReleasePhase.RELEASE}
+ if any(r.phase in _finished for r in project.releases):
action_forms[str(project.key)] = await form.render(
model_cls=shared.projects.ArchiveSelectedProject,
- action="#",
+ action=util.as_url(post.projects.archive),
form_classes=".d-inline-block.m-0",
submit_classes="btn-sm btn-outline-secondary",
submit_label="Archive project",
- submit_disabled=True,
empty=True,
defaults={"project_key": str(project.key)},
+ confirm="Are you sure you want to archive this project? It will be marked as retired.",
)
else:
action_forms[str(project.key)] = await form.render(
diff --git a/atr/post/projects.py b/atr/post/projects.py
index 6cd041a5..7474bd82 100644
--- a/atr/post/projects.py
+++ b/atr/post/projects.py
@@ -63,6 +63,28 @@ async def add_project(
)
+@post.typed
+async def archive(
+ session: web.Committer,
+ _project_archive: Literal["project/archive"],
+ archive_selected_project_form: shared.projects.ArchiveSelectedProject,
+) -> web.WerkzeugResponse:
+ """
+ URL: /project/archive
+ Archive a project as a PMC member.
+ """
+ project_key = archive_selected_project_form.project_key
+
+ async with storage.write(session) as write:
+ wacm = await write.as_project_committee_member(project_key)
+ try:
+ await wacm.project.archive(project_key)
+ except storage.AccessError as e:
+ return await session.redirect(get.projects.projects, error=f"Error archiving project: {e}")
+
+ return await session.redirect(get.projects.projects, success=f"Project '{project_key}' archived successfully.")
+
+
@post.typed
async def delete(
session: web.Committer,
diff --git a/atr/storage/writers/project.py b/atr/storage/writers/project.py
index 92781615..731121f4 100644
--- a/atr/storage/writers/project.py
+++ b/atr/storage/writers/project.py
@@ -186,17 +186,55 @@ class CommitteeMember(CommitteeParticipant):
else:
raise
+ async def archive(self, project_key: safe.ProjectKey) -> None:
+ project = await self.__data.project(key=str(project_key), status=sql.ProjectStatus.ACTIVE).get()
+
+ if not project:
+ raise storage.AccessError(f"Project '{project_key}' not found.")
+
+ if project.committee_key:
+ committee_projects = await self.__data.project(
+ committee_key=project.committee_key, status=sql.ProjectStatus.ACTIVE
+ ).all()
+ if len(committee_projects) <= 1:
+ raise storage.AccessError(
+ f"Cannot archive project '{project_key}' because it is the only project in its committee."
+ )
+
+ project = await self.__data.merge(project)
+ project.status = sql.ProjectStatus.RETIRED
+ await self.__data.commit()
+ self.__write_as.append_to_audit_log(
+ asf_uid=self.__asf_uid,
+ project_key=str(project_key),
+ )
+
async def delete(self, project_key: safe.ProjectKey) -> None:
- project = await self.__data.project(
- key=str(project_key), status=sql.ProjectStatus.ACTIVE, _releases=True, _distribution_channels=True
- ).get()
+ project = await self.__data.project(key=str(project_key), status=sql.ProjectStatus.ACTIVE, _releases=True).get()
if not project:
raise storage.AccessError(f"Project '{project_key}' not found.")
- # Prevent deletion if there are associated releases or channels
- if project.releases:
- raise storage.AccessError(f"Cannot delete project '{project_key}' because it has associated releases.")
+ if project.committee_key:
+ committee_projects = await self.__data.project(
+ committee_key=project.committee_key, status=sql.ProjectStatus.ACTIVE
+ ).all()
+ if len(committee_projects) <= 1:
+ raise storage.AccessError(
+ f"Cannot delete project '{project_key}' because it is the only project in its committee."
+ )
+
+ finished_phases = {sql.ReleasePhase.RELEASE_PREVIEW, sql.ReleasePhase.RELEASE}
+ if any(r.phase in finished_phases for r in project.releases):
+ raise storage.AccessError(
+ f"Cannot delete project '{project_key}' because it has current or archived releases."
+ )
+
+ # Candidate releases (draft or under vote) are removed along with the project
+ for release in list(project.releases):
+ error = await self.__write_as.release.delete(project_key, safe.VersionKey(release.version))
+ if error is not None:
+ raise storage.AccessError(f"Could not delete candidate release '{release.version}': {error}")
await self.__data.delete(project)
await self.__data.commit()
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org