feat: auto-release status:in-progress on close and merge (#58)

When Gitea-Tools closes an issue, merges a PR, or closes a PR without merging,
remove status:in-progress from the affected issue(s) so closed work doesn't
retain stale in-progress state (as happened with #46/#48).

Helpers (deterministic, unit-testable) in mcp_server.py:
- extract_linked_issue_numbers(text, branch_name): pure; matches
  Closes/Fixes/Resolves/Refs #N and issue-<N> in a branch name (not review/pr-N).
- release_in_progress_label(issue_numbers, repo_context): removes ONLY
  status:in-progress; absent label / undefined repo label = no-op success;
  per-issue failures recorded in `failed` (never falsely reported clean);
  emits an audit record per removal when auditing is enabled.
- cleanup_in_progress_for_pr(pr_payload, repo_context): links from PR
  title/body + head branch, then releases; empty when nothing confidently
  linked (no guessing, no mutation).

Wired into:
- gitea_close_issue → releases from that issue.
- gitea_merge_pr → releases from PR-linked issues (reuses the read-back PR).
- gitea_edit_pr (state=closed) → releases from PR-linked issues WITHOUT closing
  them.
Each returns the cleanup summary under `in_progress_cleanup`. Only necessary
label GET/DELETE calls added; no auth/profile/retry/config/worktree/release-tag
changes; backwards compatible.

Tests: tests/test_in_progress_cleanup.py (18 cases) — extraction, present/absent
label, no-issues/no-link no-op, multiple issues, failure reported, label not
defined, only-status-removed, PR keyword/branch links, close-without-merge does
not close the issue, and audit records the removal. Updated TestCloseIssue for
the added release calls.

Full suite 322 passed / 0 failures; py_compile clean; git diff --check clean;
no secrets.

Closes #58. Refs #46, #48.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-02 06:00:34 -04:00
parent 6089ec724a
commit 7c2aabf6ac
3 changed files with 326 additions and 5 deletions
+12 -3
View File
@@ -89,12 +89,21 @@ class TestCloseIssue(unittest.TestCase):
@patch("mcp_server.api_request")
@patch("mcp_server.get_auth_header", return_value=FAKE_AUTH)
def test_closes_issue(self, _auth, mock_api):
mock_api.return_value = {"state": "closed"}
# PATCH close, then status:in-progress release (labels lookup, issue
# labels, DELETE) — see #58.
mock_api.side_effect = [
{"state": "closed"}, # PATCH close
[{"id": 10, "name": "status:in-progress"}], # repo labels
[{"name": "status:in-progress"}], # issue #42 labels
None, # DELETE label
]
result = gitea_close_issue(issue_number=42)
self.assertTrue(result["success"])
self.assertIn("42", result["message"])
payload = mock_api.call_args[0][3]
self.assertEqual(payload["state"], "closed")
first = mock_api.call_args_list[0]
self.assertEqual(first.args[0], "PATCH")
self.assertEqual(first.args[3]["state"], "closed")
self.assertIn(42, result["in_progress_cleanup"]["released"])
# ---------------------------------------------------------------------------