Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
add7d64
feat: dependency scan & update
arifBurakDemiray Aug 24, 2026
f99991e
fix: post
arifBurakDemiray Aug 24, 2026
034319c
fix: post
arifBurakDemiray Aug 24, 2026
55d12fa
fix: post
arifBurakDemiray Aug 24, 2026
c961cbf
fix: codacy
arifBurakDemiray Aug 24, 2026
fe3d9a3
fix: codacy
arifBurakDemiray Aug 24, 2026
5a892d2
fix: tests
arifBurakDemiray Aug 24, 2026
a2c5734
Merge pull request #276 from Countly/dependency_scan
arifBurakDemiray Aug 24, 2026
5fda773
feat: contents and feedback widgets
arifBurakDemiray Sep 1, 2026
814c026
feat: code coverage
arifBurakDemiray Sep 1, 2026
de234eb
feat: code coverage CI and appear fix
arifBurakDemiray Sep 1, 2026
deda7cd
feat: ci and content fix
arifBurakDemiray Sep 1, 2026
dfc65ea
feat: more test to cover ui lib1
arifBurakDemiray Sep 1, 2026
ff4137b
fix: content fix for transparent
arifBurakDemiray Sep 1, 2026
7db64a4
feat: multi-platform tests
arifBurakDemiray Sep 2, 2026
ba5f723
feat: multi-platform tests
arifBurakDemiray Sep 2, 2026
d45c99d
feat: multi-platform tests
arifBurakDemiray Sep 2, 2026
6dd22c4
feat: multi-platform tests
arifBurakDemiray Sep 2, 2026
d56a3d4
feat: multi-platform tests
arifBurakDemiray Sep 2, 2026
109b941
fix: ui coverage
arifBurakDemiray Sep 2, 2026
7c81aa7
refactor: host fx webview
arifBurakDemiray Sep 2, 2026
a7e958d
fix: content comm
arifBurakDemiray Sep 2, 2026
cab66e5
fix: many fixes after manual testing
arifBurakDemiray Sep 3, 2026
294baad
fix: all placements
arifBurakDemiray Sep 4, 2026
d244c30
fix: linux & windows reviews
arifBurakDemiray Sep 4, 2026
01751a7
fix: focus on linux
arifBurakDemiray Sep 4, 2026
77ab56d
fix: focus on linux
arifBurakDemiray Sep 4, 2026
7a8423a
fix: stray fixes
arifBurakDemiray Sep 8, 2026
67ee71e
Merge pull request #277 from Countly/java_ui
arifBurakDemiray Sep 8, 2026
0b322fc
feat: 26.8.0
arifBurakDemiray Sep 8, 2026
00be3ac
Merge pull request #278 from Countly/update_vers
arifBurakDemiray Sep 8, 2026
a8486aa
fix: multiple publish plugin
arifBurakDemiray Sep 8, 2026
c180b24
Merge pull request #279 from Countly/fix_multiple_plugins
arifBurakDemiray Sep 8, 2026
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
25 changes: 25 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
version: 2
updates:
# Gradle dependencies. org.json is declared separately in sdk-java, app-java
# and app-javafx; Dependabot opens one PR per module, so check that a bump
# lands everywhere before closing the others.
- package-ecosystem: gradle
directory: "/"
schedule:
interval: weekly
day: monday
open-pull-requests-limit: 5
labels:
- dependencies
commit-message:
prefix: "chore(deps)"

- package-ecosystem: github-actions
directory: "/"
schedule:
interval: monthly
labels:
- dependencies
- ci
commit-message:
prefix: "chore(ci)"
8 changes: 8 additions & 0 deletions .github/dependency-scan-allowlist.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Vulnerabilities the dependency security scan may ignore, one per line:
#
# <OSV id> <YYYY-MM-DD expiry> <reason>
#
# The expiry is mandatory. Once it passes, the finding blocks the scan again and the entry
# has to be re-reviewed — a temporary exception must not become a permanent silence.
# Only add an entry when there is a concrete reason the advisory cannot or need not be acted
# on right now, and say what that reason is.
305 changes: 305 additions & 0 deletions .github/scripts/coverage_report.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
#!/usr/bin/env python3
"""Turn jacoco XML reports into a Markdown coverage report for a pull request comment.

Reads one report per Gradle module, plus the list of files the pull request touched, and
writes:

* a per-module line and branch coverage table
* coverage of the source files this pull request actually changed, which is the part a
reviewer can act on
* the least covered classes, as a standing to-do list
* the test failure count, so nobody reads coverage numbers off a broken run

Exit code is 0 unless a configured minimum is missed and enforcement is switched on, so the
job can start out advisory and become a gate later without touching this script.

Usage:
coverage_report.py --out coverage-report.md
--changed-files changed.txt
--module <label>:<jacoco xml>:<test-results dir>:<source root> ...
"""

import argparse
import os
import sys
import xml.etree.ElementTree as ET

# Reported but not enforced unless ENFORCE_COVERAGE is truthy, so the job can be introduced
# on a repository that is not at target yet.
MIN_OVERALL = float(os.environ.get("COVERAGE_MIN_OVERALL", "0"))
MIN_CHANGED = float(os.environ.get("COVERAGE_MIN_CHANGED", "0"))
ENFORCE = os.environ.get("ENFORCE_COVERAGE", "").lower() in ("1", "true", "yes")
WORST_CLASS_COUNT = int(os.environ.get("COVERAGE_WORST_CLASSES", "10"))


class Counter:
"""Covered and missed totals for one jacoco counter type."""

def __init__(self, covered=0, missed=0):
self.covered = covered
self.missed = missed

@property
def total(self):
return self.covered + self.missed

@property
def percent(self):
if self.total == 0:
return None
return 100.0 * self.covered / self.total

def add(self, other):
self.covered += other.covered
self.missed += other.missed

def __str__(self):
if self.total == 0:
return "n/a"
return "{:.1f}% ({}/{})".format(self.percent, self.covered, self.total)


def counters_of(element):
"""The counters declared directly on a jacoco element, keyed by type."""
result = {}
for counter in element.findall("counter"):
result[counter.get("type")] = Counter(
covered=int(counter.get("covered", 0)),
missed=int(counter.get("missed", 0)),
)
return result


def parse_report(path):
"""A jacoco report as (report counters, {(package, sourcefile): counters}, [(class, counters)])."""
# The report declares a DOCTYPE pointing at a remote DTD. ElementTree does not fetch
# external entities, so parsing stays offline.
root = ET.parse(path).getroot()

overall = counters_of(root)
source_files = {}
classes = []

for package in root.iter("package"):
package_name = package.get("name", "")

for source_file in package.findall("sourcefile"):
key = (package_name, source_file.get("name", ""))
source_files[key] = counters_of(source_file)

for klass in package.findall("class"):
name = klass.get("name", "")
# Nested and anonymous classes roll up into their outer class for reporting.
if "$" in name:
continue
classes.append((name.replace("/", "."), counters_of(klass)))

return overall, source_files, classes


def count_test_failures(results_dir):
"""Failures plus errors across a Gradle module's JUnit XML results."""
if not results_dir or not os.path.isdir(results_dir):
return 0

failures = 0
for entry in os.listdir(results_dir):
if not entry.endswith(".xml"):
continue
try:
suite = ET.parse(os.path.join(results_dir, entry)).getroot()
failures += int(suite.get("failures", 0)) + int(suite.get("errors", 0))
except ET.ParseError:
# An unreadable result file is not worth failing the report over.
continue
return failures


def read_build_log_tail(path, limit=40):
"""The last few lines of the build output, for when there is no report to show."""
if not path or not os.path.isfile(path):
return []
try:
with open(path, encoding="utf-8", errors="replace") as handle:
return [line.rstrip() for line in handle.readlines()[-limit:]]
except OSError:
return []


def read_changed_files(path):
if not path or not os.path.isfile(path):
return []
with open(path, encoding="utf-8") as handle:
return [line.strip() for line in handle if line.strip().endswith(".java")]


def percent_cell(counter):
return str(counter) if counter else "n/a"


def format_percent(value):
return "n/a" if value is None else "{:.1f}%".format(value)


def main():
parser = argparse.ArgumentParser()
parser.add_argument("--out", required=True)
parser.add_argument("--changed-files")
parser.add_argument("--build-log", help="Build output, shown when no report was produced.")
parser.add_argument(
"--module",
action="append",
default=[],
help="<label>:<jacoco xml>:<test-results dir>:<source root>",
)
args = parser.parse_args()

changed_files = read_changed_files(args.changed_files)

lines = ["## Code coverage", ""]
module_rows = []
changed_rows = []
worst = []
total_failures = 0
missing_reports = []

for spec in args.module:
label, report_path, results_dir, source_root = (spec.split(":") + ["", "", ""])[:4]

if not os.path.isfile(report_path):
missing_reports.append(label)
module_rows.append((label, "no report", "no report"))
continue

overall, source_files, classes = parse_report(report_path)
total_failures += count_test_failures(results_dir)

module_rows.append(
(label, percent_cell(overall.get("LINE")), percent_cell(overall.get("BRANCH")))
)

for name, counters in classes:
line_counter = counters.get("LINE")
if line_counter and line_counter.missed > 0:
worst.append((line_counter.missed, name, label, line_counter))

# Match a changed path back to the report through its package and file name.
for changed in changed_files:
if source_root and not changed.startswith(source_root):
continue
file_name = os.path.basename(changed)
package = os.path.dirname(changed)
if source_root:
package = package[len(source_root):]
package = package.strip("/")

counters = source_files.get((package, file_name))
if counters:
changed_rows.append((changed, percent_cell(counters.get("LINE")), counters.get("LINE")))

lines.append("| module | lines | branches |")
lines.append("|---|---|---|")
for label, line_cell, branch_cell in module_rows:
lines.append("| `{}` | {} | {} |".format(label, line_cell, branch_cell))
lines.append("")

if changed_rows:
lines.append("### Files changed by this pull request")
lines.append("")
lines.append("| file | lines covered |")
lines.append("|---|---|")
for path, cell, _ in sorted(changed_rows):
lines.append("| `{}` | {} |".format(path, cell))
lines.append("")
elif changed_files:
lines.append("_No changed file appears in a coverage report. Either none of the changed "
"Java files are in a measured module, or they are test sources._")
lines.append("")

if worst:
worst.sort(reverse=True)
lines.append("<details><summary>Least covered classes ({} shown)</summary>".format(
min(WORST_CLASS_COUNT, len(worst))))
lines.append("")
lines.append("| class | module | lines covered | uncovered |")
lines.append("|---|---|---|---|")
for missed, name, label, counter in worst[:WORST_CLASS_COUNT]:
lines.append("| `{}` | `{}` | {} | {} |".format(name, label, counter, missed))
lines.append("")
lines.append("</details>")
lines.append("")

if total_failures > 0:
lines.append("> **{} test(s) failed in this run.** Coverage is still measured over the whole "
"run, but read the test results before trusting these numbers."
.format(total_failures))
lines.append("")

if missing_reports:
lines.append("> No coverage report was produced for: {}."
.format(", ".join("`{}`".format(m) for m in missing_reports)))
lines.append("")

# A missing report almost always means the build failed before jacoco ran. Putting the
# tail of the build output in the comment saves a trip to the workflow logs.
tail = read_build_log_tail(args.build_log)
if tail:
lines.append("<details><summary>Build output (last {} lines)</summary>".format(len(tail)))
lines.append("")
lines.append("```")
lines.extend(tail)
lines.append("```")
lines.append("")
lines.append("</details>")
lines.append("")

# Threshold checks, reported either way and enforced only when asked to be.
problems = []

for label, line_cell, _ in module_rows:
if MIN_OVERALL > 0 and line_cell not in ("no report",):
value = float(line_cell.split("%")[0])
if value < MIN_OVERALL:
problems.append("`{}` line coverage {} is below the {:.1f}% minimum"
.format(label, format_percent(value), MIN_OVERALL))

if MIN_CHANGED > 0 and changed_rows:
changed_total = Counter()
for _, _, counter in changed_rows:
if counter:
changed_total.add(counter)
if changed_total.percent is not None and changed_total.percent < MIN_CHANGED:
problems.append("changed files line coverage {} is below the {:.1f}% minimum"
.format(format_percent(changed_total.percent), MIN_CHANGED))

if problems:
lines.append("**Coverage below the configured minimum:**")
lines.append("")
for problem in problems:
lines.append("* {}".format(problem))
lines.append("")
if not ENFORCE:
lines.append("_Advisory only. Set `ENFORCE_COVERAGE=true` on the workflow to make this "
"fail the build._")
lines.append("")

body = "\n".join(lines)

with open(args.out, "w", encoding="utf-8") as handle:
handle.write(body)

summary_path = os.environ.get("GITHUB_STEP_SUMMARY")
if summary_path:
with open(summary_path, "a", encoding="utf-8") as handle:
handle.write(body + "\n")

print(body)

if problems and ENFORCE:
print("::error::Coverage is below the configured minimum")
return 1
return 0


if __name__ == "__main__":
sys.exit(main())
Loading
Loading