Wheels Compatibility Matrix #4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Compatibility matrix: runs all engines x databases on a weekly schedule | |
| # and on manual dispatch. Non-blocking — informational only. | |
| name: Wheels Compatibility Matrix | |
| on: | |
| schedule: | |
| # Weekly: Sunday 02:00 UTC | |
| - cron: '0 2 * * 0' | |
| workflow_dispatch: | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true | |
| jobs: | |
| tests: | |
| name: "${{ matrix.cfengine }}" | |
| runs-on: ubuntu-latest | |
| continue-on-error: true | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| cfengine: | |
| ["lucee6", "lucee7", "adobe2023", "adobe2025", "boxlang"] | |
| experimental: [false] | |
| env: | |
| PORT_lucee6: 60006 | |
| PORT_lucee7: 60007 | |
| PORT_adobe2023: 62023 | |
| PORT_adobe2025: 62025 | |
| PORT_boxlang: 60001 | |
| steps: | |
| - name: Checkout Repository | |
| uses: actions/checkout@v5 | |
| - name: Determine databases for this engine | |
| id: db-list | |
| run: | | |
| # Every engine gets these databases | |
| DATABASES="mysql,postgres,sqlserver,cockroachdb,oracle,sqlite" | |
| # Add h2 only for engines that support it (Lucee only) | |
| case "${{ matrix.cfengine }}" in | |
| lucee6|lucee7) | |
| DATABASES="mysql,postgres,sqlserver,h2,cockroachdb,oracle,sqlite" | |
| ;; | |
| esac | |
| echo "databases=${DATABASES}" >> $GITHUB_OUTPUT | |
| echo "Databases for ${{ matrix.cfengine }}: ${DATABASES}" | |
| - name: Download ojdbc10 for Adobe engines | |
| if: startsWith(matrix.cfengine, 'adobe') | |
| run: | | |
| mkdir -p ./.engine/${{ matrix.cfengine }}/WEB-INF/lib | |
| wget -q https://download.oracle.com/otn-pub/otn_software/jdbc/1927/ojdbc10.jar \ | |
| -O ./.engine/${{ matrix.cfengine }}/WEB-INF/lib/ojdbc10.jar | |
| - name: Start CF engine | |
| run: docker compose build --no-cache ${{ matrix.cfengine }} && docker compose up -d ${{ matrix.cfengine }} | |
| - name: Start all databases | |
| run: | | |
| IFS=',' read -ra DBS <<< "${{ steps.db-list.outputs.databases }}" | |
| EXTERNAL_DBS="" | |
| for db in "${DBS[@]}"; do | |
| if [ "$db" != "h2" ] && [ "$db" != "sqlite" ]; then | |
| EXTERNAL_DBS="$EXTERNAL_DBS $db" | |
| fi | |
| done | |
| # CockroachDB needs its init sidecar to create the test database/user | |
| if echo "$EXTERNAL_DBS" | grep -q "cockroachdb"; then | |
| EXTERNAL_DBS="$EXTERNAL_DBS cockroachdb-init" | |
| fi | |
| if [ -n "$EXTERNAL_DBS" ]; then | |
| echo "Starting external databases:${EXTERNAL_DBS}" | |
| docker compose up -d ${EXTERNAL_DBS} | |
| fi | |
| - name: Wait for CF engine to be ready | |
| run: | | |
| PORT_VAR="PORT_${{ matrix.cfengine }}" | |
| PORT="${!PORT_VAR}" | |
| CONTAINER="wheels-${{ matrix.cfengine }}-1" | |
| echo "Waiting for ${{ matrix.cfengine }} on port ${PORT}..." | |
| # Wait for HTTP response, restarting container if it crashes | |
| MAX_WAIT=60 | |
| WAIT_COUNT=0 | |
| RESTARTS=0 | |
| MAX_RESTARTS=3 | |
| while [ "$WAIT_COUNT" -lt "$MAX_WAIT" ]; do | |
| WAIT_COUNT=$((WAIT_COUNT + 1)) | |
| # Check if container has exited (crashed during startup) | |
| CONTAINER_STATUS=$(docker inspect --format='{{.State.Status}}' "$CONTAINER" 2>/dev/null || echo "missing") | |
| if [ "$CONTAINER_STATUS" = "exited" ] || [ "$CONTAINER_STATUS" = "dead" ] || [ "$CONTAINER_STATUS" = "missing" ]; then | |
| RESTARTS=$((RESTARTS + 1)) | |
| if [ "$RESTARTS" -le "$MAX_RESTARTS" ]; then | |
| echo "Container $CONTAINER has status '$CONTAINER_STATUS' — restarting (attempt $RESTARTS/$MAX_RESTARTS)..." | |
| docker compose up -d ${{ matrix.cfengine }} | |
| sleep 10 | |
| continue | |
| else | |
| echo "::error::Container $CONTAINER failed to start after $MAX_RESTARTS restart attempts" | |
| docker logs "$CONTAINER" 2>&1 | tail -50 | |
| exit 1 | |
| fi | |
| fi | |
| if curl -s -o /dev/null --connect-timeout 2 --max-time 5 -w "%{http_code}" "http://localhost:${PORT}/" | grep -q "200\|404\|302"; then | |
| echo "CF engine is ready!" | |
| break | |
| fi | |
| if [ "$WAIT_COUNT" -lt "$MAX_WAIT" ]; then | |
| sleep 5 | |
| fi | |
| done | |
| if [ "$WAIT_COUNT" -ge "$MAX_WAIT" ]; then | |
| echo "::error::CF engine not ready after ${MAX_WAIT} attempts" | |
| docker logs "$CONTAINER" 2>&1 | tail -50 | |
| exit 1 | |
| fi | |
| - name: Patch Adobe CF serialfilter.txt for Oracle JDBC | |
| if: matrix.cfengine == 'adobe2023' || matrix.cfengine == 'adobe2025' | |
| run: | | |
| docker exec wheels-${{ matrix.cfengine }}-1 sh -c \ | |
| "echo ';oracle.sql.converter.**;oracle.sql.**;oracle.jdbc.**' >> /wheels-test-suite/.engine/${{ matrix.cfengine }}/WEB-INF/cfusion/lib/serialfilter.txt" | |
| docker restart wheels-${{ matrix.cfengine }}-1 | |
| # Wait for engine to come back up after restart | |
| PORT_VAR="PORT_${{ matrix.cfengine }}" | |
| PORT="${!PORT_VAR}" | |
| MAX_WAIT=30 | |
| WAIT_COUNT=0 | |
| while [ "$WAIT_COUNT" -lt "$MAX_WAIT" ]; do | |
| WAIT_COUNT=$((WAIT_COUNT + 1)) | |
| if curl -s -o /dev/null --connect-timeout 2 --max-time 5 -w "%{http_code}" "http://localhost:${PORT}/" | grep -q "200\|404\|302"; then | |
| echo "CF engine back up after restart" | |
| break | |
| fi | |
| sleep 5 | |
| done | |
| - name: Install CFPM packages (Adobe 2023/2025) | |
| if: matrix.cfengine == 'adobe2023' || matrix.cfengine == 'adobe2025' | |
| run: | | |
| MAX_RETRIES=3 | |
| RETRY_COUNT=0 | |
| while [ "$RETRY_COUNT" -lt "$MAX_RETRIES" ]; do | |
| RETRY_COUNT=$((RETRY_COUNT + 1)) | |
| echo "Attempt $RETRY_COUNT of $MAX_RETRIES: Installing CFPM packages..." | |
| if docker exec wheels-${{ matrix.cfengine }}-1 box cfpm install image,mail,zip,debugger,caching,mysql,postgresql,sqlserver,oracle; then | |
| echo "CFPM packages installed successfully" | |
| exit 0 | |
| else | |
| echo "CFPM installation failed on attempt $RETRY_COUNT" | |
| if [ "$RETRY_COUNT" -lt "$MAX_RETRIES" ]; then | |
| echo "Waiting 10 seconds before retry..." | |
| sleep 10 | |
| docker exec wheels-${{ matrix.cfengine }}-1 box server restart || true | |
| sleep 10 | |
| fi | |
| fi | |
| done | |
| echo "Failed to install CFPM packages after $MAX_RETRIES attempts" | |
| exit 1 | |
| - name: Wait for Oracle to be ready | |
| if: contains(steps.db-list.outputs.databases, 'oracle') | |
| run: | | |
| echo "Waiting for Oracle to accept connections..." | |
| MAX_WAIT=60 | |
| WAIT_COUNT=0 | |
| while [ "$WAIT_COUNT" -lt "$MAX_WAIT" ]; do | |
| WAIT_COUNT=$((WAIT_COUNT + 1)) | |
| if docker exec wheels-oracle-1 sqlplus -S wheelstestdb/wheelstestdb@localhost:1521/wheelstestdb <<< "SELECT 1 FROM DUAL; EXIT;" > /dev/null 2>&1; then | |
| echo "Oracle is ready! (attempt ${WAIT_COUNT})" | |
| exit 0 | |
| fi | |
| echo "Oracle not ready yet (attempt ${WAIT_COUNT}/${MAX_WAIT})..." | |
| sleep 5 | |
| done | |
| echo "Warning: Oracle may not be fully ready after ${MAX_WAIT} attempts" | |
| - name: Wait for other databases to be ready | |
| run: | | |
| IFS=',' read -ra DBS <<< "${{ steps.db-list.outputs.databases }}" | |
| for db in "${DBS[@]}"; do | |
| case "$db" in | |
| mysql) | |
| echo "Waiting for MySQL..." | |
| timeout 60 bash -c 'until docker exec wheels-mysql-1 mysqladmin ping -h localhost -u root -pwheelstestdb --silent 2>/dev/null; do sleep 2; done' | |
| echo "MySQL is ready" | |
| ;; | |
| postgres) | |
| echo "Waiting for PostgreSQL..." | |
| timeout 60 bash -c 'until docker exec wheels-postgres-1 pg_isready -U wheelstestdb 2>/dev/null; do sleep 2; done' | |
| echo "PostgreSQL is ready" | |
| ;; | |
| sqlserver) | |
| echo "Waiting for SQL Server..." | |
| timeout 120 bash -c 'until docker exec wheels-sqlserver-1 /opt/mssql-tools18/bin/sqlcmd -S localhost -U SA -P "x!bsT8t60yo0cTVTPq" -Q "SELECT 1" -C 2>/dev/null | grep -q "1"; do sleep 5; done' | |
| echo "SQL Server is ready" | |
| ;; | |
| cockroachdb) | |
| echo "Waiting for CockroachDB..." | |
| timeout 60 bash -c 'until docker exec wheels-cockroachdb-1 cockroach sql --insecure -e "SELECT 1" 2>/dev/null; do sleep 2; done' | |
| echo "CockroachDB is ready" | |
| echo "Waiting for CockroachDB init to complete..." | |
| timeout 60 bash -c 'while [ "$(docker inspect --format="{{.State.Status}}" wheels-cockroachdb-init-1 2>/dev/null)" != "exited" ]; do sleep 2; done' | |
| echo "CockroachDB init complete" | |
| ;; | |
| h2|sqlite) | |
| echo "$db requires no external container" | |
| ;; | |
| oracle) | |
| echo "Oracle readiness already checked above" | |
| ;; | |
| esac | |
| done | |
| - name: Run test suites for all databases | |
| id: run-tests | |
| run: | | |
| PORT_VAR="PORT_${{ matrix.cfengine }}" | |
| PORT="${!PORT_VAR}" | |
| BASE_URL="http://localhost:${PORT}/wheels/core/tests" | |
| IFS=',' read -ra DBS <<< "${{ steps.db-list.outputs.databases }}" | |
| OVERALL_STATUS=0 | |
| RESULTS_JSON="{" | |
| FIRST=true | |
| mkdir -p /tmp/test-results | |
| mkdir -p /tmp/junit-results | |
| # Warm-up: trigger Wheels onApplicationStart before first test run. | |
| # The engine readiness check only verifies the web server responds — | |
| # Wheels app initialization (datasource verification, model scanning) | |
| # happens on the first real request. | |
| echo "Warming up Wheels application before first test run..." | |
| curl -s -o /dev/null --max-time 60 "http://localhost:${PORT}/" || true | |
| sleep 2 | |
| DB_INDEX=0 | |
| for db in "${DBS[@]}"; do | |
| DB_INDEX=$((DB_INDEX + 1)) | |
| echo "" | |
| echo "==============================================" | |
| echo "Running tests: ${{ matrix.cfengine }} + ${db}" | |
| echo "==============================================" | |
| # Restart the CF engine container between database runs to ensure | |
| # a completely clean application state. Without this, cached model | |
| # metadata (application.wheels.models) and association methods from | |
| # the previous database's test run can leak into subsequent runs. | |
| # This is cheaper than a full container rebuild (~10-15s restart vs | |
| # minutes for Docker build) and guarantees no cross-DB contamination. | |
| if [ "$DB_INDEX" -gt 1 ]; then | |
| echo "Restarting ${{ matrix.cfengine }} for clean application state..." | |
| docker restart wheels-${{ matrix.cfengine }}-1 | |
| PORT_VAR="PORT_${{ matrix.cfengine }}" | |
| PORT="${!PORT_VAR}" | |
| WAIT=0 | |
| while [ "$WAIT" -lt 30 ]; do | |
| WAIT=$((WAIT + 1)) | |
| if curl -s -o /dev/null --connect-timeout 2 --max-time 5 -w "%{http_code}" "http://localhost:${PORT}/" | grep -q "200\|404\|302"; then | |
| echo "${{ matrix.cfengine }} is back up" | |
| break | |
| fi | |
| sleep 5 | |
| done | |
| # Warm-up request: trigger Wheels onApplicationStart so datasources | |
| # and ORM metadata are fully initialized before running the test suite. | |
| # The readiness check above only confirms the web server responds — | |
| # Wheels app init (datasource verification, model scanning, etc.) | |
| # happens on the first real request and can take a few seconds. | |
| echo "Warming up Wheels application..." | |
| curl -s -o /dev/null --max-time 60 "http://localhost:${PORT}/" || true | |
| sleep 2 | |
| fi | |
| TEST_URL="${BASE_URL}?db=${db}&format=json" | |
| RESULT_FILE="/tmp/test-results/${{ matrix.cfengine }}-${db}-result.txt" | |
| JUNIT_FILE="/tmp/junit-results/${{ matrix.cfengine }}-${db}-junit.xml" | |
| MAX_RETRIES=3 | |
| RETRY_COUNT=0 | |
| HTTP_CODE="000" | |
| while [ "$RETRY_COUNT" -lt "$MAX_RETRIES" ]; do | |
| RETRY_COUNT=$((RETRY_COUNT + 1)) | |
| echo "Test attempt ${RETRY_COUNT} of ${MAX_RETRIES}..." | |
| HTTP_CODE=$(curl -s -o "$RESULT_FILE" \ | |
| --max-time 900 \ | |
| --write-out "%{http_code}" \ | |
| "$TEST_URL" || echo "000") | |
| echo "HTTP Code: ${HTTP_CODE}" | |
| # Stop retrying on success (200) or test failures (417) — | |
| # 417 means tests ran to completion but some failed, which | |
| # is a definitive result. Only retry on transient errors. | |
| if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "417" ]; then | |
| break | |
| fi | |
| if [ "$RETRY_COUNT" -lt "$MAX_RETRIES" ]; then | |
| echo "Transient error (HTTP ${HTTP_CODE}), waiting 15 seconds before retry..." | |
| sleep 15 | |
| fi | |
| done | |
| # Convert JSON results to JUnit XML locally (avoids a second HTTP | |
| # request which would re-run the entire test suite — runner.cfm | |
| # does not cache results between requests) | |
| if [ -f "$RESULT_FILE" ]; then | |
| ENGINE="${{ matrix.cfengine }}" DB="${db}" \ | |
| RESULT_FILE="$RESULT_FILE" JUNIT_FILE="$JUNIT_FILE" \ | |
| python3 -c " | |
| import json, sys, os | |
| from xml.etree.ElementTree import Element, SubElement, tostring | |
| engine = os.environ['ENGINE'] | |
| db = os.environ['DB'] | |
| prefix = f'{engine}/{db}' | |
| try: | |
| d = json.load(open(os.environ['RESULT_FILE'])) | |
| except: | |
| sys.exit(0) | |
| def safe_str(val, default=''): | |
| \"\"\"Coerce None/null JSON values to string for XML serialization.\"\"\" | |
| return str(val) if val is not None else default | |
| def process_suite(parent_el, suite): | |
| \"\"\"Recursively process suites (TestBox suites can be nested).\"\"\" | |
| for sp in suite.get('specStats', []): | |
| tc = SubElement(parent_el, 'testcase', | |
| name=safe_str(sp.get('name')), | |
| classname=f\"{prefix} :: {safe_str(suite.get('name'))}\", | |
| time=str(sp.get('totalDuration', 0) / 1000)) | |
| if sp.get('status') == 'Failed': | |
| f = SubElement(tc, 'failure', message=safe_str(sp.get('failMessage'))) | |
| f.text = safe_str(sp.get('failDetail')) | |
| elif sp.get('status') == 'Error': | |
| e = SubElement(tc, 'error', message=safe_str(sp.get('failMessage'))) | |
| e.text = safe_str(sp.get('failDetail')) | |
| elif sp.get('status') == 'Skipped': | |
| SubElement(tc, 'skipped') | |
| # Recurse into child suites | |
| for child in suite.get('suiteStats', []): | |
| process_suite(parent_el, child) | |
| root = Element('testsuites', | |
| name=prefix, | |
| tests=str(int(d.get('totalSpecs', 0))), | |
| failures=str(int(d.get('totalFail', 0))), | |
| errors=str(int(d.get('totalError', 0))), | |
| time=str(d.get('totalDuration', 0) / 1000)) | |
| for b in d.get('bundleStats', []): | |
| ts = SubElement(root, 'testsuite', | |
| name=f\"{prefix} :: {b.get('name', '')}\", | |
| tests=str(int(b.get('totalSpecs', 0))), | |
| failures=str(int(b.get('totalFail', 0))), | |
| errors=str(int(b.get('totalError', 0))), | |
| time=str(b.get('totalDuration', 0) / 1000)) | |
| for s in b.get('suiteStats', []): | |
| process_suite(ts, s) | |
| with open(os.environ['JUNIT_FILE'], 'wb') as f: | |
| f.write(b'<?xml version=\"1.0\" encoding=\"UTF-8\"?>') | |
| f.write(tostring(root)) | |
| " || { echo "JUnit conversion failed for ${db} (non-fatal)"; rm -f "$JUNIT_FILE"; } | |
| fi | |
| # Databases whose failures are logged but don't block CI. | |
| SOFT_FAIL_DBS="" | |
| # Track per-database result | |
| if [ "$HTTP_CODE" = "200" ]; then | |
| echo "PASSED: ${{ matrix.cfengine }} + ${db}" | |
| DB_STATUS="pass" | |
| else | |
| echo "FAILED: ${{ matrix.cfengine }} + ${db} (HTTP ${HTTP_CODE})" | |
| DB_STATUS="fail" | |
| if echo "$SOFT_FAIL_DBS" | grep -qw "$db"; then | |
| echo "::warning::${db} tests failed but marked as soft-fail (non-blocking)" | |
| else | |
| OVERALL_STATUS=1 | |
| fi | |
| fi | |
| # Build JSON summary for matrix display | |
| if [ "$FIRST" = true ]; then | |
| FIRST=false | |
| else | |
| RESULTS_JSON="${RESULTS_JSON}," | |
| fi | |
| RESULTS_JSON="${RESULTS_JSON}\"${db}\":\"${DB_STATUS}\"" | |
| done | |
| RESULTS_JSON="${RESULTS_JSON}}" | |
| echo "results_json=${RESULTS_JSON}" >> $GITHUB_OUTPUT | |
| echo "" | |
| echo "==============================================" | |
| echo "All database suites complete for ${{ matrix.cfengine }}" | |
| echo "Results: ${RESULTS_JSON}" | |
| echo "==============================================" | |
| # Exit with failure if any database failed, but after running ALL databases | |
| if [ "$OVERALL_STATUS" -ne 0 ]; then | |
| echo "One or more database suites failed" | |
| exit 1 | |
| fi | |
| - name: Generate per-engine summary | |
| if: always() | |
| run: | | |
| echo "### ${{ matrix.cfengine }} Test Results" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "| Database | Result |" >> $GITHUB_STEP_SUMMARY | |
| echo "|----------|--------|" >> $GITHUB_STEP_SUMMARY | |
| SOFT_FAIL_DBS="" | |
| IFS=',' read -ra DBS <<< "${{ steps.db-list.outputs.databases }}" | |
| for db in "${DBS[@]}"; do | |
| RESULT_FILE="/tmp/test-results/${{ matrix.cfengine }}-${db}-result.txt" | |
| IS_SOFT_FAIL=false | |
| if echo "$SOFT_FAIL_DBS" | grep -qw "$db"; then | |
| IS_SOFT_FAIL=true | |
| fi | |
| if [ -f "$RESULT_FILE" ]; then | |
| # Check JSON for failures | |
| FAIL_COUNT=$(python3 -c " | |
| import json, sys | |
| try: | |
| d = json.load(open('$RESULT_FILE')) | |
| print(d.get('totalFail', 0) + d.get('totalError', 0)) | |
| except: | |
| print(-1) | |
| " 2>/dev/null || echo "-1") | |
| if [ "$FAIL_COUNT" = "0" ]; then | |
| echo "| ${db} | :white_check_mark: Pass |" >> $GITHUB_STEP_SUMMARY | |
| elif [ "$FAIL_COUNT" = "-1" ] && [ "$IS_SOFT_FAIL" = true ]; then | |
| echo "| ${db} | :warning: Error (soft-fail) |" >> $GITHUB_STEP_SUMMARY | |
| elif [ "$FAIL_COUNT" = "-1" ]; then | |
| echo "| ${db} | :warning: Error |" >> $GITHUB_STEP_SUMMARY | |
| elif [ "$IS_SOFT_FAIL" = true ]; then | |
| echo "| ${db} | :warning: ${FAIL_COUNT} failures (soft-fail) |" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "| ${db} | :x: ${FAIL_COUNT} failures |" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| else | |
| if [ "$IS_SOFT_FAIL" = true ]; then | |
| echo "| ${db} | :warning: No result (soft-fail) |" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "| ${db} | :grey_question: No result |" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| fi | |
| done | |
| - name: Debug information | |
| if: failure() | |
| run: | | |
| echo "=== Docker Container Status ===" | |
| docker ps -a | |
| echo -e "\n=== CF Engine Logs ===" | |
| docker logs $(docker ps -aq -f "name=${{ matrix.cfengine }}") 2>&1 | tail -100 || echo "Could not get logs" | |
| echo -e "\n=== Database Container Logs ===" | |
| for container in mysql postgres sqlserver cockroachdb oracle; do | |
| if docker ps -aq -f "name=${container}" | grep -q .; then | |
| echo "--- ${container} ---" | |
| docker logs $(docker ps -aq -f "name=${container}") 2>&1 | tail -30 || true | |
| fi | |
| done | |
| - name: Upload test result artifacts | |
| if: always() | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: test-results-${{ matrix.cfengine }} | |
| path: /tmp/test-results/ | |
| - name: Upload JUnit XML artifacts | |
| if: always() | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: junit-${{ matrix.cfengine }} | |
| path: /tmp/junit-results/ | |
| ############################################# | |
| # Publish Test Results to PR | |
| ############################################# | |
| publish-results: | |
| name: Publish Test Results | |
| needs: tests | |
| if: always() | |
| runs-on: ubuntu-latest | |
| permissions: | |
| checks: write | |
| pull-requests: write | |
| steps: | |
| - name: Download JUnit artifacts | |
| uses: actions/download-artifact@v6 | |
| with: | |
| pattern: junit-* | |
| path: junit-results/ | |
| - name: Publish Unit Test Results | |
| uses: EnricoMi/publish-unit-test-result-action@v2 | |
| with: | |
| files: junit-results/**/*.xml | |
| check_name: "Wheels Test Results" | |
| comment_title: "Wheels Test Results" | |
| report_individual_runs: true | |
| report_suite_logs: any | |
| json_file: junit-results/test-results.json | |
| json_suite_details: true | |
| json_test_case_results: true | |
| json_thousands_separator: "," | |
| ############################################# | |
| # Test Matrix Summary Grid | |
| ############################################# | |
| test-matrix-summary: | |
| name: Test Matrix Summary | |
| needs: tests | |
| if: always() | |
| runs-on: ubuntu-latest | |
| permissions: | |
| pull-requests: write | |
| steps: | |
| - name: Checkout Repository | |
| uses: actions/checkout@v5 | |
| - name: Download all test result artifacts | |
| uses: actions/download-artifact@v6 | |
| with: | |
| pattern: test-results-* | |
| path: results/ | |
| - name: Generate matrix grid | |
| id: matrix | |
| run: | | |
| MATRIX_MD="## Wheels Test Matrix" | |
| MATRIX_MD="${MATRIX_MD} | |
| " | |
| MATRIX_MD="${MATRIX_MD} | |
| | Engine | MySQL | PostgreSQL | SQL Server | H2 | CockroachDB | Oracle | SQLite |" | |
| MATRIX_MD="${MATRIX_MD} | |
| |--------|:-----:|:----------:|:----------:|:--:|:-----------:|:------:|:------:|" | |
| for engine in lucee6 lucee7 adobe2023 adobe2025 boxlang; do | |
| ROW="| **${engine}** |" | |
| for db in mysql postgres sqlserver h2 cockroachdb oracle sqlite; do | |
| FILE="results/test-results-${engine}/${engine}-${db}-result.txt" | |
| if [ -f "$FILE" ]; then | |
| FAIL=$(python3 -c " | |
| import json, sys | |
| try: | |
| d = json.load(open('$FILE')) | |
| print(int(d.get('totalFail', 0) + d.get('totalError', 0))) | |
| except: | |
| print(-1) | |
| " 2>/dev/null || echo "-1") | |
| if [ "$FAIL" = "0" ]; then | |
| ROW="${ROW} :white_check_mark: |" | |
| elif [ "$FAIL" = "-1" ]; then | |
| ROW="${ROW} :warning: |" | |
| else | |
| ROW="${ROW} :x: ${FAIL} |" | |
| fi | |
| else | |
| ROW="${ROW} -- |" | |
| fi | |
| done | |
| MATRIX_MD="${MATRIX_MD} | |
| ${ROW}" | |
| done | |
| MATRIX_MD="${MATRIX_MD} | |
| *Results for commit ${GITHUB_SHA:0:7}.*" | |
| # Write to step summary | |
| echo "$MATRIX_MD" >> $GITHUB_STEP_SUMMARY | |
| # Save for PR comment | |
| echo "$MATRIX_MD" > /tmp/matrix-comment.md | |
| - name: Post matrix to PR | |
| if: github.event_name == 'pull_request' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| PR_NUMBER=$(gh pr list --head "${{ github.head_ref || github.ref_name }}" --json number --jq '.[0].number' 2>/dev/null) | |
| if [ -z "$PR_NUMBER" ]; then | |
| echo "No PR found, skipping comment" | |
| exit 0 | |
| fi | |
| COMMENT_BODY=$(cat /tmp/matrix-comment.md) | |
| # Look for an existing matrix comment to update | |
| COMMENT_ID=$(gh api "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \ | |
| --jq '.[] | select(.user.login == "github-actions[bot]" and (.body | startswith("## Wheels Test Matrix"))) | .id' \ | |
| 2>/dev/null | head -1) | |
| if [ -n "$COMMENT_ID" ]; then | |
| gh api "repos/${{ github.repository }}/issues/comments/${COMMENT_ID}" \ | |
| --method PATCH --field body="$COMMENT_BODY" | |
| echo "Updated existing comment ${COMMENT_ID}" | |
| else | |
| gh pr comment "$PR_NUMBER" --body "$COMMENT_BODY" | |
| echo "Created new comment on PR #${PR_NUMBER}" | |
| fi |