Add GitHub report workflow (#778)

Co-authored-by: UltralyticsAssistant <[email protected]>
This commit is contained in:
Glenn Jocher
2026-06-30 16:42:22 +02:00
committed by GitHub
co-authored by UltralyticsAssistant
parent e13016ac68
commit c4961b4dd0
8 changed files with 85 additions and 30 deletions
+30
View File
@@ -0,0 +1,30 @@
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
# Summarize open PRs and failed scheduled GitHub Actions across public Ultralytics repositories
name: GitHub Report
on:
workflow_dispatch:
inputs:
days:
description: "Days of scheduled workflow history to include"
required: false
type: number
default: 1
schedule:
- cron: "0 6 * * *" # daily at 06:00 UTC
permissions:
contents: read
pull-requests: read
jobs:
github-report:
runs-on: ubuntu-latest
steps:
- uses: ultralytics/actions/github-report@main
with:
token: ${{ secrets._GITHUB_TOKEN }}
visibility: public
days: ${{ inputs.days || 1 }}
-22
View File
@@ -1,22 +0,0 @@
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
# List Open PRs across Ultralytics repositories and auto-merge eligible GitHub Actions update 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 -1
View File
@@ -10,7 +10,7 @@ Welcome to [Ultralytics Actions](https://github.com/ultralytics/actions) - a col
[![Actions CI](https://github.com/ultralytics/actions/actions/workflows/ci.yml/badge.svg)](https://github.com/ultralytics/actions/actions/workflows/ci.yml)
[![Ultralytics Actions](https://github.com/ultralytics/actions/actions/workflows/format.yml/badge.svg)](https://github.com/ultralytics/actions/actions/workflows/format.yml)
[![Scan PRs](https://github.com/ultralytics/actions/actions/workflows/scan-prs.yml/badge.svg)](https://github.com/ultralytics/actions/actions/workflows/scan-prs.yml)
[![GitHub Report](https://github.com/ultralytics/actions/actions/workflows/github_report.yml/badge.svg)](https://github.com/ultralytics/actions/actions/workflows/github_report.yml)
[![codecov](https://codecov.io/github/ultralytics/actions/branch/main/graph/badge.svg)](https://app.codecov.io/github/ultralytics/actions)
[![Ultralytics Discord](https://img.shields.io/discord/1089800235347353640?logo=discord&logoColor=white&label=Discord&color=blue)](https://discord.com/invite/ultralytics)
+1 -1
View File
@@ -10,7 +10,7 @@
[![Actions CI](https://github.com/ultralytics/actions/actions/workflows/ci.yml/badge.svg)](https://github.com/ultralytics/actions/actions/workflows/ci.yml)
[![Ultralytics Actions](https://github.com/ultralytics/actions/actions/workflows/format.yml/badge.svg)](https://github.com/ultralytics/actions/actions/workflows/format.yml)
[![Scan PRs](https://github.com/ultralytics/actions/actions/workflows/scan-prs.yml/badge.svg)](https://github.com/ultralytics/actions/actions/workflows/scan-prs.yml)
[![GitHub Report](https://github.com/ultralytics/actions/actions/workflows/github_report.yml/badge.svg)](https://github.com/ultralytics/actions/actions/workflows/github_report.yml)
[![codecov](https://codecov.io/github/ultralytics/actions/branch/main/graph/badge.svg)](https://app.codecov.io/github/ultralytics/actions)
[![Ultralytics Discord](https://img.shields.io/discord/1089800235347353640?logo=discord&logoColor=white&label=Discord&color=blue)](https://discord.com/invite/ultralytics)
+13 -6
View File
@@ -10,13 +10,19 @@ import urllib.error
import urllib.parse
import urllib.request
from collections import defaultdict
from datetime import datetime, timezone
from datetime import datetime, timedelta, timezone
from actions.scan_prs import 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."""
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)
def github_get(path, params=None, token=None, allow_skip=False):
"""Fetch JSON from the GitHub REST API."""
token = token or os.getenv("GH_TOKEN") or os.getenv("GITHUB_TOKEN")
@@ -62,9 +68,10 @@ def paginate(path, params=None, key=None, max_pages=100, token=None, allow_skip=
def collect_failed_scheduled_actions(
org="ultralytics", visibility="public", repo_visibility="public", max_run_pages=3, token=None
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."""
cutoff = datetime.now(timezone.utc) - timedelta(days=days) if days else None
filter_config = get_repo_filter(parse_visibility(visibility, repo_visibility))
repos = [
r
@@ -87,13 +94,11 @@ def collect_failed_scheduled_actions(
for run in runs:
key = run.get("workflow_id") or run.get("name")
current = latest.get(key)
started = run.get("run_started_at") or run.get("created_at") or ""
current_started = (current or {}).get("run_started_at") or (current or {}).get("created_at") or ""
if not current or started > current_started:
if not current or run_time(run) > run_time(current):
latest[key] = run
for run in latest.values():
if run.get("conclusion") in FAILED_CONCLUSIONS:
if run.get("conclusion") in FAILED_CONCLUSIONS and (not cutoff or run_time(run) >= cutoff):
failures.append(
{
"repo": full_name,
@@ -146,12 +151,14 @@ def run():
"""Write failed scheduled 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(
org,
visibility=os.getenv("VISIBILITY", "public"),
repo_visibility=os.getenv("REPO_VISIBILITY", "public"),
max_run_pages=max_run_pages,
days=days,
),
org,
)
+2
View File
@@ -24,6 +24,7 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
org: ultralytics
visibility: private,internal
days: 1
```
## Inputs
@@ -37,6 +38,7 @@ jobs:
| `failed_scheduled_actions` | Include failed scheduled Actions summary | No | `true` |
| `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` |
## Output
+5
View File
@@ -34,6 +34,10 @@ inputs:
description: "Pages of scheduled workflow runs to inspect per repository"
required: false
default: "3"
days:
description: "Days of scheduled workflow history to include"
required: false
default: "1"
runs:
using: "composite"
steps:
@@ -73,6 +77,7 @@ runs:
REPORT_FAILED_SCHEDULED_ACTIONS: ${{ inputs.failed_scheduled_actions }}
AUTO_MERGE_ACTIONS_PRS: ${{ inputs.auto_merge_actions_prs }}
MAX_RUN_PAGES: ${{ inputs.max_run_pages }}
REPORT_DAYS: ${{ inputs.days }}
run: |
echo "::group::GitHub Report"
python -m actions.github_report
+33
View File
@@ -1,5 +1,7 @@
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
from datetime import datetime, timedelta, timezone
from actions import failed_scheduled_actions, github_report
@@ -119,6 +121,37 @@ def test_collect_failed_scheduled_actions_ignores_latest_success(monkeypatch):
assert failed_scheduled_actions.collect_failed_scheduled_actions(token="token") == []
def test_collect_failed_scheduled_actions_respects_days_window(monkeypatch):
"""Failed scheduled 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):
if path == "/orgs/ultralytics/repos":
return [
{
"full_name": "ultralytics/repo",
"default_branch": "main",
"visibility": "public",
"archived": False,
}
]
if path == "/repos/ultralytics/repo/actions/runs":
return [
{
"workflow_id": 1,
"name": "Nightly",
"conclusion": "failure",
"run_started_at": old_date,
}
]
raise AssertionError(path)
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")
def test_format_report_links_failures():
"""Report output includes concise failure details and backlinks."""
report = failed_scheduled_actions.format_report(