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

   <!-- gofannon-issue-triage-bot v2 -->
   
   **Automated triage** — analyzed at `main@2da7807a`
   
   **Type:** `refactor`  •  **Classification:** `actionable`  •  
**Confidence:** `medium`
   **Application domain(s):** `release_lifecycle`
   
   ### Summary
   The issue asks to disallow phase transitions from non-latest revisions. @sbp 
clarified this only applies to the compose→vote transition (finish no longer 
has revisions). The backend in `_start_vote_no_commit` already has a check 
rejecting non-latest revisions (raising a 409), but the `promote_to_candidate` 
method still accepts a `selected_revision_number` parameter, and presumably the 
UI still presents the option. The simplification would remove the ability to 
select a revision for promotion and always use the latest.
   
   ### Where this lives in the code today
   
   #### `atr/storage/writers/release.py` — 
`CommitteeParticipant.promote_to_candidate` (lines 450-472)
   _needs modification_
   This method accepts a `selected_revision_number` parameter that the issue 
suggests removing in favor of always using the latest revision internally.
   
   ```python
       async def promote_to_candidate(
           self,
           release_key: safe.ReleaseKey,
           selected_revision_number: safe.RevisionNumber,
           *,
           allowed_vote_modes: frozenset[sql.VoteMode],
       ) -> str | None:
           """Promote a release candidate draft to a new phase."""
           try:
               await self.__data.begin_immediate()
               release, vote_seq, vote_mode = await self._start_vote_no_commit(
                   release_key,
                   selected_revision_number,
                   allowed_vote_modes=allowed_vote_modes,
                   promote=True,
               )
               await self.__data.commit()
           except storage.AccessError as e:
               await self.__data.rollback()
               return str(e)
           except Exception:
               await self.__data.rollback()
               raise
   ```
   
   #### `atr/storage/writers/release.py` — 
`CommitteeParticipant._start_vote_no_commit` (lines 483-503)
   _currently does this_
   This method already contains the enforcement check rejecting promotions from 
non-latest revisions. The issue's simplification would make this check 
unnecessary for the promote=True path by always using the latest revision 
internally.
   
   ```python
       async def _start_vote_no_commit(  # noqa: C901
           self,
           release_key: safe.ReleaseKey,
           selected_revision_number: safe.RevisionNumber,
           *,
           allowed_vote_modes: frozenset[sql.VoteMode],
           promote: bool,
           expected_podling_thread_id: str | None = None,
       ) -> tuple[sql.Release, int, sql.VoteMode]:
           release_for_pre_checks = await self.__data.release(
               key=str(release_key), _project=True, _committee=True, 
_project_release_policy=True
           ).demand(storage.AccessError("Release candidate draft not found", 
status=404))
           project_key = release_for_pre_checks.safe_project_key
           version_key = release_for_pre_checks.safe_version_key
           revision_number = release_for_pre_checks.safe_latest_revision_number
           ...
           # Check that the revision number is the latest
           if revision_number != selected_revision_number:
               raise storage.AccessError(
                   "The selected revision number does not match the latest 
revision number", status=409
               )
   ```
   
   ### Proposed approach
   The simplification involves modifying `promote_to_candidate` to no longer 
accept a `selected_revision_number` parameter, instead internally looking up 
the latest revision number. This eliminates the need for the UI to pass a 
revision number and removes the possibility of even attempting to promote from 
a non-latest revision. The `_start_vote_no_commit` method would still receive 
the revision number for its optimistic concurrency check in the SQL UPDATE 
WHERE clause (preventing races), but it would derive it from 
`release_for_pre_checks.safe_latest_revision_number` rather than from user 
input when `promote=True`.
   
   Since I cannot see the caller of `promote_to_candidate` (likely in a POST 
handler for vote/draft operations) or the template that renders the promote 
button, I cannot provide a complete diff. The core change is in 
`_start_vote_no_commit`: when `promote=True`, ignore the 
`selected_revision_number` parameter and use `revision_number` (the latest) 
directly. Then update callers to not need to pass a revision number for 
promotion.
   
   ### Suggested patches
   
   #### `atr/storage/writers/release.py`
   When promoting (compose→vote), always use the latest revision number instead 
of requiring the caller to supply it, simplifying the interface and preventing 
any possibility of non-latest promotion.
   
   ````diff
   --- a/atr/storage/writers/release.py
   +++ b/atr/storage/writers/release.py
   @@ -280,7 +280,7 @@
        async def promote_to_candidate(
            self,
            release_key: safe.ReleaseKey,
   -        selected_revision_number: safe.RevisionNumber,
   +        selected_revision_number: safe.RevisionNumber | None = None,
            *,
            allowed_vote_modes: frozenset[sql.VoteMode],
        ) -> str | None:
   @@ -330,6 +330,12 @@
            revision_number = release_for_pre_checks.safe_latest_revision_number
            if promote:
                vote_mode = release_for_pre_checks.effective_vote_mode
   +            # When promoting, always use the latest revision - disallow 
promotion
   +            # from any revision other than the latest
   +            if selected_revision_number is None:
   +                selected_revision_number = revision_number
   +            elif revision_number != selected_revision_number:
   +                raise storage.AccessError(
   +                    "Phase transitions are only allowed from the latest 
revision", status=409
   +                )
            else:
                vote_mode = release_for_pre_checks.vote_mode
                if vote_mode is None:
   @@ -350,11 +356,6 @@
                if promote:
                    raise storage.AccessError("This release is not in the 
candidate draft phase", status=409)
                raise storage.AccessError("The release state has changed, 
please refresh and try again", status=409)
    
   -        # Check that the revision number is the latest
   -        if revision_number != selected_revision_number:
   -            raise storage.AccessError(
   -                "The selected revision number does not match the latest 
revision number", status=409
   -            )
    
            if await interaction.has_blocker_checks(
                release_for_pre_checks, selected_revision_number, 
caller_data=self.__data
   ````
   
   ### Open questions
   - Where is `promote_to_candidate` called from? I don't see the POST handler 
that starts a vote (likely in `atr/post/draft.py` or similar). The caller also 
needs to be updated to stop passing a user-selected revision number.
   - Is there a template that renders a revision selector for the 
promote/start-vote button? If so, that UI element should be removed or hidden.
   - The `_start_vote_no_commit` method is also called with `promote=False` 
(for re-votes). Does the non-latest check need to be preserved for that path? 
Based on the issue, it seems only `promote=True` is affected.
   - The SQL UPDATE uses `sql.latest_revision_number_query() == 
str(selected_revision_number)` as an optimistic concurrency guard. This should 
be kept regardless, using the internally-derived latest revision number.
   
   ### Files examined
   - `atr/post/compose.py`
   - `atr/post/finish.py`
   - `atr/storage/writers/release.py`
   - `atr/storage/writers/revision.py`
   - `atr/post/revisions.py`
   - `atr/shared/finish.py`
   - `atr/get/compose.py`
   - `atr/get/finish.py`
   
   ---
   *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