forked from mkaring/ConfuserEx
-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathlocal-ci.sh
More file actions
467 lines (407 loc) · 16.8 KB
/
Copy pathlocal-ci.sh
File metadata and controls
467 lines (407 loc) · 16.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
#!/usr/bin/env bash
# =============================================================================
# local-ci.sh — Run the full CI pipeline locally (mirrors GitHub Actions)
#
# Replicates: lint.yml, ci.yml (build + package), test.yml (test + coverage)
#
# Usage:
# ./scripts/local-ci.sh # run everything
# ./scripts/local-ci.sh lint # lint only
# ./scripts/local-ci.sh build # restore + build only
# ./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
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$REPO_ROOT"
CONFIGURATION="Release"
SLN="Confuser2.sln"
RESULTS_DIR="test-results"
COVERAGE_DIR="coverage"
# ---------------------------------------------------------------------------
# Colors
# ---------------------------------------------------------------------------
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
step() { echo -e "\n${CYAN}${BOLD}==> $1${NC}"; }
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)
# ---------------------------------------------------------------------------
find_msbuild() {
# vswhere -latest finds the newest VS (2025 > 2022 > 2019).
# CI uses windows-2025 runners with VS 2025 / MSBuild 18.
local vswhere="/c/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe"
if [ -f "$vswhere" ]; then
# Get installation path first, then construct MSBuild path
local vs_path
vs_path=$("$vswhere" -latest -requires Microsoft.Component.MSBuild \
-property installationPath 2>/dev/null | head -1)
if [ -n "$vs_path" ]; then
# Convert Windows path to unix-style for bash
local unix_path
unix_path=$(cygpath -u "$vs_path" 2>/dev/null || echo "$vs_path")
MSBUILD="$unix_path/MSBuild/Current/Bin/MSBuild.exe"
if [ ! -f "$MSBUILD" ]; then
MSBUILD=""
fi
fi
fi
if [ -z "${MSBUILD:-}" ]; then
warn "MSBuild not found — will use 'dotnet build' (C++/CLI project will be skipped)"
return 1
fi
local ver
ver=$("$MSBUILD" -version 2>/dev/null | tail -1 || echo "unknown")
echo " MSBuild: $MSBUILD (v$ver)"
return 0
}
# ---------------------------------------------------------------------------
# Phase: Lint (mirrors lint.yml)
# ---------------------------------------------------------------------------
do_lint() {
step "LINT — whitespace, style, analyzers"
local any_failed=false
echo " Restoring for lint..."
dotnet restore "$SLN" --verbosity quiet 2>/dev/null
echo " Checking whitespace..."
if dotnet format whitespace "$SLN" --verify-no-changes --verbosity minimal 2>&1 | tail -5; then
success "Whitespace OK"
else
warn "Whitespace issues found"
any_failed=true
fi
echo " Checking style..."
if dotnet format style "$SLN" --verify-no-changes --severity warn 2>&1 | tail -5; then
success "Style OK"
else
warn "Style issues found"
any_failed=true
fi
echo " Checking analyzers..."
if dotnet format analyzers "$SLN" --verify-no-changes --severity warn 2>&1 | tail -5; then
success "Analyzers OK"
else
warn "Analyzer issues found"
any_failed=true
fi
if [ "$any_failed" = true ]; then
warn "Lint issues found — run 'dotnet format $SLN' to fix"
return 1
else
success "All lint checks passed"
fi
}
# ---------------------------------------------------------------------------
# Phase: Restore + Build (mirrors ci.yml)
# ---------------------------------------------------------------------------
do_build() {
step "BUILD — restore + compile ($CONFIGURATION)"
local has_msbuild=false
find_msbuild && has_msbuild=true
# Use 'dotnet build' for all SDK-style projects (handles .NET SDK resolution).
# Then use MSBuild.exe for the C++/CLI project if available.
echo " Restoring..."
dotnet restore "$SLN" --verbosity minimal
echo " Building (dotnet)..."
dotnet build "$SLN" -c "$CONFIGURATION" --no-restore 2>&1 \
| grep -v "244_ClrProtection" || true
# C++/CLI project requires MSBuild.exe (not dotnet build)
if [ "$has_msbuild" = true ]; then
local vcxproj="Tests/244_ClrProtection/244_ClrProtection.vcxproj"
if [ -f "$vcxproj" ]; then
echo " Building C++/CLI test project (msbuild)..."
"$MSBUILD" "$vcxproj" -p:Configuration="$CONFIGURATION" -verbosity:minimal 2>&1 \
| tail -3 || warn "C++/CLI project build failed (non-critical)"
fi
else
warn "Skipping C++/CLI project (no MSBuild.exe found)"
fi
# Verify key outputs exist
local cli_dll="Confuser.CLI/bin/$CONFIGURATION/net10.0/Confuser.CLI.dll"
local gui_dll="ConfuserEx/bin/$CONFIGURATION/net10.0-windows/ConfuserEx.dll"
local core48="Confuser.Core/bin/$CONFIGURATION/net48/Confuser.Core.dll"
local corenstd="Confuser.Core/bin/$CONFIGURATION/netstandard2.0/Confuser.Core.dll"
local all_ok=true
for f in "$cli_dll" "$gui_dll" "$core48" "$corenstd"; do
if [ -f "$f" ]; then
success "$(basename "$f") ($(dirname "$f" | sed "s|.*/bin/$CONFIGURATION/||"))"
else
fail "Missing: $f"
all_ok=false
fi
done
if [ "$all_ok" = false ]; then
fail "Build produced missing outputs"
return 1
fi
success "Build complete"
}
# ---------------------------------------------------------------------------
# Phase: Test (mirrors test.yml)
# ---------------------------------------------------------------------------
do_test() {
step "TEST — run all test projects with coverage"
rm -rf "$RESULTS_DIR" 2>/dev/null || true
mkdir -p "$RESULTS_DIR"
local total_passed=0
local total_failed=0
local total_skipped=0
local any_failed=false
# Confuser.GUI.Test is a FlaUI smoke test — it launches the real ConfuserEx WPF window
# and drives it, so it pops UI on screen and takes ~35s. Skip it by default for quiet local
# runs; set RUN_GUI_TESTS=1 to include it. It always runs in CI (headless Windows runner).
if [ "${RUN_GUI_TESTS:-0}" != "1" ]; then
warn "Skipping Confuser.GUI.Test (UI-interactive). Set RUN_GUI_TESTS=1 to include it."
fi
# Find all *.Test.csproj (same as CI: Get-ChildItem -Filter '*.Test.csproj' -Recurse)
while IFS= read -r proj; do
local name
name=$(basename "$proj" .csproj)
if [ "$name" = "Confuser.GUI.Test" ] && [ "${RUN_GUI_TESTS:-0}" != "1" ]; then
continue
fi
echo -e "\n ${CYAN}Testing $name...${NC}"
# NOTE: do NOT pass --collect:"XPlat Code Coverage" here. Coverage is driven by each
# project's RunSettingsFilePath (set in Tests/Directory.Build.targets) which enables it
# only for .NET (Core) test projects. A command-line --collect overrides that gate and
# forces Coverlet to instrument the signed Confuser.* assemblies on net4x, breaking their
# strong name so .NET Framework refuses to load them and every net4x test fails.
local output
output=$(dotnet test "$proj" -c "$CONFIGURATION" --no-build --verbosity minimal \
--logger "trx;LogFileName=$name.trx" \
--results-directory "$RESULTS_DIR/$name" 2>&1) || true
# Parse summary line: "Passed! - Failed: 0, Passed: 3, Skipped: 0, Total: 3"
local summary
summary=$(echo "$output" | grep -E "^(Passed!|Failed!)" | tail -1)
if [ -n "$summary" ]; then
local p f s
p=$(echo "$summary" | grep -oP 'Passed:\s+\K\d+' || echo 0)
f=$(echo "$summary" | grep -oP 'Failed:\s+\K\d+' || echo 0)
s=$(echo "$summary" | grep -oP 'Skipped:\s+\K\d+' || echo 0)
total_passed=$((total_passed + p))
total_failed=$((total_failed + f))
total_skipped=$((total_skipped + s))
if echo "$summary" | grep -q "^Failed!"; then
fail "$name — $summary"
any_failed=true
else
success "$name — Passed: $p, Failed: $f, Skipped: $s"
fi
else
# No tests discovered
local no_test
no_test=$(echo "$output" | grep -c "No test is available" || true)
if [ "$no_test" -gt 0 ]; then
warn "$name — no tests discovered (missing Microsoft.NET.Test.Sdk?)"
else
warn "$name — no test output"
fi
fi
done < <(find Tests -name "*.Test.csproj" -type f | sort)
echo ""
echo " ─────────────────────────────────────"
echo -e " ${BOLD}Total: Passed=$total_passed Failed=$total_failed Skipped=$total_skipped${NC}"
echo " ─────────────────────────────────────"
# 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" ]; 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
fail "Some tests failed"
return 1
fi
success "All tests passed"
}
# ---------------------------------------------------------------------------
# Phase: Package (mirrors ci.yml packaging steps)
# ---------------------------------------------------------------------------
do_package() {
step "PACKAGE — create release archives"
local cli_dir="Confuser.CLI/bin/$CONFIGURATION/net10.0"
local gui_dir="ConfuserEx/bin/$CONFIGURATION/net10.0-windows"
# CLI zip
if [ -d "$cli_dir" ]; then
rm -f ConfuserEx-CLI.zip 2>/dev/null || true
(cd "$cli_dir" && find . -not -name '*.pdb' -not -name '*.xml' -not -path './runtimes/*/native/*' \
-type f | sort | zip -q "$REPO_ROOT/ConfuserEx-CLI.zip" -@)
local size
size=$(du -h ConfuserEx-CLI.zip | cut -f1)
success "ConfuserEx-CLI.zip ($size)"
else
fail "CLI output not found at $cli_dir — run build first"
fi
# GUI zip
if [ -d "$gui_dir" ]; then
rm -f ConfuserEx-GUI.zip 2>/dev/null || true
(cd "$gui_dir" && find . -not -name '*.pdb' -not -name '*.xml' \
-type f | sort | zip -q "$REPO_ROOT/ConfuserEx-GUI.zip" -@)
size=$(du -h ConfuserEx-GUI.zip | cut -f1)
success "ConfuserEx-GUI.zip ($size)"
else
fail "GUI output not found at $gui_dir — run build first"
fi
# Combined zip
rm -rf combined 2>/dev/null || true
mkdir -p combined
cp "$cli_dir"/* combined/ 2>/dev/null || true
cp "$gui_dir"/* combined/ 2>/dev/null || true
rm -f combined/*.pdb combined/*.xml 2>/dev/null || true
if [ "$(ls -A combined 2>/dev/null)" ]; then
rm -f ConfuserEx.zip 2>/dev/null || true
(cd combined && find . -type f | sort | zip -q "$REPO_ROOT/ConfuserEx.zip" -@)
size=$(du -h ConfuserEx.zip | cut -f1)
success "ConfuserEx.zip ($size)"
fi
rm -rf combined
# NuGet package
local nupkg
nupkg=$(find Confuser.MSBuild.Tasks/bin/$CONFIGURATION -name "*.nupkg" 2>/dev/null | head -1)
if [ -n "$nupkg" ]; then
success "$(basename "$nupkg")"
else
warn "No .nupkg found (MSBuild-only build may be required)"
fi
success "Packaging complete"
}
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
MODE="${1:-all}"
echo -e "${BOLD}╔══════════════════════════════════════╗${NC}"
echo -e "${BOLD}║ ConfuserEx Local CI Pipeline ║${NC}"
echo -e "${BOLD}╚══════════════════════════════════════╝${NC}"
echo " Mode: $MODE"
echo " Config: $CONFIGURATION"
echo " Root: $REPO_ROOT"
ERRORS=0
case "$MODE" in
lint)
do_lint || ERRORS=$((ERRORS + 1))
;;
build)
do_build || ERRORS=$((ERRORS + 1))
;;
test)
do_build || ERRORS=$((ERRORS + 1))
do_test || ERRORS=$((ERRORS + 1))
;;
package)
do_build || ERRORS=$((ERRORS + 1))
do_package || ERRORS=$((ERRORS + 1))
;;
all)
do_lint || ERRORS=$((ERRORS + 1))
do_build || ERRORS=$((ERRORS + 1))
do_test || ERRORS=$((ERRORS + 1))
do_package || ERRORS=$((ERRORS + 1))
;;
*)
echo "Usage: $0 [lint|build|test|package|all]"
exit 1
;;
esac
echo ""
if [ "$ERRORS" -gt 0 ]; then
fail "Pipeline finished with $ERRORS failed phase(s)"
exit 1
else
success "Pipeline complete — all phases passed"
fi