Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .github/labels.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
- name: status:needs-triage
color: 'FBCA04'
description: Signal that the PR is ready for human triage

- name: status:under-review
color: '1D76DB'

- name: status:stale-review
color: '6F7A8A'
description: Applied if a PR is waiting on a reviewer for too long

- name: status:blocked
color: 'D93F0B'

- name: status:ready-to-merge
color: '0E8A16'
description: Signaling to the DevOps team that it can be safely merged

- name: status:stale
color: '6F7A8A'
description: Automatically applied if a PR is blocked waiting for the author's response for 30 days

- name: status:merged
color: '6F42C1'
description: Signify the completion of its lifecycle and provide clear visibility for reporting

- name: area:payments
color: '0052CC'

- name: devops
color: 'FFD1DC'

- name: gov:needs-tc-review
color: 'D93F0B'

- name: gov:needs-gc-review
color: 'FDE5BE'

- name: gov:approved
color: 'C2E0C6'
description: Triggers the final code ownership checks
24 changes: 24 additions & 0 deletions .github/workflows/label-sync.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Sync Labels

on:
push:
branches:
- main
paths:
- '.github/labels.yml'
workflow_dispatch:

jobs:
labeler:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Sync labels
uses: EndBug/label-sync@v2
with:
config-file: .github/labels.yml
delete-other-labels: false
29 changes: 29 additions & 0 deletions .github/workflows/pr-cron-stale-abandon.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
name: PR Stale and Abandon Tracker

on:
schedule:
- cron: '17 2 * * *' # Daily at 2:17 AM UTC to avoid peak scheduler hours
workflow_dispatch: # Allows manual trigger for testing

permissions:
pull-requests: write
issues: write
contents: read

jobs:
stale_tracker:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up uv
uses: astral-sh/setup-uv@v5

- name: Trace and Mark Stale PRs
run: uv run .github/workflows/scripts/routing/pr-cron-stale-abandon.py
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

38 changes: 38 additions & 0 deletions .github/workflows/pr-triage-automation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
name: PR Triage Automation

on:
pull_request:
types: [opened, ready_for_review, synchronize, labeled]
issue_comment:
types: [created]
check_suite:
types: [completed]

permissions:
pull-requests: write
issues: write
contents: read

jobs:
triage_realtime:
runs-on: ubuntu-latest
timeout-minutes: 10
if: github.event.pull_request.draft == false
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up uv
uses: astral-sh/setup-uv@v5

- name: Validate Routing Configurations
run: uv run .github/workflows/scripts/routing/validate-routing.py
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Check and Update PR Labels
run: uv run .github/workflows/scripts/routing/pr-triage-automation.py
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

66 changes: 66 additions & 0 deletions .github/workflows/scripts/routing/UCP_PR_REVIEW_ROUTING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# UCP PR Review Routing Configuration
# This file acts as the configuration map for pr-triage-automation script.
# It defines glob patterns for changed files and associates them with required reviewer sets and label states.

routing_rules:
- name: "Governance Files"
patterns:
- "LICENSE"
- "GOVERNANCE.md"
- "CONTRIBUTING.md"
- ".github/CODEOWNERS"
review_requirements:
"@Universal-Commerce-Protocol/governance-council":
threshold: 1
needs_review_label: "gov:needs-gc-review"
approved_label: "gov:gc-approved"

- name: "Core Protocol & Spec"
patterns:
- "schemas/**/*.json"
- "spec/**/*.md"
review_requirements:
"@Universal-Commerce-Protocol/tech-council":
threshold: "majority"
needs_review_label: "gov:needs-tc-review"
approved_label: "gov:tc-approved"
"@Universal-Commerce-Protocol/maintainers":
threshold: 1
needs_review_label: "status:review-needed-maintainers"
approved_label: "gov:maintainer-approved"

- name: "Infrastructure & Tooling"
patterns:
- ".github/workflows/**"
- ".gitignore"
- ".pre-commit-config.yaml"
- "pyproject.toml"
- "uv.lock"
review_requirements:
"@Universal-Commerce-Protocol/devops-maintainers":
threshold: 1
needs_review_label: "status:needs-triage"
approved_label: "status:under-review"

- name: "SDK Code & Maintenance"
patterns:
- "src/**"
- "templates/**"
review_requirements:
"@Universal-Commerce-Protocol/maintainers":
threshold: 1
needs_review_label: "status:needs-triage"
approved_label: "status:under-review"

- name: "Payments Custom Review Rules"
patterns:
- "src/components/payments/**"
review_requirements:
"@Universal-Commerce-Protocol/maintainers":
threshold: 1
needs_review_label: "status:review-needed-payments-maintainers"
approved_label: "status:payments-approved"
"@Universal-Commerce-Protocol/tech-council":
threshold: 1
needs_review_label: "gov:needs-tc-review"
approved_label: "gov:tc-approved"
1 change: 1 addition & 0 deletions .github/workflows/scripts/routing/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# __init__.py
77 changes: 77 additions & 0 deletions .github/workflows/scripts/routing/pr-cron-stale-abandon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/usr/bin/env python3
# /// script
# dependencies = [
# "pygithub",
# "pyyaml",
# ]
# ///
import os
import sys
from triage.github_api import GitHubAPIClient
from triage.rules_engine import RulesEngine
from triage.rules import StalePRRule

def main():
token = os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN")
if not token:
print("[ERROR] GH_TOKEN or GITHUB_TOKEN is not set.")
sys.exit(1)

# Extract active repository name dynamically from the git config of the local clone.
try:
import subprocess
script_dir = os.path.dirname(os.path.abspath(__file__))
origin_url = subprocess.check_output(["git", "-C", script_dir, "config", "--get", "remote.origin.url"]).decode("utf-8").strip()
clean_url = origin_url.replace(".git", "").replace(":", "/")
parts = clean_url.split("/")
repo_name = f"{parts[-2]}/{parts[-1]}"
print(f"[INFO] Target Repository resolved: {repo_name}")
except Exception as e:
print(f"[ERROR] Failed to dynamically determine current Git repository name: {e}")
sys.exit(1)




# Configure thresholds (stale after 30 days, abandon candidate after 37 days)
STALE_THRESHOLD_DAYS = 30
ABANDON_THRESHOLD_DAYS = 37

print(f"[START] Inactivity Scan: Scanning open PRs in '{repo_name}'...")

try:
# Initialize client and engine
client = GitHubAPIClient(token, repo_name)
engine = RulesEngine(client)

# Register only stale/abandon inactivity rules
engine.add_rule(StalePRRule(
stale_threshold_days=STALE_THRESHOLD_DAYS,
abandon_threshold_days=ABANDON_THRESHOLD_DAYS
))

# Fetch all currently open pull requests
pulls = client.repo.get_pulls(state="open")
total_scanned = 0

for pygithub_pr in pulls:
if pygithub_pr.draft:
continue

total_scanned += 1
print(f" - [SCANNING] PR #{pygithub_pr.number}: '{pygithub_pr.title}' (Updated at: {pygithub_pr.updated_at})")

try:
# Wrap the PR inside our shared context model and run engine
context = client.get_pr_context(pygithub_pr.number, event_name="schedule")
engine.run(context)
except Exception as pe:
print(f" [ERROR] Failed to run stale evaluation on PR #{pygithub_pr.number}: {pe}", file=sys.stderr)

print(f"[SUCCESS] Inactivity Scan complete. Scanned {total_scanned} open non-draft pull requests.")
except Exception as e:
print(f"[ERROR] Inactivity scan runner failed: {e}", file=sys.stderr)
sys.exit(1)

if __name__ == "__main__":
main()
73 changes: 73 additions & 0 deletions .github/workflows/scripts/routing/pr-triage-automation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#!/usr/bin/env python3
# /// script
# dependencies = [
# "pygithub",
# "pyyaml",
# ]
# ///
import json
import os
import sys
from triage.github_api import GitHubAPIClient
from triage.rules_engine import RulesEngine
from triage.rules import FileRoutingRule, ReviewerApprovalRule, LabelLifecycleRule

def main():
token = os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN")
if not token:
print("[ERROR] GH_TOKEN or GITHUB_TOKEN is not set.")
sys.exit(1)

event_path = os.environ.get("GITHUB_EVENT_PATH")
event_name = os.environ.get("GITHUB_EVENT_NAME")
repo_name = os.environ.get("GITHUB_REPOSITORY")

if not event_path or not os.path.exists(event_path):
print("[ERROR] GITHUB_EVENT_PATH is not set or invalid.")
sys.exit(1)

with open(event_path, "r", encoding="utf-8") as f:
payload = json.load(f)

# Determine pull request number from payload
pr_number = (
payload.get("pull_request", {}).get("number") or
payload.get("issue", {}).get("number") or
(payload.get("check_suite", {}).get("pull_requests") or [{}])[0].get("number")
)

if not pr_number:
print("[SKIP] Event not associated with an open Pull Request.")
return

print(f"[START] Real-time Triage webhook trigger: '{event_name}' for PR #{pr_number}")

try:
# Initialize GitHub client and Rules Engine
client = GitHubAPIClient(token, repo_name)
engine = RulesEngine(client)

# Load YML configuration
routing_config = engine.load_routing_config()

# Register real-time rules
engine.add_rule(FileRoutingRule(routing_config))
engine.add_rule(LabelLifecycleRule())
engine.add_rule(ReviewerApprovalRule(routing_config))

# Fetch live PR details and run engine
context = client.get_pr_context(pr_number, event_name=event_name, event_payload=payload)

# Skip evaluations for draft PRs
if context.is_draft:
print(f"[SKIP] Pull request #{pr_number} is a draft.")
return

engine.run(context)
print("[SUCCESS] Real-time triage automated evaluations completed.")
except Exception as e:
print(f"[ERROR] Real-time triage webhook processing failed: {e}", file=sys.stderr)
sys.exit(1)

if __name__ == "__main__":
main()
1 change: 1 addition & 0 deletions .github/workflows/scripts/routing/triage/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# __init__.py
Loading
Loading