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]