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-releases.git
The following commit(s) were added to refs/heads/main by this push:
new fe73b07 Add a summary of checks before the compose file list
fe73b07 is described below
commit fe73b070799ca919e4ea918adaf6fe3f2d7c711f
Author: Sean B. Palmer <[email protected]>
AuthorDate: Tue Dec 23 15:10:16 2025 +0000
Add a summary of checks before the compose file list
---
atr/shared/__init__.py | 56 ++++++++++++++++++++++++++
atr/static/css/atr.css | 23 +++++++++++
atr/storage/readers/releases.py | 83 ++++++++++++++++++++++++++++++++-------
atr/storage/types.py | 11 ++++++
atr/templates/check-selected.html | 4 ++
5 files changed, 163 insertions(+), 14 deletions(-)
diff --git a/atr/shared/__init__.py b/atr/shared/__init__.py
index dffeb9a..f3427d9 100644
--- a/atr/shared/__init__.py
+++ b/atr/shared/__init__.py
@@ -17,6 +17,8 @@
from typing import TYPE_CHECKING, Final
+import htpy
+
import atr.db as db
import atr.db.interaction as interaction
import atr.form as form
@@ -44,6 +46,7 @@ import atr.shared.user as user
import atr.shared.vote as vote
import atr.shared.voting as voting
import atr.storage as storage
+import atr.storage.types as types
import atr.template as template
import atr.util as util
import atr.web as web
@@ -167,6 +170,8 @@ async def check(
strict_checking = release.project.policy_strict_checking
strict_checking_errors = strict_checking and has_any_errors
+ checks_summary_html = _render_checks_summary(info, release.project.name,
release.version)
+
return await template.render(
"check-selected.html",
project_name=release.project.name,
@@ -198,9 +203,60 @@ async def check(
strict_checking_errors=strict_checking_errors,
can_vote=can_vote,
can_resolve=can_resolve,
+ checks_summary_html=checks_summary_html,
)
+def _checker_display_name(checker: str) -> str:
+ return checker.removeprefix("atr.tasks.checks.").replace("_", "
").replace(".", " ").title()
+
+
+def _render_checks_summary(info: types.PathInfo | None, project_name: str,
version_name: str) -> htm.Element | None:
+ if (info is None) or (not info.checker_stats):
+ return None
+
+ card = htm.Block(htm.div, classes=".card.mb-4")
+ card.div(".card-header")[htpy.h5(".mb-0")["Checks summary"]]
+
+ body = htm.Block(htm.div, classes=".card-body")
+ for i, stat in enumerate(info.checker_stats):
+ stripe_class = ".atr-stripe-odd" if ((i % 2) == 0) else
".atr-stripe-even"
+ details = htm.Block(htm.details, classes=f".mb-0.p-2{stripe_class}")
+
+ summary_content: list[htm.Element | str] = []
+ if stat.warning_count > 0:
+
summary_content.append(htpy.span(".badge.bg-warning.text-dark.me-2")[str(stat.warning_count)])
+ if stat.failure_count > 0:
+
summary_content.append(htpy.span(".badge.bg-danger.me-2")[str(stat.failure_count)])
+
summary_content.append(htpy.strong[_checker_display_name(stat.checker)])
+
+ details.summary[*summary_content]
+
+ files_div = htm.Block(htm.div, classes=".mt-2.atr-checks-files")
+ all_files = set(stat.failure_files.keys()) |
set(stat.warning_files.keys())
+ for file_path in sorted(all_files):
+ report_url = f"/report/{project_name}/{version_name}/{file_path}"
+ error_count = stat.failure_files.get(file_path, 0)
+ warning_count = stat.warning_files.get(file_path, 0)
+
+ file_content: list[htm.Element | str] = []
+ if error_count > 0:
+ label = "error" if (error_count == 1) else "errors"
+
file_content.append(htpy.span(".badge.bg-danger.me-2")[f"{error_count}
{label}"])
+ if warning_count > 0:
+ label = "warning" if (warning_count == 1) else "warnings"
+
file_content.append(htpy.span(".badge.bg-warning.text-dark.me-2")[f"{warning_count}
{label}"])
+
file_content.append(htpy.a(href=report_url)[htpy.strong[htpy.code[file_path]]])
+
+ files_div.div[*file_content]
+
+ details.append(files_div.collect())
+ body.append(details.collect())
+
+ card.append(body.collect())
+ return card.collect()
+
+
def _warnings_from_vote_result(vote_task: sql.Task | None) -> list[str]:
# TODO: Replace this with a schema.Strict model
# But we'd still need to do some of this parsing and validation
diff --git a/atr/static/css/atr.css b/atr/static/css/atr.css
index 7003c24..a7ff400 100644
--- a/atr/static/css/atr.css
+++ b/atr/static/css/atr.css
@@ -522,3 +522,26 @@ span.warning {
overflow-wrap: anywhere;
word-break: break-all;
}
+
+td.atr-shrink {
+ width: 1%;
+ white-space: nowrap;
+}
+
+.atr-stripe-odd {
+ background-color: #f8f9fa;
+}
+
+.atr-stripe-even {
+ background-color: #ffffff;
+}
+
+.atr-stripe-odd .badge,
+.atr-stripe-even .badge {
+ position: relative;
+ top: -1px;
+}
+
+.atr-checks-files {
+ line-height: 2.25;
+}
diff --git a/atr/storage/readers/releases.py b/atr/storage/readers/releases.py
index b1ecf6a..9d2a157 100644
--- a/atr/storage/readers/releases.py
+++ b/atr/storage/readers/releases.py
@@ -18,6 +18,7 @@
# Removing this will cause circular imports
from __future__ import annotations
+import dataclasses
import pathlib
import re
@@ -28,6 +29,15 @@ import atr.storage as storage
import atr.storage.types as types
[email protected]
+class CheckerAccumulator:
+ success: int = 0
+ warning: int = 0
+ failure: int = 0
+ warning_files: dict[str, int] = dataclasses.field(default_factory=dict)
+ failure_files: dict[str, int] = dataclasses.field(default_factory=dict)
+
+
class GeneralPublic:
def __init__(
self,
@@ -55,8 +65,53 @@ class GeneralPublic:
info.artifacts.add(path)
elif search.group("metadata"):
info.metadata.add(path)
+ self.__compute_checker_stats(info, paths)
return info
+ def __accumulate_results(
+ self,
+ results: dict[pathlib.Path, list[sql.CheckResult]],
+ paths_set: set[pathlib.Path],
+ checker_data: dict[str, CheckerAccumulator],
+ kind: str,
+ ) -> None:
+ for path, results_list in results.items():
+ if path not in paths_set:
+ continue
+ for result in results_list:
+ acc = checker_data.setdefault(result.checker,
CheckerAccumulator())
+ path_str = str(path)
+ if kind == "success":
+ acc.success += 1
+ elif kind == "warning":
+ acc.warning += 1
+ acc.warning_files[path_str] =
acc.warning_files.get(path_str, 0) + 1
+ else:
+ acc.failure += 1
+ acc.failure_files[path_str] =
acc.failure_files.get(path_str, 0) + 1
+
+ def __compute_checker_stats(self, info: types.PathInfo, paths:
list[pathlib.Path]) -> None:
+ paths_set = set(paths)
+ checker_data: dict[str, CheckerAccumulator] = {}
+
+ self.__accumulate_results(info.successes, paths_set, checker_data,
"success")
+ self.__accumulate_results(info.warnings, paths_set, checker_data,
"warning")
+ self.__accumulate_results(info.errors, paths_set, checker_data,
"failure")
+
+ for checker, acc in sorted(checker_data.items()):
+ if (acc.warning == 0) and (acc.failure == 0):
+ continue
+ info.checker_stats.append(
+ types.CheckerStats(
+ checker=checker,
+ success_count=acc.success,
+ warning_count=acc.warning,
+ failure_count=acc.failure,
+ warning_files=acc.warning_files,
+ failure_files=acc.failure_files,
+ )
+ )
+
async def __successes_errors_warnings(
self, release: sql.Release, latest_revision_number: str, info:
types.PathInfo
) -> None:
@@ -75,6 +130,20 @@ class GeneralPublic:
await self.__warnings(cs)
await self.__errors(cs)
+ async def __errors(self, cs: types.ChecksSubset) -> None:
+ errors = await self.__data.check_result(
+ release_name=cs.release.name,
+ revision_number=cs.latest_revision_number,
+ member_rel_path=None,
+ status=sql.CheckResultStatus.FAILURE,
+ ).all()
+ for error in errors:
+ if cs.match_ignore(error):
+ cs.info.ignored_errors.append(error)
+ continue
+ if primary_rel_path := error.primary_rel_path:
+ cs.info.errors.setdefault(pathlib.Path(primary_rel_path),
[]).append(error)
+
async def __successes(self, cs: types.ChecksSubset) -> None:
successes = await self.__data.check_result(
release_name=cs.release.name,
@@ -100,17 +169,3 @@ class GeneralPublic:
continue
if primary_rel_path := warning.primary_rel_path:
cs.info.warnings.setdefault(pathlib.Path(primary_rel_path),
[]).append(warning)
-
- async def __errors(self, cs: types.ChecksSubset) -> None:
- errors = await self.__data.check_result(
- release_name=cs.release.name,
- revision_number=cs.latest_revision_number,
- member_rel_path=None,
- status=sql.CheckResultStatus.FAILURE,
- ).all()
- for error in errors:
- if cs.match_ignore(error):
- cs.info.ignored_errors.append(error)
- continue
- if primary_rel_path := error.primary_rel_path:
- cs.info.errors.setdefault(pathlib.Path(primary_rel_path),
[]).append(error)
diff --git a/atr/storage/types.py b/atr/storage/types.py
index f08985c..22c5bf7 100644
--- a/atr/storage/types.py
+++ b/atr/storage/types.py
@@ -25,6 +25,16 @@ import atr.models.sql as sql
import atr.storage.outcome as outcome
[email protected]
+class CheckerStats:
+ checker: str
+ success_count: int
+ warning_count: int
+ failure_count: int
+ warning_files: dict[str, int]
+ failure_files: dict[str, int]
+
+
@dataclasses.dataclass
class CheckResults:
primary_results_list: list[sql.CheckResult]
@@ -52,6 +62,7 @@ class LinkedCommittee:
class PathInfo(schema.Strict):
artifacts: set[pathlib.Path] = schema.factory(set)
+ checker_stats: list[CheckerStats] = schema.factory(list)
errors: dict[pathlib.Path, list[sql.CheckResult]] = schema.factory(dict)
ignored_errors: list[sql.CheckResult] = schema.factory(list)
ignored_warnings: list[sql.CheckResult] = schema.factory(list)
diff --git a/atr/templates/check-selected.html
b/atr/templates/check-selected.html
index e74102e..a435da7 100644
--- a/atr/templates/check-selected.html
+++ b/atr/templates/check-selected.html
@@ -97,6 +97,10 @@
</div>
</div>
+ {% if checks_summary_html %}
+ {{ checks_summary_html|safe }}
+ {% endif %}
+
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]