diff --git a/.github/workflows/github_report.yml b/.github/workflows/github_report.yml new file mode 100644 index 0000000..e019215 --- /dev/null +++ b/.github/workflows/github_report.yml @@ -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 }} diff --git a/.github/workflows/scan-prs.yml b/.github/workflows/scan-prs.yml deleted file mode 100644 index 11c0f57..0000000 --- a/.github/workflows/scan-prs.yml +++ /dev/null @@ -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 }} diff --git a/README.md b/README.md index 8112673..c5a90f3 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/README.zh-CN.md b/README.zh-CN.md index 809d522..fd0554e 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -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) diff --git a/actions/failed_scheduled_actions.py b/actions/failed_scheduled_actions.py index ebc5a08..2a60ce4 100644 --- a/actions/failed_scheduled_actions.py +++ b/actions/failed_scheduled_actions.py @@ -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, ) diff --git a/github-report/README.md b/github-report/README.md index bf0bcba..90148fa 100644 --- a/github-report/README.md +++ b/github-report/README.md @@ -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 diff --git a/github-report/action.yml b/github-report/action.yml index 3a6f96e..28281f2 100644 --- a/github-report/action.yml +++ b/github-report/action.yml @@ -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 diff --git a/tests/test_github_report.py b/tests/test_github_report.py index cc4c3ca..86f5b86 100644 --- a/tests/test_github_report.py +++ b/tests/test_github_report.py @@ -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(