This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch sbp
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git


The following commit(s) were added to refs/heads/sbp by this push:
     new 47ffcf8  Disallow starting a vote when there are check result blockers
47ffcf8 is described below

commit 47ffcf824471957e228f73b5bc4e8e6e024318d0
Author: Sean B. Palmer <[email protected]>
AuthorDate: Fri Feb 6 17:03:21 2026 +0000

    Disallow starting a vote when there are check result blockers
---
 atr/db/interaction.py                              | 24 +++++++++++++++----
 atr/shared/web.py                                  |  4 ++++
 atr/storage/writers/vote.py                        |  5 ++++
 atr/tasks/checks/signature.py                      |  3 +--
 atr/templates/check-selected-release-info.html     | 28 ++++++++++++++++++----
 tests/e2e/announce/conftest.py                     |  8 ++++++-
 tests/e2e/compose/conftest.py                      |  8 ++++++-
 tests/e2e/report/conftest.py                       |  8 ++++++-
 tests/e2e/sbom/conftest.py                         |  6 ++++-
 tests/e2e/sbom/test_post.py                        |  2 +-
 tests/e2e/test_files/apache-test-0.2.tar.gz.asc    |  0
 tests/e2e/test_files/apache-test-0.2.tar.gz.sha512 |  1 +
 tests/e2e/vote/conftest.py                         |  8 ++++++-
 tests/e2e/voting/conftest.py                       |  8 ++++++-
 14 files changed, 95 insertions(+), 18 deletions(-)

diff --git a/atr/db/interaction.py b/atr/db/interaction.py
index b254f82..9854f2e 100644
--- a/atr/db/interaction.py
+++ b/atr/db/interaction.py
@@ -172,6 +172,21 @@ async def full_releases(project: sql.Project) -> 
list[sql.Release]:
     return await releases_by_phase(project, sql.ReleasePhase.RELEASE)
 
 
+async def has_blocker_checks(release: sql.Release, revision_number: str, 
caller_data: db.Session | None = None) -> bool:
+    async with db.ensure_session(caller_data) as data:
+        query = (
+            sqlmodel.select(sqlalchemy.func.count())
+            .select_from(sql.CheckResult)
+            .where(
+                sql.CheckResult.release_name == release.name,
+                sql.CheckResult.revision_number == revision_number,
+                sql.CheckResult.status == sql.CheckResultStatus.BLOCKER,
+            )
+        )
+        result = await data.execute(query)
+        return result.scalar_one() > 0
+
+
 async def has_failing_checks(release: sql.Release, revision_number: str, 
caller_data: db.Session | None = None) -> bool:
     async with db.ensure_session(caller_data) as data:
         query = (
@@ -180,9 +195,7 @@ async def has_failing_checks(release: sql.Release, 
revision_number: str, caller_
             .where(
                 sql.CheckResult.release_name == release.name,
                 sql.CheckResult.revision_number == revision_number,
-                
sql.validate_instrumented_attribute(sql.CheckResult.status).in_(
-                    [sql.CheckResultStatus.BLOCKER, 
sql.CheckResultStatus.FAILURE]
-                ),
+                sql.CheckResult.status == sql.CheckResultStatus.FAILURE,
             )
         )
         result = await data.execute(query)
@@ -239,7 +252,7 @@ async def release_latest_vote_task(release: sql.Release, 
caller_data: db.Session
         return task
 
 
-async def release_ready_for_vote(
+async def release_ready_for_vote(  # noqa: C901
     session: web.Committer,
     project_name: str,
     version_name: str,
@@ -272,6 +285,9 @@ async def release_ready_for_vote(
     elif (not manual_vote) and release.project.policy_manual_vote:
         return "This release has manual vote mode enabled"
 
+    if await has_blocker_checks(release, revision, caller_data=data):
+        return "This release candidate draft has blockers. Please fix the 
blockers before starting a vote."
+
     if release.project.policy_strict_checking:
         if await has_failing_checks(release, revision, caller_data=data):
             return "This release candidate draft has errors. Please fix the 
errors before starting a vote."
diff --git a/atr/shared/web.py b/atr/shared/web.py
index 3030aed..392f772 100644
--- a/atr/shared/web.py
+++ b/atr/shared/web.py
@@ -132,6 +132,9 @@ async def check(
     has_any_errors = any(info.errors.get(path, []) for path in paths) if info 
else False
     strict_checking = release.project.policy_strict_checking
     strict_checking_errors = strict_checking and has_any_errors
+    blocker_errors = False
+    if revision_number is not None:
+        blocker_errors = await interaction.has_blocker_checks(release, 
revision_number)
 
     checks_summary_html = _render_checks_summary(info, release.project.name, 
release.version)
 
@@ -165,6 +168,7 @@ async def check(
         resolve_form=resolve_form,
         has_files=has_files,
         strict_checking_errors=strict_checking_errors,
+        blocker_errors=blocker_errors,
         can_vote=can_vote,
         can_resolve=can_resolve,
         checks_summary_html=checks_summary_html,
diff --git a/atr/storage/writers/vote.py b/atr/storage/writers/vote.py
index 13ff310..4d0ab34 100644
--- a/atr/storage/writers/vote.py
+++ b/atr/storage/writers/vote.py
@@ -161,6 +161,11 @@ class CommitteeParticipant(FoundationCommitter):
             log.info(f"Invalid mailing list choice: {email_to} not in 
{permitted_recipients}")
             raise storage.AccessError("Invalid mailing list choice")
 
+        if await interaction.has_blocker_checks(release, 
selected_revision_number, caller_data=self.__data):
+            raise storage.AccessError(
+                "This release candidate draft has blockers. Please fix the 
blockers before starting a vote."
+            )
+
         if promote is True:
             # This verifies the state and sets the phase to RELEASE_CANDIDATE
             error = await self.__write_as.release.promote_to_candidate(
diff --git a/atr/tasks/checks/signature.py b/atr/tasks/checks/signature.py
index 5bbaf67..81ac1ac 100644
--- a/atr/tasks/checks/signature.py
+++ b/atr/tasks/checks/signature.py
@@ -62,8 +62,7 @@ async def check(args: checks.FunctionArguments) -> 
results.Results | None:
             signature_path=str(primary_abs_path),
         )
         if result_data.get("error"):
-            # TODO: This should perhaps be a failure
-            await recorder.blocker(result_data["error"], result_data)
+            await recorder.failure(result_data["error"], result_data)
         elif result_data.get("verified"):
             await recorder.success("Signature verified successfully", 
result_data)
         else:
diff --git a/atr/templates/check-selected-release-info.html 
b/atr/templates/check-selected-release-info.html
index e218912..293df9e 100644
--- a/atr/templates/check-selected-release-info.html
+++ b/atr/templates/check-selected-release-info.html
@@ -58,12 +58,14 @@
            title="View revision history"
            class="btn btn-secondary"><i class="bi bi-clock-history me-1"></i> 
Revisions</a>
         {% if revision_number %}
-          {% if has_files and release.project.policy_manual_vote and 
(ongoing_tasks_count == 0) %}
+          {% set vote_blocked = blocker_errors or strict_checking_errors %}
+          {% set blocked_title = "Fix blockers before starting a vote" if 
blocker_errors else "Fix errors before starting a vote" %}
+          {% if has_files and release.project.policy_manual_vote and 
(ongoing_tasks_count == 0) and (not vote_blocked) %}
             <a id="start-vote-button"
                href="{{ as_url(get.manual.start_selected_revision, 
project_name=release.project.name, version_name=release.version, 
revision=revision_number) }}"
                title="Start a manual vote on this draft"
                class="btn btn-success"><i class="bi bi-check-circle me-1"></i> 
Start manual vote</a>
-          {% elif has_files and release.project.policy_manual_vote %}
+          {% elif has_files and release.project.policy_manual_vote and (not 
vote_blocked) %}
             <a id="start-vote-button"
                href="{{ as_url(get.manual.start_selected_revision, 
project_name=release.project.name, version_name=release.version, 
revision=revision_number) }}"
                data-vote-href="{{ as_url(get.manual.start_selected_revision, 
project_name=release.project.name, version_name=release.version, 
revision=revision_number) }}"
@@ -72,12 +74,20 @@
                role="button"
                aria-disabled="true"
                tabindex="-1"><i class="bi bi-check-circle me-1"></i> Start 
manual vote</a>
-          {% elif has_files and (not strict_checking_errors) and 
(ongoing_tasks_count == 0) %}
+          {% elif has_files and release.project.policy_manual_vote and 
vote_blocked %}
+            <a id="start-vote-button"
+               href="{{ as_url(get.manual.start_selected_revision, 
project_name=release.project.name, version_name=release.version, 
revision=revision_number) }}"
+               title="{{ blocked_title }}"
+               class="btn btn-success disabled"
+               role="button"
+               aria-disabled="true"
+               tabindex="-1"><i class="bi bi-check-circle me-1"></i> Start 
manual vote</a>
+          {% elif has_files and (not vote_blocked) and (ongoing_tasks_count == 
0) %}
             <a id="start-vote-button"
                href="{{ as_url(get.voting.selected_revision, 
project_name=release.project.name, version_name=release.version, 
revision=revision_number) }}"
                title="Start a vote on this draft"
                class="btn btn-success"><i class="bi bi-check-circle me-1"></i> 
Start voting</a>
-          {% elif has_files and (not strict_checking_errors) %}
+          {% elif has_files and (not vote_blocked) %}
             <a id="start-vote-button"
                href="{{ as_url(get.voting.selected_revision, 
project_name=release.project.name, version_name=release.version, 
revision=revision_number) }}"
                data-vote-href="{{ as_url(get.voting.selected_revision, 
project_name=release.project.name, version_name=release.version, 
revision=revision_number) }}"
@@ -86,9 +96,17 @@
                role="button"
                aria-disabled="true"
                tabindex="-1"><i class="bi bi-check-circle me-1"></i> Start 
voting</a>
+          {% elif has_files and vote_blocked %}
+            <a id="start-vote-button"
+               href="{{ as_url(get.voting.selected_revision, 
project_name=release.project.name, version_name=release.version, 
revision=revision_number) }}"
+               title="{{ blocked_title }}"
+               class="btn btn-success disabled"
+               role="button"
+               aria-disabled="true"
+               tabindex="-1"><i class="bi bi-check-circle me-1"></i> Start 
voting</a>
           {% else %}
             <a id="start-vote-button"
-               href="#"
+               href="{{ as_url(get.voting.selected_revision, 
project_name=release.project.name, version_name=release.version, 
revision=revision_number) }}"
                title="Upload files to enable voting"
                class="btn btn-success disabled"
                role="button"
diff --git a/tests/e2e/announce/conftest.py b/tests/e2e/announce/conftest.py
index 1fbc4dd..33f3f7c 100644
--- a/tests/e2e/announce/conftest.py
+++ b/tests/e2e/announce/conftest.py
@@ -58,7 +58,13 @@ def announce_context(browser: Browser) -> 
Generator[BrowserContext]:
     page.wait_for_url(f"**/compose/{PROJECT_NAME}/{VERSION_NAME}")
 
     helpers.visit(page, f"/upload/{PROJECT_NAME}/{VERSION_NAME}")
-    
page.locator('input[name="file_data"]').set_input_files(f"{CURRENT_DIR}/../test_files/{FILE_NAME}")
+    page.locator('input[name="file_data"]').set_input_files(
+        [
+            f"{CURRENT_DIR}/../test_files/{FILE_NAME}",
+            f"{CURRENT_DIR}/../test_files/{FILE_NAME}.sha512",
+            f"{CURRENT_DIR}/../test_files/{FILE_NAME}.asc",
+        ]
+    )
     page.get_by_role("button", name="Add files").click()
     page.wait_for_url(f"**/compose/{PROJECT_NAME}/{VERSION_NAME}")
 
diff --git a/tests/e2e/compose/conftest.py b/tests/e2e/compose/conftest.py
index 05fcc48..1ea9b31 100644
--- a/tests/e2e/compose/conftest.py
+++ b/tests/e2e/compose/conftest.py
@@ -51,7 +51,13 @@ def compose_context(browser: Browser) -> 
Generator[BrowserContext]:
     page.wait_for_url(f"**/compose/{PROJECT_NAME}/{VERSION_NAME}")
 
     helpers.visit(page, f"/upload/{PROJECT_NAME}/{VERSION_NAME}")
-    
page.locator('input[name="file_data"]').set_input_files(f"{CURRENT_DIR}/../test_files/{FILE_NAME}")
+    page.locator('input[name="file_data"]').set_input_files(
+        [
+            f"{CURRENT_DIR}/../test_files/{FILE_NAME}",
+            f"{CURRENT_DIR}/../test_files/{FILE_NAME}.sha512",
+            f"{CURRENT_DIR}/../test_files/{FILE_NAME}.asc",
+        ]
+    )
     page.get_by_role("button", name="Add files").click()
     page.wait_for_url(f"**/compose/{PROJECT_NAME}/{VERSION_NAME}")
 
diff --git a/tests/e2e/report/conftest.py b/tests/e2e/report/conftest.py
index f9d2824..87dec79 100644
--- a/tests/e2e/report/conftest.py
+++ b/tests/e2e/report/conftest.py
@@ -106,7 +106,13 @@ def report_context(browser: Browser, 
verify_license_check_mode: None) -> Generat
     page.wait_for_url(f"**/compose/{PROJECT_NAME}/{VERSION_NAME}")
 
     helpers.visit(page, f"/upload/{PROJECT_NAME}/{VERSION_NAME}")
-    
page.locator('input[name="file_data"]').set_input_files(f"{CURRENT_DIR}/../test_files/{FILE_NAME}")
+    page.locator('input[name="file_data"]').set_input_files(
+        [
+            f"{CURRENT_DIR}/../test_files/{FILE_NAME}",
+            f"{CURRENT_DIR}/../test_files/{FILE_NAME}.sha512",
+            f"{CURRENT_DIR}/../test_files/{FILE_NAME}.asc",
+        ]
+    )
     page.get_by_role("button", name="Add files").click()
     page.wait_for_url(f"**/compose/{PROJECT_NAME}/{VERSION_NAME}")
 
diff --git a/tests/e2e/sbom/conftest.py b/tests/e2e/sbom/conftest.py
index b118d95..bd59b1d 100644
--- a/tests/e2e/sbom/conftest.py
+++ b/tests/e2e/sbom/conftest.py
@@ -40,7 +40,11 @@ def page_release_with_file(page: Page) -> Generator[Page]:
     page.get_by_role("button", name="Start new release").click()
     helpers.visit(page, 
f"/upload/{sbom_helpers.PROJECT_NAME}/{sbom_helpers.VERSION_NAME}")
     page.locator('input[name="file_data"]').set_input_files(
-        f"{sbom_helpers.CURRENT_DIR}/../test_files/{sbom_helpers.FILE_NAME}"
+        [
+            
f"{sbom_helpers.CURRENT_DIR}/../test_files/{sbom_helpers.FILE_NAME}",
+            
f"{sbom_helpers.CURRENT_DIR}/../test_files/{sbom_helpers.FILE_NAME}.sha512",
+            
f"{sbom_helpers.CURRENT_DIR}/../test_files/{sbom_helpers.FILE_NAME}.asc",
+        ]
     )
     page.get_by_role("button", name="Add files").click()
     
page.wait_for_url(f"**/compose/{sbom_helpers.PROJECT_NAME}/{sbom_helpers.VERSION_NAME}")
diff --git a/tests/e2e/sbom/test_post.py b/tests/e2e/sbom/test_post.py
index 207bef3..c95de7f 100644
--- a/tests/e2e/sbom/test_post.py
+++ b/tests/e2e/sbom/test_post.py
@@ -22,7 +22,7 @@ from playwright.sync_api import Page, expect
 
 def test_sbom_generate(page_release_with_file: Page) -> None:
     # Make sure that the test file exists
-    file_cell = page_release_with_file.get_by_role("cell", 
name=sbom_helpers.FILE_NAME)
+    file_cell = page_release_with_file.get_by_role("cell", 
name=sbom_helpers.FILE_NAME, exact=True)
     expect(file_cell).to_be_visible()
 
     # Generate an SBOM for the file
diff --git a/tests/e2e/test_files/apache-test-0.2.tar.gz.asc 
b/tests/e2e/test_files/apache-test-0.2.tar.gz.asc
new file mode 100644
index 0000000..e69de29
diff --git a/tests/e2e/test_files/apache-test-0.2.tar.gz.sha512 
b/tests/e2e/test_files/apache-test-0.2.tar.gz.sha512
new file mode 100644
index 0000000..46216b9
--- /dev/null
+++ b/tests/e2e/test_files/apache-test-0.2.tar.gz.sha512
@@ -0,0 +1 @@
+96976a7273b0b92f7faf1e34b5c7caf9e81b74fb7b992afbdbee5e3024f31b347619fcd92683e5c3627363328df08ed55461bf53284eb43aa0891367fbb998b3
  apache-test-0.2.tar.gz
diff --git a/tests/e2e/vote/conftest.py b/tests/e2e/vote/conftest.py
index 2cdf1af..b689ed6 100644
--- a/tests/e2e/vote/conftest.py
+++ b/tests/e2e/vote/conftest.py
@@ -63,7 +63,13 @@ def vote_context(browser: Browser) -> 
Generator[BrowserContext]:
     page.wait_for_url(f"**/compose/{PROJECT_NAME}/{VERSION_NAME}")
 
     helpers.visit(page, f"/upload/{PROJECT_NAME}/{VERSION_NAME}")
-    
page.locator('input[name="file_data"]').set_input_files(f"{CURRENT_DIR}/../test_files/{FILE_NAME}")
+    page.locator('input[name="file_data"]').set_input_files(
+        [
+            f"{CURRENT_DIR}/../test_files/{FILE_NAME}",
+            f"{CURRENT_DIR}/../test_files/{FILE_NAME}.sha512",
+            f"{CURRENT_DIR}/../test_files/{FILE_NAME}.asc",
+        ]
+    )
     page.get_by_role("button", name="Add files").click()
     page.wait_for_url(f"**/compose/{PROJECT_NAME}/{VERSION_NAME}")
 
diff --git a/tests/e2e/voting/conftest.py b/tests/e2e/voting/conftest.py
index 50c20af..2eb7d48 100644
--- a/tests/e2e/voting/conftest.py
+++ b/tests/e2e/voting/conftest.py
@@ -62,7 +62,13 @@ def voting_context(browser: Browser) -> 
Generator[BrowserContext]:
     page.wait_for_url(f"**/compose/{PROJECT_NAME}/{VERSION_NAME}")
 
     helpers.visit(page, f"/upload/{PROJECT_NAME}/{VERSION_NAME}")
-    
page.locator('input[name="file_data"]').set_input_files(f"{CURRENT_DIR}/../test_files/{FILE_NAME}")
+    page.locator('input[name="file_data"]').set_input_files(
+        [
+            f"{CURRENT_DIR}/../test_files/{FILE_NAME}",
+            f"{CURRENT_DIR}/../test_files/{FILE_NAME}.sha512",
+            f"{CURRENT_DIR}/../test_files/{FILE_NAME}.asc",
+        ]
+    )
     page.get_by_role("button", name="Add files").click()
     page.wait_for_url(f"**/compose/{PROJECT_NAME}/{VERSION_NAME}")
 


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

Reply via email to