Skip to content

Emphasize private Actions usage and artifact retention in reports - #8

Merged
kgrizz-git merged 9 commits into
mainfrom
feature/private-usage-emphasis
Jul 31, 2026
Merged

Emphasize private Actions usage and artifact retention in reports#8
kgrizz-git merged 9 commits into
mainfrom
feature/private-usage-emphasis

Conversation

@kgrizz-git

@kgrizz-git kgrizz-git commented Jul 31, 2026

Copy link
Copy Markdown
Owner

@coderabbitai ignore

This PR shifts reporting to a private-first view of GitHub Actions usage while keeping public usage visible as informational context.

What changed:

  • Added a visibility split for Actions minutes and storage so private/internal usage is treated as the billable free-tier basis, with public usage shown separately as free.
  • Updated limits, utilization, forecast, summary, email, CSV/XLSX/PDF, and TUI output to use the new private-first framing.
  • Added artifact storage and retention coverage, including separate treatment for Actions artifacts vs. release assets, expiry/retention annotations, and GB-hours allowance framing.
  • Flagged larger-runner SKUs as always billed and marked them throughout the reports and exports.
  • Added a Sources footer to reports and exports linking the GitHub documentation behind the billing and storage assumptions.
  • Bumped report cache version so older cached shapes are rejected cleanly.
  • Updated README, changelog, and the archived implementation plan to describe the new behavior.

Why:

  • GitHub’s free-tier Actions quota applies to private repositories, not public ones. These changes make that distinction explicit so the report highlights the usage that actually consumes the free-tier budget.
  • Artifact storage is billed differently from release assets, and retention matters. The new storage framing makes that easier to understand and act on.
  • Larger runners are always billed, so they now stand out clearly wherever Actions SKUs are shown.

Summary by CodeRabbit

  • New Features

    • Added private/public Actions usage reporting with quota tracking, larger-runner identification, and storage allowances.
    • Added separate artifact and release-asset storage details, including retention and expiration information.
    • Enhanced CSV, PDF, XLSX, email, terminal, forecast, and JSON reports with visibility summaries and source references.
    • Added storage analysis, private-usage, and documentation source sections to exports.
  • Bug Fixes

    • Improved usage calculations, cache validation, filtered-scan handling, and security-reporting updates.
  • Documentation

    • Added guidance for private-first accounting and billing interpretation.

kgrizz-git and others added 4 commits July 30, 2026 23:51
Wire visibility split through legacy/email data, limits, forecast, and storage sections; fix SKU summing, avg-MB conversion, and cache version invalidation. Also clear CodeQL FPs in setup_ci.

Co-authored-by: Cursor <cursoragent@cursor.com>
CSV/XLSX/PDF/JSON and TUI rows now surface private-vs-public Actions usage, storage analysis, Sources, and larger-runner markers; archive the finished plan and document the behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@kgrizz-git, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 34 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6f5f3933-23d3-4d78-8318-74632a2fa757

📥 Commits

Reviewing files that changed from the base of the PR and between b12bf4c and 9eb7bfa.

📒 Files selected for processing (10)
  • CHANGELOG.md
  • src/github_usage/email_report_html.py
  • src/github_usage/legacy_report_summary.py
  • src/github_usage/legacy_terminal.py
  • src/github_usage/report_actions_limits.py
  • src/github_usage/report_forecast_data.py
  • src/github_usage/report_storage.py
  • src/github_usage/report_summary_insights.py
  • src/github_usage/storage.py
  • src/github_usage/usage_split.py
📝 Walkthrough

Walkthrough

The PR adds private-first GitHub Actions accounting. It aggregates usage by repository visibility, separates artifact and release storage, adds retention metadata, updates report surfaces and exports, invalidates older caches, and documents the accounting model.

Changes

Private-first Actions reporting

Layer / File(s) Summary
Visibility aggregation and storage analysis
src/github_usage/usage_split.py, src/github_usage/storage.py, src/github_usage/report_storage.py, src/github_usage/legacy_report_data.py, src/github_usage/report_optional.py, tests/test_usage_split.py, tests/test_storage.py
Actions minutes, storage, and SKUs are split by visibility. Internal repositories are folded into private usage. Storage tracks artifacts, release assets, expiry, retention, allowances, and repository rollups.
Report summaries, limits, and forecasts
src/github_usage/report_actions_limits.py, src/github_usage/report_summary_insights.py, src/github_usage/report_forecast_data.py, src/github_usage/report_data.py, src/github_usage/legacy_report_summary.py, src/github_usage/legacy_terminal.py, src/github_usage/report_cache.py, tests/test_report_actions.py, tests/test_legacy_report_summary.py, tests/test_report_cache.py
Reports use private usage for quota calculations and label public usage as free. Summaries, findings, recommendations, forecasts, terminal output, and cache loading support the new fields.
Email and file export surfaces
src/github_usage/email_report_*.py, src/github_usage/export_visibility.py, src/github_usage/export_csv.py, src/github_usage/export_pdf.py, src/github_usage/export_xlsx.py, tests/test_export_*.py, tests/test_export_json.py
Email and exports include visibility summaries, storage analysis, larger-runner annotations, retention data, and source URLs.
Documentation and supporting updates
README.md, CHANGELOG.md, TO_DO.md, docs/superpowers/plans/archived/*, src/github_usage/setup_ci.py
Documentation records the accounting model and verification scope. CI configuration entry naming is updated.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant RepositoryActions
  participant UsageSplit
  participant StorageAnalysis
  participant ReportAndExports
  RepositoryActions->>UsageSplit: Aggregate minutes, storage, and SKUs by visibility
  RepositoryActions->>StorageAnalysis: Build artifact and release storage analysis
  UsageSplit->>ReportAndExports: Provide private/public usage and quota fields
  StorageAnalysis->>ReportAndExports: Provide retention and storage summary fields
  ReportAndExports->>ReportAndExports: Render reports and export source metadata
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.28% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the PR's primary changes to emphasize private Actions usage and artifact retention in reports.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/private-usage-emphasis

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread src/github_usage/legacy_terminal.py Fixed
Use non-equality zero checks for private-minute guards, print REPORT_SOURCES constants in the legacy footer, and note the CodeQL false-positive dismissal.

Co-authored-by: Cursor <cursoragent@cursor.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/github_usage/report_summary.py (1)

145-152: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Filter public repositories before calculating private-quota recommendations.

Line 152 enables a private-only quota basis in _print_recommendations. That helper still ranks all repo_data rows. A public-heavy repository can make the top-two percentage exceed 100% and produce a false private-quota recommendation. Filter to private and internal repositories before the top-two calculation when split Actions data exists.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/github_usage/report_summary.py` around lines 145 - 152, When split
Actions data exists (actions parameter is provided), filter the repo_data to
only include private and internal repositories before passing it to the
_print_recommendations call. This prevents public repositories from skewing the
top-two percentage calculation that informs the private-quota recommendations,
ensuring the percentage basis stays accurate and within the expected range.
🧹 Nitpick comments (9)
src/github_usage/report_storage.py (2)

23-45: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reduce _expiry_note cognitive complexity.

SonarCloud reports cognitive complexity 17 against a limit of 15 for this function. Extract the per-item "earliest non-expired days-to-expiry" scan (lines 33-45) into its own helper (for example _earliest_days_to_expiry(repo)), and keep _expiry_note as the three early-return branches plus a single call to the new helper.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/github_usage/report_storage.py` around lines 23 - 45, The _expiry_note
function exceeds the cognitive-complexity limit because it also performs the
item expiry scan. Extract that scan into a helper such as
_earliest_days_to_expiry(repo), then keep _expiry_note limited to its existing
three early-return branches, one helper call, and the resulting days formatting
logic.

Source: Linters/SAST tools


48-129: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reduce render_artifact_storage_section cognitive complexity.

SonarCloud reports cognitive complexity 31 against a limit of 15 for this function. Split the allowance/accrual print block (lines 68-86) and the per-repo table print block (lines 94-126) into two helper functions, each called once from render_artifact_storage_section. This keeps the branching in each function small enough to pass the threshold while preserving the current output.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/github_usage/report_storage.py` around lines 48 - 129, Reduce cognitive
complexity in render_artifact_storage_section by extracting the
allowance/accrual output block into one helper and the per-repo table output
block into another, calling each helper once from the original function. Pass
the existing values and context needed to preserve all current formatting,
branching, ranking, and output exactly.

Source: Linters/SAST tools

src/github_usage/storage.py (1)

124-186: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reduce get_storage_analysis cognitive complexity.

SonarCloud reports cognitive complexity 26 against a limit of 15 for this function. Extract the artifact-fetch loop and the release-fetch loop into small helper functions (for example _fetch_artifact_items(api, owner, name, today) and _fetch_release_items(api, owner, name)), each wrapping its own try/except RuntimeError and returning the accumulated items and GB total. get_storage_analysis then only orchestrates the two helpers and builds the entry dict, which brings the branching count under the threshold.

♻️ Suggested extraction
-            try:
-                artifacts = api.get_all_pages(
-                    f"/repos/{owner}/{name}/actions/artifacts",
-                    {"per_page": 100},
-                )
-            except RuntimeError:
-                artifacts = []
-            artifact_items = []
-            for art in artifacts or []:
-                item = _artifact_item(art, today=today)
-                artifact_items.append(item)
-                items.append(item)
-                artifact_storage_gb += float(item["storage"])
+            artifact_items, artifact_storage_gb = _fetch_artifact_items(api, owner, name, today)
+            items.extend(artifact_items)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/github_usage/storage.py` around lines 124 - 186, Reduce cognitive
complexity in get_storage_analysis by extracting artifact retrieval and
accumulation into a helper such as _fetch_artifact_items(api, owner, name,
today), and release retrieval and accumulation into _fetch_release_items(api,
owner, name). Each helper should handle its own RuntimeError fallback and return
the collected items with its GB total; keep get_storage_analysis focused on
orchestration, totals, rollups, and entry construction.

Source: Linters/SAST tools

src/github_usage/export_visibility.py (1)

121-121: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace ambiguous × with x in the docstring.

Ruff flags × (U+00D7 MULTIPLICATION SIGN) as ambiguous versus x (RUF002). Replace it with a plain x in "SKU × visibility table" to satisfy the linter.

✏️ Proposed fix
-    """SKU × visibility table from ``actions['skus']`` when present."""
+    """SKU x visibility table from ``actions['skus']`` when present."""
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/github_usage/export_visibility.py` at line 121, Update the docstring in
the SKU/visibility table declaration to replace the ambiguous multiplication
sign with a plain ASCII “x”, preserving the rest of the documentation unchanged
and satisfying Ruff RUF002.

Source: Linters/SAST tools

src/github_usage/usage_split.py (2)

115-162: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Reduce cognitive complexity of split_rows_by_visibility.

SonarCloud reports cognitive complexity 27, above the allowed 15. The function mixes bucket initialization, per-row visibility resolution, internal-repo counting, and SKU merging in one loop body. Extract the per-row update logic (internal-count bump, minutes/storage accumulation, SKU merge) into one or two small helper functions to bring this under the threshold.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/github_usage/usage_split.py` around lines 115 - 162, Reduce cognitive
complexity in split_rows_by_visibility by extracting the per-row bucket update
logic into one or two focused helpers, including internal_repo_count handling,
minutes/storage accumulation, and SKU merging. Keep bucket initialization and
visibility resolution in split_rows_by_visibility, and preserve the current
aggregation behavior and optional-key handling.

Source: Linters/SAST tools


27-28: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Private free-tier limits (2000 min / 500 MB) are re-declared independently in four files. usage_split.py defines the canonical _PRIVATE_MINUTES_LIMIT/_PRIVATE_STORAGE_LIMIT_GB, but each other module redeclares its own copy of the same values instead of importing them, risking drift if the free-tier limit ever changes.

  • src/github_usage/usage_split.py#L27-L28: promote _PRIVATE_STORAGE_LIMIT_GB and _PRIVATE_MINUTES_LIMIT to public names (drop the leading underscore) so other modules can import them directly.
  • src/github_usage/report_actions_limits.py#L17-L18: replace the locally redefined _PRIVATE_MINUTES_LIMIT/_PRIVATE_STORAGE_LIMIT_MB with an import from usage_split.
  • src/github_usage/report_summary_insights.py#L31-L31: replace the local free_min_limit = 2000 (also at Line 51 free_storage_mb = 500 and Line 202 free_min_limit = 2000) with imports from usage_split.
  • src/github_usage/export_visibility.py#L30-L30: replace the inline 2000 literal in visibility_summary_rows with the imported constant.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/github_usage/usage_split.py` around lines 27 - 28, Centralize the private
free-tier limits by renaming _PRIVATE_STORAGE_LIMIT_GB and
_PRIVATE_MINUTES_LIMIT to public constants in usage_split.py. In
src/github_usage/report_actions_limits.py lines 17-18, import and use those
constants instead of local copies; in
src/github_usage/report_summary_insights.py lines 31, 51, and 202, replace the
duplicated 2000 and 500 values with the imported constants; and in
src/github_usage/export_visibility.py line 30, replace the inline 2000 in
visibility_summary_rows with the imported minutes constant.
src/github_usage/report_data.py (1)

202-223: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Storage insight still uses the legacy combined storage_percent, not private-first.

The new insight at Line 206-211 uses private_minutes_percent for private-first framing, but the storage insight at Line 221-222 still checks the legacy storage_percent (private+public combined), diverging from the private-first emphasis this PR otherwise applies consistently. Consider mirroring the minutes insight by computing a private-storage-based check (e.g., using private_storage_avg_mb against the 500 MB private limit) when the visibility split is present, falling back to storage_percent otherwise.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/github_usage/report_data.py` around lines 202 - 223, The storage insight
in get_key_insights should use private-first storage data when the visibility
split is available: compare private_storage_avg_mb against the 500 MB private
limit, and only fall back to the existing storage_percent check when that
private metric is unavailable. Preserve the current insight text and three-item
limit.
src/github_usage/report_summary_insights.py (1)

19-271: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Reduce cognitive complexity in three functions.

SonarCloud reports complexity failures above the allowed 15 for:

  • _print_utilization (Line 19): complexity 32.
  • _print_impactful_findings (Line 85): complexity 43.
  • _print_recommendations (Line 181): complexity 44.

Each function interleaves visibility-split branching, formatting, and threshold checks. Extract the minutes-block and storage-block logic in _print_utilization into helpers, and extract each finding/recommendation category in the other two functions into small helper functions that return an optional string to append to the list.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/github_usage/report_summary_insights.py` around lines 19 - 271, Reduce
cognitive complexity in _print_utilization, _print_impactful_findings, and
_print_recommendations by extracting their distinct minutes, storage,
finding-category, and recommendation-category logic into small helpers. Have
each finding/recommendation helper return an optional string, while preserving
the existing ordering, thresholds, visibility handling, formatting, and output
behavior; keep the top-level functions focused on assembling and printing
results.

Source: Linters/SAST tools

src/github_usage/report_actions_limits.py (1)

98-178: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Reduce cognitive complexity of render_limits_summary.

SonarCloud reports cognitive complexity 41, above the allowed 15. The function branches on skip_quota, has_split, and per-visibility storage math all in one body. Extract the minutes block and the storage block into separate helper functions (mirroring _print_usage_by_visibility above) to bring this under the threshold.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/github_usage/report_actions_limits.py` around lines 98 - 178, Reduce the
cognitive complexity of render_limits_summary by extracting its Actions Minutes
output into a dedicated helper and its visibility-split or legacy Actions
Storage output into another helper, mirroring _print_usage_by_visibility. Pass
the computed values and flags needed for formatting, keep quota suppression and
existing output behavior unchanged, and leave Copilot Pro rendering in
render_limits_summary.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CHANGELOG.md`:
- Line 13: Update the changelog entry’s “public usage is shown separately as
free” wording to limit the free designation to public standard-runner usage,
while preserving the nearby larger-runner `*` billing annotation.

In `@src/github_usage/email_report_html.py`:
- Around line 168-185: At src/github_usage/email_report_html.py lines 168-185,
extract the public storage value (similar to how priv_mb is extracted from the
private bucket) by adding pub_mb assignment, then update the public summary text
in the f-string to include the public storage metric alongside pub_min. At
src/github_usage/email_report_text.py lines 105-156, similarly extract the
public storage value from the public visibility bucket and add it to the public
summary line output so both HTML and text versions display the public storage
information as required context for the visibility storage split.

In `@src/github_usage/export_csv.py`:
- Around line 76-97: Update the top-level Actions Usage loop in the CSV export
to exclude all visibility-split fields produced by
attach_actions_visibility_split, including larger_runner_skus and the related
minutes, storage, averages, status, and repository-count keys. Keep
sku_breakdown and skus excluded, and leave visibility_summary_rows responsible
for emitting the curated visibility section once.

In `@src/github_usage/report_forecast_data.py`:
- Around line 76-77: Update src/github_usage/report_forecast_data.py lines 76-77
in the forecast data-building logic to store a condition-neutral filtered-scan
indicator or the actual filter mode instead of setting scanned_private_only for
the --only-public case. Update src/github_usage/report_forecast.py lines 50-53
so the filtered-scan note reflects the selected mode accurately and public
minutes are rendered independently rather than being suppressed by an elif.

In `@src/github_usage/report_summary_insights.py`:
- Around line 134-136: Update both repository lookups in
_print_impactful_findings and _print_recommendations to use the existing
`(storage_analysis or {})` guard before calling `.get("repos", [])`, matching
the guarded access already used in this file and preserving the current sorting
behavior.

In `@TO_DO.md`:
- Line 9: Address the email redaction requirement before release: ensure both
plain-text and HTML email bodies are redacted, or document an explicit security
decision covering recipients and retention controls. Update the TODO entry and
related documentation to reflect the completed implementation or approved
decision, without leaving email redaction as an unchecked follow-up.

---

Outside diff comments:
In `@src/github_usage/report_summary.py`:
- Around line 145-152: When split Actions data exists (actions parameter is
provided), filter the repo_data to only include private and internal
repositories before passing it to the _print_recommendations call. This prevents
public repositories from skewing the top-two percentage calculation that informs
the private-quota recommendations, ensuring the percentage basis stays accurate
and within the expected range.

---

Nitpick comments:
In `@src/github_usage/export_visibility.py`:
- Line 121: Update the docstring in the SKU/visibility table declaration to
replace the ambiguous multiplication sign with a plain ASCII “x”, preserving the
rest of the documentation unchanged and satisfying Ruff RUF002.

In `@src/github_usage/report_actions_limits.py`:
- Around line 98-178: Reduce the cognitive complexity of render_limits_summary
by extracting its Actions Minutes output into a dedicated helper and its
visibility-split or legacy Actions Storage output into another helper, mirroring
_print_usage_by_visibility. Pass the computed values and flags needed for
formatting, keep quota suppression and existing output behavior unchanged, and
leave Copilot Pro rendering in render_limits_summary.

In `@src/github_usage/report_data.py`:
- Around line 202-223: The storage insight in get_key_insights should use
private-first storage data when the visibility split is available: compare
private_storage_avg_mb against the 500 MB private limit, and only fall back to
the existing storage_percent check when that private metric is unavailable.
Preserve the current insight text and three-item limit.

In `@src/github_usage/report_storage.py`:
- Around line 23-45: The _expiry_note function exceeds the cognitive-complexity
limit because it also performs the item expiry scan. Extract that scan into a
helper such as _earliest_days_to_expiry(repo), then keep _expiry_note limited to
its existing three early-return branches, one helper call, and the resulting
days formatting logic.
- Around line 48-129: Reduce cognitive complexity in
render_artifact_storage_section by extracting the allowance/accrual output block
into one helper and the per-repo table output block into another, calling each
helper once from the original function. Pass the existing values and context
needed to preserve all current formatting, branching, ranking, and output
exactly.

In `@src/github_usage/report_summary_insights.py`:
- Around line 19-271: Reduce cognitive complexity in _print_utilization,
_print_impactful_findings, and _print_recommendations by extracting their
distinct minutes, storage, finding-category, and recommendation-category logic
into small helpers. Have each finding/recommendation helper return an optional
string, while preserving the existing ordering, thresholds, visibility handling,
formatting, and output behavior; keep the top-level functions focused on
assembling and printing results.

In `@src/github_usage/storage.py`:
- Around line 124-186: Reduce cognitive complexity in get_storage_analysis by
extracting artifact retrieval and accumulation into a helper such as
_fetch_artifact_items(api, owner, name, today), and release retrieval and
accumulation into _fetch_release_items(api, owner, name). Each helper should
handle its own RuntimeError fallback and return the collected items with its GB
total; keep get_storage_analysis focused on orchestration, totals, rollups, and
entry construction.

In `@src/github_usage/usage_split.py`:
- Around line 115-162: Reduce cognitive complexity in split_rows_by_visibility
by extracting the per-row bucket update logic into one or two focused helpers,
including internal_repo_count handling, minutes/storage accumulation, and SKU
merging. Keep bucket initialization and visibility resolution in
split_rows_by_visibility, and preserve the current aggregation behavior and
optional-key handling.
- Around line 27-28: Centralize the private free-tier limits by renaming
_PRIVATE_STORAGE_LIMIT_GB and _PRIVATE_MINUTES_LIMIT to public constants in
usage_split.py. In src/github_usage/report_actions_limits.py lines 17-18, import
and use those constants instead of local copies; in
src/github_usage/report_summary_insights.py lines 31, 51, and 202, replace the
duplicated 2000 and 500 values with the imported constants; and in
src/github_usage/export_visibility.py line 30, replace the inline 2000 in
visibility_summary_rows with the imported minutes constant.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8723639a-70ee-4162-b158-d8a8972918f9

📥 Commits

Reviewing files that changed from the base of the PR and between 5df3c99 and b12bf4c.

📒 Files selected for processing (38)
  • CHANGELOG.md
  • README.md
  • TO_DO.md
  • docs/superpowers/plans/archived/2026-07-30-private-usage-emphasis.md
  • src/github_usage/email_report_html.py
  • src/github_usage/email_report_text.py
  • src/github_usage/export_csv.py
  • src/github_usage/export_pdf.py
  • src/github_usage/export_visibility.py
  • src/github_usage/export_xlsx.py
  • src/github_usage/legacy_report_data.py
  • src/github_usage/legacy_report_summary.py
  • src/github_usage/legacy_terminal.py
  • src/github_usage/report_actions.py
  • src/github_usage/report_actions_limits.py
  • src/github_usage/report_cache.py
  • src/github_usage/report_data.py
  • src/github_usage/report_forecast.py
  • src/github_usage/report_forecast_data.py
  • src/github_usage/report_optional.py
  • src/github_usage/report_storage.py
  • src/github_usage/report_summary.py
  • src/github_usage/report_summary_insights.py
  • src/github_usage/setup_ci.py
  • src/github_usage/storage.py
  • src/github_usage/usage_split.py
  • tests/fixtures/export_report_data.json
  • tests/test_export_csv.py
  • tests/test_export_json.py
  • tests/test_export_pdf.py
  • tests/test_export_xlsx.py
  • tests/test_legacy_report_data.py
  • tests/test_legacy_report_summary.py
  • tests/test_report_actions.py
  • tests/test_report_cache.py
  • tests/test_report_optional.py
  • tests/test_storage.py
  • tests/test_usage_split.py

Comment thread CHANGELOG.md Outdated

### Added

- **Private-usage emphasis for Actions free-tier reporting** ([plan](docs/superpowers/plans/archived/2026-07-30-private-usage-emphasis.md)): Limits Summary, utilization bars, and forecasts measure **private-repo** Actions minutes/storage against the free tier; public usage is shown separately as free. Artifact storage section frames the 500 MB private allowance as GB-hrs accrual, splits artifacts vs release assets, and surfaces expiry/retention. Larger-runner SKUs are flagged `*`. Legacy reports include a Sources footer. Email `repo_consumers` gains `by_visibility`; report cache version bumped to 2 (stale v1 snapshots rejected). CSV/XLSX/PDF/JSON exports and TUI summary rows carry the same private-vs-public framing, storage analysis, and Sources section.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Limit the free-public statement to standard runners.

public usage is shown separately as free is broader than the billing rule. Public standard-runner usage is free, but larger runners are billed in public repositories. Change this phrase so the * annotation is not the only place that carries the exception. (docs.github.com)

Proposed wording
- public usage is shown separately as free.
+ public standard-runner usage is shown separately as free.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- **Private-usage emphasis for Actions free-tier reporting** ([plan](docs/superpowers/plans/archived/2026-07-30-private-usage-emphasis.md)): Limits Summary, utilization bars, and forecasts measure **private-repo** Actions minutes/storage against the free tier; public usage is shown separately as free. Artifact storage section frames the 500 MB private allowance as GB-hrs accrual, splits artifacts vs release assets, and surfaces expiry/retention. Larger-runner SKUs are flagged `*`. Legacy reports include a Sources footer. Email `repo_consumers` gains `by_visibility`; report cache version bumped to 2 (stale v1 snapshots rejected). CSV/XLSX/PDF/JSON exports and TUI summary rows carry the same private-vs-public framing, storage analysis, and Sources section.
- **Private-usage emphasis for Actions free-tier reporting** ([plan](docs/superpowers/plans/archived/2026-07-30-private-usage-emphasis.md)): Limits Summary, utilization bars, and forecasts measure **private-repo** Actions minutes/storage against the free tier; public standard-runner usage is shown separately as free. Artifact storage section frames the 500 MB private allowance as GB-hrs accrual, splits artifacts vs release assets, and surfaces expiry/retention. Larger-runner SKUs are flagged `*`. Legacy reports include a Sources footer. Email `repo_consumers` gains `by_visibility`; report cache version bumped to 2 (stale v1 snapshots rejected). CSV/XLSX/PDF/JSON exports and TUI summary rows carry the same private-vs-public framing, storage analysis, and Sources section.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` at line 13, Update the changelog entry’s “public usage is shown
separately as free” wording to limit the free designation to public
standard-runner usage, while preserving the nearby larger-runner `*` billing
annotation.

Comment thread src/github_usage/email_report_html.py Outdated
Comment on lines +168 to +185
by_vis = consumers.get("by_visibility")
if by_vis:
priv = by_vis.get("private") or {}
pub = by_vis.get("public") or {}
priv_min = float(priv.get("minutes", 0.0) or 0.0)
pub_min = float(pub.get("minutes", 0.0) or 0.0)
priv_mb = float(priv.get("storage_avg_mb", 0.0) or 0.0)
pct = (priv_min / 2000.0 * 100.0) if priv_min else 0.0
parts.extend(
[
"<h2>Private vs public Actions</h2>",
'<p class="visibility-tag">'
f"Private: {priv_min:,.1f} min / 2,000 free ({pct:.0f}%) · "
f"{priv_mb:,.1f} MB avg · Public: {pub_min:,.1f} min (free)"
"</p>",
'<p class="visibility-tag">Retention: 90 days default; artifacts auto-expire.</p>',
]
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Include public storage in both email visibility summaries.

Both renderers read the public visibility bucket but only display public minutes. Public storage is free, but it remains required informational context in the visibility storage split.

  • src/github_usage/email_report_html.py#L168-L185: add public["storage_avg_mb"] to the public summary text.
  • src/github_usage/email_report_text.py#L105-L156: add public["storage_avg_mb"] to the public summary line.
📍 Affects 2 files
  • src/github_usage/email_report_html.py#L168-L185 (this comment)
  • src/github_usage/email_report_text.py#L105-L156
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/github_usage/email_report_html.py` around lines 168 - 185, At
src/github_usage/email_report_html.py lines 168-185, extract the public storage
value (similar to how priv_mb is extracted from the private bucket) by adding
pub_mb assignment, then update the public summary text in the f-string to
include the public storage metric alongside pub_min. At
src/github_usage/email_report_text.py lines 105-156, similarly extract the
public storage value from the public visibility bucket and add it to the public
summary line output so both HTML and text versions display the public storage
information as required context for the visibility storage split.

Comment on lines 76 to +97
_write_section_header(writer, "Actions Usage")
actions = _coerce_section(data.get("actions"), {})
for key, value in actions.items():
if key == "sku_breakdown":
_write_nested(writer, "sku_breakdown", "sku", value)
else:
if key in {"sku_breakdown", "skus"}:
continue
writer.writerow([key, value])
sku = annotated_sku_breakdown(actions.get("sku_breakdown") or {})
if sku:
_write_nested(writer, "sku_breakdown", "sku", sku)
if any(str(name).endswith(" *") for name in sku):
writer.writerow(
[
"*",
"GitHub-hosted larger runner - always billed, not covered by free tier",
]
)

vis_rows = visibility_summary_rows(actions)
if vis_rows:
_write_section_header(writer, "Actions Usage by Visibility")
for row in vis_rows:
writer.writerow(row)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Visibility-split fields are written twice, once in an inconsistent format.

The flat "Actions Usage" dump at Line 78-81 only excludes sku_breakdown/skus, so all visibility-split fields merged into actions by attach_actions_visibility_split (private_minutes, public_minutes, unattributed_minutes, private_minutes_percent, private_storage_gb_hours, public_storage_gb_hours, unattributed_storage_gb_hours, private_storage_avg_mb, public_storage_avg_mb, larger_runner_skus, filtered, reconciled, internal_repo_count) get written here, and then written again at Line 93-97 via visibility_summary_rows. export_xlsx.py's _write_actions_sheet avoids this by only emitting curated top-level rows and keeping the split in its own sheet. larger_runner_skus is additionally worse here: the flat dump writes the raw Python list (stringified by csv.writer as e.g. "['linux_4_core']"), while the visibility section writes a clean comma-joined string. Exclude the visibility-split keys from the flat dump the same way sku_breakdown/skus are excluded.

🔧 Proposed fix
+    _VISIBILITY_SPLIT_KEYS = {
+        "private_minutes", "public_minutes", "unattributed_minutes",
+        "private_minutes_percent", "private_storage_gb_hours",
+        "public_storage_gb_hours", "unattributed_storage_gb_hours",
+        "private_storage_avg_mb", "public_storage_avg_mb",
+        "larger_runner_skus", "internal_repo_count", "reconciled",
+    }
     for key, value in actions.items():
-        if key in {"sku_breakdown", "skus"}:
+        if key in {"sku_breakdown", "skus"} | _VISIBILITY_SPLIT_KEYS:
             continue
         writer.writerow([key, value])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/github_usage/export_csv.py` around lines 76 - 97, Update the top-level
Actions Usage loop in the CSV export to exclude all visibility-split fields
produced by attach_actions_visibility_split, including larger_runner_skus and
the related minutes, storage, averages, status, and repository-count keys. Keep
sku_breakdown and skus excluded, and leave visibility_summary_rows responsible
for emitting the curated visibility section once.

Comment on lines +76 to +77
if actions.get("filtered") and float(actions.get("private_minutes") or 0.0) == 0.0:
forecast["scanned_private_only"] = True

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Correct the filtered-scan label and keep public minutes visible.

The condition at src/github_usage/report_forecast_data.py is documented as --only-public, but it sets scanned_private_only. The renderer then prints an inverted message and suppresses public minutes through elif.

  • src/github_usage/report_forecast_data.py#L76-L77: use a condition-neutral flag, or carry the actual filter mode.
  • src/github_usage/report_forecast.py#L50-L53: print an accurate filtered-scan note and print public minutes independently of that note.
🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[warning] 76-76: Do not perform equality checks with floating point values.

See more on https://sonarcloud.io/project/issues?id=kgrizz-git_github-usage&issues=AZ-4yenXS6kZCEdk_g9W&open=AZ-4yenXS6kZCEdk_g9W&pullRequest=8

📍 Affects 2 files
  • src/github_usage/report_forecast_data.py#L76-L77 (this comment)
  • src/github_usage/report_forecast.py#L50-L53
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/github_usage/report_forecast_data.py` around lines 76 - 77, Update
src/github_usage/report_forecast_data.py lines 76-77 in the forecast
data-building logic to store a condition-neutral filtered-scan indicator or the
actual filter mode instead of setting scanned_private_only for the --only-public
case. Update src/github_usage/report_forecast.py lines 50-53 so the
filtered-scan note reflects the selected mode accurately and public minutes are
rendered independently rather than being suppressed by an elif.

Comment on lines +134 to +136
sorted_by_storage = sorted(
storage_analysis.get("repos", []), key=lambda x: x["total_storage"], reverse=True
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Fix missing None guard on storage_analysis in two call sites.

storage_analysis.get("repos", []) at Line 134-136 (_print_impactful_findings) and Line 249-251 (_print_recommendations) crashes with AttributeError when storage_analysis is None. Line 115 in this same file already guards the same parameter with (storage_analysis or {}).get("repos", []), showing None is an expected input for this function. Apply the same guard at both unguarded sites.

🐛 Proposed fix
     sorted_by_storage = sorted(
-        storage_analysis.get("repos", []), key=lambda x: x["total_storage"], reverse=True
+        (storage_analysis or {}).get("repos", []), key=lambda x: x["total_storage"], reverse=True
     )

Apply the same change at both locations (lines 134-136 and 249-251).

Also applies to: 249-251

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/github_usage/report_summary_insights.py` around lines 134 - 136, Update
both repository lookups in _print_impactful_findings and _print_recommendations
to use the existing `(storage_analysis or {})` guard before calling
`.get("repos", [])`, matching the guarded access already used in this file and
preserving the current sorting behavior.

Comment thread TO_DO.md

- [ ] Broaden generated-content guardrails beyond the current filename-only `forbid-generated-reports` hook (matches `github-usage-*.json`) into a content-aware check in `scripts/security` + the `Security` CI workflow that flags committed files containing:
- absolute local paths (e.g. `/Users/`, `C:\`, `/tmp/`, `/var/`)
- unredacted report output in **email bodies (plain-text + HTML)**, **text**, and **PDF** exports — email is the main gap today (`redact.py` covers file exports only, not email bodies; PDF output needs verification), plus generated report artifacts in any format (`.json`, `.txt`, `.pdf`, `.xlsx`, `.csv`)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Do not leave email redaction as an unchecked follow-up.

README.md states that email bodies are not redacted, and this item identifies email as the main gap. Redact email output or record an explicit security decision with recipient and retention controls before release.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@TO_DO.md` at line 9, Address the email redaction requirement before release:
ensure both plain-text and HTML email bodies are redacted, or document an
explicit security decision covering recipients and retention controls. Update
the TODO entry and related documentation to reflect the completed implementation
or approved decision, without leaving email redaction as an unchecked follow-up.

Extract focused helpers from utilization, limits, storage, forecast, visibility split, HTML consumers, and related paths so new-code complexity stays under Sonar thresholds without changing report behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread src/github_usage/legacy_terminal.py Fixed
Use local documentation URL literals instead of REPORT_SOURCES lookups so clear-text-logging heuristics stop flagging the public docs footer.

Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread src/github_usage/legacy_terminal.py Fixed
Add .coderabbit.yaml with auto_review off, ignore on PR #8, harden the Sources footer against CodeQL, and apply the clear CodeRabbit fixes for CSV, email, forecast labeling, and storage_analysis None guards.

Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread src/github_usage/report_sources.py Dismissed
Public Actions logs must not expose REPORT_EMAIL; confirm with a generic success line instead.

Co-authored-by: Cursor <cursoragent@cursor.com>
@sonarqubecloud

Copy link
Copy Markdown

@kgrizz-git
kgrizz-git merged commit 1fb6817 into main Jul 31, 2026
11 checks passed
@kgrizz-git
kgrizz-git deleted the feature/private-usage-emphasis branch August 7, 2026 23:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants