fix(workflow): retire status:pr-open on every terminal PR transition (Closes #780)

status:pr-open was applied by gitea_create_pr and never removed again. Every
terminal path finished without touching it, so a repository audit found 40
closed issues still advertising an open PR.

Add terminal_pr_label_cleanup.py as the single authoritative rule and route
every sanctioned terminal path through it, so the paths cannot drift:

- merge (gitea_merge_pr)
- close without merge (gitea_edit_pr)
- supersession/abandonment (gitea_reconcile_superseded_by_merged_pr)
- already-landed reconciliation (gitea_reconcile_already_landed_pr)
- controller closure (gitea_close_issue)
- retry/recovery (new gitea_cleanup_terminal_pr_labels)

The rule removes only status:pr-open, preserves every other label, allows an
empty resulting set, is a no-op when the label is absent (so retries are
safe), and confirms the outcome by read-after-write rather than assumption.

Controller closure runs the cleanup before the state change and fails closed
if it cannot be completed and verified; closing first would bake in the stale
label with no later step to catch it. Post-merge cleanup never blocks the
merge, which already happened, and reports failures with a safe next action.

Also:
- gitea_assess_terminal_label_hygiene: read-only terminal validation that
  reports residual status:pr-open, exempting issues with a genuinely open PR.
- _put_issue_label_names now accepts Gitea's empty response body when the
  requested set is empty, so clearing the last label works.
- test_audit's close_issue fixture keys on the request instead of call order,
  since closing now also reads labels for the cleanup and its read-back.

Docs: label-taxonomy terminal-transition section, runbook pointer, and the
review-merge / reconcile-landed final-report terminal-label requirements.

Suite: 4045 passed, 11 failed, 6 skipped. The same 11 failures reproduce on
clean master df31674 (4010 passed, 11 failed) and are pre-existing.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
2026-07-21 15:41:24 -04:00
co-authored by Claude Opus 4.8
parent df3167488c
commit ed0e8c82de
9 changed files with 1448 additions and 8 deletions
+42
View File
@@ -131,6 +131,44 @@ Suggested lifecycle:
The helper module `issue_workflow_labels.py` is the source of truth for the
canonical label specs and status transition replacement behavior.
## Terminal PR transitions retire `status:pr-open` (#780)
`status:pr-open` states that a linked PR is *currently open*. The moment that
stops being true the label must go, whatever ended the PR:
| Terminal reason | Raised by |
|---|---|
| `merged` | `gitea_merge_pr` |
| `closed_without_merge` | `gitea_edit_pr` closing the PR |
| `superseded` | `gitea_reconcile_superseded_by_merged_pr` |
| `already_landed` | `gitea_reconcile_already_landed_pr` |
| `controller_closure` | `gitea_close_issue` |
| `abandoned` | abandonment handling |
| `retry_recovery` | `gitea_cleanup_terminal_pr_labels` after a partial failure |
All of these route through one rule in `terminal_pr_label_cleanup.py`, so the
paths cannot drift apart. The rule guarantees:
- only `status:pr-open` is removed — every other label is preserved verbatim;
- an empty resulting label set is valid (it was the issue's only label);
- an issue that no longer carries the label is a no-op, so retries are safe;
- the result is confirmed by a read-after-write re-read, not assumed.
Controller closure runs the cleanup **before** changing issue state and fails
closed if it cannot be completed and verified — closing first would bake in the
stale label with no later step to catch it. Post-merge cleanup never blocks the
merge: the transition already happened, so failures are reported with a
`safe_next_action` instead.
Use `gitea_assess_terminal_label_hygiene` as terminal validation before
declaring a transition or cleanup batch complete. It enumerates issues plus the
live open PRs and reports any issue still carrying `status:pr-open` without an
open PR to justify it. Issues with a genuinely open PR are exempt, not
residual.
Recovery from a partial failure is `gitea_cleanup_terminal_pr_labels` with
`terminal_reason='retry_recovery'`.
## Discussion Issues
Discussion issues must be labeled `type:discussion`.
@@ -157,6 +195,10 @@ If a discussion produces implementation work, either:
be applied to the locked issue, then applies it after the PR is created.
- `gitea_set_issue_labels` accepts an explicit `worktree_path` so author
sessions can satisfy the branches-only mutation guard while changing labels.
- `gitea_cleanup_terminal_pr_labels` retires `status:pr-open` after a terminal
PR transition; it is idempotent, so it is also the retry/recovery path.
- `gitea_assess_terminal_label_hygiene` is the read-only terminal validation
for residual `status:pr-open`.
## Existing Non-Workflow Labels