77 description : " Target branch to compare against (e.g., main)."
88 required : true
99 type : string
10+ # Python/pytest options
1011 python-version :
1112 description : " Python version for pytest."
1213 required : false
1314 type : string
1415 default : " 3.10"
16+ # Rust/cargo options
1517 rust-version :
1618 description : " Rust toolchain version for cargo test."
1719 required : false
1820 type : string
1921 default : " stable"
22+ # C++/CMake options
23+ cmake-version :
24+ description : " CMake version for C++ tests."
25+ required : false
26+ type : string
27+ default : " 3.28"
28+ cpp-compiler :
29+ description : " C++ compiler (gcc, clang). Auto-detects if empty."
30+ required : false
31+ type : string
32+ default : " "
33+ cpp-build-type :
34+ description : " CMake build type (Debug, Release, RelWithDebInfo, MinSizeRel)."
35+ required : false
36+ type : string
37+ default : " Release"
38+ cpp-build-dir :
39+ description : " Build directory for C++ projects."
40+ required : false
41+ type : string
42+ default : " build"
43+ cpp-cmake-args :
44+ description : " Additional CMake configuration arguments."
45+ required : false
46+ type : string
47+ default : " "
48+ cpp-test-args :
49+ description : " Additional CTest arguments."
50+ required : false
51+ type : string
52+ default : " "
53+ # Common options
2054 runs_on :
2155 description : " Runner label."
2256 required : false
2761 required : false
2862 type : string
2963 default : " "
64+ use_target_cache :
65+ description : " Whether to use caching for target branch test results. When false, always runs fresh tests."
66+ required : false
67+ type : boolean
68+ default : false
3069 secrets :
3170 DISCORD_WEBHOOK_URL :
3271 required : false
4584 regression_count_cargo :
4685 description : " Number of regressions (cargo)"
4786 value : ${{ jobs.compare-cargo.outputs.regression_count }}
87+ cpp_has_regressions :
88+ description : " Whether regressions were detected (C++)"
89+ value : ${{ jobs.compare-cpp.outputs.has_regressions }}
90+ cpp_regression_count :
91+ description : " Number of regressions (C++)"
92+ value : ${{ jobs.compare-cpp.outputs.regression_count }}
4893
4994jobs :
5095 # Detect which test frameworks are present
5398 outputs :
5499 has_pytest : ${{ steps.detect.outputs.has_pytest }}
55100 has_cargo : ${{ steps.detect.outputs.has_cargo }}
101+ has_cpp : ${{ steps.detect.outputs.has_cpp }}
56102 steps :
57103 - uses : actions/checkout@v4.2.2
58104 - name : Detect test frameworks
@@ -74,6 +120,25 @@ jobs:
74120 echo "has_cargo=false" >> "$GITHUB_OUTPUT"
75121 fi
76122
123+ # Detect C++ with CMake and tests
124+ HAS_CPP="false"
125+ if [ -f "CMakeLists.txt" ]; then
126+ # Check for test-related CMake content
127+ if grep -rqE "(enable_testing|add_test|gtest|catch|boost.*test)" CMakeLists.txt 2>/dev/null || \
128+ find . -name "CMakeLists.txt" -exec grep -lE "(enable_testing|add_test|gtest|catch)" {} \; 2>/dev/null | head -1 | grep -q .; then
129+ HAS_CPP="true"
130+ echo "✅ Detected: C++ (CMake with tests)"
131+ fi
132+ fi
133+ # Check for test source files
134+ if [ "$HAS_CPP" = "false" ]; then
135+ if find . \( -name "*_test.cpp" -o -name "*_test.cc" -o -name "test_*.cpp" -o -name "test_*.cc" \) 2>/dev/null | head -1 | grep -q .; then
136+ HAS_CPP="true"
137+ echo "✅ Detected: C++ test files"
138+ fi
139+ fi
140+ echo "has_cpp=$HAS_CPP" >> "$GITHUB_OUTPUT"
141+
77142 # Test source branch (always fresh, no caching)
78143 test-source :
79144 needs : detect-frameworks
@@ -111,6 +176,7 @@ jobs:
111176 # Define cache keys
112177 - name : Set cache keys
113178 id : cache-keys
179+ if : inputs.use_target_cache
114180 run : |
115181 # Version bump forces cache invalidation when extraction logic changes
116182 CACHE_VERSION="v4"
@@ -122,6 +188,7 @@ jobs:
122188 # Try to restore complete results first
123189 - name : Check for complete cache
124190 id : cache-complete
191+ if : inputs.use_target_cache
125192 uses : actions/cache/restore@v4
126193 with :
127194 path : cached_target
@@ -130,7 +197,7 @@ jobs:
130197 # If no complete cache, check for any pending cache (someone else is running)
131198 - name : Check for pending cache
132199 id : cache-pending
133- if : steps.cache-complete.outputs.cache-hit != 'true'
200+ if : inputs.use_target_cache && steps.cache-complete.outputs.cache-hit != 'true'
134201 uses : actions/cache/restore@v4
135202 with :
136203 path : cached_pending
@@ -141,7 +208,10 @@ jobs:
141208 - name : Determine initial status
142209 id : initial-status
143210 run : |
144- if [ "${{ steps.cache-complete.outputs.cache-hit }}" == "true" ]; then
211+ if [ "${{ inputs.use_target_cache }}" != "true" ]; then
212+ echo "status=disabled" >> $GITHUB_OUTPUT
213+ echo "🔄 Cache disabled - will run fresh tests"
214+ elif [ "${{ steps.cache-complete.outputs.cache-hit }}" == "true" ]; then
145215 echo "status=complete" >> $GITHUB_OUTPUT
146216 echo "✅ Found complete cache - will use it"
147217 elif [ "${{ steps.cache-pending.outputs.cache-hit }}" == "true" ]; then
@@ -154,15 +224,15 @@ jobs:
154224
155225 # If cache miss, immediately save a pending marker so others know to wait
156226 - name : Create pending marker
157- if : steps.initial-status.outputs.status == 'miss'
227+ if : inputs.use_target_cache && steps.initial-status.outputs.status == 'miss'
158228 run : |
159229 mkdir -p cached_pending_marker
160230 echo "pending" > cached_pending_marker/status
161231 echo "started=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> cached_pending_marker/status
162232 echo "run_id=${{ github.run_id }}" >> cached_pending_marker/status
163233
164234 - name : Save pending marker
165- if : steps.initial-status.outputs.status == 'miss'
235+ if : inputs.use_target_cache && steps.initial-status.outputs.status == 'miss'
166236 uses : actions/cache/save@v4
167237 with :
168238 path : cached_pending_marker
@@ -171,7 +241,7 @@ jobs:
171241 # If pending found, poll for complete cache with exponential backoff
172242 - name : Poll for complete cache
173243 id : poll-cache
174- if : steps.initial-status.outputs.status == 'pending'
244+ if : inputs.use_target_cache && steps.initial-status.outputs.status == 'pending'
175245 env :
176246 GH_TOKEN : ${{ github.token }}
177247 run : |
@@ -209,7 +279,7 @@ jobs:
209279 # Restore complete cache after polling found it
210280 - name : Restore cache after poll
211281 id : cache-after-poll
212- if : steps.poll-cache.outputs.found == 'true'
282+ if : inputs.use_target_cache && steps.poll-cache.outputs.found == 'true'
213283 uses : actions/cache/restore@v4
214284 with :
215285 path : cached_target
@@ -218,7 +288,10 @@ jobs:
218288 - name : Determine final status
219289 id : final-status
220290 run : |
221- if [ "${{ steps.cache-complete.outputs.cache-hit }}" == "true" ]; then
291+ if [ "${{ inputs.use_target_cache }}" != "true" ]; then
292+ echo "cache_hit=false" >> $GITHUB_OUTPUT
293+ echo "🔄 Cache disabled - running fresh tests"
294+ elif [ "${{ steps.cache-complete.outputs.cache-hit }}" == "true" ]; then
222295 echo "cache_hit=true" >> $GITHUB_OUTPUT
223296 echo "✅ Using complete cache (found immediately)"
224297 elif [ "${{ steps.cache-after-poll.outputs.cache-hit }}" == "true" ]; then
@@ -231,15 +304,15 @@ jobs:
231304
232305 - name : Load cached results
233306 id : load-cache
234- if : steps.final-status.outputs.cache_hit == 'true'
307+ if : inputs.use_target_cache && steps.final-status.outputs.cache_hit == 'true'
235308 run : |
236309 echo "✅ Loading cached target results (skipping test run)"
237310 if [ -f cached_target/outputs.env ]; then
238311 cat cached_target/outputs.env >> $GITHUB_OUTPUT
239312 fi
240313
241314 - name : Upload cached artifact
242- if : steps.final-status.outputs.cache_hit == 'true'
315+ if : inputs.use_target_cache && steps.final-status.outputs.cache_hit == 'true'
243316 uses : actions/upload-artifact@v4
244317 with :
245318 name : pytest_target_${{ github.event.pull_request.number || github.run_id }}
@@ -480,7 +553,7 @@ jobs:
480553 "
481554
482555 - name : Save results to cache
483- if : steps.final-status.outputs.cache_hit != 'true'
556+ if : inputs.use_target_cache && steps.final-status.outputs.cache_hit != 'true'
484557 run : |
485558 echo "💾 Saving results to cache..."
486559 mkdir -p cached_target
@@ -515,7 +588,7 @@ jobs:
515588
516589 # Save complete results so other PRs can find it
517590 - name : Upload to cache
518- if : steps.final-status.outputs.cache_hit != 'true'
591+ if : inputs.use_target_cache && steps.final-status.outputs.cache_hit != 'true'
519592 uses : actions/cache/save@v4
520593 with :
521594 path : cached_target
@@ -644,6 +717,69 @@ jobs:
644717 current_no_tests_found : ${{ needs.test-source-cargo.outputs.no_tests_found }}
645718 artifact_name : regression_cargo_${{ github.event.pull_request.number || github.run_id }}
646719
720+ # ============================================
721+ # C++ Tests (GTest/CTest)
722+ # ============================================
723+
724+ # Test C++ source branch
725+ test-source-cpp :
726+ needs : detect-frameworks
727+ if : needs.detect-frameworks.outputs.has_cpp == 'true'
728+ uses : ./.github/workflows/test-cpp-gtest.yml
729+ with :
730+ ref : " " # Default checkout = PR branch
731+ cmake-version : ${{ inputs.cmake-version }}
732+ compiler : ${{ inputs.cpp-compiler }}
733+ build-type : ${{ inputs.cpp-build-type }}
734+ build-dir : ${{ inputs.cpp-build-dir }}
735+ cmake-args : ${{ inputs.cpp-cmake-args }}
736+ test-args : ${{ inputs.cpp-test-args }}
737+ runs_on : ${{ inputs.runs_on }}
738+ artifact_name : cpp_source_${{ github.event.pull_request.number || github.run_id }}
739+ parallel_workers : ${{ inputs.parallel_workers }}
740+
741+ # Test C++ target branch
742+ test-target-cpp :
743+ needs : detect-frameworks
744+ if : needs.detect-frameworks.outputs.has_cpp == 'true'
745+ uses : ./.github/workflows/test-cpp-gtest.yml
746+ with :
747+ ref : ${{ inputs.target_branch }}
748+ cmake-version : ${{ inputs.cmake-version }}
749+ compiler : ${{ inputs.cpp-compiler }}
750+ build-type : ${{ inputs.cpp-build-type }}
751+ build-dir : ${{ inputs.cpp-build-dir }}
752+ cmake-args : ${{ inputs.cpp-cmake-args }}
753+ test-args : ${{ inputs.cpp-test-args }}
754+ runs_on : ${{ inputs.runs_on }}
755+ artifact_name : cpp_target_${{ github.event.pull_request.number || github.run_id }}
756+ parallel_workers : ${{ inputs.parallel_workers }}
757+
758+ # Compare C++ results
759+ compare-cpp :
760+ needs : [test-source-cpp, test-target-cpp]
761+ if : always() && needs.test-source-cpp.result == 'success'
762+ uses : ./.github/workflows/regression-test.yml
763+ with :
764+ runs_on : ${{ inputs.runs_on }}
765+ baseline_label : ${{ inputs.target_branch }}
766+ baseline_results_artifact : cpp_target_${{ github.event.pull_request.number || github.run_id }}
767+ baseline_results_filename : test_data.json
768+ current_label : ${{ github.head_ref || github.ref_name }}
769+ current_results_artifact : cpp_source_${{ github.event.pull_request.number || github.run_id }}
770+ current_results_filename : test_data.json
771+ baseline_passed : ${{ needs.test-target-cpp.outputs.passed }}
772+ baseline_total : ${{ needs.test-target-cpp.outputs.total }}
773+ baseline_percentage : ${{ needs.test-target-cpp.outputs.percentage }}
774+ current_passed : ${{ needs.test-source-cpp.outputs.passed }}
775+ current_total : ${{ needs.test-source-cpp.outputs.total }}
776+ current_percentage : ${{ needs.test-source-cpp.outputs.percentage }}
777+ baseline_collection_errors : ${{ needs.test-target-cpp.outputs.collection_errors }}
778+ baseline_no_tests_found : ${{ needs.test-target-cpp.outputs.no_tests_found }}
779+ current_collection_errors : ${{ needs.test-source-cpp.outputs.collection_errors }}
780+ current_no_tests_found : ${{ needs.test-source-cpp.outputs.no_tests_found }}
781+ artifact_name : regression_cpp_${{ github.event.pull_request.number || github.run_id }}
782+
647783 # ============== NOTIFICATIONS ==============
648784
649785 # Notify on pytest regressions
@@ -705,3 +841,33 @@ jobs:
705841 curl -s -H "Content-Type: application/json" \
706842 -d "{\"content\": \"$(echo -e "$MSG")\"}" \
707843 "$WEBHOOK" || true
844+
845+ # Notify on C++ regressions
846+ notify-cpp :
847+ needs : [detect-frameworks, test-source-cpp, test-target-cpp, compare-cpp]
848+ if : |
849+ always() &&
850+ needs.detect-frameworks.outputs.has_cpp == 'true' &&
851+ (needs.compare-cpp.outputs.has_regressions == 'true' || needs.compare-cpp.result == 'failure')
852+ runs-on : ${{ fromJSON(inputs.runs_on) }}
853+ steps :
854+ - name : Send notification
855+ env :
856+ WEBHOOK : ${{ secrets.DISCORD_WEBHOOK_URL }}
857+ run : |
858+ if [ -z "$WEBHOOK" ]; then
859+ echo "No Discord webhook configured, skipping notification"
860+ exit 0
861+ fi
862+
863+ MSG="**C++ Test Regression Alert**\n"
864+ MSG+="PR #${{ github.event.pull_request.number }}: ${{ github.event.pull_request.title }}\n"
865+ MSG+="\`${{ github.head_ref }}\` → \`${{ inputs.target_branch }}\`\n\n"
866+ MSG+="Source: ${{ needs.test-source-cpp.outputs.passed }}/${{ needs.test-source-cpp.outputs.total }}\n"
867+ MSG+="Target: ${{ needs.test-target-cpp.outputs.passed }}/${{ needs.test-target-cpp.outputs.total }}\n"
868+ MSG+="Regressions: ${{ needs.compare-cpp.outputs.regression_count || '?' }}\n\n"
869+ MSG+="[View Run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
870+
871+ curl -s -H "Content-Type: application/json" \
872+ -d "{\"content\": \"$(echo -e "$MSG")\"}" \
873+ "$WEBHOOK" || true
0 commit comments