Skip to content

Commit 56574a2

Browse files
author
RandomCrocodile
committed
chore: add local-ci.sh script mirroring GitHub Actions pipeline
Full local CI script that replicates lint.yml, ci.yml, and test.yml: - lint: whitespace, style, and analyzer checks via dotnet format - build: dotnet build for SDK projects + MSBuild.exe for C++/CLI - test: discovers all *.Test.csproj, runs with coverage, summary - package: creates CLI, GUI, and combined zip archives Usage: ./scripts/local-ci.sh [lint|build|test|package|all]
1 parent cf631ae commit 56574a2

1 file changed

Lines changed: 361 additions & 0 deletions

File tree

scripts/local-ci.sh

Lines changed: 361 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,361 @@
1+
#!/usr/bin/env bash
2+
# =============================================================================
3+
# local-ci.sh — Run the full CI pipeline locally (mirrors GitHub Actions)
4+
#
5+
# Replicates: lint.yml, ci.yml (build + package), test.yml (test + coverage)
6+
#
7+
# Usage:
8+
# ./scripts/local-ci.sh # run everything
9+
# ./scripts/local-ci.sh lint # lint only
10+
# ./scripts/local-ci.sh build # restore + build only
11+
# ./scripts/local-ci.sh test # build + test only
12+
# ./scripts/local-ci.sh package # build + package only
13+
# ./scripts/local-ci.sh all # everything (default)
14+
# =============================================================================
15+
16+
set -euo pipefail
17+
18+
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
19+
cd "$REPO_ROOT"
20+
21+
CONFIGURATION="Release"
22+
SLN="Confuser2.sln"
23+
RESULTS_DIR="test-results"
24+
COVERAGE_DIR="coverage"
25+
26+
# ---------------------------------------------------------------------------
27+
# Colors
28+
# ---------------------------------------------------------------------------
29+
RED='\033[0;31m'
30+
GREEN='\033[0;32m'
31+
YELLOW='\033[1;33m'
32+
CYAN='\033[0;36m'
33+
BOLD='\033[1m'
34+
NC='\033[0m'
35+
36+
step() { echo -e "\n${CYAN}${BOLD}==> $1${NC}"; }
37+
success() { echo -e "${GREEN}$1${NC}"; }
38+
warn() { echo -e "${YELLOW}$1${NC}"; }
39+
fail() { echo -e "${RED}$1${NC}"; }
40+
41+
# ---------------------------------------------------------------------------
42+
# Find MSBuild via vswhere (CI uses microsoft/setup-msbuild@v2)
43+
# ---------------------------------------------------------------------------
44+
find_msbuild() {
45+
# vswhere -latest finds the newest VS (2025 > 2022 > 2019).
46+
# CI uses windows-2025 runners with VS 2025 / MSBuild 18.
47+
local vswhere="/c/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe"
48+
if [ -f "$vswhere" ]; then
49+
# Get installation path first, then construct MSBuild path
50+
local vs_path
51+
vs_path=$("$vswhere" -latest -requires Microsoft.Component.MSBuild \
52+
-property installationPath 2>/dev/null | head -1)
53+
if [ -n "$vs_path" ]; then
54+
# Convert Windows path to unix-style for bash
55+
local unix_path
56+
unix_path=$(cygpath -u "$vs_path" 2>/dev/null || echo "$vs_path")
57+
MSBUILD="$unix_path/MSBuild/Current/Bin/MSBuild.exe"
58+
if [ ! -f "$MSBUILD" ]; then
59+
MSBUILD=""
60+
fi
61+
fi
62+
fi
63+
64+
if [ -z "${MSBUILD:-}" ]; then
65+
warn "MSBuild not found — will use 'dotnet build' (C++/CLI project will be skipped)"
66+
return 1
67+
fi
68+
69+
local ver
70+
ver=$("$MSBUILD" -version 2>/dev/null | tail -1 || echo "unknown")
71+
echo " MSBuild: $MSBUILD (v$ver)"
72+
return 0
73+
}
74+
75+
# ---------------------------------------------------------------------------
76+
# Phase: Lint (mirrors lint.yml)
77+
# ---------------------------------------------------------------------------
78+
do_lint() {
79+
step "LINT — whitespace, style, analyzers"
80+
81+
local any_failed=false
82+
83+
echo " Restoring for lint..."
84+
dotnet restore "$SLN" --verbosity quiet 2>/dev/null
85+
86+
echo " Checking whitespace..."
87+
if dotnet format whitespace "$SLN" --verify-no-changes --verbosity minimal 2>&1 | tail -5; then
88+
success "Whitespace OK"
89+
else
90+
warn "Whitespace issues found"
91+
any_failed=true
92+
fi
93+
94+
echo " Checking style..."
95+
if dotnet format style "$SLN" --verify-no-changes --severity warn 2>&1 | tail -5; then
96+
success "Style OK"
97+
else
98+
warn "Style issues found"
99+
any_failed=true
100+
fi
101+
102+
echo " Checking analyzers..."
103+
if dotnet format analyzers "$SLN" --verify-no-changes --severity warn 2>&1 | tail -5; then
104+
success "Analyzers OK"
105+
else
106+
warn "Analyzer issues found"
107+
any_failed=true
108+
fi
109+
110+
if [ "$any_failed" = true ]; then
111+
warn "Lint issues found — run 'dotnet format $SLN' to fix"
112+
return 1
113+
else
114+
success "All lint checks passed"
115+
fi
116+
}
117+
118+
# ---------------------------------------------------------------------------
119+
# Phase: Restore + Build (mirrors ci.yml)
120+
# ---------------------------------------------------------------------------
121+
do_build() {
122+
step "BUILD — restore + compile ($CONFIGURATION)"
123+
124+
local has_msbuild=false
125+
find_msbuild && has_msbuild=true
126+
127+
# Use 'dotnet build' for all SDK-style projects (handles .NET SDK resolution).
128+
# Then use MSBuild.exe for the C++/CLI project if available.
129+
echo " Restoring..."
130+
dotnet restore "$SLN" --verbosity minimal
131+
132+
echo " Building (dotnet)..."
133+
dotnet build "$SLN" -c "$CONFIGURATION" --no-restore 2>&1 \
134+
| grep -v "244_ClrProtection" || true
135+
136+
# C++/CLI project requires MSBuild.exe (not dotnet build)
137+
if [ "$has_msbuild" = true ]; then
138+
local vcxproj="Tests/244_ClrProtection/244_ClrProtection.vcxproj"
139+
if [ -f "$vcxproj" ]; then
140+
echo " Building C++/CLI test project (msbuild)..."
141+
"$MSBUILD" "$vcxproj" -p:Configuration="$CONFIGURATION" -verbosity:minimal 2>&1 \
142+
| tail -3 || warn "C++/CLI project build failed (non-critical)"
143+
fi
144+
else
145+
warn "Skipping C++/CLI project (no MSBuild.exe found)"
146+
fi
147+
148+
# Verify key outputs exist
149+
local cli_dll="Confuser.CLI/bin/$CONFIGURATION/net10.0/Confuser.CLI.dll"
150+
local gui_dll="ConfuserEx/bin/$CONFIGURATION/net10.0-windows/ConfuserEx.dll"
151+
local core48="Confuser.Core/bin/$CONFIGURATION/net48/Confuser.Core.dll"
152+
local corenstd="Confuser.Core/bin/$CONFIGURATION/netstandard2.0/Confuser.Core.dll"
153+
154+
local all_ok=true
155+
for f in "$cli_dll" "$gui_dll" "$core48" "$corenstd"; do
156+
if [ -f "$f" ]; then
157+
success "$(basename "$f") ($(dirname "$f" | sed "s|.*/bin/$CONFIGURATION/||"))"
158+
else
159+
fail "Missing: $f"
160+
all_ok=false
161+
fi
162+
done
163+
164+
if [ "$all_ok" = false ]; then
165+
fail "Build produced missing outputs"
166+
return 1
167+
fi
168+
169+
success "Build complete"
170+
}
171+
172+
# ---------------------------------------------------------------------------
173+
# Phase: Test (mirrors test.yml)
174+
# ---------------------------------------------------------------------------
175+
do_test() {
176+
step "TEST — run all test projects with coverage"
177+
178+
rm -rf "$RESULTS_DIR" 2>/dev/null || true
179+
mkdir -p "$RESULTS_DIR"
180+
181+
local total_passed=0
182+
local total_failed=0
183+
local total_skipped=0
184+
local any_failed=false
185+
186+
# Find all *.Test.csproj (same as CI: Get-ChildItem -Filter '*.Test.csproj' -Recurse)
187+
while IFS= read -r proj; do
188+
local name
189+
name=$(basename "$proj" .csproj)
190+
echo -e "\n ${CYAN}Testing $name...${NC}"
191+
192+
local output
193+
output=$(dotnet test "$proj" -c "$CONFIGURATION" --no-build --verbosity minimal \
194+
--collect:"XPlat Code Coverage" \
195+
--logger "trx;LogFileName=$name.trx" \
196+
--results-directory "$RESULTS_DIR/$name" 2>&1) || true
197+
198+
# Parse summary line: "Passed! - Failed: 0, Passed: 3, Skipped: 0, Total: 3"
199+
local summary
200+
summary=$(echo "$output" | grep -E "^(Passed!|Failed!)" | tail -1)
201+
202+
if [ -n "$summary" ]; then
203+
local p f s
204+
p=$(echo "$summary" | grep -oP 'Passed:\s+\K\d+' || echo 0)
205+
f=$(echo "$summary" | grep -oP 'Failed:\s+\K\d+' || echo 0)
206+
s=$(echo "$summary" | grep -oP 'Skipped:\s+\K\d+' || echo 0)
207+
total_passed=$((total_passed + p))
208+
total_failed=$((total_failed + f))
209+
total_skipped=$((total_skipped + s))
210+
211+
if echo "$summary" | grep -q "^Failed!"; then
212+
fail "$name$summary"
213+
any_failed=true
214+
else
215+
success "$name — Passed: $p, Failed: $f, Skipped: $s"
216+
fi
217+
else
218+
# No tests discovered
219+
local no_test
220+
no_test=$(echo "$output" | grep -c "No test is available" || true)
221+
if [ "$no_test" -gt 0 ]; then
222+
warn "$name — no tests discovered (missing Microsoft.NET.Test.Sdk?)"
223+
else
224+
warn "$name — no test output"
225+
fi
226+
fi
227+
done < <(find Tests -name "*.Test.csproj" -type f | sort)
228+
229+
echo ""
230+
echo " ─────────────────────────────────────"
231+
echo -e " ${BOLD}Total: Passed=$total_passed Failed=$total_failed Skipped=$total_skipped${NC}"
232+
echo " ─────────────────────────────────────"
233+
234+
# Generate coverage report if reportgenerator is available
235+
local reports
236+
reports=$(find "$RESULTS_DIR" -name "coverage.cobertura.xml" 2>/dev/null | tr '\n' ';')
237+
if [ -n "$reports" ] && command -v reportgenerator &>/dev/null; then
238+
step "COVERAGE — generating report"
239+
mkdir -p "$COVERAGE_DIR/report"
240+
reportgenerator "-reports:$reports" \
241+
"-targetdir:$COVERAGE_DIR/report" \
242+
"-reporttypes:TextSummary" 2>/dev/null
243+
cat "$COVERAGE_DIR/report/Summary.txt" 2>/dev/null || true
244+
elif [ -n "$reports" ]; then
245+
warn "Install reportgenerator for coverage reports: dotnet tool install -g dotnet-reportgenerator-globaltool"
246+
fi
247+
248+
if [ "$any_failed" = true ]; then
249+
fail "Some tests failed"
250+
return 1
251+
fi
252+
253+
success "All tests passed"
254+
}
255+
256+
# ---------------------------------------------------------------------------
257+
# Phase: Package (mirrors ci.yml packaging steps)
258+
# ---------------------------------------------------------------------------
259+
do_package() {
260+
step "PACKAGE — create release archives"
261+
262+
local cli_dir="Confuser.CLI/bin/$CONFIGURATION/net10.0"
263+
local gui_dir="ConfuserEx/bin/$CONFIGURATION/net10.0-windows"
264+
265+
# CLI zip
266+
if [ -d "$cli_dir" ]; then
267+
rm -f ConfuserEx-CLI.zip 2>/dev/null || true
268+
(cd "$cli_dir" && find . -not -name '*.pdb' -not -name '*.xml' -not -path './runtimes/*/native/*' \
269+
-type f | sort | zip -q "$REPO_ROOT/ConfuserEx-CLI.zip" -@)
270+
local size
271+
size=$(du -h ConfuserEx-CLI.zip | cut -f1)
272+
success "ConfuserEx-CLI.zip ($size)"
273+
else
274+
fail "CLI output not found at $cli_dir — run build first"
275+
fi
276+
277+
# GUI zip
278+
if [ -d "$gui_dir" ]; then
279+
rm -f ConfuserEx-GUI.zip 2>/dev/null || true
280+
(cd "$gui_dir" && find . -not -name '*.pdb' -not -name '*.xml' \
281+
-type f | sort | zip -q "$REPO_ROOT/ConfuserEx-GUI.zip" -@)
282+
size=$(du -h ConfuserEx-GUI.zip | cut -f1)
283+
success "ConfuserEx-GUI.zip ($size)"
284+
else
285+
fail "GUI output not found at $gui_dir — run build first"
286+
fi
287+
288+
# Combined zip
289+
rm -rf combined 2>/dev/null || true
290+
mkdir -p combined
291+
cp "$cli_dir"/* combined/ 2>/dev/null || true
292+
cp "$gui_dir"/* combined/ 2>/dev/null || true
293+
rm -f combined/*.pdb combined/*.xml 2>/dev/null || true
294+
if [ "$(ls -A combined 2>/dev/null)" ]; then
295+
rm -f ConfuserEx.zip 2>/dev/null || true
296+
(cd combined && find . -type f | sort | zip -q "$REPO_ROOT/ConfuserEx.zip" -@)
297+
size=$(du -h ConfuserEx.zip | cut -f1)
298+
success "ConfuserEx.zip ($size)"
299+
fi
300+
rm -rf combined
301+
302+
# NuGet package
303+
local nupkg
304+
nupkg=$(find Confuser.MSBuild.Tasks/bin/$CONFIGURATION -name "*.nupkg" 2>/dev/null | head -1)
305+
if [ -n "$nupkg" ]; then
306+
success "$(basename "$nupkg")"
307+
else
308+
warn "No .nupkg found (MSBuild-only build may be required)"
309+
fi
310+
311+
success "Packaging complete"
312+
}
313+
314+
# ---------------------------------------------------------------------------
315+
# Main
316+
# ---------------------------------------------------------------------------
317+
MODE="${1:-all}"
318+
319+
echo -e "${BOLD}╔══════════════════════════════════════╗${NC}"
320+
echo -e "${BOLD}║ ConfuserEx Local CI Pipeline ║${NC}"
321+
echo -e "${BOLD}╚══════════════════════════════════════╝${NC}"
322+
echo " Mode: $MODE"
323+
echo " Config: $CONFIGURATION"
324+
echo " Root: $REPO_ROOT"
325+
326+
ERRORS=0
327+
328+
case "$MODE" in
329+
lint)
330+
do_lint || ERRORS=$((ERRORS + 1))
331+
;;
332+
build)
333+
do_build || ERRORS=$((ERRORS + 1))
334+
;;
335+
test)
336+
do_build || ERRORS=$((ERRORS + 1))
337+
do_test || ERRORS=$((ERRORS + 1))
338+
;;
339+
package)
340+
do_build || ERRORS=$((ERRORS + 1))
341+
do_package || ERRORS=$((ERRORS + 1))
342+
;;
343+
all)
344+
do_lint || ERRORS=$((ERRORS + 1))
345+
do_build || ERRORS=$((ERRORS + 1))
346+
do_test || ERRORS=$((ERRORS + 1))
347+
do_package || ERRORS=$((ERRORS + 1))
348+
;;
349+
*)
350+
echo "Usage: $0 [lint|build|test|package|all]"
351+
exit 1
352+
;;
353+
esac
354+
355+
echo ""
356+
if [ "$ERRORS" -gt 0 ]; then
357+
fail "Pipeline finished with $ERRORS failed phase(s)"
358+
exit 1
359+
else
360+
success "Pipeline complete — all phases passed"
361+
fi

0 commit comments

Comments
 (0)