diff --git a/.github/autograding/coverage-annotations-ai.json b/.github/autograding/coverage-annotations-ai.json new file mode 100644 index 00000000..3cbcc154 --- /dev/null +++ b/.github/autograding/coverage-annotations-ai.json @@ -0,0 +1,27 @@ +{ + "coverage": [ + { + "name": "AI Coverage Annotations", + "sourcePath": "ai/src/main/java", + "maxScore": 100, + "coveredPercentageImpact": 1, + "missedPercentageImpact": 0, + "tools": [ + { + "id": "jacoco", + "name": "Line Coverage", + "metric": "line", + "sourcePath": "ai/src/main/java", + "pattern": "**/pr-artifacts/ai/build/reports/kover/report.xml" + }, + { + "id": "jacoco", + "name": "Branch Coverage", + "metric": "branch", + "sourcePath": "ai/src/main/java", + "pattern": "**/pr-artifacts/ai/build/reports/kover/report.xml" + } + ] + } + ] +} diff --git a/.github/autograding/coverage-annotations-app.json b/.github/autograding/coverage-annotations-app.json new file mode 100644 index 00000000..c115eaaf --- /dev/null +++ b/.github/autograding/coverage-annotations-app.json @@ -0,0 +1,27 @@ +{ + "coverage": [ + { + "name": "App Coverage Annotations", + "sourcePath": "app/src/main/java", + "maxScore": 100, + "coveredPercentageImpact": 1, + "missedPercentageImpact": 0, + "tools": [ + { + "id": "jacoco", + "name": "Line Coverage", + "metric": "line", + "sourcePath": "app/src/main/java", + "pattern": "**/pr-artifacts/app/build/reports/kover/report.xml" + }, + { + "id": "jacoco", + "name": "Branch Coverage", + "metric": "branch", + "sourcePath": "app/src/main/java", + "pattern": "**/pr-artifacts/app/build/reports/kover/report.xml" + } + ] + } + ] +} diff --git a/.github/autograding/coverage-annotations-data.json b/.github/autograding/coverage-annotations-data.json new file mode 100644 index 00000000..fd27fae0 --- /dev/null +++ b/.github/autograding/coverage-annotations-data.json @@ -0,0 +1,27 @@ +{ + "coverage": [ + { + "name": "Data Coverage Annotations", + "sourcePath": "data/src/main/java", + "maxScore": 100, + "coveredPercentageImpact": 1, + "missedPercentageImpact": 0, + "tools": [ + { + "id": "jacoco", + "name": "Line Coverage", + "metric": "line", + "sourcePath": "data/src/main/java", + "pattern": "**/pr-artifacts/data/build/reports/kover/report.xml" + }, + { + "id": "jacoco", + "name": "Branch Coverage", + "metric": "branch", + "sourcePath": "data/src/main/java", + "pattern": "**/pr-artifacts/data/build/reports/kover/report.xml" + } + ] + } + ] +} diff --git a/.github/autograding/coverage-annotations-domain.json b/.github/autograding/coverage-annotations-domain.json new file mode 100644 index 00000000..a3e04870 --- /dev/null +++ b/.github/autograding/coverage-annotations-domain.json @@ -0,0 +1,27 @@ +{ + "coverage": [ + { + "name": "Domain Coverage Annotations", + "sourcePath": "domain/src/main/java", + "maxScore": 100, + "coveredPercentageImpact": 1, + "missedPercentageImpact": 0, + "tools": [ + { + "id": "jacoco", + "name": "Line Coverage", + "metric": "line", + "sourcePath": "domain/src/main/java", + "pattern": "**/pr-artifacts/domain/build/reports/kover/report.xml" + }, + { + "id": "jacoco", + "name": "Branch Coverage", + "metric": "branch", + "sourcePath": "domain/src/main/java", + "pattern": "**/pr-artifacts/domain/build/reports/kover/report.xml" + } + ] + } + ] +} diff --git a/.github/quality-monitor/coverage-config.json b/.github/quality-monitor/coverage-config.json new file mode 100644 index 00000000..c10bcf0f --- /dev/null +++ b/.github/quality-monitor/coverage-config.json @@ -0,0 +1,24 @@ +{ + "coverage": [ + { + "name": "Line Coverage", + "tools": [ + { + "id": "jacoco", + "metric": "line", + "pattern": "**/pr-artifacts/build/reports/kover/report.xml" + } + ] + }, + { + "name": "Instruction Coverage", + "tools": [ + { + "id": "jacoco", + "metric": "instruction", + "pattern": "**/pr-artifacts/build/reports/kover/report.xml" + } + ] + } + ] +} diff --git a/.github/scripts/pr/build_pr_summary.py b/.github/scripts/pr/build_pr_summary.py index f62a22c5..b82ccb29 100644 --- a/.github/scripts/pr/build_pr_summary.py +++ b/.github/scripts/pr/build_pr_summary.py @@ -63,23 +63,6 @@ def count_sarif_results(files: list[Path]) -> int: return count -def aggregate_line_coverage(files: list[Path]) -> tuple[int, int]: - covered = 0 - missed = 0 - for file in files: - try: - root = ET.parse(file).getroot() - except ET.ParseError: - continue - - for counter in root.findall("./counter"): - if counter.attrib.get("type") == "LINE": - covered += int(counter.attrib.get("covered", "0")) - missed += int(counter.attrib.get("missed", "0")) - break - return covered, missed - - def format_test_line(label: str, metrics: dict[str, int]) -> str: total_failures = metrics["failures"] + metrics["errors"] status = "PASS" if total_failures == 0 else "FAIL" @@ -104,7 +87,6 @@ def main() -> int: instrumentation_xml = sorted(artifacts_dir.glob("**/instrumentation-results.xml")) lint_xml = sorted(artifacts_dir.glob("**/build/reports/lint-results-*.xml")) ktlint_xml = sorted(artifacts_dir.glob("**/build/reports/ktlint/**/*.xml")) - kover_xml = sorted(artifacts_dir.glob("**/build/reports/kover/**/*.xml")) detekt_sarif = sorted(artifacts_dir.glob("**/build/reports/detekt/*.sarif")) gitleaks_sarif = sorted(artifacts_dir.glob("**/build/reports/gitleaks/*.sarif")) @@ -114,10 +96,6 @@ def main() -> int: ktlint_issues = count_ktlint_issues(ktlint_xml) detekt_findings = count_sarif_results(detekt_sarif) gitleaks_findings = count_sarif_results(gitleaks_sarif) - covered, missed = aggregate_line_coverage(kover_xml) - total_lines = covered + missed - coverage = (covered / total_lines * 100.0) if total_lines else None - pr_number = sys.argv[3] head_sha = sys.argv[4] run_url = sys.argv[5] @@ -145,13 +123,6 @@ def main() -> int: f"- detekt: {detekt_findings} finding(s) uploaded via SARIF" if detekt_sarif else "- detekt: no SARIF reports found", f"- gitleaks: {gitleaks_findings} finding(s) uploaded via SARIF" if gitleaks_sarif else "- gitleaks: no SARIF reports found", "- CodeQL: see the Code Scanning section in this PR", - "", - "### Coverage", - ( - f"- Line coverage: {coverage:.2f}% ({covered} covered / {total_lines} total)" - if coverage is not None - else "- Line coverage: no Kover XML reports found" - ), ] output_file.write_text("\n".join(line for line in lines if line is not None) + "\n") diff --git a/.github/scripts/pr/list_downloaded_artifacts.sh b/.github/scripts/pr/list_downloaded_artifacts.sh index 4951d124..8e00cf26 100644 --- a/.github/scripts/pr/list_downloaded_artifacts.sh +++ b/.github/scripts/pr/list_downloaded_artifacts.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash set -euo pipefail -find "${1:-pr-artifacts}" -maxdepth 4 -type f | sort +find "${1:-pr-artifacts}" -maxdepth 8 -type f | sort diff --git a/.github/scripts/quality/build_coverage_summary.py b/.github/scripts/quality/build_coverage_summary.py new file mode 100644 index 00000000..b0d9a5b5 --- /dev/null +++ b/.github/scripts/quality/build_coverage_summary.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import sys +import xml.etree.ElementTree as ET +from pathlib import Path + + +MetricCounts = dict[str, tuple[int, int]] +METRICS: list[tuple[str, str]] = [ + ("LINE", "Line"), + ("INSTRUCTION", "Instruction"), + ("BRANCH", "Branch"), + ("METHOD", "Method"), + ("CLASS", "Class"), +] + + +def read_counters(report: Path) -> MetricCounts | None: + try: + root = ET.parse(report).getroot() + except ET.ParseError: + return None + + counters: MetricCounts = {} + for counter in root.findall("./counter"): + metric = counter.attrib.get("type") + if metric is None: + continue + counters[metric] = ( + int(counter.attrib.get("covered", "0")), + int(counter.attrib.get("missed", "0")), + ) + + return counters or None + + +def format_ratio(covered: int, missed: int) -> str: + total = covered + missed + if total == 0: + return "n/a" + return f"{covered / total * 100:.2f}%" + + +def format_metric_cell(counts: MetricCounts, metric: str) -> str: + covered, missed = counts.get(metric, (0, 0)) + total = covered + missed + if total == 0: + return "n/a (0/0)" + return f"{format_ratio(covered, missed)} ({covered}/{total})" + + +def render_table_row(cells: list[str]) -> str: + return f"| {' | '.join(cells)} |" + + +def discover_reports(reports_root: Path) -> tuple[MetricCounts | None, list[tuple[str, MetricCounts]]]: + aggregate_report = reports_root / "build" / "reports" / "kover" / "report.xml" + if aggregate_report.exists(): + aggregate_counts = read_counters(aggregate_report) + module_rows: list[tuple[str, MetricCounts]] = [] + for report in reports_root.glob("*/build/reports/kover/report.xml"): + counts = read_counters(report) + if counts is None: + continue + module_rows.append((report.relative_to(reports_root).parts[0], counts)) + return aggregate_counts, module_rows + + aggregate_counts = None + module_rows = [] + fallback_rows = [] + + for report in sorted(reports_root.glob("**/build/reports/kover/report.xml")): + counts = read_counters(report) + if counts is None: + continue + + rel = report.relative_to(reports_root) + parts = rel.parts + + if parts == ("build", "reports", "kover", "report.xml"): + aggregate_counts = counts + elif len(parts) >= 5 and parts[1:5] == ("build", "reports", "kover", "report.xml"): + module_rows.append((parts[0], counts)) + else: + fallback_rows.append((str(rel.parent), counts)) + + return aggregate_counts, module_rows or fallback_rows + + +def main() -> int: + if len(sys.argv) != 3: + print("usage: build_coverage_summary.py ", file=sys.stderr) + return 2 + + reports_root = Path(sys.argv[1]) + output_file = Path(sys.argv[2]) + + aggregate_counts, rows = discover_reports(reports_root) + + if aggregate_counts is None and rows: + aggregate_counts = {} + for metric, _ in METRICS: + covered = sum(counts.get(metric, (0, 0))[0] for _, counts in rows) + missed = sum(counts.get(metric, (0, 0))[1] for _, counts in rows) + aggregate_counts[metric] = (covered, missed) + + lines = [ + "## Coverage Details", + "", + ] + + if aggregate_counts is not None: + lines.extend( + [ + "", + "### Overall", + "", + "| Metric | Coverage | Covered | Missed | Total |", + "| --- | ---: | ---: | ---: | ---: |", + ] + ) + + for metric, label in METRICS: + covered, missed = aggregate_counts.get(metric, (0, 0)) + total = covered + missed + lines.append( + render_table_row([label, format_ratio(covered, missed), str(covered), str(missed), str(total)]) + ) + + if rows: + lines.extend( + [ + "", + "### By Module", + "", + "| Module | Line | Instruction | Branch | Method | Class |", + "| --- | ---: | ---: | ---: | ---: | ---: |", + ] + ) + + for module, counts in sorted(rows): + lines.append( + render_table_row( + [ + f"`{module}`", + format_metric_cell(counts, "LINE"), + format_metric_cell(counts, "INSTRUCTION"), + format_metric_cell(counts, "BRANCH"), + format_metric_cell(counts, "METHOD"), + format_metric_cell(counts, "CLASS"), + ] + ) + ) + elif aggregate_counts is None: + lines.extend(["", "- No Kover XML reports found"]) + + output_file.write_text("\n".join(lines) + "\n", encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.github/scripts/quality/build_uncovered_coverage_comment.py b/.github/scripts/quality/build_uncovered_coverage_comment.py new file mode 100644 index 00000000..fedf4072 --- /dev/null +++ b/.github/scripts/quality/build_uncovered_coverage_comment.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import sys +import xml.etree.ElementTree as ET +from pathlib import Path + + +MAX_FILES = 10 +MAX_RANGES_PER_FILE = 5 + + +def collect_uncovered_ranges(report: Path, module: str, repo_root: Path) -> list[tuple[str, list[str]]]: + try: + root = ET.parse(report).getroot() + except ET.ParseError: + return [] + + uncovered: list[tuple[str, list[str]]] = [] + + for package in root.findall("./package"): + package_name = package.attrib.get("name", "").strip("/") + for sourcefile in package.findall("./sourcefile"): + source_name = sourcefile.attrib.get("name") + if not source_name: + continue + + lines = [] + for line in sourcefile.findall("./line"): + missed = int(line.attrib.get("mi", "0")) + covered = int(line.attrib.get("ci", "0")) + if missed > 0 and covered == 0: + lines.append(int(line.attrib["nr"])) + + if not lines: + continue + + file_path = resolve_source_path(repo_root, module, package_name, source_name) + uncovered.append((file_path.as_posix(), collapse_ranges(sorted(lines)))) + + return uncovered + + +def resolve_source_path(repo_root: Path, module: str, package_name: str, source_name: str) -> Path: + package_path = Path(package_name) if package_name else Path() + candidates = [ + repo_root / module / "src" / "main" / "java" / package_path / source_name, + repo_root / module / "src" / "main" / "kotlin" / package_path / source_name, + repo_root / module / "src" / "commonMain" / "kotlin" / package_path / source_name, + ] + + for candidate in candidates: + if candidate.exists(): + return candidate.relative_to(repo_root) + + return Path(module) / "src" / "main" / "java" / package_path / source_name + + +def collapse_ranges(lines: list[int]) -> list[str]: + ranges: list[str] = [] + if not lines: + return ranges + + start = lines[0] + end = lines[0] + + for current in lines[1:]: + if current == end + 1: + end = current + continue + ranges.append(format_range(start, end)) + start = current + end = current + + ranges.append(format_range(start, end)) + return ranges + + +def format_range(start: int, end: int) -> str: + if start == end: + return str(start) + return f"{start}-{end}" + + +def build_blob_url(repo: str, sha: str, file_path: str, line_range: str) -> str: + if "-" in line_range: + start, end = line_range.split("-", 1) + anchor = f"#L{start}-L{end}" + else: + anchor = f"#L{line_range}" + return f"https://github.com/{repo}/blob/{sha}/{file_path}{anchor}" + + +def render_file_cell(repo: str, sha: str, file_path: str) -> str: + if not repo or not sha: + return f"`{file_path}`" + return f"[`{file_path}`](https://github.com/{repo}/blob/{sha}/{file_path})" + + +def render_ranges_cell(repo: str, sha: str, file_path: str, ranges: list[str]) -> str: + if not repo or not sha: + return f"`{', '.join(ranges)}`" + links = [f"[`{line_range}`]({build_blob_url(repo, sha, file_path, line_range)})" for line_range in ranges] + return ", ".join(links) + + +def detect_language(file_path: str) -> str: + suffix = Path(file_path).suffix.lower() + if suffix == ".kt": + return "kotlin" + if suffix == ".java": + return "java" + return "text" + + +def parse_range(line_range: str) -> tuple[int, int]: + if "-" in line_range: + start, end = line_range.split("-", 1) + return int(start), int(end) + number = int(line_range) + return number, number + + +def render_snippet(repo_root: Path, file_path: str, line_range: str) -> str: + start, end = parse_range(line_range) + file_on_disk = repo_root / file_path + if not file_on_disk.exists(): + return "" + + content = file_on_disk.read_text(encoding="utf-8").splitlines() + start_index = max(start - 1, 0) + end_index = min(end, len(content)) + + snippet_lines = [ + f"{line_number:>4} | {content[line_number - 1]}" + for line_number in range(start_index + 1, end_index + 1) + ] + language = detect_language(file_path) + return "\n".join([f"```{language}", *snippet_lines, "```"]) + + +def main() -> int: + if len(sys.argv) not in {3, 5}: + print( + "usage: build_uncovered_coverage_comment.py [repo sha]", + file=sys.stderr, + ) + return 2 + + reports_root = Path(sys.argv[1]) + output_file = Path(sys.argv[2]) + repo = sys.argv[3] if len(sys.argv) == 5 else "" + sha = sys.argv[4] if len(sys.argv) == 5 else "" + repo_root = Path.cwd() + + items: list[tuple[str, list[str]]] = [] + for report in sorted(reports_root.glob("*/build/reports/kover/report.xml")): + module = report.relative_to(reports_root).parts[0] + items.extend(collect_uncovered_ranges(report, module, repo_root)) + + lines = [ + "## Uncovered Code", + "", + ] + + if not items: + lines.append("- No completely uncovered line ranges found in the published module reports.") + else: + selected_items = items[:MAX_FILES] + lines.extend( + [ + "The links below point to the exact file and line ranges in the PR head commit.", + "", + "| File | Uncovered Lines |", + "| --- | --- |", + ] + ) + for file_path, ranges in selected_items: + limited_ranges = ranges[:MAX_RANGES_PER_FILE] + lines.append( + f"| {render_file_cell(repo, sha, file_path)} | " + f"{render_ranges_cell(repo, sha, file_path, limited_ranges)} |" + ) + + lines.extend(["", "### Code Snippets", ""]) + for file_path, ranges in selected_items: + limited_ranges = ranges[:MAX_RANGES_PER_FILE] + lines.append(f"#### {render_file_cell(repo, sha, file_path)}") + lines.append("") + for line_range in limited_ranges: + if repo and sha: + lines.append(f"- Range {render_ranges_cell(repo, sha, file_path, [line_range])}") + else: + lines.append(f"- Range `{line_range}`") + snippet = render_snippet(repo_root, file_path, line_range) + if snippet: + lines.append("") + lines.append(snippet) + lines.append("") + + output_file.write_text("\n".join(lines) + "\n", encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.github/scripts/quality/write_coverage_summary.sh b/.github/scripts/quality/write_coverage_summary.sh index ea3bbd3c..cd2648e9 100644 --- a/.github/scripts/quality/write_coverage_summary.sh +++ b/.github/scripts/quality/write_coverage_summary.sh @@ -1,9 +1,13 @@ #!/usr/bin/env bash set -euo pipefail +summary_file="$(mktemp)" +trap 'rm -f "$summary_file"' EXIT + +python3 .github/scripts/quality/build_coverage_summary.py . "$summary_file" + { - echo "## Coverage" + cat "$summary_file" echo - echo "Kover XML report generated at \`build/reports/kover/report.xml\`" echo "Coverage artifacts are attached to this workflow run." } >> "${GITHUB_STEP_SUMMARY:?}" diff --git a/.github/workflows/pr-report.yml b/.github/workflows/pr-report.yml index 9308c6b7..25759e6c 100644 --- a/.github/workflows/pr-report.yml +++ b/.github/workflows/pr-report.yml @@ -24,6 +24,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v6.0.2 + with: + ref: ${{ github.event.workflow_run.head_sha }} - name: Download workflow artifacts uses: actions/download-artifact@v8.0.1 @@ -37,6 +39,28 @@ jobs: shell: bash run: bash .github/scripts/pr/list_downloaded_artifacts.sh pr-artifacts + - name: Detect coverage reports + id: coverage-reports + shell: bash + run: | + aggregate=false + modules=false + if [[ -f pr-artifacts/build/reports/kover/report.xml ]]; then + aggregate=true + fi + echo "aggregate=$aggregate" >> "$GITHUB_OUTPUT" + + for module in ai app data domain; do + present=false + if [[ -f "pr-artifacts/$module/build/reports/kover/report.xml" ]]; then + present=true + modules=true + fi + echo "$module=$present" >> "$GITHUB_OUTPUT" + done + + echo "modules=$modules" >> "$GITHUB_OUTPUT" + - name: Publish unit test report if: hashFiles('pr-artifacts/**/build/test-results/testDebugUnitTest/*.xml') != '' continue-on-error: true @@ -65,19 +89,156 @@ jobs: update_check: true detailed_summary: true + - name: Load quality monitor coverage config + if: steps.coverage-reports.outputs.aggregate == 'true' + id: quality-monitor-config + shell: bash + run: | + { + echo 'json<> "$GITHUB_OUTPUT" + - name: Publish coverage report - if: hashFiles('pr-artifacts/**/build/reports/kover/*.xml') != '' + if: steps.coverage-reports.outputs.aggregate == 'true' continue-on-error: true - uses: madrapps/jacoco-report@v1.7.2 + uses: uhafner/quality-monitor@v4.7.1 with: pr-number: ${{ github.event.workflow_run.pull_requests[0].number }} - paths: ${{ github.workspace }}/pr-artifacts/**/build/reports/kover/*.xml - token: ${{ github.token }} - min-coverage-overall: 60 - min-coverage-changed-files: 60 - title: Coverage - update-comment: true - comment-type: both + sha: ${{ github.event.workflow_run.head_sha }} + github-token: ${{ github.token }} + checks-name: Coverage + title-metric: line + show-headers: true + skip-annotations: true + config: ${{ steps.quality-monitor-config.outputs.json }} + + - name: Load AI coverage annotations config + if: steps.coverage-reports.outputs.ai == 'true' + id: coverage-annotations-ai-config + shell: bash + run: | + { + echo 'json<> "$GITHUB_OUTPUT" + + - name: Publish AI coverage annotations + if: steps.coverage-reports.outputs.ai == 'true' + continue-on-error: true + uses: uhafner/autograding-github-action@v6.0.1 + env: + SHA: ${{ github.event.workflow_run.head_sha }} + with: + github-token: ${{ github.token }} + checks-name: AI Coverage Annotations + max-coverage-annotations: 200 + config: ${{ steps.coverage-annotations-ai-config.outputs.json }} + + - name: Load App coverage annotations config + if: steps.coverage-reports.outputs.app == 'true' + id: coverage-annotations-app-config + shell: bash + run: | + { + echo 'json<> "$GITHUB_OUTPUT" + + - name: Publish App coverage annotations + if: steps.coverage-reports.outputs.app == 'true' + continue-on-error: true + uses: uhafner/autograding-github-action@v6.0.1 + env: + SHA: ${{ github.event.workflow_run.head_sha }} + with: + github-token: ${{ github.token }} + checks-name: App Coverage Annotations + max-coverage-annotations: 200 + config: ${{ steps.coverage-annotations-app-config.outputs.json }} + + - name: Load Data coverage annotations config + if: steps.coverage-reports.outputs.data == 'true' + id: coverage-annotations-data-config + shell: bash + run: | + { + echo 'json<> "$GITHUB_OUTPUT" + + - name: Publish Data coverage annotations + if: steps.coverage-reports.outputs.data == 'true' + continue-on-error: true + uses: uhafner/autograding-github-action@v6.0.1 + env: + SHA: ${{ github.event.workflow_run.head_sha }} + with: + github-token: ${{ github.token }} + checks-name: Data Coverage Annotations + max-coverage-annotations: 200 + config: ${{ steps.coverage-annotations-data-config.outputs.json }} + + - name: Load Domain coverage annotations config + if: steps.coverage-reports.outputs.domain == 'true' + id: coverage-annotations-domain-config + shell: bash + run: | + { + echo 'json<> "$GITHUB_OUTPUT" + + - name: Publish Domain coverage annotations + if: steps.coverage-reports.outputs.domain == 'true' + continue-on-error: true + uses: uhafner/autograding-github-action@v6.0.1 + env: + SHA: ${{ github.event.workflow_run.head_sha }} + with: + github-token: ${{ github.token }} + checks-name: Domain Coverage Annotations + max-coverage-annotations: 200 + config: ${{ steps.coverage-annotations-domain-config.outputs.json }} + + - name: Build coverage details comment + if: steps.coverage-reports.outputs.aggregate == 'true' + shell: bash + run: | + python3 .github/scripts/quality/build_coverage_summary.py \ + pr-artifacts \ + coverage-details.md + + - name: Publish coverage details comment + if: steps.coverage-reports.outputs.aggregate == 'true' + uses: marocchino/sticky-pull-request-comment@v3.0.2 + with: + number: ${{ github.event.workflow_run.pull_requests[0].number }} + header: coverage-details + path: coverage-details.md + + - name: Build uncovered coverage comment + if: steps.coverage-reports.outputs.modules == 'true' + shell: bash + run: | + python3 .github/scripts/quality/build_uncovered_coverage_comment.py \ + pr-artifacts \ + coverage-uncovered.md \ + "${{ github.repository }}" \ + "${{ github.event.workflow_run.head_sha }}" + + - name: Publish uncovered coverage comment + if: steps.coverage-reports.outputs.modules == 'true' + uses: marocchino/sticky-pull-request-comment@v3.0.2 + with: + number: ${{ github.event.workflow_run.pull_requests[0].number }} + header: coverage-uncovered + path: coverage-uncovered.md - name: Build PR summary shell: bash diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index a85fe197..d6f349b7 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -255,7 +255,11 @@ jobs: name: quality-coverage-reports if-no-files-found: ignore path: | - **/build/reports/kover/** + build/reports/kover/report.xml + ai/build/reports/kover/report.xml + app/build/reports/kover/report.xml + data/build/reports/kover/report.xml + domain/build/reports/kover/report.xml - name: Publish coverage summary if: always() diff --git a/build.gradle.kts b/build.gradle.kts index 8c4364cd..5e1a3388 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -16,6 +16,13 @@ plugins { alias(libs.plugins.kover) } +dependencies { + kover(project(":ai")) + kover(project(":app")) + kover(project(":data")) + kover(project(":domain")) +} + configure { android.set(true) ignoreFailures.set(false) @@ -64,6 +71,10 @@ kover { } } +tasks.matching { it.name == "koverVerify" }.configureEach { + enabled = false +} + subprojects { apply(plugin = "org.jlleitschuh.gradle.ktlint") apply(plugin = "io.gitlab.arturbosch.detekt")