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]

Reply via email to