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]