New scan PRs action (#623)
Signed-off-by: Glenn Jocher <[email protected]> Co-authored-by: UltralyticsAssistant <[email protected]>
This commit is contained in:
co-authored by
UltralyticsAssistant
parent
846c9c00ea
commit
8be7f6bb65
@@ -1,116 +0,0 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# List Open PRs across Ultralytics repositories
|
||||
# This workflow scans all public repos in the Ultralytics organization and lists open PRs
|
||||
# For repos with >10 open PRs, only the 10 most recent are shown
|
||||
|
||||
name: List Open PRs
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 3 * * *" # daily at 03:00 UTC
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
list-prs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: List open PRs
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
shell: python
|
||||
run: |
|
||||
import os, json, subprocess
|
||||
from datetime import datetime, timezone
|
||||
|
||||
# Get active public repos
|
||||
result = subprocess.run(
|
||||
["gh", "repo", "list", "ultralytics", "--limit", "1000", "--json", "name,url,isArchived", "--visibility", "public"],
|
||||
capture_output=True, text=True, check=True
|
||||
)
|
||||
repos = {r["name"]: r["url"] for r in json.loads(result.stdout) if not r["isArchived"]}
|
||||
|
||||
# Get all open PRs
|
||||
result = subprocess.run(
|
||||
["gh", "search", "prs", "--owner", "ultralytics", "--state", "open", "--limit", "1000",
|
||||
"--json", "repository,number,title,url,createdAt", "--sort", "created", "--order", "desc"],
|
||||
capture_output=True, text=True, check=True
|
||||
)
|
||||
all_prs = json.loads(result.stdout)
|
||||
|
||||
summary = ["# 🔍 Open Pull Requests - Ultralytics Organization\n"]
|
||||
|
||||
for repo_name in sorted(set(pr["repository"]["name"] for pr in all_prs)):
|
||||
if repo_name not in repos:
|
||||
continue
|
||||
|
||||
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 ''}")
|
||||
|
||||
for pr in repo_prs[:30]:
|
||||
age_days = (datetime.now(timezone.utc) - datetime.fromisoformat(pr["createdAt"].replace("Z", "+00:00"))).days
|
||||
if age_days == 0:
|
||||
emoji = "🆕"
|
||||
elif age_days <= 7:
|
||||
emoji = "🟢"
|
||||
elif age_days <= 30:
|
||||
emoji = "🟡"
|
||||
else:
|
||||
emoji = "🔴"
|
||||
|
||||
if age_days == 0:
|
||||
summary.append(f"- 🔀 [#{pr['number']}]({pr['url']}) {pr['title']} {emoji} NEW")
|
||||
else:
|
||||
summary.append(f"- 🔀 [#{pr['number']}]({pr['url']}) {pr['title']} {emoji} {age_days} days")
|
||||
|
||||
if len(repo_prs) > 30:
|
||||
summary.append(f"- ... {len(repo_prs) - 30} more PRs")
|
||||
summary.append("")
|
||||
|
||||
with open(os.environ["GITHUB_STEP_SUMMARY"], "a") as f:
|
||||
f.write("\n".join(summary))
|
||||
|
||||
- name: Merge Dependabot Actions PRs
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets._GITHUB_TOKEN }}
|
||||
shell: python
|
||||
run: |
|
||||
import os, json, subprocess
|
||||
|
||||
result = subprocess.run(
|
||||
["gh", "repo", "list", "ultralytics", "--limit", "1000", "--json", "name,isArchived", "--visibility", "public"],
|
||||
capture_output=True, text=True, check=True
|
||||
)
|
||||
repos = [r["name"] for r in json.loads(result.stdout) if not r["isArchived"]]
|
||||
|
||||
summary = ["\n# 🤖 Merging Dependabot GitHub Actions PRs\n"]
|
||||
|
||||
for repo in repos:
|
||||
result = subprocess.run(
|
||||
["gh", "pr", "list", "--repo", f"ultralytics/{repo}", "--author", "app/dependabot",
|
||||
"--state", "open", "--json", "number,files,mergeable"],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
if result.returncode != 0:
|
||||
continue
|
||||
|
||||
merged = 0
|
||||
for pr in json.loads(result.stdout):
|
||||
if merged >= 1 or pr["mergeable"] != "MERGEABLE":
|
||||
continue
|
||||
if all(f["path"].startswith(".github/workflows/") for f in pr["files"]):
|
||||
result = subprocess.run(
|
||||
["gh", "pr", "merge", str(pr["number"]), "--repo", f"ultralytics/{repo}",
|
||||
"--squash", "--admin"],
|
||||
capture_output=True
|
||||
)
|
||||
if result.returncode == 0:
|
||||
summary.append(f"- ✅ Merged ultralytics/{repo}#{pr['number']}")
|
||||
merged += 1
|
||||
|
||||
with open(os.environ["GITHUB_STEP_SUMMARY"], "a") as f:
|
||||
f.write("\n".join(summary))
|
||||
@@ -0,0 +1,22 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# List Open PRs across Ultralytics repositories and auto-merge eligible Dependabot PRs
|
||||
|
||||
name: Scan PRs
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 6 * * *" # daily at 06:00 UTC
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
scan-prs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: ultralytics/actions/scan-prs@main
|
||||
with:
|
||||
token: ${{ secrets._GITHUB_TOKEN }}
|
||||
@@ -1,110 +1,158 @@
|
||||
<a href="https://www.ultralytics.com/"><img src="https://raw.githubusercontent.com/ultralytics/assets/main/logo/Ultralytics_Logotype_Original.svg" width="320" alt="Ultralytics logo"></a>
|
||||
|
||||
# 🚀 Ultralytics Actions: AI-powered formatting, labeling & PR summaries for Python and Markdown
|
||||
# 🚀 Ultralytics Actions
|
||||
|
||||
Welcome to the [Ultralytics Actions](https://github.com/ultralytics/actions) repository, your go-to solution for maintaining consistent code quality across Ultralytics Python and Swift projects. This GitHub Action is designed to automate the formatting of Python, Markdown, and Swift files, ensuring adherence to our coding standards and enhancing project maintainability.
|
||||
Welcome to [Ultralytics Actions](https://github.com/ultralytics/actions) - a collection of GitHub Actions and Python tools for automating code quality, PR management, and CI/CD workflows across Ultralytics projects.
|
||||
|
||||
[](https://github.com/marketplace/actions/ultralytics-actions)
|
||||
|
||||
[](https://github.com/ultralytics/actions/actions/workflows/ci.yml)
|
||||
[](https://github.com/ultralytics/actions/actions/workflows/format.yml)
|
||||
[](https://github.com/ultralytics/actions/actions/workflows/open-prs.yml)
|
||||
[](https://github.com/ultralytics/actions/actions/workflows/scan-prs.yml)
|
||||
[](https://codecov.io/github/ultralytics/actions)
|
||||
|
||||
[](https://discord.com/invite/ultralytics)
|
||||
[](https://community.ultralytics.com/)
|
||||
[](https://reddit.com/r/ultralytics)
|
||||
|
||||
## 📄 Actions Description
|
||||
## 📦 Repository Contents
|
||||
|
||||
Ultralytics Actions automatically applies formats, updates, and enhancements using a suite of powerful tools:
|
||||
This repository provides three main components:
|
||||
|
||||
- **Python Code:** Formatted using [Ruff](https://github.com/astral-sh/ruff), an extremely fast Python linter and formatter.
|
||||
- **Markdown Files:** Styled with [Prettier](https://github.com/prettier/prettier) to ensure consistent documentation appearance.
|
||||
- **Docstrings:** Cleaned and standardized using [docformatter](https://github.com/PyCQA/docformatter).
|
||||
- **Swift Code:** Formatted with [`swift-format`](https://github.com/swiftlang/swift-format) to maintain a uniform coding style across Swift projects. _(Note: Requires the `macos-latest` runner.)_
|
||||
- **Spell Check:** Common misspellings are caught using [codespell](https://github.com/codespell-project/codespell).
|
||||
- **Broken Links Check:** Broken links in documentation and Markdown files are identified using [Lychee](https://github.com/lycheeverse/lychee).
|
||||
- **PR Summary:** Concise Pull Request summaries are generated using [OpenAI](https://openai.com/) GPT-5, improving clarity and review efficiency.
|
||||
- **PR Review:** AI-powered code reviews identify critical bugs, security issues, and code quality concerns with suggested fixes.
|
||||
- **Auto-labeling:** Applies relevant labels to issues and PRs via [OpenAI](https://openai.com/) GPT-5 for intelligent categorization.
|
||||
1. **[Ultralytics Actions](#ultralytics-actions-main-action)** - Main GitHub Action for AI-powered code formatting, PR summaries, and auto-labeling
|
||||
2. **[Standalone Actions](#standalone-actions)** - Reusable composite actions for common CI/CD tasks
|
||||
3. **[Python Package](#python-package)** - `ultralytics-actions` package for programmatic use
|
||||
|
||||
## 🛠️ How It Works
|
||||
## Ultralytics Actions (Main Action)
|
||||
|
||||
Ultralytics Actions triggers on various GitHub events to streamline workflows:
|
||||
AI-powered formatting, labeling, and PR summaries for Python, Swift, and Markdown files.
|
||||
|
||||
- **Push Events:** Automatically formats code when changes are pushed to the `main` branch.
|
||||
- **Pull Requests:**
|
||||
- Ensures contributions meet formatting standards before merging.
|
||||
- Generates a concise summary of changes using GPT-5.
|
||||
- Provides AI-powered inline code reviews with suggested fixes for critical issues.
|
||||
- Applies relevant labels using GPT-5 for intelligent categorization.
|
||||
- **Issues:** Automatically applies relevant labels using GPT-5 when new issues are created.
|
||||
### 📄 Features
|
||||
|
||||
These automated actions help maintain high code quality, improve documentation clarity, and streamline the review process by providing consistent formatting, informative summaries, and appropriate categorization.
|
||||
- **Python Code:** Formatted using [Ruff](https://github.com/astral-sh/ruff), an extremely fast Python linter and formatter
|
||||
- **Markdown Files:** Styled with [Prettier](https://github.com/prettier/prettier) to ensure consistent documentation appearance
|
||||
- **Docstrings:** Cleaned and standardized using [docformatter](https://github.com/PyCQA/docformatter)
|
||||
- **Swift Code:** Formatted with [`swift-format`](https://github.com/swiftlang/swift-format) _(requires `macos-latest` runner)_
|
||||
- **Spell Check:** Common misspellings caught using [codespell](https://github.com/codespell-project/codespell)
|
||||
- **Broken Links Check:** Broken links identified using [Lychee](https://github.com/lycheeverse/lychee)
|
||||
- **PR Summary:** Concise Pull Request summaries generated using [OpenAI](https://openai.com/) GPT-5
|
||||
- **PR Review:** AI-powered code reviews identify critical bugs, security issues, and quality concerns with suggested fixes
|
||||
- **Auto-labeling:** Applies relevant labels to issues and PRs via [OpenAI](https://openai.com/) GPT-5
|
||||
|
||||
## 🔧 Setting Up the Action
|
||||
### 🛠️ How It Works
|
||||
|
||||
To integrate this action into your Ultralytics repository:
|
||||
Triggers on GitHub events to streamline workflows:
|
||||
|
||||
1. **Create a Workflow File:** In your repository, create a YAML file under `.github/workflows/`, for example, `ultralytics-actions.yml`.
|
||||
- **Push Events:** Automatically formats code when changes are pushed to `main`
|
||||
- **Pull Requests:** Ensures formatting standards, generates summaries, provides AI reviews, and applies labels
|
||||
- **Issues:** Automatically applies relevant labels using GPT-5
|
||||
|
||||
2. **Add the Action:** Configure the Ultralytics Actions in your workflow file as shown below:
|
||||
### 🔧 Setup
|
||||
|
||||
```yaml
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
Create `.github/workflows/ultralytics-actions.yml`:
|
||||
|
||||
# Ultralytics Actions https://github.com/ultralytics/actions
|
||||
# This workflow formats code and documentation in PRs to Ultralytics standards
|
||||
```yaml
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
name: Ultralytics Actions
|
||||
# Ultralytics Actions https://github.com/ultralytics/actions
|
||||
# This workflow formats code and documentation in PRs to Ultralytics standards
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types: [opened, closed, synchronize, review_requested]
|
||||
name: Ultralytics Actions
|
||||
|
||||
permissions:
|
||||
contents: write # Modify code in PRs
|
||||
pull-requests: write # Add comments and labels to PRs
|
||||
issues: write # Add comments and labels to issues
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types: [opened, closed, synchronize, review_requested]
|
||||
|
||||
jobs:
|
||||
actions:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Run Ultralytics Actions
|
||||
uses: ultralytics/actions@main
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }} # Auto-generated token
|
||||
labels: true # Auto-label issues/PRs using AI
|
||||
python: true # Format Python with Ruff and docformatter
|
||||
prettier: true # Format YAML, JSON, Markdown, CSS
|
||||
swift: false # Format Swift (requires macos-latest)
|
||||
dart: false # Format Dart/Flutter
|
||||
spelling: true # Check spelling with codespell
|
||||
links: true # Check broken links with Lychee
|
||||
summary: true # Generate AI-powered PR summaries
|
||||
openai_api_key: ${{ secrets.OPENAI_API_KEY }} # Powers PR summaries, labels and comments
|
||||
brave_api_key: ${{ secrets.BRAVE_API_KEY }} # Used for broken link resolution
|
||||
```
|
||||
permissions:
|
||||
contents: write # Modify code in PRs
|
||||
pull-requests: write # Add comments and labels to PRs
|
||||
issues: write # Add comments and labels to issues
|
||||
|
||||
3. **Customize:** Adjust the `runs-on` runner and the boolean flags (`labels`, `python`, `prettier`, `swift`, `spelling`, `links`, `summary`) based on your project's needs. Remember to add your `OPENAI_API_KEY` as a secret in your repository settings if you enable `labels` or `summary`.
|
||||
jobs:
|
||||
actions:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Run Ultralytics Actions
|
||||
uses: ultralytics/actions@main
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }} # Auto-generated token
|
||||
labels: true # Auto-label issues/PRs using AI
|
||||
python: true # Format Python with Ruff and docformatter
|
||||
prettier: true # Format YAML, JSON, Markdown, CSS
|
||||
swift: false # Format Swift (requires macos-latest)
|
||||
dart: false # Format Dart/Flutter
|
||||
spelling: true # Check spelling with codespell
|
||||
links: true # Check broken links with Lychee
|
||||
summary: true # Generate AI-powered PR summaries
|
||||
openai_api_key: ${{ secrets.OPENAI_API_KEY }} # Powers PR summaries, labels and reviews
|
||||
brave_api_key: ${{ secrets.BRAVE_API_KEY }} # Used for broken link resolution
|
||||
```
|
||||
|
||||
## Standalone Actions
|
||||
|
||||
Reusable composite actions for common CI/CD tasks. Each can be used independently in your workflows.
|
||||
|
||||
### 1. Retry Action
|
||||
|
||||
Retry failed commands with exponential backoff.
|
||||
|
||||
```yaml
|
||||
- uses: ultralytics/actions/retry@main
|
||||
with:
|
||||
command: npm install
|
||||
max_attempts: 3
|
||||
timeout_minutes: 5
|
||||
```
|
||||
|
||||
[**📖 Full Documentation →**](retry/README.md)
|
||||
|
||||
### 2. Cleanup Disk Action
|
||||
|
||||
Free up disk space on GitHub runners by removing unnecessary packages and files.
|
||||
|
||||
```yaml
|
||||
- uses: ultralytics/actions/cleanup-disk@main
|
||||
```
|
||||
|
||||
[**📖 Full Documentation →**](cleanup-disk/README.md)
|
||||
|
||||
### 3. Scan PRs Action
|
||||
|
||||
List open PRs across an organization and auto-merge eligible Dependabot PRs.
|
||||
|
||||
```yaml
|
||||
- uses: ultralytics/actions/scan-prs@main
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
org: ultralytics # Optional: defaults to ultralytics
|
||||
visibility: public # Optional: public, private, internal, or all
|
||||
```
|
||||
|
||||
[**📖 Full Documentation →**](scan-prs/README.md)
|
||||
|
||||
## Python Package
|
||||
|
||||
Install the `ultralytics-actions` Python package directly with Pip:
|
||||
Install `ultralytics-actions` for programmatic access to action utilities.
|
||||
|
||||
[](https://pypi.org/project/ultralytics-actions/)
|
||||
[](https://clickpy.clickhouse.com/dashboard/ultralytics-actions)
|
||||
[](https://pypi.org/project/ultralytics-actions/)
|
||||
|
||||
```sh
|
||||
```bash
|
||||
pip install ultralytics-actions
|
||||
```
|
||||
|
||||
**Available Modules:**
|
||||
|
||||
- `actions.review_pr` - AI-powered PR review
|
||||
- `actions.summarize_pr` - Generate PR summaries
|
||||
- `actions.scan_prs` - Scan and manage organization PRs
|
||||
- `actions.first_interaction` - Welcome message for new contributors
|
||||
- And more in `actions/` directory
|
||||
|
||||
## 💡 Contribute
|
||||
|
||||
Ultralytics thrives on community collaboration, and we deeply value your contributions! Please see our [Contributing Guide](https://docs.ultralytics.com/help/contributing/) for details on how you can get involved. We also encourage you to share your feedback through our [Survey](https://www.ultralytics.com/survey?utm_source=github&utm_medium=social&utm_campaign=Survey). A huge thank you 🙏 to all our contributors!
|
||||
|
||||
+4
-1
@@ -12,10 +12,13 @@
|
||||
# │ │ ├── github_utils.py
|
||||
# │ │ ├── openai_utils.py
|
||||
# │ │ └── common_utils.py
|
||||
# │ ├── dispatch_actions.py
|
||||
# │ ├── first_interaction.py
|
||||
# │ ├── review_pr.py
|
||||
# │ ├── scan_prs.py
|
||||
# │ ├── summarize_pr.py
|
||||
# │ ├── summarize_release.py
|
||||
# │ ├── update_file_headers.py
|
||||
# │ └── update_markdown_code_blocks.py
|
||||
# └── tests/
|
||||
# ├── __init__.py
|
||||
@@ -23,4 +26,4 @@
|
||||
# ├── test_summarize_pr.py
|
||||
# └── ...
|
||||
|
||||
__version__ = "0.2.0"
|
||||
__version__ = "0.2.1"
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
"""List and auto-merge open PRs across GitHub organization."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
from datetime import datetime, timezone
|
||||
|
||||
|
||||
def get_age_days(created_at):
|
||||
"""Calculate PR age in days from ISO timestamp."""
|
||||
return (datetime.now(timezone.utc) - datetime.fromisoformat(created_at.replace("Z", "+00:00"))).days
|
||||
|
||||
|
||||
def get_phase_emoji(age_days):
|
||||
"""Return emoji and label for PR age phase."""
|
||||
if age_days == 0:
|
||||
return "🆕", "NEW"
|
||||
elif age_days <= 7:
|
||||
return "🟢", f"{age_days} days"
|
||||
elif age_days <= 30:
|
||||
return "🟡", f"{age_days} days"
|
||||
else:
|
||||
return "🔴", f"{age_days} days"
|
||||
|
||||
|
||||
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"}
|
||||
|
||||
if visibility not in valid_visibilities:
|
||||
print(f"⚠️ Invalid visibility '{visibility}', defaulting to 'public'")
|
||||
visibility = "public"
|
||||
|
||||
# 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"]}
|
||||
|
||||
if not repos:
|
||||
print("⚠️ No repositories found")
|
||||
return
|
||||
|
||||
# Get all open PRs
|
||||
result = subprocess.run(
|
||||
[
|
||||
"gh",
|
||||
"search",
|
||||
"prs",
|
||||
"--owner",
|
||||
org,
|
||||
"--state",
|
||||
"open",
|
||||
"--limit",
|
||||
"1000",
|
||||
"--json",
|
||||
"repository,number,title,url,createdAt",
|
||||
"--sort",
|
||||
"created",
|
||||
"--order",
|
||||
"desc",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
all_prs = json.loads(result.stdout)
|
||||
|
||||
if not all_prs:
|
||||
print("✅ No open PRs found")
|
||||
return
|
||||
|
||||
# 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
|
||||
|
||||
repo_count = len({pr["repository"]["name"] for pr in all_prs if pr["repository"]["name"] in repos})
|
||||
summary = [
|
||||
f"# 🔍 Open Pull Requests - {org.title()} Organization\n",
|
||||
f"**Total:** {len(all_prs)} open PRs across {repo_count} repos",
|
||||
f"**By Phase:** 🆕 {phase_counts['new']} New | 🟢 {phase_counts['green']} Green (≤7d) | 🟡 {phase_counts['yellow']} Yellow (≤30d) | 🔴 {phase_counts['red']} Red (>30d)\n",
|
||||
]
|
||||
|
||||
for repo_name in sorted({pr["repository"]["name"] for pr in all_prs}):
|
||||
if repo_name not in repos:
|
||||
continue
|
||||
|
||||
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 ''}"
|
||||
)
|
||||
|
||||
for pr in repo_prs[:30]:
|
||||
emoji, age_str = get_phase_emoji(get_age_days(pr["createdAt"]))
|
||||
summary.append(f"- 🔀 [#{pr['number']}]({pr['url']}) {pr['title']} {emoji} {age_str}")
|
||||
|
||||
if len(repo_prs) > 30:
|
||||
summary.append(f"- ... {len(repo_prs) - 30} more PRs")
|
||||
summary.append("")
|
||||
|
||||
# Auto-merge Dependabot GitHub Actions PRs
|
||||
print("\n🤖 Checking for Dependabot PRs to auto-merge...")
|
||||
summary.append("\n# 🤖 Auto-Merge Dependabot GitHub Actions PRs\n")
|
||||
total_found = total_merged = total_skipped = 0
|
||||
|
||||
for repo_name in repos:
|
||||
result = subprocess.run(
|
||||
[
|
||||
"gh",
|
||||
"pr",
|
||||
"list",
|
||||
"--repo",
|
||||
f"{org}/{repo_name}",
|
||||
"--author",
|
||||
"app/dependabot",
|
||||
"--state",
|
||||
"open",
|
||||
"--json",
|
||||
"number,title,files,mergeable,statusCheckRollup",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
continue
|
||||
|
||||
merged = 0
|
||||
for pr in json.loads(result.stdout):
|
||||
if not all(f["path"].startswith(".github/workflows/") for f in pr["files"]):
|
||||
continue
|
||||
|
||||
total_found += 1
|
||||
pr_ref = f"{org}/{repo_name}#{pr['number']}"
|
||||
print(f" Found: {pr_ref} - {pr['title']}")
|
||||
|
||||
if merged >= 1:
|
||||
print(f" ⏭️ Skipped (already merged 1 PR in {repo_name})")
|
||||
total_skipped += 1
|
||||
continue
|
||||
|
||||
if pr["mergeable"] != "MERGEABLE":
|
||||
print(f" ❌ Skipped (not mergeable: {pr['mergeable']})")
|
||||
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:
|
||||
print(f" ❌ Failing check: {check.get('name', 'unknown')} = {check.get('conclusion')}")
|
||||
total_skipped += 1
|
||||
continue
|
||||
|
||||
print(" ✅ All checks passed, merging...")
|
||||
result = subprocess.run(
|
||||
["gh", "pr", "merge", str(pr["number"]), "--repo", f"{org}/{repo_name}", "--squash", "--admin"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
print(f" ✅ Successfully merged {pr_ref}")
|
||||
summary.append(f"- ✅ Merged {pr_ref}")
|
||||
total_merged += 1
|
||||
merged += 1
|
||||
else:
|
||||
print(f" ❌ Merge failed: {result.stderr.strip()}")
|
||||
total_skipped += 1
|
||||
|
||||
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))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
@@ -0,0 +1,61 @@
|
||||
# Scan PRs Action
|
||||
|
||||
List open PRs across an organization and auto-merge eligible Dependabot PRs.
|
||||
|
||||
## Features
|
||||
|
||||
- **PR Overview**: Lists all open PRs with age-based categorization (New, Green ≤7d, Yellow ≤30d, Red >30d)
|
||||
- **Auto-merge**: Automatically merges Dependabot PRs that update GitHub Actions workflows when all checks pass
|
||||
- **Console Logging**: Detailed output showing which PRs were found, merged, or skipped with reasons
|
||||
|
||||
## Usage
|
||||
|
||||
```yaml
|
||||
name: Scan PRs
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 3 * * *" # daily at 03:00 UTC
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
scan-prs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: ultralytics/actions/scan-prs@main
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
org: ultralytics # Optional: defaults to ultralytics
|
||||
visibility: public # Optional: public, private, internal, or all
|
||||
```
|
||||
|
||||
## 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` |
|
||||
|
||||
**Security Note:** If the calling repository is public, scanning is automatically restricted to public repos only, even if a different visibility is specified.
|
||||
|
||||
## Auto-merge Criteria
|
||||
|
||||
Dependabot PRs are automatically merged if they meet ALL criteria:
|
||||
|
||||
1. Update files in `.github/workflows/` only
|
||||
2. PR is mergeable (no conflicts)
|
||||
3. All status checks passed (SUCCESS, SKIPPED, or NEUTRAL)
|
||||
4. Maximum 1 PR merged per repository per run
|
||||
|
||||
## Output
|
||||
|
||||
The action generates a GitHub step summary with:
|
||||
|
||||
- Total PR count across all repos
|
||||
- PR breakdown by phase (New, Green, Yellow, Red)
|
||||
- Detailed list of PRs per repository
|
||||
- Summary of Dependabot PRs found, merged, and skipped
|
||||
@@ -0,0 +1,56 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
name: "Scan PRs"
|
||||
author: "Ultralytics"
|
||||
description: "List open PRs and auto-merge eligible Dependabot PRs across organization"
|
||||
branding:
|
||||
icon: "git-pull-request"
|
||||
color: "blue"
|
||||
inputs:
|
||||
token:
|
||||
description: "GitHub token with admin permissions for merging"
|
||||
required: true
|
||||
org:
|
||||
description: "GitHub organization name"
|
||||
required: false
|
||||
default: "ultralytics"
|
||||
visibility:
|
||||
description: "Repository visibility to scan: public, private, internal, or all"
|
||||
required: false
|
||||
default: "public"
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
ignore-empty-workdir: true
|
||||
enable-cache: false
|
||||
version: "0.9.4"
|
||||
|
||||
- name: Install ultralytics-actions
|
||||
env:
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
GITHUB_SHA: ${{ github.sha }}
|
||||
run: |
|
||||
# Install from git commit if testing in ultralytics/actions repo, otherwise from main
|
||||
if [ "$GITHUB_REPOSITORY" = "ultralytics/actions" ]; then
|
||||
echo "Installing from commit: $GITHUB_SHA"
|
||||
packages="git+https://github.com/ultralytics/actions@${GITHUB_SHA}"
|
||||
else
|
||||
packages="git+https://github.com/ultralytics/actions@main"
|
||||
fi
|
||||
if [ "$(uname)" = "Darwin" ]; then
|
||||
uv pip install --system --break-system-packages $packages
|
||||
else
|
||||
sudo env "PATH=$PATH" uv pip install --system --break-system-packages $packages
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Scan PRs
|
||||
env:
|
||||
GH_TOKEN: ${{ inputs.token }}
|
||||
ORG: ${{ inputs.org }}
|
||||
VISIBILITY: ${{ inputs.visibility }}
|
||||
REPO_VISIBILITY: ${{ github.event.repository.visibility }}
|
||||
run: python -m actions.scan_prs
|
||||
shell: bash
|
||||
Reference in New Issue
Block a user