Skip to content

Commit d8ca0c1

Browse files
authored
Merge pull request #4 from pemamian/pr-rules-engine
feat: updating with guardrails on label removal
2 parents b4e1173 + 12927e0 commit d8ca0c1

3 files changed

Lines changed: 69 additions & 6 deletions

File tree

.github/workflows/scripts/routing/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ Triage logic is organized into specialized rule subclasses inheriting from `Base
5959
1. **`FileRoutingRule`**: Compiles modified files against the configuration and dynamically applies the corresponding `needs_review_label` and removes the `approved_label`.
6060
2. **`ReviewerApprovalRule`**: Tracks active `pygithub` approvals against the resolved organization team members list:
6161
* **Superpower Override**: If a designated superpower user (like Amit `amithanda`) approves, all TC and GC rules are satisfied instantly, transitioning the PR to `gov:approved` / `status:ready-to-merge`.
62-
* **Label Security Guardrail**: Restricts `gov:tc-approved` and `status:tc-majority-approved` application. If applied by an unauthorized user outside Tech Council or DevOps, the script revokes the label with an automated warning comment.
62+
* **Label Application & Removal Guardrails**:
63+
* Restricts `gov:tc-approved` and `status:tc-majority-approved` application. If applied by an unauthorized user outside Tech Council or DevOps, the script revokes the label.
64+
* If an active `needs_review_label` is manually **unlabeled (removed)** by an unauthorized user while approvals are still pending, the engine **automatically re-applies the label** dynamically and posts a warning comment on the PR.
6365
* **SDK Relaxed Mode**: Repositories matching `sdk` or `meeting-minutes` automatically default team thresholds to `1` to expedite SDK review cycles.
6466
3. **`LabelLifecycleRule`**: Resolves blocked feedback loops. Clears `Label.LABEL_BLOCKED` and restores `Label.LABEL_UNDER_REVIEW` when the author pushes a new commit or comments on the PR.
6567
4. **`StalePRRule`**: Scans active timestamps:

.github/workflows/scripts/routing/test_routing.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,38 @@ def test_amit_superpower_override_approval(self):
265265
self.assertTrue(Label.LABEL_READY_TO_MERGE in result.labels_to_add)
266266
self.assertTrue(result.satisfied)
267267

268+
def test_unauthorized_needs_review_label_removal_guardrail(self):
269+
"""Verifies ReviewerApprovalRule blocks unauthorized removal of active needs-review labels."""
270+
# Scenario: Changed schemas file (Core Spec), user 'unauthorized_tester' manually removed needs_review label
271+
# Target org/team handles: @Universal-Commerce-Protocol/tech-council
272+
self.mock_client.check_team_membership.return_value = False # User is not a member of TC or DevOps
273+
274+
context_unlabel = PRContext(
275+
pr_number=111,
276+
repo_name="Universal-Commerce-Protocol/ucp",
277+
title="feat: corespec modifications",
278+
author="developer2",
279+
is_draft=False,
280+
labels=set(), # Set is empty because the user removed the label
281+
modified_files=["schemas/v1/transaction.json"],
282+
reviews=[],
283+
event_name="pull_request",
284+
event_payload={
285+
"action": "unlabeled",
286+
"sender": {"login": "unauthorized_tester"},
287+
"label": {"name": "gov:needs-tc-review"} # Removed label name
288+
}
289+
)
290+
291+
rule = ReviewerApprovalRule(self.mock_config)
292+
result = rule.evaluate(context_unlabel, self.mock_client)
293+
294+
# Assert that the needs_review label is dynamically re-applied
295+
self.assertTrue("gov:needs-tc-review" in result.labels_to_add)
296+
# Assert warning comment is prepared
297+
self.assertEqual(len(result.comments_to_create), 1)
298+
self.assertTrue("Warning: @unauthorized_tester, you do not have permission to remove `gov:needs-tc-review`." in result.comments_to_create[0])
299+
268300
def test_label_lifecycle_blocked_resume(self):
269301
"""Verifies LabelLifecycleRule handles blocked and resumed triggers."""
270302
rule = LabelLifecycleRule()

.github/workflows/scripts/routing/triage/rules.py

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -185,14 +185,16 @@ def evaluate(self, context: PRContext, client: GitHubAPIClient) -> RuleResult:
185185
if approved_label:
186186
labels_to_add.add(approved_label)
187187

188-
# Enforce guardrail logic for governance labels
188+
# Enforce guardrail logic for governance labels application & removal security
189+
event_user = context.event_payload.get("sender", {}).get("login")
190+
event_action = context.event_payload.get("action")
191+
org_name = context.repo_name.split("/")[0]
192+
193+
# 1. Guard against unauthorized addition of TC Majority Label
189194
if Label.LABEL_TC_MAJORITY_APPROVED in context.labels:
190-
# Ensure applier was actually TC or DevOps
191-
event_user = context.event_payload.get("sender", {}).get("login")
192-
if event_user and context.event_name == "pull_request" and context.event_payload.get("action") == "labeled":
195+
if event_user and context.event_name == "pull_request" and event_action == "labeled":
193196
label_added = context.event_payload.get("label", {}).get("name")
194197
if label_added == Label.LABEL_TC_MAJORITY_APPROVED:
195-
org_name = context.repo_name.split("/")[0]
196198
is_tc = client.check_team_membership(org_name, "tech-council", event_user)
197199
is_devops = client.check_team_membership(org_name, "devops-maintainers", event_user)
198200
if not is_tc and not is_devops:
@@ -203,6 +205,33 @@ def evaluate(self, context: PRContext, client: GitHubAPIClient) -> RuleResult:
203205
f"`{Label.LABEL_TC_MAJORITY_APPROVED}`. This action has been automatically reverted."
204206
)
205207

208+
# 2. Guard against unauthorized removal of active needs-review labels
209+
if context.event_name == "pull_request" and event_action == "unlabeled":
210+
label_removed = context.event_payload.get("label", {}).get("name")
211+
212+
for rule in self.config:
213+
for team_handle, req_details in rule.get("review_requirements", {}).items():
214+
needs_label = req_details.get("needs_review_label")
215+
216+
if needs_label and label_removed == needs_label:
217+
# Verify if the user who removed it is a member of the team or DevOps
218+
clean_handle = team_handle.lstrip("@")
219+
team_org, team_slug = clean_handle.split("/", 1)
220+
221+
is_team_member = client.check_team_membership(team_org, team_slug, event_user)
222+
is_devops = client.check_team_membership(org_name, "devops-maintainers", event_user)
223+
224+
# If the requirements are not satisfied and the user is unauthorized, re-apply!
225+
satisfied, _ = verify_team_approvals(context, team_handle, req_details.get("threshold", 1), client)
226+
if not satisfied and not is_team_member and not is_devops:
227+
print(f"[GUARDRAIL] Unauthorized user {event_user} removed required label {needs_label}. Re-applying.")
228+
labels_to_add.add(needs_label)
229+
comments.append(
230+
f"Warning: @{event_user}, you do not have permission to remove "
231+
f"`{needs_label}`. Reviews from `{team_handle}` are still pending. "
232+
f"This action has been automatically reverted."
233+
)
234+
206235
# If all rules passed, transition to ready-to-merge
207236
if all_rules_satisfied and rules_evaluated > 0:
208237
labels_to_add.add(Label.LABEL_APPROVED)

0 commit comments

Comments
 (0)