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 5de5f82  Add documentation to the announce and checks list API 
endpoints
5de5f82 is described below

commit 5de5f82ea9d2c9656ab7718f0641ec9da64f1e08
Author: Sean B. Palmer <[email protected]>
AuthorDate: Mon Jul 28 18:41:15 2025 +0100

    Add documentation to the announce and checks list API endpoints
---
 atr/blueprints/api/api.py | 40 +++++++++++++++++++++++++++++++++++++---
 atr/models/api.py         | 25 +++++++++++++++++--------
 atr/models/sql.py         | 40 ++++++++++++++++++++++++++++++----------
 3 files changed, 84 insertions(+), 21 deletions(-)

diff --git a/atr/blueprints/api/api.py b/atr/blueprints/api/api.py
index b98d29a..5ec71c4 100644
--- a/atr/blueprints/api/api.py
+++ b/atr/blueprints/api/api.py
@@ -70,6 +70,14 @@ DictResponse = tuple[dict[str, Any], int]
 @quart_schema.validate_request(models.api.AnnounceArgs)
 @quart_schema.validate_response(models.api.AnnounceResults, 201)
 async def announce_post(data: models.api.AnnounceArgs) -> DictResponse:
+    """
+    Announce a release to the public, making it final.
+
+    After a vote on a release has passed, if everything is in order and all
+    paths are correct, the release can be announced. This will send an email to
+    the specified announement address, and promote the release to the finished
+    release phase.
+    """
     asf_uid = _jwt_asf_uid()
 
     try:
@@ -96,15 +104,40 @@ async def announce_post(data: models.api.AnnounceArgs) -> 
DictResponse:
 @api.BLUEPRINT.route("/checks/list/<project>/<version>")
 @quart_schema.validate_response(models.api.ChecksListResults, 200)
 async def checks_list(project: str, version: str) -> DictResponse:
-    """List all check results for a given release."""
+    """
+    List all of the check results for a release.
+
+    Checks are only conducted during the compose a draft phase. This endpoint
+    only returns the checks for the most recent draft revision. Once a release
+    has been promoted to the vote phase or beyond, the checks returned are
+    still those for the compose phase.
+
+    Warning: the check results include results for archive members, so there
+    may potentially be thousands or results or more.
+    """
+    # TODO: We should perhaps paginate this
+    # TODO: Add phase in the response, and the revision too
     _simple_check(project, version)
     # TODO: Merge with checks_list_project_version_revision
     async with db.session() as data:
         release_name = sql.release_name(project, version)
+        release = await 
data.release(name=release_name).demand(exceptions.NotFound(f"Release 
{release_name} not found"))
         check_results = await 
data.check_result(release_name=release_name).all()
+
+    revision = None
+    for check_result in check_results:
+        if revision is None:
+            revision = check_result.revision_number
+        elif revision != check_result.revision_number:
+            raise exceptions.InternalServerError("Revision mismatch")
+    if revision is None:
+        raise exceptions.InternalServerError("No revision found")
+
     return models.api.ChecksListResults(
         endpoint="/checks/list",
         checks=check_results,
+        checks_revision=revision,
+        current_phase=release.phase,
     ).model_dump(), 200
 
 
@@ -131,6 +164,8 @@ async def checks_list_revision(project: str, version: str, 
revision: str) -> Dic
     return models.api.ChecksListResults(
         endpoint="/checks/list",
         checks=check_results,
+        checks_revision=revision,
+        current_phase=release_result.phase,
     ).model_dump(), 200
 
 
@@ -630,9 +665,8 @@ async def releases_project(project: str, query_args: 
models.api.ReleasesProjectQ
 
 
 # TODO: If we validate as sql.Release, quart_schema silently corrupts 
latest_revision_number to None
-# @quart_schema.validate_response(sql.Release, 200)
+# @quart_schema.validate_response(models.api.ReleasesVersionResults, 200)
 @api.BLUEPRINT.route("/releases/version/<project>/<version>")
-@quart_schema.validate_response(models.api.ReleasesVersionResults, 200)
 async def releases_version(project: str, version: str) -> DictResponse:
     """Return a single release by project and version."""
     _simple_check(project, version)
diff --git a/atr/models/api.py b/atr/models/api.py
index eaa9a2f..f876170 100644
--- a/atr/models/api.py
+++ b/atr/models/api.py
@@ -26,28 +26,37 @@ from . import schema, sql, tabulate
 T = TypeVar("T")
 
 
+def example(value: Any) -> dict[Literal["json_schema_extra"], dict[str, Any]]:
+    return {"json_schema_extra": {"example": value}}
+
+
 class ResultsTypeError(TypeError):
     pass
 
 
 class AnnounceArgs(schema.Strict):
-    project: str
-    version: str
-    revision: str
-    email_to: str
-    subject: str
-    body: str
-    path_suffix: str
+    project: str = schema.Field(..., **example("example"))
+    version: str = schema.Field(..., **example("1.0.0"))
+    revision: str = schema.Field(..., **example("00005"))
+    email_to: str = schema.Field(..., **example("[email protected]"))
+    subject: str = schema.Field(..., **example("[ANNOUNCE] Apache Example 
1.0.0 release"))
+    body: str = schema.Field(
+        ...,
+        **example("The Apache Example team is pleased to announce the release 
of Example 1.0.0..."),
+    )
+    path_suffix: str = schema.Field(..., **example("example/1.0.0"))
 
 
 class AnnounceResults(schema.Strict):
     endpoint: Literal["/announce"] = schema.Field(alias="endpoint")
-    success: str
+    success: str = schema.Field(..., **example("Announcement sent"))
 
 
 class ChecksListResults(schema.Strict):
     endpoint: Literal["/checks/list"] = schema.Field(alias="endpoint")
     checks: Sequence[sql.CheckResult]
+    checks_revision: str = schema.Field(..., **example("00005"))
+    current_phase: sql.ReleasePhase = schema.Field(..., 
**example(sql.ReleasePhase.RELEASE_CANDIDATE))
 
 
 class ChecksOngoingResults(schema.Strict):
diff --git a/atr/models/sql.py b/atr/models/sql.py
index 20b8857..b224a5e 100644
--- a/atr/models/sql.py
+++ b/atr/models/sql.py
@@ -24,7 +24,7 @@
 
 import datetime
 import enum
-from typing import Any, Final, Optional
+from typing import Any, Final, Literal, Optional, TypeVar
 
 import pydantic
 import sqlalchemy
@@ -35,6 +35,8 @@ import sqlmodel
 
 from . import results, schema
 
+T = TypeVar("T")
+
 sqlmodel.SQLModel.metadata = sqlalchemy.MetaData(
     naming_convention={
         "ix": "ix_%(table_name)s_%(column_0_N_name)s",
@@ -64,6 +66,12 @@ class ProjectStatus(str, enum.Enum):
 
 
 class ReleasePhase(str, enum.Enum):
+    # TODO: Rename these to the UI names?
+    # COMPOSE, VOTE, FINISH, "DISTRIBUTE"
+    # Compose a draft
+    # Vote on a candidate
+    # Finish a preview
+    # Distribute a (finished) release
     # Step 1: The candidate files are added from external sources and checked 
by ATR
     RELEASE_CANDIDATE_DRAFT = "release_candidate_draft"
     # Step 2: The project members are voting on the candidate release
@@ -638,9 +646,14 @@ class Release(sqlmodel.SQLModel, table=True):
 # SQL models referencing Committee, Project, or Release
 
 
+def example(value: Any) -> dict[Literal["schema_extra"], dict[str, Any]]:
+    return {"schema_extra": {"json_schema_extra": {"examples": [value]}}}
+
+
 # CheckResult: Release
 class CheckResult(sqlmodel.SQLModel, table=True):
-    id: int = sqlmodel.Field(default=None, primary_key=True)
+    # TODO: We have default=None here with a field typed int, not int | None
+    id: int = sqlmodel.Field(default=None, primary_key=True, **example(123))
 
     # M-1: CheckResult -> Release
     # 1-M: Release -C-> [CheckResult]
@@ -648,14 +661,21 @@ class CheckResult(sqlmodel.SQLModel, table=True):
     release: Release = sqlmodel.Relationship(back_populates="check_results")
 
     # We don't call this latest_revision_number, because it might not be the 
latest
-    revision_number: str | None = sqlmodel.Field(default=None, index=True)
-    checker: str
-    primary_rel_path: str | None = sqlmodel.Field(default=None, index=True)
-    member_rel_path: str | None = sqlmodel.Field(default=None, index=True)
-    created: datetime.datetime = 
sqlmodel.Field(sa_column=sqlalchemy.Column(UTCDateTime))
-    status: CheckResultStatus
-    message: str
-    data: Any = sqlmodel.Field(sa_column=sqlalchemy.Column(sqlalchemy.JSON))
+    revision_number: str | None = sqlmodel.Field(default=None, index=True, 
**example("00005"))
+    checker: str = 
sqlmodel.Field(**example("atr.tasks.checks.hashing.HashingCheck"))
+    primary_rel_path: str | None = sqlmodel.Field(
+        default=None, index=True, 
**example("apache-example-0.0.1-source.tar.gz")
+    )
+    member_rel_path: str | None = sqlmodel.Field(default=None, index=True, 
**example("apache-example-0.0.1/pom.xml"))
+    created: datetime.datetime = sqlmodel.Field(
+        sa_column=sqlalchemy.Column(UTCDateTime),
+        **example(datetime.datetime(2025, 1, 1, 12, 0, 0, 
tzinfo=datetime.UTC)),
+    )
+    status: CheckResultStatus = 
sqlmodel.Field(default=CheckResultStatus.SUCCESS, 
**example(CheckResultStatus.SUCCESS))
+    message: str = sqlmodel.Field(**example("sha512 matches for 
apache-example-0.0.1/pom.xml"))
+    data: Any = sqlmodel.Field(
+        sa_column=sqlalchemy.Column(sqlalchemy.JSON), **example({"expected": 
"...", "found": "..."})
+    )
 
 
 # DistributionChannel: Project


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

Reply via email to