Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ versions follow [SemVer](https://semver.org/spec/v2.0.0.html).
- Refresh coverage overview ordering and visibility on PR merge, closure, or reopening,
without waiting for main CI; support manual coverage-only refreshes.
- Add bounded Rust coverage CI with immutable per-run/per-attempt LCOV and HTML artifacts.
- Publish retained coverage history to Pages, including late-attempt archives and pull-request
state filtering.
- Publish one coverage result per PR, preferring exact merge-commit coverage, with a separate
pre-/post-merge view and immutable archives for all runs and attempts.
- Restrict CI cache writes to main, preserve Cargo build timings, verify draft release assets
before immutable publication, and add deployment-only website artwork and shared contributor rules.

Expand Down
16 changes: 10 additions & 6 deletions docs/infrastructure.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,16 @@ Completed, Commit, Workflow, Lines, Branches, and Functions. Coderef's Python ge
rates from summed LCOV counters, links each immutable report and its LCOV/metadata downloads,
and displays completion times in UTC. Zero or unavailable metric totals display `n/a`, including
branch coverage when Rust instrumentation produces no branch measurements. Existing archives
are read without modification. The overview shows the latest run per target, with main first;
`history.html` retains every run and retry, including closed PRs. Run creation time determines
which run is latest, so retrying an older run cannot replace a newer run. Attempts within a run
are compared numerically. PR rows retain their own PR CI result, sorted by merge time after merging;
the main row contains the latest main CI result. Open PRs follow merged PRs. The coverage figures
describe Rust, not the Python or TypeScript scripts.
are read without modification. The overview shows one result per PR, ordered by merge time,
then open PRs. Pre-merge coverage is replaced only when a main report tests that PR's exact
merge commit; unrelated main runs are omitted. Aggregation PR coverage is never attributed to
its constituent PRs. PRs without pre-merge reports can still show post-merge coverage.
`history.html` shows at most one pre-merge and one post-merge result per PR. Run creation time
selects the newest run within each phase, with numeric attempt ordering for retries. Late
pre-merge reports cannot displace post-merge results. Closed unmerged PRs are omitted from both
views and reappear when reopened. All immutable run URLs remain available, including omitted
runs and retries. A minimal `pull-requests.json` registry records merge SHAs for exact attribution.
The coverage figures describe Rust, not the Python or TypeScript scripts.
PR closure (including merge) and reopening trigger a metadata-only refresh using trusted `main`
publisher code. This updates overview ordering and visibility without downloading artifacts or
replacing measured coverage. Manual refreshes use `gh workflow run pages.yml -f coverage_refresh=true`;
Expand Down
52 changes: 40 additions & 12 deletions tools/coverage_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ def _pulls(path: Path) -> dict[int, dict]:
def history(root: Path, pulls_path: Path | None = None) -> list[dict]:
root.mkdir(parents=True, exist_ok=True)
pulls = _pulls(pulls_path) if pulls_path else {}
registry = [{key: pull.get(key) for key in ("number", "state", "merged_at", "merge_commit_sha")}
for pull in pulls.values()]
(root / "pull-requests.json").write_text(json.dumps(registry, indent=2) + "\n", encoding="utf-8")
reports = []
for report in _reports(root):
target = report.get("target", "main")
Expand All @@ -118,24 +121,47 @@ def _run_order(report: dict) -> tuple:

def regenerate(root: Path) -> int:
reports = _read(root / "history.json") if (root / "history.json").exists() else _reports(root)
reports.sort(key=_run_order, reverse=True)
pulls = _pulls(root / "pull-requests.json")
merges = {pull["merge_commit_sha"]: number for number, pull in pulls.items()
if pull.get("merged_at") and pull.get("merge_commit_sha")}
latest = {}
for report in reports:
if report.get("visible", True):
latest.setdefault(report.get("target", "main"), report)
visible = sorted(latest.values(), key=lambda report: (
report.get("target", "main") == "main",
for original in sorted(reports, key=_run_order, reverse=True):
report = dict(original)
target = report.get("target", "main")
phase = ""
if target == "main":
number = merges.get(report.get("head_sha") or report.get("sha"))
if number is None:
continue
target = f"pr/{number}"
phase = "post-merge"
elif target.startswith("pr/"):
phase = "pre-merge"
if target.startswith("pr/"):
pull = pulls.get(int(target[3:]))
if pull is not None:
report["visible"] = pull.get("state") != "closed" or bool(pull.get("merged_at"))
report["reference_time"] = pull.get("merged_at")
if not report.get("visible", True):
continue
report.update(target=target, phase=phase)
latest.setdefault((target, phase), report)
phases = sorted(latest.values(), key=lambda report: (
bool(report.get("reference_time")),
_time(report.get("reference_time") or report.get("created_at") or report.get("completed_at")),
_run_order(report)), reverse=True)
report["phase"], _run_order(report)), reverse=True)
visible = [report for report in phases
if report["phase"] != "pre-merge" or (report["target"], "post-merge") not in latest]
(root / "index.html").write_text(_render(
root, visible, "coderef coverage reports",
"Latest report per target: main first, then merged PRs by merge time, then open PRs. "
"PR rows show PR CI coverage; main shows post-merge CI coverage. Closed unmerged PRs are omitted.",
_link("history.html", "All retained runs and attempts")), encoding="utf-8")
"One result per PR, ordered by merge time, then open PRs. Pre-merge coverage is replaced "
"only by coverage of the exact merge commit. Closed unmerged PRs are omitted; "
"their direct report URLs remain available.",
_link("history.html", "Pre-merge and post-merge results")), encoding="utf-8")
(root / "history.html").write_text(_render(
root, reports, "coderef coverage run history",
"All retained CI runs and attempts, newest run first, including closed PRs.",
root, phases, "coderef pre-merge and post-merge coverage",
"One result per PR phase. Post-merge results test the exact merge commit; "
"retries and unrelated main runs are omitted.",
_link("index.html", "Current coverage overview")), encoding="utf-8")
return len(visible)

Expand All @@ -146,6 +172,8 @@ def _render(root: Path, reports: list[dict], title: str, description: str, navig
target = report.get("target", "main")
path = report["path"]
label = f"PR {target[3:]}" if target.startswith("pr/") else target
if report.get("phase"):
label += f" ({report['phase']})"
if target == "main":
source = _link(f"{_REPOSITORY}/tree/main", "main branch")
elif target.startswith("pr/"):
Expand Down
95 changes: 81 additions & 14 deletions tools/coverage_index_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def test_closed_unmerged_report_hidden(self):
self.assertFalse(next(item for item in reports if item["target"] == "pr/12")["visible"])
regenerate(root)
self.assertNotIn("PR 12", (root / "index.html").read_text())
self.assertIn("PR 12", (root / "history.html").read_text())
self.assertNotIn("PR 12", (root / "history.html").read_text())

def test_coverage_cli_archive_history_regenerate_links_resolve(self):
script = Path(__file__).with_name("coverage_index.py")
Expand Down Expand Up @@ -140,17 +140,17 @@ def test_coverage_overview_empty_and_legacy_metadata_render_safely(self):
self.assertEqual(regenerate(root), 0)
self.assertIn("No coverage reports", (root / "index.html").read_text())
records = [{"path": "runs/7/1", "target": '<script>alert("x")</script>'},
{"path": "runs/8/1", "target": "main", "sha": "123456789"}]
{"path": "runs/8/1", "target": "pr/8", "sha": "123456789"}]
(root / "history.json").write_text(json.dumps(records))
self.assertEqual(regenerate(root), 2)
html = (root / "index.html").read_text()
self.assertNotIn('<script>', html)
self.assertIn('&lt;script&gt;', html)
self.assertIn('href="https://github.com/mboworks/coderef/tree/main"', html)
self.assertIn('href="https://github.com/mboworks/coderef/pull/8"', html)
self.assertIn('href="https://github.com/mboworks/coderef/commit/123456789"', html)
self.assertIn('n/a', html)

def test_coverage_overview_history_preserves_order_and_attempts(self):
def test_coverage_phase_view_collapses_attempts_preserves_archives(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory) / "site"
incoming = Path(directory) / "incoming"
Expand All @@ -159,7 +159,7 @@ def test_coverage_overview_history_preserves_order_and_attempts(self):
("7", "2", "2026-01-03T00:00:00Z"),
("8", "1", "2026-01-02T00:00:00Z")):
(incoming / "metadata.json").write_text(json.dumps({
"run_id": run, "run_attempt": attempt, "target": "main", "completed_at": completed}))
"run_id": run, "run_attempt": attempt, "target": "pr/12", "completed_at": completed}))
archive(root, incoming)
history(root)
self.assertEqual(regenerate(root), 1)
Expand All @@ -169,19 +169,20 @@ def test_coverage_overview_history_preserves_order_and_attempts(self):
self.assertIn('href="history.html"', html)
archived = (root / "history.html").read_text()
links = re.findall(r'href="(runs/[^"]+/html/index.html)"', archived)
self.assertEqual(links, ["runs/7/2/html/index.html", "runs/8/1/html/index.html",
"runs/7/1/html/index.html"])
self.assertEqual(links, ["runs/7/2/html/index.html"])
self.assertTrue((root / "runs/8/1/html/index.html").is_file())
self.assertTrue((root / "runs/7/1/html/index.html").is_file())
self.assertIn('href="index.html"', archived)

def test_overview_late_retry_keeps_latest_run_and_main_first(self):
def test_overview_late_retry_keeps_latest_run_and_numeric_attempt(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
reports = [
{"target": "main", "run_id": "7", "run_attempt": "10", "path": "runs/7/10",
{"target": "pr/11", "run_id": "7", "run_attempt": "10", "path": "runs/7/10",
"created_at": "2026-01-01T00:00:00Z", "completed_at": "2026-01-04T00:00:00Z"},
{"target": "main", "run_id": "8", "run_attempt": "2", "path": "runs/8/2",
{"target": "pr/11", "run_id": "8", "run_attempt": "2", "path": "runs/8/2",
"created_at": "2026-01-02T00:00:00Z"},
{"target": "main", "run_id": "8", "run_attempt": "10", "path": "runs/8/10",
{"target": "pr/11", "run_id": "8", "run_attempt": "10", "path": "runs/8/10",
"created_at": "2026-01-02T00:00:00Z"},
{"target": "pr/12", "run_id": "9", "path": "runs/9/1",
"completed_at": "2026-01-05T00:00:00Z"},
Expand All @@ -193,9 +194,9 @@ def test_overview_late_retry_keeps_latest_run_and_main_first(self):
self.assertEqual(regenerate(root), 2)
html = (root / "index.html").read_text()
links = re.findall(r'href="(runs/[^"]+/html/index.html)"', html)
self.assertEqual(links, ["runs/8/10/html/index.html", "runs/9/1/html/index.html"])
self.assertEqual(links, ["runs/9/1/html/index.html", "runs/8/10/html/index.html"])
archived = (root / "history.html").read_text()
self.assertEqual(len(re.findall(r'href="runs/[^"]+/html/index.html"', archived)), 5)
self.assertEqual(len(re.findall(r'href="runs/[^"]+/html/index.html"', archived)), 2)
self.assertEqual((root / "history.json").read_text(), original)

def test_overview_merged_pr_keeps_pr_run_and_uses_merge_time(self):
Expand All @@ -222,7 +223,7 @@ def test_overview_merged_pr_keeps_pr_run_and_uses_merge_time(self):
regenerate(root)
html = (root / "index.html").read_text()
links = re.findall(r'href="(runs/[^"]+/html/index.html)"', html)
self.assertEqual(links, [f"runs/{run}/1/html/index.html" for run in (1, 2, 3, 4)])
self.assertEqual(links, [f"runs/{run}/1/html/index.html" for run in (2, 3, 4)])

def test_history_refresh_merge_reorders_without_replacing_reports(self):
with tempfile.TemporaryDirectory() as directory:
Expand Down Expand Up @@ -258,6 +259,72 @@ def test_history_refresh_merge_reorders_without_replacing_reports(self):
self.assertEqual({path.relative_to(root): path.read_bytes()
for path in (root / "runs").rglob("*") if path.is_file()}, snapshots)

def test_coverage_exact_merge_reports_replace_pre_merge_preserve_snapshots(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory) / "site"
incoming = Path(directory) / "incoming"
self.report(incoming)
pulls = Path(directory) / "pulls.json"
values = [
{"number": 12, "state": "closed", "merged_at": "2026-01-02T00:00:00Z",
"merge_commit_sha": "merge-12"},
{"number": 13, "state": "closed", "merged_at": "2026-01-03T00:00:00Z",
"merge_commit_sha": "aggregate-13"},
{"number": 14, "state": "closed", "merged_at": "2026-01-04T00:00:00Z",
"merge_commit_sha": "merge-14"},
{"number": 15, "state": "closed", "merged_at": None,
"merge_commit_sha": "unmerged-15"},
]
pulls.write_text(json.dumps([values]))

def publish(run, target, sha, attempt=1):
(incoming / "metadata.json").write_text(json.dumps({
"run_id": run, "run_attempt": attempt, "target": target, "head_sha": sha,
"created_at": f"2026-01-{run:02}T00:00:00Z"}))
archive(root, incoming)
history(root, pulls)
regenerate(root)

def links(page):
return re.findall(r'href="(runs/[^"]+/html/index.html)"',
(root / page).read_text())

publish(1, "pr/12", "pre-12")
publish(2, "pr/13", "pre-13")
publish(5, "main", "aggregate-13")
# Aggregation coverage must not be attributed to its constituent PR.
self.assertEqual(links("index.html"), ["runs/5/1/html/index.html", "runs/1/1/html/index.html"])
publish(6, "main", "merge-14") # Post-merge-only PR.
publish(4, "main", "merge-12") # Older main arrives late.
publish(4, "main", "merge-12", attempt=10)
publish(4, "main", "merge-12", attempt=2)
publish(9, "pr/12", "late-pre-12") # Never displace post-merge.
publish(10, "main", "unrelated")
publish(11, "main", "unmerged-15") # A synthetic merge SHA is insufficient.
self.assertEqual(links("index.html"), ["runs/6/1/html/index.html",
"runs/5/1/html/index.html", "runs/4/10/html/index.html"])
self.assertEqual(links("history.html"), ["runs/6/1/html/index.html",
"runs/2/1/html/index.html", "runs/5/1/html/index.html",
"runs/9/1/html/index.html", "runs/4/10/html/index.html"])
self.assertIn("PR 12 (post-merge)", (root / "index.html").read_text())
self.assertIn("PR 12 (pre-merge)", (root / "history.html").read_text())
snapshots = {path.relative_to(root): path.read_bytes()
for path in (root / "runs").rglob("*") if path.is_file()}
# Closure hides both views; reopening restores the retained pre-merge report.
values[0].update(merged_at=None, state="closed")
pulls.write_text(json.dumps(values))
history(root, pulls)
regenerate(root)
for page in ("index.html", "history.html"):
self.assertNotIn("PR 12", (root / page).read_text())
values[0]["state"] = "open"
pulls.write_text(json.dumps(values))
history(root, pulls)
regenerate(root)
self.assertIn("PR 12 (pre-merge)", (root / "index.html").read_text())
self.assertEqual({path.relative_to(root): path.read_bytes()
for path in (root / "runs").rglob("*") if path.is_file()}, snapshots)

def test_history_refresh_before_first_report_renders_empty_overview(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory) / "coverage"
Expand Down
Loading