Skip to content
Merged
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
110 changes: 100 additions & 10 deletions scripts/local-ci.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@
# ./scripts/local-ci.sh test # build + test only
# ./scripts/local-ci.sh package # build + package only
# ./scripts/local-ci.sh all # everything (default)
#
# GitHub Actions minutes are limited, so full test+coverage runs are expensive to
# do on every push. This script produces the SAME coverage artifacts locally,
# including SummaryGithub.md — the markdown that CI would post to the PR. To share
# it without spending Actions minutes, opt in to posting it as a single sticky PR
# comment (requires the 'gh' CLI, authenticated):
#
# POST_COVERAGE_PR=88 ./scripts/local-ci.sh test # post/update coverage on PR #88
#
# Posting is opt-in and never happens unless POST_COVERAGE_PR is set.
# =============================================================================

set -euo pipefail
Expand Down Expand Up @@ -38,6 +48,57 @@ success() { echo -e "${GREEN}✓ $1${NC}"; }
warn() { echo -e "${YELLOW}⚠ $1${NC}"; }
fail() { echo -e "${RED}✗ $1${NC}"; }

# ---------------------------------------------------------------------------
# Post a coverage summary as a single sticky PR comment (opt-in).
# Finds a prior comment by a hidden marker and edits it, so repeated runs update
# one comment instead of spamming — the same behaviour as the CI sticky comment.
# Failures are warnings only; posting must never fail the pipeline.
# ---------------------------------------------------------------------------
post_coverage_comment() {
local pr="$1" file="$2"
local marker="<!-- local-ci-coverage -->"

if ! command -v gh &>/dev/null; then
warn "gh CLI not found; cannot post coverage comment. Install: https://cli.github.com"
return 0
fi
if [ ! -f "$file" ]; then
warn "No coverage summary at $file; nothing to post."
return 0
fi

local repo
repo=$(gh repo view --json nameWithOwner --jq .nameWithOwner 2>/dev/null || true)
if [ -z "$repo" ]; then
warn "Could not resolve the GitHub repository; skipping coverage comment."
return 0
fi

local body_file
body_file=$(mktemp)
{ printf '%s\n\n' "$marker"; cat "$file"; } > "$body_file"

local existing_id
existing_id=$(gh api "repos/$repo/issues/$pr/comments" --paginate \
--jq "map(select(.body | contains(\"$marker\"))) | last | .id" 2>/dev/null || true)

if [ -n "$existing_id" ] && [ "$existing_id" != "null" ]; then
if gh api -X PATCH "repos/$repo/issues/comments/$existing_id" -F body=@"$body_file" >/dev/null 2>&1; then
success "Updated sticky coverage comment on PR #$pr"
else
warn "Failed to update coverage comment on PR #$pr."
fi
else
if gh api -X POST "repos/$repo/issues/$pr/comments" -F body=@"$body_file" >/dev/null 2>&1; then
success "Posted coverage comment on PR #$pr"
else
warn "Failed to post coverage comment on PR #$pr."
fi
fi

rm -f "$body_file"
}

# ---------------------------------------------------------------------------
# Find MSBuild via vswhere (CI uses microsoft/setup-msbuild@v2)
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -247,18 +308,47 @@ do_test() {
echo -e " ${BOLD}Total: Passed=$total_passed Failed=$total_failed Skipped=$total_skipped${NC}"
echo " ─────────────────────────────────────"

# Generate coverage report if reportgenerator is available
# Generate coverage report. reportgenerator is required for the coverage summary;
# install it on demand (mirroring the CI workflow) so local runs always produce a
# report — including the same markdown summary CI posts to the PR.
local reports
reports=$(find "$RESULTS_DIR" -name "coverage.cobertura.xml" 2>/dev/null | tr '\n' ';')
if [ -n "$reports" ] && command -v reportgenerator &>/dev/null; then
step "COVERAGE — generating report"
mkdir -p "$COVERAGE_DIR/report"
reportgenerator "-reports:$reports" \
"-targetdir:$COVERAGE_DIR/report" \
"-reporttypes:TextSummary" 2>/dev/null
cat "$COVERAGE_DIR/report/Summary.txt" 2>/dev/null || true
elif [ -n "$reports" ]; then
warn "Install reportgenerator for coverage reports: dotnet tool install -g dotnet-reportgenerator-globaltool"
if [ -n "$reports" ]; then
if ! command -v reportgenerator &>/dev/null; then
step "COVERAGE — installing reportgenerator (dotnet global tool)"
dotnet tool install -g dotnet-reportgenerator-globaltool 2>/dev/null \
|| dotnet tool update -g dotnet-reportgenerator-globaltool 2>/dev/null || true
# Ensure the dotnet global-tools directory is on PATH for this session.
export PATH="$PATH:$HOME/.dotnet/tools"
if command -v cygpath &>/dev/null && [ -n "${USERPROFILE:-}" ]; then
export PATH="$PATH:$(cygpath -u "$USERPROFILE")/.dotnet/tools"
fi
fi

if command -v reportgenerator &>/dev/null; then
step "COVERAGE — generating report"
mkdir -p "$COVERAGE_DIR/report"
# Same report types as .github/workflows/test.yml so local output matches CI:
# HTML (openable offline), Cobertura, a text summary, and the GitHub markdown
# summary that CI posts as the sticky PR comment.
reportgenerator "-reports:$reports" \
"-targetdir:$COVERAGE_DIR/report" \
"-reporttypes:HtmlInline;Cobertura;TextSummary;MarkdownSummaryGithub" 2>/dev/null
cat "$COVERAGE_DIR/report/Summary.txt" 2>/dev/null || true
[ -f "$COVERAGE_DIR/report/SummaryGithub.md" ] \
&& success "Markdown summary: $COVERAGE_DIR/report/SummaryGithub.md"
[ -f "$COVERAGE_DIR/report/index.html" ] \
&& success "HTML report: $COVERAGE_DIR/report/index.html"

# Opt-in: post the markdown summary to a PR as a single sticky comment.
if [ -n "${POST_COVERAGE_PR:-}" ]; then
step "COVERAGE — posting summary to PR #$POST_COVERAGE_PR"
post_coverage_comment "$POST_COVERAGE_PR" "$COVERAGE_DIR/report/SummaryGithub.md"
fi
else
warn "reportgenerator unavailable even after install attempt; skipping coverage report."
warn "Install manually: dotnet tool install -g dotnet-reportgenerator-globaltool"
fi
fi

if [ "$any_failed" = true ]; then
Expand Down
Loading