Scan PRs cleanup (#626)

Signed-off-by: Glenn Jocher <[email protected]>
Co-authored-by: UltralyticsAssistant <[email protected]>
This commit is contained in:
Glenn Jocher
2025-10-30 17:29:20 +08:00
committed by GitHub
co-authored by UltralyticsAssistant
parent 28d7623445
commit f458ecc7be
5 changed files with 74 additions and 52 deletions
+1 -1
View File
@@ -19,4 +19,4 @@ jobs:
steps:
- uses: ultralytics/actions/scan-prs@main
with:
token: ${{ secrets.GITHUB_TOKEN }}
token: ${{ secrets._GITHUB_TOKEN }}
+1 -1
View File
@@ -128,7 +128,7 @@ List open PRs across an organization and auto-merge eligible Dependabot PRs.
with:
token: ${{ secrets.GITHUB_TOKEN }}
org: ultralytics # Optional: defaults to ultralytics
visibility: public # Optional: public, private, internal, or all
visibility: private,internal # Optional: public, private, internal, all, or comma-separated
```
[**📖 Full Documentation →**](scan-prs/README.md)
+64 -42
View File
@@ -24,32 +24,70 @@ def get_phase_emoji(age_days):
return "🔴", f"{age_days} days"
def parse_visibility(visibility_input, repo_visibility):
"""Parse and validate visibility settings with security checks."""
valid = {"public", "private", "internal", "all"}
stripped = [v.strip() for v in visibility_input.lower().split(",") if v.strip()]
repo_visibility = (repo_visibility or "").lower()
# Warn about invalid values
if invalid := [v for v in stripped if v not in valid]:
print(f"⚠️ Invalid visibility values: {', '.join(invalid)} - ignoring")
visibility_list = [v for v in stripped if v in valid]
if not visibility_list:
print("⚠️ No valid visibility values, defaulting to 'public'")
return ["public"]
# Security: public repos can only scan public repos
if repo_visibility == "public" and visibility_list != ["public"]:
print("⚠️ Security: Public repo cannot scan non-public repos. Restricting to public only.")
return ["public"]
return visibility_list
def get_repo_filter(visibility_list):
"""Return filtering strategy for repo visibility."""
if len(visibility_list) == 1 and visibility_list[0] != "all":
return {"flag": ["--visibility", visibility_list[0]], "filter": None, "str": visibility_list[0]}
filter_set = {"public", "private", "internal"} if "all" in visibility_list else set(visibility_list)
return {
"flag": [],
"filter": filter_set,
"str": "all" if "all" in visibility_list else ", ".join(sorted(visibility_list)),
}
def get_status_checks(rollup):
"""Extract and validate status checks from rollup, return failed checks."""
checks = rollup if isinstance(rollup, list) else rollup.get("contexts", []) if isinstance(rollup, dict) else []
return [c for c in checks if c.get("conclusion") not in ["SUCCESS", "SKIPPED", "NEUTRAL"]]
def run():
"""List open PRs across organization and auto-merge eligible Dependabot PRs."""
# Get and validate settings
org = os.getenv("ORG", "ultralytics")
visibility = os.getenv("VISIBILITY", "public").lower()
repo_visibility = os.getenv("REPO_VISIBILITY", "public").lower()
valid_visibilities = {"public", "private", "internal", "all"}
visibility_list = parse_visibility(os.getenv("VISIBILITY", "public"), os.getenv("REPO_VISIBILITY", "public"))
filter_config = get_repo_filter(visibility_list)
if visibility not in valid_visibilities:
print(f"⚠️ Invalid visibility '{visibility}', defaulting to 'public'")
visibility = "public"
print(f"🔍 Scanning {filter_config['str']} repositories in {org} organization...")
# Security: if calling repo is public, restrict to public repos only
if repo_visibility == "public" and visibility != "public":
print(f"⚠️ Security: Public repo cannot scan {visibility} repos. Restricting to public only.")
visibility = "public"
print(f"🔍 Scanning {visibility} repositories in {org} organization...")
# Get active repos with specified visibility
cmd = ["gh", "repo", "list", org, "--limit", "1000", "--json", "name,url,isArchived"]
if visibility != "all":
cmd.extend(["--visibility", visibility])
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
repos = {r["name"]: r["url"] for r in json.loads(result.stdout) if not r["isArchived"]}
# Get active repos
result = subprocess.run(
["gh", "repo", "list", org, "--limit", "1000", "--json", "name,url,isArchived,visibility"]
+ filter_config["flag"],
capture_output=True,
text=True,
check=True,
)
all_repos = [r for r in json.loads(result.stdout) if not r["isArchived"]]
repos = {
r["name"]: r["url"]
for r in all_repos
if not filter_config["filter"] or r["visibility"].lower() in filter_config["filter"]
}
if not repos:
print("⚠️ No repositories found")
@@ -87,10 +125,8 @@ def run():
# Count PRs by phase
phase_counts = {"new": 0, "green": 0, "yellow": 0, "red": 0}
for pr in all_prs:
age_days = get_age_days(pr["createdAt"])
phase_counts[
"new" if age_days == 0 else "green" if age_days <= 7 else "yellow" if age_days <= 30 else "red"
] += 1
age = get_age_days(pr["createdAt"])
phase_counts["new" if age == 0 else "green" if age <= 7 else "yellow" if age <= 30 else "red"] += 1
repo_count = len({pr["repository"]["name"] for pr in all_prs if pr["repository"]["name"] in repos})
summary = [
@@ -99,10 +135,7 @@ def run():
f"**By Phase:** 🆕 {phase_counts['new']} New | 🟢 {phase_counts['green']} ≤7d | 🟡 {phase_counts['yellow']} ≤30d | 🔴 {phase_counts['red']} >30d\n",
]
for repo_name in sorted({pr["repository"]["name"] for pr in all_prs}):
if repo_name not in repos:
continue
for repo_name in sorted({pr["repository"]["name"] for pr in all_prs if pr["repository"]["name"] in repos}):
repo_prs = [pr for pr in all_prs if pr["repository"]["name"] == repo_name]
summary.append(
f"## 📦 [{repo_name}]({repos[repo_name]}) - {len(repo_prs)} open PR{'s' if len(repo_prs) > 1 else ''}"
@@ -161,18 +194,8 @@ def run():
total_skipped += 1
continue
# Check if all status checks passed (normalize rollup structure)
rollup = pr.get("statusCheckRollup")
if isinstance(rollup, list):
checks = rollup
elif isinstance(rollup, dict):
checks = rollup.get("contexts", [])
else:
checks = []
failed_checks = [c for c in checks if c.get("conclusion") not in ["SUCCESS", "SKIPPED", "NEUTRAL"]]
if failed_checks:
for check in failed_checks:
if failed := get_status_checks(pr.get("statusCheckRollup")):
for check in failed:
print(f" ❌ Failing check: {check.get('name', 'unknown')} = {check.get('conclusion')}")
total_skipped += 1
continue
@@ -195,7 +218,6 @@ def run():
summary.append(f"\n**Summary:** Found {total_found} | Merged {total_merged} | Skipped {total_skipped}")
print(f"\n📊 Dependabot Summary: Found {total_found} | Merged {total_merged} | Skipped {total_skipped}")
# Write to GitHub step summary if available
if summary_file := os.getenv("GITHUB_STEP_SUMMARY"):
with open(summary_file, "a") as f:
f.write("\n".join(summary))
+7 -7
View File
@@ -29,18 +29,18 @@ jobs:
with:
token: ${{ secrets.GITHUB_TOKEN }}
org: ultralytics # Optional: defaults to ultralytics
visibility: public # Optional: public, private, internal, or all
visibility: private,internal # Optional: public, private, internal, all, or comma-separated
```
## Inputs
| Input | Description | Required | Default |
| ------------ | ------------------------------------------------------------------------ | -------- | ------------- |
| `token` | GitHub token with admin permissions for merging | Yes | - |
| `org` | GitHub organization name | No | `ultralytics` |
| `visibility` | Repository visibility to scan: `public`, `private`, `internal`, or `all` | No | `public` |
| Input | Description | Required | Default |
| ------------ | ------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- |
| `token` | GitHub token with admin permissions for merging | Yes | - |
| `org` | GitHub organization name | No | `ultralytics` |
| `visibility` | Repository visibility to scan: `public`, `private`, `internal`, `all`, or comma-separated list (e.g., `private,internal`) | No | `public` |
**Security Note:** If the calling repository is public, scanning is automatically restricted to public repos only, even if a different visibility is specified.
**Security Note:** If the calling repository is public, scanning is automatically restricted to public repos only, even if non-public visibilities are specified.
## Auto-merge Criteria
+1 -1
View File
@@ -15,7 +15,7 @@ inputs:
required: false
default: "ultralytics"
visibility:
description: "Repository visibility to scan: public, private, internal, or all"
description: "Repository visibility to scan: public, private, internal, all, or comma-separated (e.g., 'private,internal')"
required: false
default: "public"
runs: