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 1eccebd Add an option to preserve existing download files, false by
default
1eccebd is described below
commit 1eccebdb077c017a4413005d09abe2334972c56f
Author: Sean B. Palmer <[email protected]>
AuthorDate: Tue Sep 9 16:58:09 2025 +0100
Add an option to preserve existing download files, false by default
---
atr/models/sql.py | 7 ++++++
atr/routes/projects.py | 25 +++++++++++++--------
atr/storage/writers/announce.py | 17 ++++++++++++---
atr/templates/project-view.html | 11 ++++++++++
migrations/versions/0028_2025.09.09_a0037268.py | 29 +++++++++++++++++++++++++
5 files changed, 77 insertions(+), 12 deletions(-)
diff --git a/atr/models/sql.py b/atr/models/sql.py
index ed655a9..a5d9a9f 100644
--- a/atr/models/sql.py
+++ b/atr/models/sql.py
@@ -676,6 +676,12 @@ Thanks,
return []
return policy.github_finish_workflow_path or []
+ @property
+ def policy_preserve_download_files(self) -> bool:
+ if (policy := self.release_policy) is None:
+ return False
+ return policy.preserve_download_files
+
# Release: Project ReleasePolicy Revision CheckResult
class Release(sqlmodel.SQLModel, table=True):
@@ -990,6 +996,7 @@ class ReleasePolicy(sqlmodel.SQLModel, table=True):
github_finish_workflow_path: list[str] = sqlmodel.Field(
default_factory=list, sa_column=sqlalchemy.Column(sqlalchemy.JSON,
nullable=False)
)
+ preserve_download_files: bool = sqlmodel.Field(default=False)
# 1-1: ReleasePolicy -> Project
# 1-1: Project -C-> ReleasePolicy
diff --git a/atr/routes/projects.py b/atr/routes/projects.py
index ceb797e..9c87c8c 100644
--- a/atr/routes/projects.py
+++ b/atr/routes/projects.py
@@ -153,6 +153,10 @@ class ReleasePolicyForm(forms.Typed):
description="The full paths to the GitHub workflows to use for the
release,"
" including the .github/workflows/ prefix.",
)
+ preserve_download_files = forms.boolean(
+ "Preserve download files",
+ description="If enabled, existing download files will not be
overwritten.",
+ )
submit_policy = forms.submit("Save")
@@ -520,32 +524,34 @@ async def _policy_edit(
util.unwrap(policy_form.binary_artifact_paths.data)
)
release_policy.github_repository_name =
util.unwrap(policy_form.github_repository_name.data)
+ # TODO: Change to paths, plural
release_policy.github_compose_workflow_path = _parse_artifact_paths(
util.unwrap(policy_form.github_compose_workflow_path.data)
)
- release_policy.github_vote_workflow_path = _parse_artifact_paths(
- util.unwrap(policy_form.github_vote_workflow_path.data)
- )
- release_policy.github_finish_workflow_path = _parse_artifact_paths(
- util.unwrap(policy_form.github_finish_workflow_path.data)
- )
release_policy.strict_checking =
util.unwrap(policy_form.strict_checking.data)
# Vote section
release_policy.manual_vote = policy_form.manual_vote.data or False
if not release_policy.manual_vote:
+ release_policy.github_vote_workflow_path = _parse_artifact_paths(
+ util.unwrap(policy_form.github_vote_workflow_path.data)
+ )
release_policy.mailto_addresses =
[util.unwrap(policy_form.mailto_addresses.data)]
- _set_default_min_hours(policy_form, project, release_policy)
+ _set_default_min_hours(policy_form, project, release_policy) #
TODO
release_policy.pause_for_rm =
util.unwrap(policy_form.pause_for_rm.data)
release_policy.release_checklist =
util.unwrap(policy_form.release_checklist.data)
- _set_default_start_vote_template(policy_form, project,
release_policy)
+ _set_default_start_vote_template(policy_form, project,
release_policy) # TODO
elif project.committee and project.committee.is_podling:
# The caller ensures that project.committee is not None
await quart.flash("Manual voting is not allowed for podlings.",
"error")
return False, policy_form
# Finish section
- _set_default_announce_release_template(policy_form, project,
release_policy)
+ release_policy.github_finish_workflow_path = _parse_artifact_paths(
+ util.unwrap(policy_form.github_finish_workflow_path.data)
+ )
+ _set_default_announce_release_template(policy_form, project,
release_policy) # TODO
+ release_policy.preserve_download_files =
util.unwrap(policy_form.preserve_download_files.data)
await data.commit()
await quart.flash("Release policy updated successfully.", "success")
@@ -576,6 +582,7 @@ async def _policy_form_create(project: sql.Project) ->
ReleasePolicyForm:
policy_form.github_compose_workflow_path.data =
"\n".join(project.policy_github_compose_workflow_path)
policy_form.github_vote_workflow_path.data =
"\n".join(project.policy_github_vote_workflow_path)
policy_form.github_finish_workflow_path.data =
"\n".join(project.policy_github_finish_workflow_path)
+ policy_form.preserve_download_files.data =
project.policy_preserve_download_files
# Set the hashes and value of the current defaults
policy_form.default_start_vote_template_hash.data = util.compute_sha3_256(
diff --git a/atr/storage/writers/announce.py b/atr/storage/writers/announce.py
index 57bd213..7cc67c7 100644
--- a/atr/storage/writers/announce.py
+++ b/atr/storage/writers/announce.py
@@ -151,7 +151,9 @@ class CommitteeMember(CommitteeParticipant):
raise storage.AccessError("Release already exists")
# TODO: This is not reliable because of race conditions
# But it adds a layer of protection in most cases
- await self.__hard_link_downloads(committee, unfinished_path,
download_path_suffix, dry_run=True)
+ preserve = release.project.policy_preserve_download_files
+ if preserve is True:
+ await self.__hard_link_downloads(committee, unfinished_path,
download_path_suffix, dry_run=True)
try:
task = sql.Task(
@@ -201,7 +203,12 @@ class CommitteeMember(CommitteeParticipant):
raise storage.AccessError(f"Database updated, but error moving
files: {e!s}. Manual cleanup needed.")
# TODO: Add an audit log entry here
- await self.__hard_link_downloads(committee, finished_path,
download_path_suffix)
+ await self.__hard_link_downloads(
+ committee,
+ finished_path,
+ download_path_suffix,
+ preserve=preserve,
+ )
async def __hard_link_downloads(
self,
@@ -209,16 +216,20 @@ class CommitteeMember(CommitteeParticipant):
unfinished_path: pathlib.Path,
download_path_suffix: str,
dry_run: bool = False,
+ preserve: bool = False,
) -> None:
"""Hard link the release files to the downloads directory."""
# TODO: Rename *_dir functions to _path functions
downloads_base_path = util.get_downloads_dir()
downloads_path = downloads_base_path / committee.name /
download_path_suffix.removeprefix("/")
+ # The "exist_ok" parameter means to overwrite files if True
+ # We only overwrite if we're not preserving, so we supply "not
preserve"
+ # TODO: Add a test for this
await util.create_hard_link_clone(
unfinished_path,
downloads_path,
do_not_create_dest_dir=dry_run,
- exist_ok=True,
+ exist_ok=not preserve,
dry_run=dry_run,
)
diff --git a/atr/templates/project-view.html b/atr/templates/project-view.html
index dc91643..65eb066 100644
--- a/atr/templates/project-view.html
+++ b/atr/templates/project-view.html
@@ -239,6 +239,17 @@
</div>
</div>
+ <div class="mb-3 pb-3 row border-bottom">
+ {{ forms.label(policy_form.preserve_download_files,
col="md3-high") }}
+ <div class="col-sm-8">
+ <div class="form-check">
+ {{ forms.widget(policy_form.preserve_download_files,
classes="form-check-input", boolean_label="Enable") }}
+ {{ forms.errors(policy_form.preserve_download_files,
classes="invalid-feedback d-block") }}
+ </div>
+ {{ forms.description(policy_form.preserve_download_files) }}
+ </div>
+ </div>
+
<div class="row">
<div class="col-sm-9 offset-sm-3">{{
policy_form.submit_policy(class_="btn btn-primary mt-2") }}</div>
</div>
diff --git a/migrations/versions/0028_2025.09.09_a0037268.py
b/migrations/versions/0028_2025.09.09_a0037268.py
new file mode 100644
index 0000000..d370503
--- /dev/null
+++ b/migrations/versions/0028_2025.09.09_a0037268.py
@@ -0,0 +1,29 @@
+"""Add a release policy field to preserve existing download files
+
+Revision ID: 0028_2025.09.09_a0037268
+Revises: 0027_2025.09.08_69e565eb
+Create Date: 2025-09-09 15:45:39.857545+00:00
+"""
+
+from collections.abc import Sequence
+
+import sqlalchemy as sa
+from alembic import op
+
+# Revision identifiers, used by Alembic
+revision: str = "0028_2025.09.09_a0037268"
+down_revision: str | None = "0027_2025.09.08_69e565eb"
+branch_labels: str | Sequence[str] | None = None
+depends_on: str | Sequence[str] | None = None
+
+
+def upgrade() -> None:
+ with op.batch_alter_table("releasepolicy", schema=None) as batch_op:
+ batch_op.add_column(
+ sa.Column("preserve_download_files", sa.Boolean(), nullable=False,
server_default=sa.false())
+ )
+
+
+def downgrade() -> None:
+ with op.batch_alter_table("releasepolicy", schema=None) as batch_op:
+ batch_op.drop_column("preserve_download_files")
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]