asf-tooling commented on issue #473:
URL:
https://github.com/apache/tooling-trusted-releases/issues/473#issuecomment-4410232527
<!-- gofannon-issue-triage-bot v2 -->
**Automated triage** — analyzed at `main@2da7807a`
**Type:** `new_feature` • **Classification:** `actionable` •
**Confidence:** `medium`
**Application domain(s):** `release_lifecycle`, `distribution_tracking`
### Summary
The issue requests a new finish policy that allows users to automatically
archive a prior release when finishing a new one. At start time, users select a
prior version to archive; at finish/publish time, the prior release is
archived. @sbp confirmed (2026-04-22) that archiving will only be implemented
for prior releases made on ATR, not legacy releases. @dave2wave noted that a
manual archive operation should also be available for all releases. The feature
requires changes to the start form, release model storage, finish page UI, and
the actual archive logic.
### Where this lives in the code today
#### `atr/shared/start.py` — `StartReleaseForm` (lines 24-38)
_needs modification_
The start release form needs new fields: a checkbox to enable archiving and
a select for the prior version to archive.
```python
class StartReleaseForm(form.Form):
version_key: str = form.label(
"Version",
"Enter the version string for this new release."
" This cannot be changed later, and must be the version of the
finished release."
" ATR generates a unique revision serial number for each voting
round,"
" and you can also set your own tag before a vote starts.",
)
@pydantic.field_validator("version_key", mode="after")
@classmethod
def validate_version_key(cls, value: str) -> str:
if error := util.version_key_error(value):
raise ValueError(error)
return value
```
#### `atr/get/start.py` — `selected` (lines 37-53)
_needs modification_
The start page GET handler already fetches existing releases; it needs to
pass published ATR releases as choices for the archive-prior-version select
field.
```python
@get.typed
async def selected(session: web.Committer, _start: Literal["start"],
project_key: safe.ProjectKey) -> str:
"""
URL: /start/<project_key>
"""
await session.prevent_confusing_ui_display(project_key)
async with db.session() as data:
project = await data.project(key=str(project_key),
status=sql.ProjectStatus.ACTIVE).demand(
base.ASFQuartException(f"Project {project_key} not found",
errorcode=404)
)
releases = await interaction.all_releases(project)
content = await _render_page(project, releases)
return await template.blank(
title=f"Start a new release for {project.display_name}",
content=content,
)
```
#### `atr/post/start.py` — `selected` (lines 30-47)
_needs modification_
The start POST handler needs to pass the archive_prior_version value to the
release.start() method or store it on the release.
```python
@post.typed
async def selected(
session: web.Committer,
_start: Literal["start"],
project_key: safe.ProjectKey,
start_release_form: shared.start.StartReleaseForm,
) -> web.WerkzeugResponse:
"""
URL: /start/<project_key>
"""
try:
async with storage.write(session) as write:
wacp = await write.as_project_committee_participant(project_key)
new_release, _project = await wacp.release.start(
project_key,
safe.VersionKey(start_release_form.version_key),
)
```
#### `atr/shared/finish.py` — `FinishForm` (lines 31-47)
_needs modification_
The FinishForm discriminated union needs a new ARCHIVE_PRIOR variant for
manually triggering prior release archival from the finish page.
```python
type DELETE_DIR = Literal["DELETE_DIR"]
type REMOVE_RC_TAGS = Literal["REMOVE_RC_TAGS"]
class DeleteEmptyDirectoryForm(form.Form):
variant: DELETE_DIR = form.value(DELETE_DIR)
directory_to_delete: safe.RelPath = form.label("Directory to delete",
widget=form.Widget.SELECT)
class RemoveRCTagsForm(form.Empty):
variant: REMOVE_RC_TAGS = form.value(REMOVE_RC_TAGS)
type FinishForm = Annotated[
DeleteEmptyDirectoryForm | RemoveRCTagsForm,
form.DISCRIMINATOR,
]
```
#### `atr/post/finish.py` — `selected` (lines 32-49)
_needs modification_
The finish POST handler needs a new match case for ArchivePriorReleaseForm
to handle the archive action.
```python
@post.typed
async def selected(
session: web.Committer,
_finish: Literal["finish"],
project_key: safe.ProjectKey,
version_key: safe.VersionKey,
finish_form: shared.finish.FinishForm,
) -> tuple[web.QuartResponse, int] | web.WerkzeugResponse:
"""
URL: /finish/<project_key>/<version_key>
"""
respond = _respond_helper(session, project_key, version_key)
match finish_form:
case shared.finish.DeleteEmptyDirectoryForm() as delete_form:
return await _delete_empty_directory(delete_form, session,
project_key, version_key, respond)
case shared.finish.RemoveRCTagsForm():
return await _remove_rc_tags(session, project_key, version_key,
respond)
```
#### `atr/get/finish.py` — `_render_page` (lines 288-311)
_needs modification_
The finish page renderer needs a new section for the 'Archive prior release'
action with a form showing archivable prior releases.
```python
async def _render_page(
release: sql.Release,
deletable_dirs: list[tuple[str, str]],
rc_analysis: RCTagAnalysisResult,
distribution_tasks: Sequence[sql.Task],
announce_disable_message: str,
) -> str:
"""Render the finish page using htm.py."""
page = htm.Block()
render.html_nav(
page,
back_url=util.as_url(root.index),
back_anchor="Select a release",
phase="FINISH",
)
# Page heading
page.h1[
"Finish ",
htm.strong[release.project.short_display_name],
" ",
htm.em[release.version],
]
```
### Where new code would go
- `atr/models/sql.py` — Release model class
A new nullable column (e.g., archive_prior_version) on the Release model
to store which prior version should be archived when this release is published.
- `atr/storage/writers/release.py` — within CommitteeMember or
CommitteeParticipant class
A new method like archive_prior_release() to handle the actual archiving
logic (moving prior release artifacts to archive state).
### Proposed approach
The implementation has two main parts, based on @sbp's decision to only
support archiving prior ATR releases:
1. **Start-time selection**: Extend `StartReleaseForm` with an optional
`archive_prior_version` field (a select populated with prior published ATR
releases for the same project). Store this on the Release model as a nullable
field. The `atr/get/start.py` page will query for published releases of the
project and pass them as choices.
2. **Finish-time action**: Add a new `ArchivePriorReleaseForm` variant to
`FinishForm` in `atr/shared/finish.py`. On the finish page, render a section
showing which prior release is queued for archival (from the start-time
selection) with a button to execute it, plus a manual selector for cases where
it wasn't set at start time (per @dave2wave's requirement). The archive
operation itself marks the prior release as archived in the database and
moves/removes its artifacts from the distribution directory. The actual
archiving logic belongs in `atr/storage/writers/release.py` since it modifies
release state.
### Suggested patches
#### `atr/shared/start.py`
Add optional archive_prior_version field to StartReleaseForm
````diff
--- a/atr/shared/start.py
+++ b/atr/shared/start.py
@@ -18,6 +18,7 @@
import pydantic
import atr.form as form
+import atr.models.safe as safe
import atr.util as util
@@ -29,6 +30,13 @@ class StartReleaseForm(form.Form):
" and you can also set your own tag before a vote starts.",
)
+ archive_prior_version: str = form.label(
+ "Archive prior release",
+ "Optionally select a prior ATR release to archive when this release
is published.",
+ widget=form.Widget.SELECT,
+ required=False,
+ )
+
@pydantic.field_validator("version_key", mode="after")
@classmethod
def validate_version_key(cls, value: str) -> str:
````
#### `atr/shared/finish.py`
Add ArchivePriorReleaseForm variant to the FinishForm discriminated union
````diff
--- a/atr/shared/finish.py
+++ b/atr/shared/finish.py
@@ -27,6 +27,7 @@ type Respond = Callable[[int, str],
Awaitable[tuple[web.QuartResponse, int] | we
type DELETE_DIR = Literal["DELETE_DIR"]
type REMOVE_RC_TAGS = Literal["REMOVE_RC_TAGS"]
+type ARCHIVE_PRIOR = Literal["ARCHIVE_PRIOR"]
class DeleteEmptyDirectoryForm(form.Form):
@@ -38,7 +39,12 @@ class RemoveRCTagsForm(form.Empty):
variant: REMOVE_RC_TAGS = form.value(REMOVE_RC_TAGS)
+class ArchivePriorReleaseForm(form.Form):
+ variant: ARCHIVE_PRIOR = form.value(ARCHIVE_PRIOR)
+ prior_version: str = form.label("Prior release to archive",
widget=form.Widget.SELECT)
+
+
type FinishForm = Annotated[
- DeleteEmptyDirectoryForm | RemoveRCTagsForm,
+ DeleteEmptyDirectoryForm | RemoveRCTagsForm | ArchivePriorReleaseForm,
form.DISCRIMINATOR,
]
````
#### `atr/post/finish.py`
Add match case for ArchivePriorReleaseForm to handle archive requests
````diff
--- a/atr/post/finish.py
+++ b/atr/post/finish.py
@@ -37,6 +37,8 @@ async def selected(
case shared.finish.DeleteEmptyDirectoryForm() as delete_form:
return await _delete_empty_directory(delete_form, session,
project_key, version_key, respond)
case shared.finish.RemoveRCTagsForm():
return await _remove_rc_tags(session, project_key, version_key,
respond)
+ case shared.finish.ArchivePriorReleaseForm() as archive_form:
+ return await _archive_prior_release(archive_form, session,
project_key, version_key, respond)
async def _delete_empty_directory(
@@ -92,6 +94,33 @@ async def _remove_rc_tags(
return await _server_error(respond, e, f"Unexpected error: {e!s}")
+async def _archive_prior_release(
+ archive_form: shared.finish.ArchivePriorReleaseForm,
+ session: web.Committer,
+ project_key: safe.ProjectKey,
+ version_key: safe.VersionKey,
+ respond: shared.finish.Respond,
+) -> tuple[web.QuartResponse, int] | web.WerkzeugResponse:
+ prior_version = archive_form.prior_version
+ try:
+ async with storage.write(session) as write:
+ wacp = await write.as_project_committee_member(project_key)
+ # TODO: Implement wacp.release.archive_prior_release() in
storage/writers/release.py
+ creation_error = await wacp.release.archive_prior_release(
+ project_key,
+ version_key,
+ safe.VersionKey(prior_version),
+ )
+ except Exception as e:
+ log.exception(
+ f"Unexpected error archiving prior release {prior_version} for
{project_key}/{version_key}"
+ )
+ return await _server_error(respond, e, "An unexpected error
occurred while archiving.")
+
+ if creation_error is not None:
+ return await respond(400, creation_error)
+ return await respond(200, f"Archived prior release '{prior_version}'.")
+
+
def _respond_helper(
session: web.Committer, project_key: safe.ProjectKey, version_key:
safe.VersionKey
) -> shared.finish.Respond:
````
### Open questions
- What does 'archive' mean concretely in this context? Is it setting a
release phase/status (e.g., ARCHIVED) in the DB, moving files from dist to
archive.apache.org, or both?
- Where in atr/models/sql.py should the archive_prior_version field be
stored? On the Release model directly, or in a separate table/policy?
- The archive_prior_release() method needs to be implemented in
atr/storage/writers/release.py — what exact operations does it perform (DB
status change, filesystem move, both)?
- Should archiving automatically trigger when 'Announce and publish' is
clicked (if the flag was set at start time), or should it always be a separate
manual action on the finish page?
- The form.label() and form.Widget.SELECT usage in StartReleaseForm for
archive_prior_version needs validation — is 'required=False' the correct way to
make a form field optional in this framework?
- How should the list of archivable prior versions be populated? Only
RELEASE phase releases for the same project that were created on ATR (per
@sbp's decision)?
### Files examined
- `atr/get/finish.py`
- `atr/post/finish.py`
- `atr/shared/finish.py`
- `atr/get/start.py`
- `atr/post/start.py`
- `atr/shared/start.py`
- `atr/storage/writers/distributions.py`
- `atr/post/distribution.py`
---
*Draft from a triage agent. A human reviewer should validate before merging
any change. The agent did not run tests or verify diffs apply.*
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]