Emphasize private Actions usage and artifact retention in reports - #8
Conversation
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>
|
Warning Review limit reached
Next review available in: 34 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (10)
📝 WalkthroughWalkthroughThe 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. ChangesPrivate-first Actions reporting
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
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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. Comment |
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>
There was a problem hiding this comment.
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 winFilter public repositories before calculating private-quota recommendations.
Line 152 enables a private-only quota basis in
_print_recommendations. That helper still ranks allrepo_datarows. 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 winReduce
_expiry_notecognitive 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_noteas 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 winReduce
render_artifact_storage_sectioncognitive 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 winReduce
get_storage_analysiscognitive 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 owntry/except RuntimeErrorand returning the accumulated items and GB total.get_storage_analysisthen 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 winReplace ambiguous
×withxin the docstring.Ruff flags
×(U+00D7 MULTIPLICATION SIGN) as ambiguous versusx(RUF002). Replace it with a plainxin "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 liftReduce 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 winPrivate free-tier limits (2000 min / 500 MB) are re-declared independently in four files.
usage_split.pydefines 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_GBand_PRIVATE_MINUTES_LIMITto 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_MBwith an import fromusage_split.src/github_usage/report_summary_insights.py#L31-L31: replace the localfree_min_limit = 2000(also at Line 51free_storage_mb = 500and Line 202free_min_limit = 2000) with imports fromusage_split.src/github_usage/export_visibility.py#L30-L30: replace the inline2000literal invisibility_summary_rowswith 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 winStorage insight still uses the legacy combined
storage_percent, not private-first.The new insight at Line 206-211 uses
private_minutes_percentfor private-first framing, but the storage insight at Line 221-222 still checks the legacystorage_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., usingprivate_storage_avg_mbagainst the 500 MB private limit) when the visibility split is present, falling back tostorage_percentotherwise.🤖 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 liftReduce 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_utilizationinto 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 liftReduce 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_visibilityabove) 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
📒 Files selected for processing (38)
CHANGELOG.mdREADME.mdTO_DO.mddocs/superpowers/plans/archived/2026-07-30-private-usage-emphasis.mdsrc/github_usage/email_report_html.pysrc/github_usage/email_report_text.pysrc/github_usage/export_csv.pysrc/github_usage/export_pdf.pysrc/github_usage/export_visibility.pysrc/github_usage/export_xlsx.pysrc/github_usage/legacy_report_data.pysrc/github_usage/legacy_report_summary.pysrc/github_usage/legacy_terminal.pysrc/github_usage/report_actions.pysrc/github_usage/report_actions_limits.pysrc/github_usage/report_cache.pysrc/github_usage/report_data.pysrc/github_usage/report_forecast.pysrc/github_usage/report_forecast_data.pysrc/github_usage/report_optional.pysrc/github_usage/report_storage.pysrc/github_usage/report_summary.pysrc/github_usage/report_summary_insights.pysrc/github_usage/setup_ci.pysrc/github_usage/storage.pysrc/github_usage/usage_split.pytests/fixtures/export_report_data.jsontests/test_export_csv.pytests/test_export_json.pytests/test_export_pdf.pytests/test_export_xlsx.pytests/test_legacy_report_data.pytests/test_legacy_report_summary.pytests/test_report_actions.pytests/test_report_cache.pytests/test_report_optional.pytests/test_storage.pytests/test_usage_split.py
|
|
||
| ### 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. |
There was a problem hiding this comment.
🗄️ 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.
| - **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.
| 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>', | ||
| ] | ||
| ) |
There was a problem hiding this comment.
🎯 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: addpublic["storage_avg_mb"]to the public summary text.src/github_usage/email_report_text.py#L105-L156: addpublic["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.
| _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) |
There was a problem hiding this comment.
🗄️ 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.
| if actions.get("filtered") and float(actions.get("private_minutes") or 0.0) == 0.0: | ||
| forecast["scanned_private_only"] = True |
There was a problem hiding this comment.
🎯 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.
📍 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.
| sorted_by_storage = sorted( | ||
| storage_analysis.get("repos", []), key=lambda x: x["total_storage"], reverse=True | ||
| ) |
There was a problem hiding this comment.
🩺 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.
|
|
||
| - [ ] 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`) |
There was a problem hiding this comment.
🔒 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>
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>
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>
Public Actions logs must not expose REPORT_EMAIL; confirm with a generic success line instead. Co-authored-by: Cursor <cursoragent@cursor.com>
|



@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:
Why:
Summary by CodeRabbit
New Features
Bug Fixes
Documentation