asf-tooling commented on issue #1216:
URL: 
https://github.com/apache/tooling-trusted-releases/issues/1216#issuecomment-4409663686

   <!-- gofannon-issue-triage-bot v2 -->
   
   **Automated triage** — analyzed at `main@2da7807a`
   
   **Type:** `new_feature`  •  **Classification:** `actionable`  •  
**Confidence:** `medium`
   **Application domain(s):** `voting`, `web_api_infrastructure`
   
   ### Summary
   Issue #1216 is a tracking issue for four distinct enhancements to Trusted 
Vote mode: (1) API endpoints for vote operations, (2) sending reminders to 
binding voters who vote on the mailing list instead of through ATR, (3) 
carrying IPMC member votes from podling round 1 to round 2, and (4) 
automatically resolving hybrid votes. Some infrastructure already exists 
(auto_resolve in tasks/vote.py, automatic_resolve_when_finished parameter in 
writers/vote.py), but the specific features requested are largely new. The 
issue was created 4 days ago with no discussion.
   
   ### Where this lives in the code today
   
   #### `atr/storage/writers/vote.py` — `FoundationCommitter.cast_trusted` 
(lines 65-74)
   _currently does this_
   Existing trusted vote casting logic that would be reused by a new API 
endpoint for casting votes programmatically.
   
   ```python
       async def cast_trusted(  # noqa: C901
           self,
           project_key: safe.ProjectKey,
           version_key: safe.VersionKey,
           choice: sql.VoteChoice,
           comment: str,
           fullname: str,
           expected_vote_seq: int | None = None,
           expected_vote_mode: sql.VoteMode | None = None,
       ) -> tuple[list[str], str]:
   ```
   
   #### `atr/tasks/vote.py` — `auto_resolve` (lines 40-50)
   _currently does this_
   Existing auto_resolve task handles automatic resolution for Trusted Vote 
non-podling votes. Would need extension for 'hybrid votes' mentioned in the 
issue.
   
   ```python
   @checks.with_model(args.VoteAutoResolve)
   async def auto_resolve(task_args: args.VoteAutoResolve) -> results.Results | 
None:  # noqa: C901
       """Automatically resolve a non-podling Trusted Vote after its scheduled 
end."""
       async with db.session() as data:
           release = await data.release(
               key=task_args.release_key,
               _project=True,
               _committee=True,
               _release_policy=True,
               _project_release_policy=True,
           ).get()
   ```
   
   #### `atr/storage/writers/vote.py` — `CommitteeParticipant.start` (lines 
292-311)
   _currently does this_
   Vote initiation already accepts automatic_resolve_when_finished; this is the 
infrastructure for item 4 (auto-resolve hybrid votes).
   
   ```python
       async def start(  # noqa: C901
           self,
           email_to: str,
           project_key: safe.ProjectKey,
           version_key: safe.VersionKey,
           selected_revision_number: safe.RevisionNumber,
           vote_duration_choice: int,
           subject: str,
           body_data: str,
           asf_fullname: str,
           release: sql.Release | None = None,
           promote: bool = True,
           permitted_recipients: list[str] | None = None,
           email_cc: list[str] | None = None,
           email_bcc: list[str] | None = None,
           second_round_email_to: str | None = None,
           expected_vote_mode: sql.VoteMode | None = None,
           notify_when_finished: bool = False,
           automatic_resolve_when_finished: bool = False,
       ) -> sql.Task:
   ```
   
   #### `atr/storage/writers/vote.py` — `CommitteeMember.resolve_release` 
(lines 620-629)
   _needs modification_
   When resolving a podling round 1 vote (voting_round == 1 and passed), this 
is where IPMC member ballot carry-through from round 1 to round 2 would be 
implemented.
   
   ```python
       async def resolve_release(  # noqa: C901
           self,
           project_key: safe.ProjectKey,
           release: sql.Release,
           voting_round: int | None,
           vote_result: Literal["passed", "failed", "cancelled"],
           latest_vote_task: sql.Task,
           asf_fullname: str,
           resolution_body: str,
       ) -> tuple[sql.Release, int | None, str, str | None]:
   ```
   
   #### `atr/tasks/vote.py` — `end_notify` (lines 139-146)
   _currently does this_
   Existing end_notify sends reminders to the vote initiator. The new feature 
(item 2) would need a similar mechanism but targeting binding voters who voted 
via mailing list.
   
   ```python
   @checks.with_model(args.VoteEndNotify)
   async def end_notify(task_args: args.VoteEndNotify) -> results.Results | 
None:
       """Send a self addressed reminder when a vote ends, if it has not been 
resolved."""
       async with db.session() as data:
           release = await data.release(key=task_args.release_key, 
_project=True, _committee=True).get()
           if release is None:
               log.info(f"Vote end notify skipped: release 
{task_args.release_key} not found")
               return _end_notify_skipped("release_not_found")
   ```
   
   #### `atr/tabulate.py` — `votes` (lines 201-210)
   _currently does this_
   Vote tabulation from mailing list threads. This would be the detection point 
for identifying binding voters who voted via email (item 2 - send them a 
reminder).
   
   ```python
   async def votes(  # noqa: C901
       committee: sql.Committee | None,
       thread_id: str,
       *,
       excluded_message_ids: set[str] | None = None,
   ) -> tuple[int | None, dict[str, models.tabulate.VoteEmail]]:
       """Tabulate votes."""
       excluded_message_ids_normalized = {
           _message_id_normalize(message_id) for message_id in 
(excluded_message_ids or set()) if message_id
       }
   ```
   
   ### Where new code would go
   - `atr/api/__init__.py` — after symbol checks_ongoing
     New API endpoints for vote operations (cast trusted vote, resolve vote, 
list ballots) would be added alongside existing API endpoints.
   - `atr/models/api.py` — new models section
     Request/response models for new vote API endpoints (VoteCastArgs, 
VoteResolveArgs, VoteBallotListResults, etc.).
   - `atr/tasks/vote.py` — after symbol end_notify
     A new task for detecting and notifying binding voters who voted on the 
mailing list instead of through ATR.
   - `atr/storage/writers/vote.py` — in CommitteeMember.resolve_release where 
voting_round == 1 and vote_result == passed
     Logic to copy IPMC member ballots from round 1 to round 2 during podling 
vote transition.
   
   ### Proposed approach
   This tracking issue contains four distinct features that should likely be 
implemented as separate PRs:
   
   1. **API endpoints**: Add API endpoints in `atr/api/__init__.py` mirroring 
the web POST handlers in `atr/post/vote.py` and `atr/post/resolve.py`. These 
would use JWT authentication (via `@jwtoken.require`), call the same storage 
layer methods (`cast_trusted`, `resolve`), and return JSON responses. Models 
would be added to `atr/models/api.py`.
   
   2. **Binding voter reminders**: When tabulating votes from the mailing list 
thread (in `atr/tabulate.py` or during periodic monitoring), detect if a 
binding voter has voted via email. If so, schedule a task (similar to 
`end_notify`) that sends them a direct email reminder that in Trusted Vote 
mode, their vote must be recorded through ATR. This likely requires a new task 
type and integration with the vote monitoring loop.
   
   3. **IPMC ballot carry-through**: In `CommitteeMember.resolve_release` 
(atr/storage/writers/vote.py), when a podling round 1 vote passes and 
transitions to round 2, query the round 1 BallotPaper records for voters who 
are IPMC members (binding in round 2). Create corresponding round 2 BallotPaper 
entries for those voters, preserving their original choice.
   
   4. **Auto-resolve hybrid votes**: The infrastructure for auto-resolve 
already exists (`auto_resolve` task, `automatic_resolve_when_finished` 
parameter). This item likely means extending it to work with votes that have 
both email and trusted ballots (hybrid mode), or making auto-resolve the 
default option in the UI when starting a Trusted Vote.
   
   ### Suggested patches
   
   #### `atr/api/__init__.py`
   Add a vote/cast API endpoint that allows programmatic trusted vote casting 
with JWT authentication, reusing existing storage layer logic.
   
   ````diff
   --- a/atr/api/__init__.py
   +++ b/atr/api/__init__.py
   @@ -56,6 +56,48 @@ ROUTES_MODULE: Final[Literal[True]] = True
    
    
    @api.typed
   [email protected]
   +@quart_schema.security_scheme([{"BearerAuth": []}])
   +@quart_schema.validate_response(models.api.VoteCastResults, 200)
   +async def vote_cast(
   +    _vote_cast: Literal["vote/cast"],
   +    data: models.api.VoteCastArgs,
   +) -> DictResponse:
   +    """
   +    URL: POST /vote/cast
   +
   +    Cast a trusted vote on a release candidate.
   +
   +    The release must be in Trusted Vote mode and in the RELEASE_CANDIDATE 
phase.
   +    The caller must be authenticated via JWT (obtained from a PAT).
   +    """
   +    asf_uid = _jwt_asf_uid()
   +    fullname = await _jwt_fullname(asf_uid)
   +    async with storage.write(asf_uid) as write:
   +        wafc = write.as_foundation_committer()
   +        email_to, error_message = await wafc.vote.cast_trusted(
   +            data.project_key,
   +            data.version_key,
   +            sql.VoteChoice(data.choice),
   +            data.comment,
   +            fullname,
   +            expected_vote_seq=data.vote_seq,
   +            expected_vote_mode=sql.VoteMode.TRUSTED,
   +        )
   +    if error_message:
   +        raise exceptions.Conflict(error_message)
   +    return models.api.VoteCastResults(
   +        endpoint="/vote/cast",
   +        success=True,
   +        email_to=email_to,
   +    ).model_dump(mode="json"), 200
   +
   +
   +async def _jwt_fullname(asf_uid: str) -> str:
   +    """Look up the full name for a JWT-authenticated user."""
   +    # TODO: confirm how to resolve fullname from asf_uid (likely via LDAP)
   +    person = await ldap.person_by_uid(asf_uid)
   +    if person is None:
   +        raise exceptions.Forbidden("User not found")
   +    return person.fullname
   +
   +
   [email protected]
    @quart_schema.validate_response(models.api.ChecksListResults, 200)
    async def checks_list(
        _checks_list: Literal["checks/list"],
   ````
   
   ### Open questions
   - What does 'hybrid votes' mean in item 4? Is it a specific VoteMode, or 
does it refer to votes where both email and trusted ballots coexist?
   - For item 2 (binding voter reminders), should the detection happen during 
periodic vote monitoring, or at vote resolution time when tabulating mailing 
list votes?
   - For item 3 (IPMC ballot carry-through), should the carried ballots be 
exact copies, or should they be marked differently (e.g., with a flag 
indicating they originated from round 1)?
   - What is issue #1204 that is referenced? Its scope would affect this 
issue's implementation.
   - The _jwt_fullname helper function proposed in the diff needs verification 
- how does the codebase currently resolve fullnames for API-authenticated users?
   - Should the API endpoints support all vote operations (cast, resolve, list, 
start) or just a subset initially?
   - For the VoteCastArgs and VoteCastResults models that would go in 
atr/models/api.py - what exact fields are needed beyond project_key, 
version_key, choice, comment, and vote_seq?
   
   ### Files examined
   - `atr/storage/writers/vote.py`
   - `atr/blueprints/api.py`
   - `atr/post/resolve.py`
   - `atr/post/vote.py`
   - `atr/post/voting.py`
   - `atr/tabulate.py`
   - `atr/tasks/vote.py`
   - `atr/api/__init__.py`
   
   ### Related issues
   This issue appears related to: #1226.
   
   _Both address improvements to Trusted Vote mode, with #1226 being a specific 
sub-task of #1216_
   
   ---
   *Draft from a triage agent. A human reviewer should validate before merging 
any change. The agent did not run tests or verify diffs apply.*


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to