-
Notifications
You must be signed in to change notification settings - Fork 6
ci: add python-core-coverage gate for band-sdk-core-core #636
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
AlexanderZ-Band
wants to merge
18
commits into
main
Choose a base branch
from
int-1304-add-80-band-sdk-core-coverage-gate-from-band-sdk-python
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
0198fd3
ci: add python-core-coverage gate for band-sdk-core-core
AlexanderZ-Band 10b5622
ci: allow manual dispatch of python-core-coverage
AlexanderZ-Band 4947bb5
test: throwaway trigger for python-core-coverage.yml verification
AlexanderZ-Band e07764e
fix: authenticate the band-sdk-core checkout with a scoped App token
AlexanderZ-Band 259f318
test: remove throwaway python-core-coverage.yml trigger
AlexanderZ-Band e61dcdd
fix: harden python-core-coverage.yml against automated review findings
AlexanderZ-Band 71f94df
fix: address cycle-2 review findings on python-core-coverage.yml
AlexanderZ-Band f4ed698
fix: run coverage guard test with Git Bash on Windows
AlexanderZ-Band a7651e2
fix: use scoped key for Core coverage checkout
AlexanderZ-Band a0bf60c
fix: bump Core coverage dependency
AlexanderZ-Band ea8b00e
ci: report weekly Core coverage
AlexanderZ-Band b66931b
fix: skip cancelled weekly coverage reports
AlexanderZ-Band 50e859d
ci: report manual Core coverage runs
AlexanderZ-Band 3ef1a51
ci: clarify Core coverage digests
AlexanderZ-Band f9f24d8
chore: schedule Core coverage for Sunday night
AlexanderZ-Band f778a0a
ci: improve Core coverage email layout
AlexanderZ-Band 5b17eb1
style: format Core coverage report files
AlexanderZ-Band 36641cf
fix: address Core coverage review findings
AlexanderZ-Band File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| name: "Configure git for HTTPS GitHub access" | ||
| description: >- | ||
| Rewrites git@github.com: URLs to https://github.com/ so a later checkout | ||
| or clone authenticates over HTTPS (e.g. with a token) instead of SSH. | ||
|
|
||
| runs: | ||
| using: composite | ||
| steps: | ||
| - name: Configure git to use HTTPS for GitHub | ||
| shell: bash | ||
| run: git config --global url."https://github.com/".insteadOf "git@github.com:" | ||
|
AlexanderZ-Band marked this conversation as resolved.
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,202 @@ | ||
| #!/usr/bin/env python3 | ||
| """Post a compact weekly digest for a Core consumer-coverage artifact.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import os | ||
| import subprocess | ||
| from dataclasses import dataclass | ||
| from pathlib import Path | ||
|
|
||
| LOW_COVERAGE_PERCENT = 80.0 | ||
| MAX_MISSED_LINE_RANGES = 8 | ||
| MAX_LOW_COVERAGE_FILES = 8 | ||
|
|
||
|
|
||
| @dataclass(frozen=True) | ||
| class FileCoverage: | ||
| path: str | ||
| found: int | ||
| hit: int | ||
| functions_found: int | ||
| functions_hit: int | ||
| missed_lines: tuple[int, ...] | ||
|
|
||
| @property | ||
| def percent(self) -> float: | ||
| return 100 * self.hit / self.found if self.found else 100.0 | ||
|
|
||
| @property | ||
| def missed(self) -> int: | ||
| return self.found - self.hit | ||
|
|
||
|
|
||
| def display_path(path: str) -> str: | ||
| marker = "/crates/" | ||
| return path[path.index(marker) + 1 :] if marker in path else Path(path).name | ||
|
|
||
|
|
||
| def format_line_ranges(numbers: tuple[int, ...]) -> str: | ||
| ranges: list[str] = [] | ||
| start = previous = None | ||
| for number in numbers: | ||
| if start is None: | ||
| start = previous = number | ||
| elif number == previous + 1: | ||
| previous = number | ||
| else: | ||
| ranges.append(str(start) if start == previous else f"{start}-{previous}") | ||
| start = previous = number | ||
| if start is not None: | ||
| ranges.append(str(start) if start == previous else f"{start}-{previous}") | ||
| shown = ", ".join(ranges[:MAX_MISSED_LINE_RANGES]) | ||
| return ( | ||
| shown | ||
| if len(ranges) <= MAX_MISSED_LINE_RANGES | ||
| else f"{shown}, … ({len(ranges) - MAX_MISSED_LINE_RANGES} more ranges)" | ||
| ) | ||
|
|
||
|
|
||
| def coverage_marker(percent: float) -> str: | ||
| if percent >= LOW_COVERAGE_PERCENT: | ||
| return "🟢" | ||
| if percent >= 50: | ||
| return "🟠" | ||
| return "🔴" | ||
|
|
||
|
|
||
| def parse_lcov(path: Path) -> list[FileCoverage]: | ||
| records: list[FileCoverage] = [] | ||
| source: str | None = None | ||
| found = hit = functions_found = functions_hit = 0 | ||
| missed_lines: list[int] = [] | ||
| for line in path.read_text(encoding="utf-8").splitlines(): | ||
| if line.startswith("SF:"): | ||
| source = line[3:] | ||
| elif line.startswith("LF:"): | ||
| found = int(line[3:]) | ||
| elif line.startswith("LH:"): | ||
| hit = int(line[3:]) | ||
| elif line.startswith("FNF:"): | ||
| functions_found = int(line[4:]) | ||
| elif line.startswith("FNH:"): | ||
| functions_hit = int(line[4:]) | ||
| elif line.startswith("DA:"): | ||
| line_number, count = line[3:].split(",", maxsplit=1) | ||
| if count == "0": | ||
| missed_lines.append(int(line_number)) | ||
| elif line == "end_of_record" and source is not None: | ||
| records.append( | ||
| FileCoverage( | ||
| display_path(source), | ||
| found, | ||
| hit, | ||
| functions_found, | ||
| functions_hit, | ||
| tuple(missed_lines), | ||
| ) | ||
| ) | ||
| source = None | ||
| found = hit = functions_found = functions_hit = 0 | ||
| missed_lines = [] | ||
| return records | ||
|
|
||
|
|
||
| def render_digest( | ||
| *, lcov_path: Path, label: str, recipients: str, run_url: str, result: str | ||
| ) -> str: | ||
| header = "## 📊 Weekly Core coverage" | ||
| if not lcov_path.is_file(): | ||
| return "\n".join( | ||
| [ | ||
| header, | ||
| recipients, | ||
| "", | ||
| f"⚠️ **Coverage unavailable** · workflow `{result}`", | ||
| "", | ||
| "No LCOV report was produced. Open the run for the failure details.", | ||
| "", | ||
| f"[Open run]({run_url})", | ||
| ] | ||
| ) | ||
|
|
||
| files = parse_lcov(lcov_path) | ||
| found = sum(item.found for item in files) | ||
| hit = sum(item.hit for item in files) | ||
| functions_found = sum(item.functions_found for item in files) | ||
| functions_hit = sum(item.functions_hit for item in files) | ||
| gaps = sorted( | ||
| (item for item in files if item.percent < LOW_COVERAGE_PERCENT), | ||
| key=lambda item: (item.percent, -item.found, item.path), | ||
| ) | ||
| line_percent = 100 * hit / found if found else 0.0 | ||
| function_percent = 100 * functions_hit / functions_found if functions_found else 0.0 | ||
| healthy_files = len(files) - len(gaps) | ||
| lines = [ | ||
| header, | ||
| "", | ||
| recipients, | ||
| "", | ||
| f"**{label}**", | ||
| "", | ||
| "### Coverage snapshot", | ||
| "", | ||
| "| Signal | Result |", | ||
| "| --- | --- |", | ||
| f"| Lines | {coverage_marker(line_percent)} **{line_percent:.2f}%** · {hit}/{found} covered · {found - hit} missing |", | ||
| f"| Functions | {coverage_marker(function_percent)} **{function_percent:.2f}%** · {functions_hit}/{functions_found} covered · {functions_found - functions_hit} missing |", | ||
| f"| Files at target | {coverage_marker(100 * healthy_files / len(files) if files else 0)} **{healthy_files}/{len(files)}** at or above {LOW_COVERAGE_PERCENT:.0f}% |", | ||
| "", | ||
| ] | ||
| if gaps: | ||
| lines.extend( | ||
| [ | ||
| "### 🎯 Where to focus", | ||
| "", | ||
| "| Source file | Coverage gap |", | ||
| "| --- | --- |", | ||
| ] | ||
| ) | ||
| lines.extend( | ||
| f"| `{item.path}` | {coverage_marker(item.percent)} **{item.percent:.2f}%** · {item.missed} lines missing<br>Lines `{format_line_ranges(item.missed_lines)}` |" | ||
| for item in gaps[:MAX_LOW_COVERAGE_FILES] | ||
| ) | ||
| if len(gaps) > MAX_LOW_COVERAGE_FILES: | ||
| lines.extend( | ||
| [ | ||
| "", | ||
| f"_Plus {len(gaps) - MAX_LOW_COVERAGE_FILES} more low-coverage files in the artifact._", | ||
| ] | ||
| ) | ||
| else: | ||
| lines.append("✅ Every measured source file meets the coverage target.") | ||
| lines.extend( | ||
| ["", f"[View the run and full coverage artifact →]({run_url}#artifacts)"] | ||
| ) | ||
| return "\n".join(lines) | ||
|
|
||
|
|
||
| def main() -> None: | ||
| digest = render_digest( | ||
| lcov_path=Path(os.environ["LCOV_PATH"]), | ||
| label=os.environ["REPORT_LABEL"], | ||
| recipients=os.environ["RECIPIENTS"], | ||
| run_url=os.environ["RUN_URL"], | ||
| result=os.environ["WORKFLOW_RESULT"], | ||
| ) | ||
| subprocess.run( | ||
| [ | ||
| "gh", | ||
| "api", | ||
| f"repos/{os.environ['REPO']}/commits/{os.environ['SHA']}/comments", | ||
| "--method", | ||
| "POST", | ||
| "-f", | ||
| f"body={digest}", | ||
| ], | ||
| check=True, | ||
| ) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.