diff --git a/CHANGELOG.md b/CHANGELOG.md index be19ac1..4074d36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ This project follows the structure from Keep a Changelog and intends to use Sema ### Fixed +- **SonarCloud reliability (S6466):** `get_key_insights` in `report_data.py` adds an explicit `None`-guard (`or []`) on `by_minutes` before indexing, satisfying SonarCloud's S6466 check; new test covers `by_minutes: None`. - **CodeRabbit follow-ups (PR #10):** Soft-fail `workflow_breakdown` fetch on `RuntimeError` (email + legacy paths) so partial reports still render; skip redundant private concentration recommendations when private top-2 matches overall top-2; share `repo_label` / `WORKFLOW_MINUTES_REQUEST_HEADROOM` / public `parse_iso_datetime`; narrow workflow-name-map soft-fail to `RuntimeError`; normalize non-UTC ISO offsets to UTC in `parse_iso_datetime` so expiry/retention day math stays calendar-stable. - **SonarCloud quality gate (PR #10):** Safer list indexing for private consumer findings and workflow breakdown (`S6466`); reduced cognitive complexity in `_format_consumers_section` and `_repo_rows`; deduplicated HTML `` literals; consolidated repeated consumer test fixtures into `tests/_consumer_fixtures.py`. - **Calendar-dependent forecast export/email tests:** Pin `report_forecast_data.date.today` to a mid-month date in CSV/PDF/XLSX section-presence tests and the cached email-report CLI forecast assertion. Forecast is intentionally omitted when `day_of_month < 3`, so those tests failed on the 1st–2nd of each month without a date pin. diff --git a/src/github_usage/report_data.py b/src/github_usage/report_data.py index 2535cd0..527c76a 100644 --- a/src/github_usage/report_data.py +++ b/src/github_usage/report_data.py @@ -211,14 +211,16 @@ def get_key_insights(report_data: dict) -> list[str]: f"Private repos used {pct:.0f}% of the 2,000 free Actions minutes this month." ) consumers = report_data.get("repo_consumers") - if actions and consumers and consumers.get("by_minutes"): - top = consumers["by_minutes"][0] - minutes = float(actions.get("minutes", 0.0)) - if minutes: - vis = visibility_label(repo_visibility(top)) - insights.append( - f"{top['repo']}{vis} accounts for {top['minutes'] / minutes * 100:.0f}% of Actions minutes." - ) + if actions and consumers: + by_minutes = consumers.get("by_minutes") or [] + if by_minutes: + top = by_minutes[0] + minutes = float(actions.get("minutes", 0.0)) + if minutes: + vis = visibility_label(repo_visibility(top)) + insights.append( + f"{top['repo']}{vis} accounts for {top['minutes'] / minutes * 100:.0f}% of Actions minutes." + ) if actions and float(actions.get("storage_percent", 0.0)) < 100: insights.append("Actions storage is below the free-tier limit.") return insights[:3] diff --git a/tests/test_report_data.py b/tests/test_report_data.py index 97291db..f37a100 100644 --- a/tests/test_report_data.py +++ b/tests/test_report_data.py @@ -159,6 +159,18 @@ def test_get_key_insights_reports_storage_below_free_tier(self): self.assertEqual(insights, ["Actions storage is below the free-tier limit."]) + def test_get_key_insights_handles_none_by_minutes_without_index_error(self): + from github_usage.report_data import get_key_insights + + report = { + "actions": {"minutes": 100.0, "storage_percent": 50.0}, + "repo_consumers": {"by_minutes": None}, + } + + insights = get_key_insights(report) + + self.assertEqual(insights, ["Actions storage is below the free-tier limit."]) + def test_get_key_insights_caps_at_three(self): from github_usage.report_data import get_key_insights