Skip to content

Commit bd673d6

Browse files
mcpolo99RandomCrocodile
andauthored
chore: reliable offline coverage reporting in local-ci (#89)
* chore: auto-install reportgenerator and emit markdown coverage in local-ci The offline CI now installs the reportgenerator global tool on demand (mirroring .github/workflows/test.yml) instead of silently skipping the coverage report when it is absent. It also emits the same report types as CI — HTML, Cobertura, TextSummary and MarkdownSummaryGithub — so a local run produces SummaryGithub.md, the offline equivalent of the coverage summary CI posts to the PR. * chore: add opt-in PR coverage-comment posting to local-ci Since GitHub Actions minutes are limited, local-ci now produces the same coverage markdown as CI (SummaryGithub.md) and can optionally post it to a PR as a single sticky comment via the gh CLI, gated behind POST_COVERAGE_PR=<num>. It edits a prior marked comment instead of spamming, mirroring the CI sticky comment. Posting is opt-in and never runs unless the variable is set; all posting failures are warnings that never fail the pipeline. --------- Co-authored-by: RandomCrocodile <mawi@polosab.com>
1 parent a3c177f commit bd673d6

1 file changed

Lines changed: 100 additions & 10 deletions

File tree

scripts/local-ci.sh

Lines changed: 100 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,16 @@
1111
# ./scripts/local-ci.sh test # build + test only
1212
# ./scripts/local-ci.sh package # build + package only
1313
# ./scripts/local-ci.sh all # everything (default)
14+
#
15+
# GitHub Actions minutes are limited, so full test+coverage runs are expensive to
16+
# do on every push. This script produces the SAME coverage artifacts locally,
17+
# including SummaryGithub.md — the markdown that CI would post to the PR. To share
18+
# it without spending Actions minutes, opt in to posting it as a single sticky PR
19+
# comment (requires the 'gh' CLI, authenticated):
20+
#
21+
# POST_COVERAGE_PR=88 ./scripts/local-ci.sh test # post/update coverage on PR #88
22+
#
23+
# Posting is opt-in and never happens unless POST_COVERAGE_PR is set.
1424
# =============================================================================
1525

1626
set -euo pipefail
@@ -38,6 +48,57 @@ success() { echo -e "${GREEN}✓ $1${NC}"; }
3848
warn() { echo -e "${YELLOW}$1${NC}"; }
3949
fail() { echo -e "${RED}$1${NC}"; }
4050

51+
# ---------------------------------------------------------------------------
52+
# Post a coverage summary as a single sticky PR comment (opt-in).
53+
# Finds a prior comment by a hidden marker and edits it, so repeated runs update
54+
# one comment instead of spamming — the same behaviour as the CI sticky comment.
55+
# Failures are warnings only; posting must never fail the pipeline.
56+
# ---------------------------------------------------------------------------
57+
post_coverage_comment() {
58+
local pr="$1" file="$2"
59+
local marker="<!-- local-ci-coverage -->"
60+
61+
if ! command -v gh &>/dev/null; then
62+
warn "gh CLI not found; cannot post coverage comment. Install: https://cli.github.com"
63+
return 0
64+
fi
65+
if [ ! -f "$file" ]; then
66+
warn "No coverage summary at $file; nothing to post."
67+
return 0
68+
fi
69+
70+
local repo
71+
repo=$(gh repo view --json nameWithOwner --jq .nameWithOwner 2>/dev/null || true)
72+
if [ -z "$repo" ]; then
73+
warn "Could not resolve the GitHub repository; skipping coverage comment."
74+
return 0
75+
fi
76+
77+
local body_file
78+
body_file=$(mktemp)
79+
{ printf '%s\n\n' "$marker"; cat "$file"; } > "$body_file"
80+
81+
local existing_id
82+
existing_id=$(gh api "repos/$repo/issues/$pr/comments" --paginate \
83+
--jq "map(select(.body | contains(\"$marker\"))) | last | .id" 2>/dev/null || true)
84+
85+
if [ -n "$existing_id" ] && [ "$existing_id" != "null" ]; then
86+
if gh api -X PATCH "repos/$repo/issues/comments/$existing_id" -F body=@"$body_file" >/dev/null 2>&1; then
87+
success "Updated sticky coverage comment on PR #$pr"
88+
else
89+
warn "Failed to update coverage comment on PR #$pr."
90+
fi
91+
else
92+
if gh api -X POST "repos/$repo/issues/$pr/comments" -F body=@"$body_file" >/dev/null 2>&1; then
93+
success "Posted coverage comment on PR #$pr"
94+
else
95+
warn "Failed to post coverage comment on PR #$pr."
96+
fi
97+
fi
98+
99+
rm -f "$body_file"
100+
}
101+
41102
# ---------------------------------------------------------------------------
42103
# Find MSBuild via vswhere (CI uses microsoft/setup-msbuild@v2)
43104
# ---------------------------------------------------------------------------
@@ -247,18 +308,47 @@ do_test() {
247308
echo -e " ${BOLD}Total: Passed=$total_passed Failed=$total_failed Skipped=$total_skipped${NC}"
248309
echo " ─────────────────────────────────────"
249310

250-
# Generate coverage report if reportgenerator is available
311+
# Generate coverage report. reportgenerator is required for the coverage summary;
312+
# install it on demand (mirroring the CI workflow) so local runs always produce a
313+
# report — including the same markdown summary CI posts to the PR.
251314
local reports
252315
reports=$(find "$RESULTS_DIR" -name "coverage.cobertura.xml" 2>/dev/null | tr '\n' ';')
253-
if [ -n "$reports" ] && command -v reportgenerator &>/dev/null; then
254-
step "COVERAGE — generating report"
255-
mkdir -p "$COVERAGE_DIR/report"
256-
reportgenerator "-reports:$reports" \
257-
"-targetdir:$COVERAGE_DIR/report" \
258-
"-reporttypes:TextSummary" 2>/dev/null
259-
cat "$COVERAGE_DIR/report/Summary.txt" 2>/dev/null || true
260-
elif [ -n "$reports" ]; then
261-
warn "Install reportgenerator for coverage reports: dotnet tool install -g dotnet-reportgenerator-globaltool"
316+
if [ -n "$reports" ]; then
317+
if ! command -v reportgenerator &>/dev/null; then
318+
step "COVERAGE — installing reportgenerator (dotnet global tool)"
319+
dotnet tool install -g dotnet-reportgenerator-globaltool 2>/dev/null \
320+
|| dotnet tool update -g dotnet-reportgenerator-globaltool 2>/dev/null || true
321+
# Ensure the dotnet global-tools directory is on PATH for this session.
322+
export PATH="$PATH:$HOME/.dotnet/tools"
323+
if command -v cygpath &>/dev/null && [ -n "${USERPROFILE:-}" ]; then
324+
export PATH="$PATH:$(cygpath -u "$USERPROFILE")/.dotnet/tools"
325+
fi
326+
fi
327+
328+
if command -v reportgenerator &>/dev/null; then
329+
step "COVERAGE — generating report"
330+
mkdir -p "$COVERAGE_DIR/report"
331+
# Same report types as .github/workflows/test.yml so local output matches CI:
332+
# HTML (openable offline), Cobertura, a text summary, and the GitHub markdown
333+
# summary that CI posts as the sticky PR comment.
334+
reportgenerator "-reports:$reports" \
335+
"-targetdir:$COVERAGE_DIR/report" \
336+
"-reporttypes:HtmlInline;Cobertura;TextSummary;MarkdownSummaryGithub" 2>/dev/null
337+
cat "$COVERAGE_DIR/report/Summary.txt" 2>/dev/null || true
338+
[ -f "$COVERAGE_DIR/report/SummaryGithub.md" ] \
339+
&& success "Markdown summary: $COVERAGE_DIR/report/SummaryGithub.md"
340+
[ -f "$COVERAGE_DIR/report/index.html" ] \
341+
&& success "HTML report: $COVERAGE_DIR/report/index.html"
342+
343+
# Opt-in: post the markdown summary to a PR as a single sticky comment.
344+
if [ -n "${POST_COVERAGE_PR:-}" ]; then
345+
step "COVERAGE — posting summary to PR #$POST_COVERAGE_PR"
346+
post_coverage_comment "$POST_COVERAGE_PR" "$COVERAGE_DIR/report/SummaryGithub.md"
347+
fi
348+
else
349+
warn "reportgenerator unavailable even after install attempt; skipping coverage report."
350+
warn "Install manually: dotnet tool install -g dotnet-reportgenerator-globaltool"
351+
fi
262352
fi
263353

264354
if [ "$any_failed" = true ]; then

0 commit comments

Comments
 (0)