Unify GitHub report repo headings (#779)

Co-authored-by: UltralyticsAssistant <[email protected]>
This commit is contained in:
Glenn Jocher
2026-06-30 17:11:28 +02:00
committed by GitHub
co-authored by UltralyticsAssistant
parent c4961b4dd0
commit d55a0546a5
13 changed files with 120 additions and 84 deletions
+1 -1
View File
@@ -31,7 +31,7 @@ body:
- "Ultralytics Actions workflow (ultralytics/actions@main)"
- "Retry Action"
- "Cleanup Disk Action"
- "Scan PRs Action"
- "GitHub Report Action"
- "Dependabot Action"
- "Python package (`ultralytics-actions`)"
- "Repository documentation / README"
+1 -1
View File
@@ -31,7 +31,7 @@ body:
- "Ultralytics Actions workflow (ultralytics/actions@main)"
- "Retry Action"
- "Cleanup Disk Action"
- "Scan PRs Action"
- "GitHub Report Action"
- "Dependabot Action"
- "Python package (`ultralytics-actions`)"
- "Repository documentation / README"
+1 -1
View File
@@ -29,7 +29,7 @@ body:
- "Ultralytics Actions workflow (ultralytics/actions@main)"
- "Retry Action"
- "Cleanup Disk Action"
- "Scan PRs Action"
- "GitHub Report Action"
- "Dependabot Action"
- "Python package (`ultralytics-actions`)"
- "Repository documentation / README"
+2 -2
View File
@@ -1,6 +1,6 @@
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
# Summarize open PRs and failed scheduled GitHub Actions across public Ultralytics repositories
# Summarize open PRs and failed default-branch GitHub Actions across public Ultralytics repositories
name: GitHub Report
@@ -8,7 +8,7 @@ on:
workflow_dispatch:
inputs:
days:
description: "Days of scheduled workflow history to include"
description: "Days of workflow history to include"
required: false
type: number
default: 1
+4 -17
View File
@@ -144,23 +144,9 @@ Free up disk space on GitHub runners by removing unnecessary packages and files.
[**📖 Full Documentation →**](cleanup-disk/README.md)
### 3. Scan PRs Action
### 3. GitHub Report Action
List open PRs across an organization and auto-merge eligible GitHub Actions update PRs.
```yaml
- uses: ultralytics/actions/scan-prs@main
with:
token: ${{ secrets.GITHUB_TOKEN }}
org: ultralytics # Optional: defaults to ultralytics
visibility: private,internal # Optional: public, private, internal, all, or comma-separated
```
[**📖 Full Documentation →**](scan-prs/README.md)
### 4. GitHub Report Action
Summarize open PRs and failed scheduled GitHub Actions across organization repositories.
Summarize open PRs and failed default-branch GitHub Actions across organization repositories.
```yaml
- uses: ultralytics/actions/github-report@main
@@ -172,7 +158,7 @@ Summarize open PRs and failed scheduled GitHub Actions across organization repos
[**📖 Full Documentation →**](github-report/README.md)
### 5. Dependabot Action
### 4. Dependabot Action
Update GitHub Actions versions across organization repositories with cached release resolution.
@@ -198,6 +184,7 @@ pip install ultralytics-actions
- `actions.review_pr` - AI-powered PR review
- `actions.summarize_pr` - Generate PR summaries
- `actions.github_report` - Generate GitHub organization reports
- `actions.scan_prs` - Scan and manage organization PRs
- `actions.first_interaction` - Welcome message for new contributors
- And more in `actions/` directory
+4 -17
View File
@@ -144,23 +144,9 @@ jobs:
[**📖 完整文档 →**](cleanup-disk/README.md)
### 3. Scan PRs Action
### 3. GitHub Report Action
列出组织中的开放 PR,并自动合并符合条件的 GitHub Actions 更新 PR。
```yaml
- uses: ultralytics/actions/scan-prs@main
with:
token: ${{ secrets.GITHUB_TOKEN }}
org: ultralytics # Optional: defaults to ultralytics
visibility: private,internal # Optional: public, private, internal, all, or comma-separated
```
[**📖 完整文档 →**](scan-prs/README.md)
### 4. GitHub Report Action
汇总组织仓库中的开放 PR 和失败的定时 GitHub Actions。
汇总组织仓库中的开放 PR 和默认分支上失败的 GitHub Actions。
```yaml
- uses: ultralytics/actions/github-report@main
@@ -172,7 +158,7 @@ jobs:
[**📖 完整文档 →**](github-report/README.md)
### 5. Dependabot Action
### 4. Dependabot Action
使用缓存的 release 解析,批量更新组织仓库中的 GitHub Actions 版本。
@@ -198,6 +184,7 @@ pip install ultralytics-actions
- `actions.review_pr` - AI 驱动的 PR review
- `actions.summarize_pr` - 生成 PR 摘要
- `actions.github_report` - 生成 GitHub 组织报告
- `actions.scan_prs` - 扫描并管理组织 PRs
- `actions.first_interaction` - 为新贡献者生成欢迎消息
- 更多模块见 `actions/` 目录
+37 -16
View File
@@ -1,5 +1,5 @@
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
"""Report failed scheduled GitHub Actions on default branches across an organization."""
"""Report failed GitHub Actions on default branches across an organization."""
from __future__ import annotations
@@ -9,16 +9,16 @@ import time
import urllib.error
import urllib.parse
import urllib.request
from collections import defaultdict
from collections import Counter, defaultdict
from datetime import datetime, timedelta, timezone
from actions.scan_prs import get_repo_filter, parse_visibility
from actions.scan_prs import format_repo_heading, get_repo_filter, parse_visibility
FAILED_CONCLUSIONS = {"failure", "timed_out", "action_required", "startup_failure", "cancelled"}
def run_time(run):
"""Return the scheduled run timestamp used for recency filtering and latest-run selection."""
"""Return the run timestamp used for recency filtering and latest-run selection."""
value = run.get("run_started_at") or run.get("created_at") or ""
return datetime.fromisoformat(value.replace("Z", "+00:00")) if value else datetime.min.replace(tzinfo=timezone.utc)
@@ -67,10 +67,10 @@ def paginate(path, params=None, key=None, max_pages=100, token=None, allow_skip=
return items
def collect_failed_scheduled_actions(
def collect_failed_actions(
org="ultralytics", visibility="public", repo_visibility="public", max_run_pages=3, days=1, token=None
):
"""Collect latest failed scheduled workflow runs on each non-archived repo's default branch."""
"""Collect latest failed workflow runs on each non-archived repo's default branch."""
cutoff = datetime.now(timezone.utc) - timedelta(days=days) if days else None
filter_config = get_repo_filter(parse_visibility(visibility, repo_visibility))
repos = [
@@ -84,7 +84,7 @@ def collect_failed_scheduled_actions(
branch = repo.get("default_branch") or "main"
runs = paginate(
f"/repos/{full_name}/actions/runs",
{"event": "schedule", "branch": branch},
{"branch": branch},
key="workflow_runs",
max_pages=max_run_pages,
token=token,
@@ -102,8 +102,10 @@ def collect_failed_scheduled_actions(
failures.append(
{
"repo": full_name,
"repo_url": repo.get("html_url") or f"https://github.com/{full_name}",
"visibility": repo.get("visibility", "private" if repo.get("private") else "public"),
"workflow": run.get("name") or run.get("display_title") or "Workflow",
"event": run.get("event") or "unknown",
"branch": branch,
"run_number": run.get("run_number"),
"failed_at": run.get("updated_at") or run.get("created_at") or "",
@@ -116,44 +118,63 @@ def collect_failed_scheduled_actions(
def format_report(failures, org="ultralytics"):
"""Format failed scheduled workflow runs as Markdown."""
"""Format failed workflow runs as Markdown."""
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
lines = [
"# Failed Scheduled Actions",
"# Failed Default Branch Actions",
"",
f"Failed scheduled workflow runs on default branches for `{org}` repositories as of {now}.",
f"Failed workflow runs on default branches for `{org}` repositories as of {now}.",
"",
]
if not failures:
lines.append("No failed scheduled workflow runs found.")
lines.append("No failed default-branch workflow runs found.")
return "\n".join(lines) + "\n"
repo_count = len({failure["repo"] for failure in failures})
lines.append(f"**{len(failures)} failing scheduled workflow runs** across **{repo_count} repositories**.")
run_word = "run" if len(failures) == 1 else "runs"
repo_word = "repository" if repo_count == 1 else "repositories"
event_counts = Counter(failure.get("event") or "unknown" for failure in failures)
lines.extend(
[
f"**{len(failures)} failing default-branch workflow {run_word}** across **{repo_count} {repo_word}**.",
"**By Event:** " + " | ".join(f"`{event}` {event_counts[event]}" for event in sorted(event_counts)),
]
)
grouped = defaultdict(list)
for failure in failures:
grouped[failure["repo"]].append(failure)
for repo, repo_failures in grouped.items():
lines.extend(["", f"## `{repo}` ({repo_failures[0]['visibility']})"])
count = len(repo_failures)
lines.extend(
[
"",
format_repo_heading(
repo,
repo_failures[0].get("repo_url") or f"https://github.com/{repo}",
f"{count} failed run{'s' if count != 1 else ''}",
),
]
)
for failure in repo_failures:
failed_at = failure["failed_at"].replace("T", " ").replace("Z", " UTC")
details = f" @ `{failure['sha']}`" if failure["sha"] else ""
details += f" - {failure['title']}" if failure["title"] else ""
lines.append(
f"- **{failure['workflow']}** on `{failure['branch']}` failed at {failed_at}{details}. "
f"- **{failure['workflow']}** (`{failure.get('event') or 'unknown'}`) on `{failure['branch']}` "
f"failed at {failed_at}{details}. "
f"[Run #{failure['run_number']}]({failure['url']})"
)
return "\n".join(lines) + "\n"
def run():
"""Write failed scheduled Actions report to stdout and the GitHub step summary."""
"""Write failed Actions report to stdout and the GitHub step summary."""
org = os.getenv("ORG", "ultralytics")
max_run_pages = int(os.getenv("MAX_RUN_PAGES", "3"))
days = int(os.getenv("REPORT_DAYS", "1"))
report = format_report(
collect_failed_scheduled_actions(
collect_failed_actions(
org,
visibility=os.getenv("VISIBILITY", "public"),
repo_visibility=os.getenv("REPO_VISIBILITY", "public"),
+6 -1
View File
@@ -9,13 +9,18 @@ def enabled(value):
return str(value).lower() == "true"
def enabled_any(*values, default="true"):
"""Return the first explicitly set boolean value, otherwise the default."""
return enabled(next((value for value in values if value not in {None, ""}), default))
def run():
"""Run enabled GitHub report sections."""
import os
if enabled(os.getenv("REPORT_PRS", "true")):
scan_prs.run()
if enabled(os.getenv("REPORT_FAILED_SCHEDULED_ACTIONS", "true")):
if enabled_any(os.getenv("REPORT_FAILED_ACTIONS"), os.getenv("REPORT_FAILED_SCHEDULED_ACTIONS")):
failed_scheduled_actions.run()
+8 -1
View File
@@ -60,6 +60,11 @@ def get_repo_filter(visibility_list):
}
def format_repo_heading(repo_name, repo_url, detail):
"""Format a repository section heading for GitHub report Markdown."""
return f"## 📦 [{repo_name}]({repo_url}) - {detail}"
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 []
@@ -147,7 +152,9 @@ def run():
for repo_name in sorted({pr["repository"]["name"] for pr in all_prs}):
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 ''}"
format_repo_heading(
repo_name, repos[repo_name], f"{len(repo_prs)} open PR{'s' if len(repo_prs) > 1 else ''}"
)
)
for pr in repo_prs[:30]:
+6 -5
View File
@@ -1,6 +1,6 @@
# GitHub Report Action
Summarize open pull requests and failed scheduled GitHub Actions across organization repositories.
Summarize open pull requests and failed default-branch GitHub Actions across organization repositories.
## Usage
@@ -35,10 +35,11 @@ jobs:
| `org` | GitHub organization name | No | `ultralytics` |
| `visibility` | Repository visibility for PR scanning: `public`, `private`, `internal`, `all` | No | `public` |
| `prs` | Include open PR summary | No | `true` |
| `failed_scheduled_actions` | Include failed scheduled Actions summary | No | `true` |
| `failed_actions` | Include failed default-branch Actions summary | No | `true` |
| `failed_scheduled_actions` | Deprecated alias for `failed_actions` | No | - |
| `auto_merge_actions_prs` | Auto-merge eligible GitHub Actions update PRs in the PR section | No | `false` |
| `max_run_pages` | Pages of scheduled workflow runs to inspect per repository | No | `3` |
| `days` | Days of scheduled workflow history to include | No | `1` |
| `max_run_pages` | Pages of workflow runs to inspect per repository | No | `3` |
| `days` | Days of workflow history to include | No | `1` |
## Output
@@ -46,4 +47,4 @@ The action writes a GitHub step summary with:
- Open PR totals and age buckets
- Eligible GitHub Actions update PR merge results
- Latest failed scheduled workflow runs on each repository's default branch
- Latest failed workflow runs on each repository's default branch, grouped by event
+11 -6
View File
@@ -2,7 +2,7 @@
name: "GitHub Report"
author: "Ultralytics"
description: "Summarize open PRs and failed scheduled GitHub Actions across organization repositories"
description: "Summarize open PRs and failed default-branch GitHub Actions across organization repositories"
branding:
icon: "activity"
color: "blue"
@@ -22,20 +22,24 @@ inputs:
description: "Include open PR summary"
required: false
default: "true"
failed_scheduled_actions:
description: "Include failed scheduled Actions summary"
failed_actions:
description: "Include failed default-branch Actions summary"
required: false
default: "true"
default: ""
failed_scheduled_actions:
description: "Deprecated alias for failed_actions"
required: false
default: ""
auto_merge_actions_prs:
description: "Auto-merge eligible GitHub Actions update PRs in the PR section"
required: false
default: "false"
max_run_pages:
description: "Pages of scheduled workflow runs to inspect per repository"
description: "Pages of workflow runs to inspect per repository"
required: false
default: "3"
days:
description: "Days of scheduled workflow history to include"
description: "Days of workflow history to include"
required: false
default: "1"
runs:
@@ -74,6 +78,7 @@ runs:
VISIBILITY: ${{ inputs.visibility }}
REPO_VISIBILITY: ${{ github.event.repository.visibility }}
REPORT_PRS: ${{ inputs.prs }}
REPORT_FAILED_ACTIONS: ${{ inputs.failed_actions }}
REPORT_FAILED_SCHEDULED_ACTIONS: ${{ inputs.failed_scheduled_actions }}
AUTO_MERGE_ACTIONS_PRS: ${{ inputs.auto_merge_actions_prs }}
MAX_RUN_PAGES: ${{ inputs.max_run_pages }}
+1
View File
@@ -92,6 +92,7 @@ ultralytics-actions-update-markdown-code-blocks = "actions.update_markdown_code_
ultralytics-actions-headers = "actions.update_file_headers:main"
ultralytics-actions-format-python-docstrings = "actions.format_python_docstrings:main"
ultralytics-actions-dependabot = "actions.dependabot:run"
ultralytics-actions-failed-actions = "actions.failed_scheduled_actions:run"
ultralytics-actions-failed-scheduled-actions = "actions.failed_scheduled_actions:run"
ultralytics-actions-github-report = "actions.github_report:run"
ultralytics-actions-info = "actions.utils:ultralytics_actions_info"
+38 -16
View File
@@ -25,14 +25,15 @@ def test_paginate_only_skips_http_errors_when_allowed(monkeypatch):
raise AssertionError("Expected org listing failure to raise")
def test_collect_failed_scheduled_actions_latest_run_per_workflow(monkeypatch):
"""Only the latest scheduled run for each workflow should determine whether it is reported."""
def test_collect_failed_actions_latest_run_per_workflow(monkeypatch):
"""Only the latest default-branch run for each workflow should determine whether it is reported."""
def fake_paginate(path, params=None, key=None, max_pages=100, token=None, allow_skip=False):
if path == "/orgs/ultralytics/repos":
return [
{
"full_name": "ultralytics/private-repo",
"html_url": "https://github.com/ultralytics/private-repo",
"default_branch": "main",
"visibility": "private",
"archived": False,
@@ -61,6 +62,7 @@ def test_collect_failed_scheduled_actions_latest_run_per_workflow(monkeypatch):
{
"workflow_id": 2,
"name": "Links",
"event": "push",
"conclusion": "timed_out",
"run_started_at": "2026-06-30T04:00:00Z",
"run_number": 42,
@@ -76,18 +78,19 @@ def test_collect_failed_scheduled_actions_latest_run_per_workflow(monkeypatch):
monkeypatch.setattr(failed_scheduled_actions, "paginate", fake_paginate)
failures = failed_scheduled_actions.collect_failed_scheduled_actions(
failures = failed_scheduled_actions.collect_failed_actions(
visibility="all", repo_visibility="private", token="token"
)
assert len(failures) == 1
assert failures[0]["repo"] == "ultralytics/private-repo"
assert failures[0]["workflow"] == "Links"
assert failures[0]["event"] == "push"
assert failures[0]["sha"] == "abcdef1"
def test_collect_failed_scheduled_actions_ignores_latest_success(monkeypatch):
"""Older failed scheduled runs should not be reported after a newer success."""
def test_collect_failed_actions_ignores_latest_success(monkeypatch):
"""Older failed runs should not be reported after a newer success."""
def fake_paginate(path, params=None, key=None, max_pages=100, token=None, allow_skip=False):
if path == "/orgs/ultralytics/repos":
@@ -118,11 +121,11 @@ def test_collect_failed_scheduled_actions_ignores_latest_success(monkeypatch):
monkeypatch.setattr(failed_scheduled_actions, "paginate", fake_paginate)
assert failed_scheduled_actions.collect_failed_scheduled_actions(token="token") == []
assert failed_scheduled_actions.collect_failed_actions(token="token") == []
def test_collect_failed_scheduled_actions_respects_days_window(monkeypatch):
"""Failed scheduled runs older than the requested window should be omitted."""
def test_collect_failed_actions_respects_days_window(monkeypatch):
"""Failed runs older than the requested window should be omitted."""
old_date = (datetime.now(timezone.utc) - timedelta(days=2)).strftime("%Y-%m-%dT%H:%M:%SZ")
def fake_paginate(path, params=None, key=None, max_pages=100, token=None, allow_skip=False):
@@ -148,8 +151,8 @@ def test_collect_failed_scheduled_actions_respects_days_window(monkeypatch):
monkeypatch.setattr(failed_scheduled_actions, "paginate", fake_paginate)
assert failed_scheduled_actions.collect_failed_scheduled_actions(days=1, token="token") == []
assert failed_scheduled_actions.collect_failed_scheduled_actions(days=3, token="token")
assert failed_scheduled_actions.collect_failed_actions(days=1, token="token") == []
assert failed_scheduled_actions.collect_failed_actions(days=3, token="token")
def test_format_report_links_failures():
@@ -158,8 +161,10 @@ def test_format_report_links_failures():
[
{
"repo": "ultralytics/private-repo",
"repo_url": "https://github.com/ultralytics/private-repo",
"visibility": "private",
"workflow": "Nightly",
"event": "push",
"branch": "main",
"run_number": 7,
"failed_at": "2026-06-30T01:02:03Z",
@@ -170,16 +175,20 @@ def test_format_report_links_failures():
]
)
assert "# Failed Scheduled Actions" in report
assert "**Nightly** on `main` failed at 2026-06-30 01:02:03 UTC" in report
assert "# Failed Default Branch Actions" in report
assert "**1 failing default-branch workflow run** across **1 repository**." in report
assert "**By Event:** `push` 1" in report
assert "## 📦 [ultralytics/private-repo](https://github.com/ultralytics/private-repo) - 1 failed run" in report
assert "(private)" not in report
assert "**Nightly** (`push`) on `main` failed at 2026-06-30 01:02:03 UTC" in report
assert "[Run #7](https://github.com/ultralytics/private-repo/actions/runs/7)" in report
def test_github_report_runs_enabled_sections(monkeypatch):
"""The shared report driver gates PR and failed scheduled Actions sections by env flags."""
"""The shared report driver gates PR and failed Actions sections by env flags."""
calls = []
monkeypatch.setenv("REPORT_PRS", "true")
monkeypatch.setenv("REPORT_FAILED_SCHEDULED_ACTIONS", "false")
monkeypatch.setenv("REPORT_FAILED_ACTIONS", "false")
monkeypatch.setattr(github_report.scan_prs, "run", lambda: calls.append("prs"))
monkeypatch.setattr(github_report.failed_scheduled_actions, "run", lambda: calls.append("actions"))
@@ -188,13 +197,26 @@ def test_github_report_runs_enabled_sections(monkeypatch):
assert calls == ["prs"]
def test_github_report_keeps_failed_scheduled_actions_alias(monkeypatch):
"""The old failed_scheduled_actions input remains a compatibility alias."""
calls = []
monkeypatch.setenv("REPORT_PRS", "false")
monkeypatch.setenv("REPORT_FAILED_SCHEDULED_ACTIONS", "false")
monkeypatch.setattr(github_report.scan_prs, "run", lambda: calls.append("prs"))
monkeypatch.setattr(github_report.failed_scheduled_actions, "run", lambda: calls.append("actions"))
github_report.run()
assert calls == []
def test_failed_scheduled_actions_summary_appends_section_break(tmp_path, monkeypatch):
"""Appending after the PR section should not concatenate Markdown headings."""
summary = tmp_path / "summary.md"
summary.write_text("**Summary:** Found 0 | Merged 0 | Skipped 0\n")
monkeypatch.setenv("GITHUB_STEP_SUMMARY", str(summary))
monkeypatch.setattr(failed_scheduled_actions, "collect_failed_scheduled_actions", lambda *args, **kwargs: [])
monkeypatch.setattr(failed_scheduled_actions, "collect_failed_actions", lambda *args, **kwargs: [])
failed_scheduled_actions.run()
assert "Skipped 0\n\n# Failed Scheduled Actions" in summary.read_text()
assert "Skipped 0\n\n# Failed Default Branch Actions" in summary.read_text()