Skip to content

Commit 80b467c

Browse files
committed
feat(triage): implement dynamic modular PR triage rules engine
1 parent 98a2e4c commit 80b467c

13 files changed

Lines changed: 977 additions & 140 deletions

.github/workflows/pr-cron-stale-abandon.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ jobs:
2323
uses: astral-sh/setup-uv@v5
2424

2525
- name: Trace and Mark Stale PRs
26-
run: uv run .github/workflows/scripts/pr-cron-stale-abandon.py
26+
run: uv run .github/workflows/scripts/routing/pr-cron-stale-abandon.py
2727
env:
2828
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
29+
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
name: PR Triage Automation
3+
4+
on:
5+
pull_request:
6+
types: [opened, ready_for_review, synchronize, labeled]
7+
issue_comment:
8+
types: [created]
9+
check_suite:
10+
types: [completed]
11+
12+
permissions:
13+
pull-requests: write
14+
issues: write
15+
contents: read
16+
17+
jobs:
18+
triage_realtime:
19+
runs-on: ubuntu-latest
20+
timeout-minutes: 10
21+
if: github.event.pull_request.draft == false
22+
steps:
23+
- name: Checkout repository
24+
uses: actions/checkout@v4
25+
26+
- name: Set up uv
27+
uses: astral-sh/setup-uv@v5
28+
29+
- name: Validate Routing Configurations
30+
run: uv run .github/workflows/scripts/routing/validate-routing.py
31+
env:
32+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
33+
34+
- name: Check and Update PR Labels
35+
run: uv run .github/workflows/scripts/routing/pr-triage-automation.py
36+
env:
37+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
38+

.github/workflows/scripts/pr-cron-stale-abandon.py

Lines changed: 0 additions & 139 deletions
This file was deleted.
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# UCP PR Review Routing Configuration
2+
# This file acts as the configuration map for pr-triage-automation script.
3+
# It defines glob patterns for changed files and associates them with required reviewer sets and label states.
4+
5+
routing_rules:
6+
- name: "Governance Files"
7+
patterns:
8+
- "LICENSE"
9+
- "GOVERNANCE.md"
10+
- "CONTRIBUTING.md"
11+
- ".github/CODEOWNERS"
12+
review_requirements:
13+
"@Universal-Commerce-Protocol/governance-council":
14+
threshold: 1
15+
needs_review_label: "gov:needs-gc-review"
16+
approved_label: "gov:gc-approved"
17+
18+
- name: "Core Protocol & Spec"
19+
patterns:
20+
- "schemas/**/*.json"
21+
- "spec/**/*.md"
22+
review_requirements:
23+
"@Universal-Commerce-Protocol/tech-council":
24+
threshold: "majority"
25+
needs_review_label: "gov:needs-tc-review"
26+
approved_label: "gov:tc-approved"
27+
"@Universal-Commerce-Protocol/maintainers":
28+
threshold: 1
29+
needs_review_label: "status:review-needed-maintainers"
30+
approved_label: "gov:maintainer-approved"
31+
32+
- name: "Infrastructure & Tooling"
33+
patterns:
34+
- ".github/workflows/**"
35+
- ".gitignore"
36+
- ".pre-commit-config.yaml"
37+
- "pyproject.toml"
38+
- "uv.lock"
39+
review_requirements:
40+
"@Universal-Commerce-Protocol/devops-maintainers":
41+
threshold: 1
42+
needs_review_label: "status:needs-triage"
43+
approved_label: "status:under-review"
44+
45+
- name: "SDK Code & Maintenance"
46+
patterns:
47+
- "src/**"
48+
- "templates/**"
49+
review_requirements:
50+
"@Universal-Commerce-Protocol/maintainers":
51+
threshold: 1
52+
needs_review_label: "status:needs-triage"
53+
approved_label: "status:under-review"
54+
55+
- name: "Payments Custom Review Rules"
56+
patterns:
57+
- "src/components/payments/**"
58+
review_requirements:
59+
"@Universal-Commerce-Protocol/maintainers":
60+
threshold: 1
61+
needs_review_label: "status:review-needed-payments-maintainers"
62+
approved_label: "status:payments-approved"
63+
"@Universal-Commerce-Protocol/tech-council":
64+
threshold: 1
65+
needs_review_label: "gov:needs-tc-review"
66+
approved_label: "gov:tc-approved"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# __init__.py
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#!/usr/bin/env python3
2+
# /// script
3+
# dependencies = [
4+
# "pygithub",
5+
# "pyyaml",
6+
# ]
7+
# ///
8+
import os
9+
import sys
10+
from triage.github_api import GitHubAPIClient
11+
from triage.rules_engine import RulesEngine
12+
from triage.rules import StalePRRule
13+
14+
def main():
15+
token = os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN")
16+
if not token:
17+
print("[ERROR] GH_TOKEN or GITHUB_TOKEN is not set.")
18+
sys.exit(1)
19+
20+
# Extract active repository name dynamically from the git config of the local clone.
21+
try:
22+
import subprocess
23+
script_dir = os.path.dirname(os.path.abspath(__file__))
24+
origin_url = subprocess.check_output(["git", "-C", script_dir, "config", "--get", "remote.origin.url"]).decode("utf-8").strip()
25+
clean_url = origin_url.replace(".git", "").replace(":", "/")
26+
parts = clean_url.split("/")
27+
repo_name = f"{parts[-2]}/{parts[-1]}"
28+
print(f"[INFO] Target Repository resolved: {repo_name}")
29+
except Exception as e:
30+
print(f"[ERROR] Failed to dynamically determine current Git repository name: {e}")
31+
sys.exit(1)
32+
33+
34+
35+
36+
# Configure thresholds (stale after 30 days, abandon candidate after 37 days)
37+
STALE_THRESHOLD_DAYS = 30
38+
ABANDON_THRESHOLD_DAYS = 37
39+
40+
print(f"[START] Inactivity Scan: Scanning open PRs in '{repo_name}'...")
41+
42+
try:
43+
# Initialize client and engine
44+
client = GitHubAPIClient(token, repo_name)
45+
engine = RulesEngine(client)
46+
47+
# Register only stale/abandon inactivity rules
48+
engine.add_rule(StalePRRule(
49+
stale_threshold_days=STALE_THRESHOLD_DAYS,
50+
abandon_threshold_days=ABANDON_THRESHOLD_DAYS
51+
))
52+
53+
# Fetch all currently open pull requests
54+
pulls = client.repo.get_pulls(state="open")
55+
total_scanned = 0
56+
57+
for pygithub_pr in pulls:
58+
if pygithub_pr.draft:
59+
continue
60+
61+
total_scanned += 1
62+
print(f" - [SCANNING] PR #{pygithub_pr.number}: '{pygithub_pr.title}' (Updated at: {pygithub_pr.updated_at})")
63+
64+
try:
65+
# Wrap the PR inside our shared context model and run engine
66+
context = client.get_pr_context(pygithub_pr.number, event_name="schedule")
67+
engine.run(context)
68+
except Exception as pe:
69+
print(f" [ERROR] Failed to run stale evaluation on PR #{pygithub_pr.number}: {pe}", file=sys.stderr)
70+
71+
print(f"[SUCCESS] Inactivity Scan complete. Scanned {total_scanned} open non-draft pull requests.")
72+
except Exception as e:
73+
print(f"[ERROR] Inactivity scan runner failed: {e}", file=sys.stderr)
74+
sys.exit(1)
75+
76+
if __name__ == "__main__":
77+
main()

0 commit comments

Comments
 (0)