Skip to content

Commit 0f171c2

Browse files
committed
Fix monthly commit discovery and backfill safeguards
1 parent 627d608 commit 0f171c2

7 files changed

Lines changed: 823 additions & 74 deletions

File tree

.codex/tasks/todo.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
## task-monthly-commit-repair | 2026-07-25 13:44 | Repair and backfill monthly commit statistics
2+
3+
- [x] Fast-forward the clean local `master` branch.
4+
- [x] Add monthly-only repository discovery and completeness validation.
5+
- [x] Preserve cached current-month counts on incomplete or regressing refreshes.
6+
- [x] Add atomic dry-run and force-backfill behavior with reporting.
7+
- [x] Enable strict workflow validation and action-summary warnings.
8+
- [x] Add regression and metric-scope isolation coverage.
9+
- [x] Run the full unit suite and generated-output validation.
10+
- [ ] Run and review a live 13-month dry-run with a minimally scoped token.
11+
- [ ] Apply a validated backfill and regenerate dependent artifacts if all months are complete.
12+
- [x] Review the final diff and record residual risks.
13+
14+
Completion review, 2026-07-25 14:20 CDT:
15+
16+
- The 13-month GraphQL discovery smoke test is complete for every month, and July now discovers 10 contribution repositories with no coverage gap.
17+
- Python 3.8 and local-environment suites pass with 98 tests; workflow lint, bytecode compilation, generated-output validation, and diff checks pass.
18+
- Residual blocker: the only available local credentials are broader than the approved rollout scope. A minimally scoped token is still required before the full REST dry-run, artifact backfill, Actions secret update, and manual workflow dispatch.

.github/workflows/main.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ permissions:
1212

1313
env:
1414
GENERATE_EXPERIMENTAL: true
15+
STRICT_MONTHLY_COMMIT_VALIDATION: true
1516

1617
jobs:
1718
build:

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,19 @@ Local generation may update `generated/*.svg`, `generated/*.json`, and
145145
`run-report.json`. Do not commit `.env`, and do not paste personal access
146146
tokens into READMEs, issues, workflow logs, or bug reports.
147147

148+
To audit the complete rolling monthly-commit window without changing generated
149+
files, run:
150+
151+
```sh
152+
GITHUB_ACTOR=pRizz python3 scripts/backfill_monthly_commits.py --force --dry-run
153+
```
154+
155+
Provide `ACCESS_TOKEN` through a secure environment or credential manager using
156+
a minimally scoped token with access to the repositories the metric should
157+
cover. Review every month in the printed diff before omitting `--dry-run` to
158+
apply the backfill. Incomplete GraphQL discovery or any degraded commit request
159+
prevents a forced backfill from replacing the committed JSON or SVG.
160+
148161
# Installation
149162

150163
<!-- TODO: Add details and screenshots -->

generate_images.py

Lines changed: 143 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import json
88
import os
99
import re
10+
import tempfile
1011
from typing import Any, Dict, List, Optional, Set, Tuple
1112

1213
import aiohttp
@@ -183,6 +184,14 @@ def render_action_summary(report: Dict[str, Any]) -> str:
183184
monthly = report.get("monthly_commits", {})
184185
monthly_total = monthly.get("total", 0)
185186
monthly_current = monthly.get("current_month", {})
187+
monthly_source = monthly.get("source", {})
188+
monthly_discovery = monthly_source.get("discovery", {})
189+
monthly_discovery_warnings = monthly_discovery.get("warnings", [])
190+
monthly_discovery_warning_text = (
191+
"\n".join(f"- {warning}" for warning in monthly_discovery_warnings)
192+
if monthly_discovery_warnings
193+
else "- None"
194+
)
186195
experimental = report.get("experimental", {})
187196
experimental_limited = experimental.get("limited_data", [])
188197
star_history = report.get("star_history", {})
@@ -199,6 +208,10 @@ def render_action_summary(report: Dict[str, Any]) -> str:
199208
- Merged pull requests: {stats["merged_pull_requests"]:,}
200209
- Monthly commits shown: {monthly_total:,}
201210
- Current month commits: {monthly_current.get("count", 0):,}
211+
- Monthly scan repositories: {monthly_source.get("repo_count", 0):,}
212+
- Monthly contribution repositories discovered: {monthly_discovery.get("contribution_repo_count", 0):,}
213+
- Monthly discovery incomplete: {len(monthly_discovery.get("incomplete_months", []))}
214+
- Monthly discovery warnings: {len(monthly_discovery.get("warnings", []))}
202215
- Repositories: {stats["repos"]:,}
203216
- Repository views: {stats["views"]:,}
204217
- Star history samples: {star_history.get("samples", 0):,}
@@ -235,6 +248,9 @@ def render_action_summary(report: Dict[str, Any]) -> str:
235248
### Monthly Commit Scan Warnings
236249
237250
{_format_degradation_items(monthly_commit_items)}
251+
### Monthly Discovery Warnings
252+
253+
{monthly_discovery_warning_text}
238254
"""
239255

240256

@@ -261,6 +277,20 @@ def validate_run_report(report: Dict[str, Any]) -> None:
261277
f"{traffic_degraded_count} traffic endpoints degraded."
262278
)
263279

280+
strict_monthly_commits = env_truthy("STRICT_MONTHLY_COMMIT_VALIDATION")
281+
monthly = report.get("monthly_commits", {})
282+
monthly_source = monthly.get("source", {})
283+
current_month = monthly.get("current_month", {})
284+
if strict_monthly_commits and (
285+
current_month.get("scan_degraded")
286+
or int(monthly_source.get("degraded_months", 0)) > 0
287+
or len(api.get("monthly_commits_degraded", [])) > 0
288+
):
289+
failures.append(
290+
"Monthly commit discovery or scanning is incomplete; "
291+
"generated monthly artifacts were not accepted."
292+
)
293+
264294
if failures:
265295
raise RuntimeError(
266296
"Generated metrics failed validation:\n- " + "\n- ".join(failures)
@@ -295,6 +325,7 @@ async def build_run_report(s: Stats) -> Dict[str, Any]:
295325
"total": sum(int(item.get("count", 0)) for item in months),
296326
"current_month": current_month,
297327
"month_count": len(months),
328+
"source": monthly_cache.get("source", {}),
298329
},
299330
"star_history": {
300331
"samples": len(star_history_records),
@@ -544,9 +575,10 @@ async def build_monthly_commit_cache(
544575
force_backfill: bool = False,
545576
now: Optional[dt.datetime] = None,
546577
cache_path: str = MONTHLY_COMMITS_CACHE,
578+
write_cache: bool = True,
547579
) -> Dict[str, Any]:
548580
windows = rolling_month_windows(now)
549-
existing = {} if force_backfill else load_monthly_commits_cache(cache_path)
581+
existing = load_monthly_commits_cache(cache_path)
550582
existing_by_key = _monthly_cache_records(existing)
551583
windows_to_scan = [
552584
window
@@ -558,30 +590,61 @@ async def build_monthly_commit_cache(
558590
scanned_counts: Dict[str, int] = {}
559591
audit_counts: Dict[str, int] = {}
560592
degraded_scan_keys: Set[str] = set()
561-
repo_count = len(await s.repos)
593+
discovery_repo_count = 0
594+
discovery_incomplete_keys: Set[str] = set()
595+
warnings: List[str] = []
596+
base_repos = set(await s.repos)
597+
configured_extra_repos = parse_repo_list(
598+
os.getenv("MONTHLY_COMMITS_EXTRA_REPOS")
599+
)
600+
repo_count = len(base_repos | configured_extra_repos)
562601
if windows_to_scan:
563-
audit_counts = await s.audit_monthly_commit_counts(windows_to_scan)
602+
discovery = await s.discover_monthly_commit_repositories(windows_to_scan)
603+
audit_counts = discovery.attributed_counts
604+
discovery_repo_count = len(discovery.repositories)
605+
discovery_incomplete_keys = discovery.incomplete_months
606+
degraded_scan_keys.update(discovery_incomplete_keys)
564607
scan_result = await s.scan_monthly_commits(
565608
windows_to_scan,
566609
identity_patterns=patterns,
567-
extra_repos=parse_repo_list(os.getenv("MONTHLY_COMMITS_EXTRA_REPOS")),
610+
extra_repos=discovery.repositories | configured_extra_repos,
568611
)
569612
scanned_counts = scan_result.counts
570-
degraded_scan_keys = scan_result.degraded_months
613+
degraded_scan_keys.update(scan_result.degraded_months)
571614
repo_count = scan_result.repo_count
572615

573616
records = []
574617
for window in windows:
618+
cached_count = int(existing_by_key.get(window.key, {}).get("count", 0))
619+
maybe_scanned_count = scanned_counts.get(window.key)
620+
if (
621+
not force_backfill
622+
and window.is_current
623+
and window.key in existing_by_key
624+
and maybe_scanned_count is not None
625+
and maybe_scanned_count < cached_count
626+
):
627+
degraded_scan_keys.add(window.key)
628+
warnings.append(
629+
f"{window.key}: candidate count {maybe_scanned_count} "
630+
f"regressed below cached count {cached_count}"
631+
)
632+
575633
should_preserve_cached_count = (
576634
window.key in degraded_scan_keys and window.key in existing_by_key
577635
)
578636
if window.key in scanned_counts and not should_preserve_cached_count:
579637
record = _record_from_window(window, scanned_counts[window.key])
580638
else:
581-
cached_count = int(existing_by_key.get(window.key, {}).get("count", 0))
582639
record = _record_from_window(window, cached_count)
583640
record_data = record.__dict__
584641
record_data["scan_degraded"] = window.key in degraded_scan_keys
642+
if maybe_scanned_count is not None:
643+
record_data["scan_candidate_count"] = int(maybe_scanned_count)
644+
elif "scan_candidate_count" in existing_by_key.get(window.key, {}):
645+
record_data["scan_candidate_count"] = int(
646+
existing_by_key[window.key]["scan_candidate_count"]
647+
)
585648
if window.key in audit_counts:
586649
record_data["github_attributed_count"] = audit_counts[window.key]
587650
else:
@@ -611,10 +674,24 @@ async def build_monthly_commit_cache(
611674
"scanned_months": len(windows_to_scan),
612675
"degraded_months": len(degraded_scan_keys),
613676
"degraded_repo_months": len(s.report.monthly_commits_degraded),
677+
"discovery": {
678+
"base_repo_count": len(base_repos),
679+
"contribution_repo_count": discovery_repo_count,
680+
"configured_extra_repo_count": len(configured_extra_repos),
681+
"scan_repo_count": repo_count,
682+
"incomplete_months": sorted(discovery_incomplete_keys),
683+
"warnings": warnings,
684+
},
614685
},
615686
"months": records,
616687
}
617-
write_monthly_commits_cache(cache, cache_path)
688+
if force_backfill and degraded_scan_keys and write_cache:
689+
raise RuntimeError(
690+
"Monthly commit backfill is incomplete for: "
691+
+ ", ".join(sorted(degraded_scan_keys))
692+
)
693+
if write_cache:
694+
write_monthly_commits_cache(cache, cache_path)
618695
return cache
619696

620697

@@ -1191,19 +1268,8 @@ def _monthly_commit_bars(months: List[Dict[str, Any]]) -> str:
11911268
return "\n".join(bars)
11921269

11931270

1194-
async def generate_monthly_commits(
1195-
s: Stats,
1196-
force_backfill: bool = False,
1197-
now: Optional[dt.datetime] = None,
1198-
cache_path: str = MONTHLY_COMMITS_CACHE,
1199-
) -> None:
1200-
cache = await build_monthly_commit_cache(
1201-
s,
1202-
force_backfill=force_backfill,
1203-
now=now,
1204-
cache_path=cache_path,
1205-
)
1206-
1271+
def render_monthly_commits_svg(cache: Dict[str, Any]) -> str:
1272+
"""Render the monthly commit SVG from a validated cache candidate."""
12071273
with open("templates/monthly-commits.svg", "r") as f:
12081274
output = f.read()
12091275

@@ -1220,15 +1286,68 @@ async def generate_monthly_commits(
12201286
html.escape(str(current_month.get("label", ""))),
12211287
output,
12221288
)
1223-
output = re.sub(
1289+
return re.sub(
12241290
r"{{ current_count }}",
12251291
f"{int(current_month.get('count', 0)):,}",
12261292
output,
12271293
)
12281294

1229-
generate_output_folder()
1230-
with open("generated/monthly-commits.svg", "w") as f:
1231-
f.write(output)
1295+
1296+
def _atomic_write_text(path: str, content: str) -> None:
1297+
parent = os.path.dirname(path)
1298+
if parent and not os.path.isdir(parent):
1299+
os.makedirs(parent)
1300+
temp_parent = parent or "."
1301+
target_mode = os.stat(path).st_mode & 0o777 if os.path.exists(path) else 0o644
1302+
temporary_path = ""
1303+
try:
1304+
with tempfile.NamedTemporaryFile(
1305+
"w",
1306+
encoding="utf-8",
1307+
dir=temp_parent,
1308+
delete=False,
1309+
) as temporary_file:
1310+
temporary_file.write(content)
1311+
temporary_path = temporary_file.name
1312+
os.chmod(temporary_path, target_mode)
1313+
os.replace(temporary_path, path)
1314+
finally:
1315+
if temporary_path and os.path.exists(temporary_path):
1316+
os.unlink(temporary_path)
1317+
1318+
1319+
def write_monthly_commit_artifacts(
1320+
cache: Dict[str, Any],
1321+
cache_path: str = MONTHLY_COMMITS_CACHE,
1322+
svg_path: str = os.path.join("generated", "monthly-commits.svg"),
1323+
) -> None:
1324+
"""Replace monthly JSON and SVG via same-directory atomic writes."""
1325+
cache_output = json.dumps(cache, indent=2) + "\n"
1326+
svg_output = render_monthly_commits_svg(cache)
1327+
_atomic_write_text(cache_path, cache_output)
1328+
_atomic_write_text(svg_path, svg_output)
1329+
1330+
1331+
async def generate_monthly_commits(
1332+
s: Stats,
1333+
force_backfill: bool = False,
1334+
now: Optional[dt.datetime] = None,
1335+
cache_path: str = MONTHLY_COMMITS_CACHE,
1336+
) -> None:
1337+
cache = await build_monthly_commit_cache(
1338+
s,
1339+
force_backfill=force_backfill,
1340+
now=now,
1341+
cache_path=cache_path,
1342+
write_cache=False,
1343+
)
1344+
degraded_months = int(cache.get("source", {}).get("degraded_months", 0))
1345+
if force_backfill and degraded_months > 0:
1346+
raise RuntimeError(
1347+
"Refusing to write incomplete monthly commit backfill "
1348+
f"({degraded_months} degraded months)."
1349+
)
1350+
write_monthly_commit_artifacts(cache, cache_path=cache_path)
12321351

12331352

12341353
def _svg_text(value: Any) -> str:

0 commit comments

Comments
 (0)