Scan PRs cleanup (#626)
Signed-off-by: Glenn Jocher <[email protected]> Co-authored-by: UltralyticsAssistant <[email protected]>
This commit is contained in:
co-authored by
UltralyticsAssistant
parent
28d7623445
commit
f458ecc7be
@@ -19,4 +19,4 @@ jobs:
|
||||
steps:
|
||||
- uses: ultralytics/actions/scan-prs@main
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
token: ${{ secrets._GITHUB_TOKEN }}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user