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 6681862 Add an endpoint for getting checks for a specific revision
6681862 is described below
commit 66818624e697ce2db41cfc9904dcb1c68d0b8721
Author: Sean B. Palmer <[email protected]>
AuthorDate: Mon Jul 7 14:46:56 2025 +0100
Add an endpoint for getting checks for a specific revision
---
atr/blueprints/api/api.py | 26 ++++++++++++++++++--
client/atr | 62 +++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 86 insertions(+), 2 deletions(-)
diff --git a/atr/blueprints/api/api.py b/atr/blueprints/api/api.py
index 89efb0e..72fb713 100644
--- a/atr/blueprints/api/api.py
+++ b/atr/blueprints/api/api.py
@@ -256,9 +256,9 @@ async def releases_project_version(project: str, version:
str) -> tuple[Mapping,
return release.model_dump(), 200
[email protected]("/releases/<project>/<version>/check-results")
[email protected]("/checks/<project>/<version>")
@quart_schema.validate_response(list[models.CheckResult], 200)
-async def releases_project_version_check_results(project: str, version: str)
-> tuple[list[Mapping], int]:
+async def checks_project_version(project: str, version: str) ->
tuple[list[Mapping], int]:
"""List all check results for a given release."""
async with db.session() as data:
release_name = models.release_name(project, version)
@@ -276,6 +276,28 @@ async def releases_project_version_revisions(project: str,
version: str) -> tupl
return [rev.model_dump() for rev in revisions], 200
[email protected]("/checks/<project>/<version>/<revision>")
+@quart_schema.validate_response(list[models.CheckResult], 200)
+async def checks_project_version_revision(project: str, version: str,
revision: str) -> tuple[list[Mapping], int]:
+ """List all check results for a specific revision of a release."""
+ async with db.session() as data:
+ project_result = await data.project(name=project).get()
+ if project_result is None:
+ raise exceptions.NotFound(f"Project '{project}' does not exist")
+
+ release_name = models.release_name(project, version)
+ release_result = await data.release(name=release_name).get()
+ if release_result is None:
+ raise exceptions.NotFound(f"Release '{project}-{version}' does not
exist")
+
+ revision_result = await data.revision(release_name=release_name,
number=revision).get()
+ if revision_result is None:
+ raise exceptions.NotFound(f"Revision '{revision}' does not exist
for release '{project}-{version}'")
+
+ check_results = await data.check_result(release_name=release_name,
revision_number=revision).all()
+ return [cr.model_dump() for cr in check_results], 200
+
+
@api.BLUEPRINT.route("/secret")
@jwtoken.require
@quart_schema.security_scheme([{"BearerAuth": []}])
diff --git a/client/atr b/client/atr
index 8f80250..1c22188 100755
--- a/client/atr
+++ b/client/atr
@@ -73,6 +73,7 @@ if TYPE_CHECKING:
from collections.abc import Generator
APP: cyclopts.App = cyclopts.App()
+CHECKS: cyclopts.App = cyclopts.App(name="checks", help="Check result
operations.")
CONFIG: cyclopts.App = cyclopts.App(name="config", help="Configuration
operations.")
DEV: cyclopts.App = cyclopts.App(name="dev", help="Developer operations.")
JWT: cyclopts.App = cyclopts.App(name="jwt", help="JWT operations.")
@@ -92,12 +93,31 @@ YAML_SCHEMA: strictyaml.Map = strictyaml.Map(
}
)
+APP.command(CHECKS)
APP.command(CONFIG)
APP.command(DEV)
APP.command(JWT)
APP.command(RELEASE)
[email protected](name="status", help="Get check status for a release revision.")
+def app_checks_status(project: str, version: str, revision: str) -> None:
+ with config_lock() as config:
+ jwt_value = config_get(config, ["tokens", "jwt"])
+
+ if jwt_value is None:
+ LOGGER.error("No JWT stored in configuration.")
+ sys.exit(1)
+
+ host = config.get("atr", {}).get("host", "release-test.apache.org")
+ url = f"https://{host}/api/checks/{project}/{version}/{revision}"
+
+ verify_ssl = not host.startswith("127.0.0.1")
+ results = asyncio.run(web_get(url, jwt_value, verify_ssl))
+
+ check_results_display(results)
+
+
@CONFIG.command(name="file", help="Display the configuration file contents.")
def app_config_file() -> None:
path = config_path()
@@ -383,6 +403,27 @@ def config_write(data: dict[str, Any]) -> None:
os.replace(tmp, path)
+def check_results_display(results: list[dict[str, Any]]) -> None:
+ if not results:
+ print("No check results found for this revision.")
+ return
+
+ by_status = {}
+ for result in results:
+ status = result["status"]
+ by_status.setdefault(status, []).append(result)
+
+ print(f"Total checks: {len(results)}")
+ for status, checks in by_status.items():
+ print(f" {status}: {len(checks)}")
+
+ for status in ["FAILURE", "EXCEPTION", "WARNING"]:
+ if status in by_status:
+ print(f"\n{status}:")
+ for check in by_status[status]:
+ print(f" - {check['checker']}: {check['message']}")
+
+
def main() -> None:
logging.basicConfig(level=logging.INFO, format="%(message)s")
APP()
@@ -476,6 +517,8 @@ def timestamp_format(ts: int | str | None) -> str | None:
async def web_fetch(url: str, asfuid: str, pat_token: str, verify_ssl: bool =
True) -> str:
+ # TODO: This is PAT request specific
+ # Should give this a more specific name
connector = None if verify_ssl else aiohttp.TCPConnector(ssl=False)
async with aiohttp.ClientSession(connector=connector) as session:
payload = {"asfuid": asfuid, "pat": pat_token}
@@ -491,6 +534,25 @@ async def web_fetch(url: str, asfuid: str, pat_token: str,
verify_ssl: bool = Tr
raise RuntimeError(f"Unexpected response: {data}")
+async def web_get(url: str, jwt_token: str, verify_ssl: bool = True) -> Any:
+ connector = None if verify_ssl else aiohttp.TCPConnector(ssl=False)
+ headers = {"Authorization": f"Bearer {jwt_token}"}
+ async with aiohttp.ClientSession(connector=connector, headers=headers) as
session:
+ async with session.get(url) as resp:
+ if resp.status != 200:
+ text = await resp.text()
+ try:
+ error_data = json.loads(text)
+ if isinstance(error_data, dict) and "message" in
error_data:
+ LOGGER.error(error_data["message"])
+ else:
+ LOGGER.error(f"Request failed: {resp.status} {text}")
+ except json.JSONDecodeError:
+ LOGGER.error(f"Request failed: {resp.status} {text}")
+ sys.exit(1)
+ return await resp.json()
+
+
async def web_post(url: str, payload: dict[str, Any], jwt_token: str,
verify_ssl: bool = True) -> Any:
connector = None if verify_ssl else aiohttp.TCPConnector(ssl=False)
headers = {"Authorization": f"Bearer {jwt_token}"}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]