ci(tracker): require one metadata owner per pull request

This commit is contained in:
2026-07-26 06:49:06 +00:00
committed by didericis
parent 5828f5e900
commit c0493f0b01
3 changed files with 31 additions and 8 deletions
@@ -29,7 +29,8 @@ item exists. Every issue has at least one label. An issue opened or left
without labels receives `Status/Needs Triage` automatically until it is
classified.
Every new pull request is tracked in one of two ways:
Every new pull request is tracked in exactly one of two mutually exclusive
ways:
1. It deliberately references at least one existing issue in its title or
description. Tracker metadata stays on that issue and the PR remains
@@ -43,9 +44,9 @@ Issue references use one of these forms:
- `Part of #123`, `Related to #123`, `Refs #123`, or `References #123` when it
contributes without completing it.
Gitea Actions enforces the either/or PR rule, validates any issue references,
and repairs the empty issue-label state. Branch protection makes the PR policy
check required.
Gitea Actions enforces the exclusive either/or PR rule, validates any issue
references, and repairs the empty issue-label state. Branch protection makes
the PR policy check required.
The policy applies from 2026-07-18 onward. Existing issues may be labelled as
they are encountered, but closed PRs are grandfathered: no retrospective
@@ -70,5 +71,6 @@ issues or PR labels are created solely to make history conform.
## Links
- Issue #405.
- `.gitea/workflows/tracker-policy.yml`.
- `.gitea/workflows/tracker-policy-pr.yml`.
- `.gitea/workflows/tracker-policy-issues.yml`.
- `scripts/tracker_policy.py`.
+6
View File
@@ -55,6 +55,12 @@ def check_pull_request(event: dict[str, Any], api: GiteaApi) -> list[str]:
errors: list[str] = []
labels = pull.get("labels") or []
numbers = deliberate_issue_numbers(pull.get("title", ""), pull.get("body", ""))
if labels and numbers:
errors.append(
"PR must use exactly one tracking mode: remove PR labels when "
"linking an issue, or remove the issue reference when labels "
"belong on the PR."
)
if not numbers:
if not labels:
errors.append(
+18 -3
View File
@@ -47,7 +47,21 @@ class TestCheckPullRequest(unittest.TestCase):
self.assertIn("either have a label or reference an issue", errors[0])
api.request.assert_not_called()
def test_validates_issue_reference_even_when_pr_is_labelled(self):
def test_rejects_labelled_pr_linked_to_real_issue(self):
api = Mock()
api.request.return_value = {"number": 12, "pull_request": None}
event = {
"pull_request": {
"title": "Change",
"body": "Closes #12",
"labels": [{"name": "Kind/Documentation"}],
}
}
errors = check_pull_request(event, api)
self.assertEqual(len(errors), 1)
self.assertIn("exactly one tracking mode", errors[0])
def test_still_validates_issue_reference_when_both_modes_are_used(self):
api = Mock()
api.request.return_value = {"number": 12, "pull_request": {}}
event = {
@@ -58,8 +72,9 @@ class TestCheckPullRequest(unittest.TestCase):
}
}
errors = check_pull_request(event, api)
self.assertEqual(len(errors), 1)
self.assertIn("not an issue", errors[0])
self.assertEqual(len(errors), 2)
self.assertIn("exactly one tracking mode", errors[0])
self.assertIn("not an issue", errors[1])
class TestEnsureIssueLabel(unittest.TestCase):