Skip to content

Wheels Compatibility Matrix #91

Wheels Compatibility Matrix

Wheels Compatibility Matrix #91

Workflow file for this run

# 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
# Retry the build to absorb transient external download failures
# (GitHub Releases 503/504s for Adoptium JDK/JRE and similar). The
# build itself is idempotent; only `up -d` runs after success.
env:
CFENGINE: ${{ matrix.cfengine }}
run: |
set -e
MAX_ATTEMPTS=3
ATTEMPT=1
until docker compose build --no-cache "$CFENGINE"; do
if [ $ATTEMPT -ge $MAX_ATTEMPTS ]; then
echo "::error::docker compose build failed after ${MAX_ATTEMPTS} attempts"
exit 1
fi
echo "::warning::Build attempt ${ATTEMPT} failed — sleeping 30s before retry"
ATTEMPT=$((ATTEMPT + 1))
sleep 30
done
docker compose up -d "$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
env:
CFENGINE: ${{ matrix.cfengine }}
run: |
PORT_VAR="PORT_${CFENGINE}"
PORT="${!PORT_VAR}"
CONTAINER="wheels-${CFENGINE}-1"
echo "Waiting for ${CFENGINE} on port ${PORT}..."
# Wait for HTTP response, restarting container if it crashes.
# Tracks the last HTTP status code so timeout diagnostics can
# distinguish "no response" (engine didn't bind) from "5xx"
# (engine bound but app returning errors — e.g. issue #2646).
MAX_WAIT=60
WAIT_COUNT=0
RESTARTS=0
MAX_RESTARTS=3
LAST_HTTP_CODE="000"
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 "${CFENGINE}"
sleep 10
continue
else
echo "::error::Container $CONTAINER failed to start after $MAX_RESTARTS restart attempts"
docker logs "$CONTAINER" 2>&1 | tail -100
exit 1
fi
fi
# curl with -w "%{http_code}" always prints the code (000 if no
# response). Don't add a `|| echo "000"` fallback — it would
# concatenate with curl's own 000 output and produce "000000".
LAST_HTTP_CODE=$(curl -s -o /dev/null --connect-timeout 2 --max-time 5 -w "%{http_code}" "http://localhost:${PORT}/" 2>/dev/null || true)
LAST_HTTP_CODE=${LAST_HTTP_CODE:-000}
if echo "$LAST_HTTP_CODE" | grep -qE "^(200|302|404)$"; then
echo "CF engine is ready! (HTTP $LAST_HTTP_CODE on attempt $WAIT_COUNT)"
break
fi
# Surface partial progress every 10 attempts so logs show
# whether we're stuck on "no response" or "5xx" early.
if [ $((WAIT_COUNT % 10)) -eq 0 ]; then
echo " attempt $WAIT_COUNT/$MAX_WAIT: container=$CONTAINER_STATUS, http=$LAST_HTTP_CODE"
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 (last HTTP code: $LAST_HTTP_CODE)"
if [ "$LAST_HTTP_CODE" = "000" ]; then
echo "::notice::No HTTP response received — engine likely never bound to port $PORT."
else
echo "::notice::HTTP $LAST_HTTP_CODE received — engine bound to port $PORT but app is returning errors."
echo "=== Final response body (first 500 bytes) ==="
curl -s --max-time 5 "http://localhost:${PORT}/" 2>/dev/null | head -c 500 || true
echo
echo "=== /end response body ==="
fi
echo "=== Container logs (stack frames stripped, last 100 lines) ==="
docker logs "$CONTAINER" 2>&1 | grep -vE '^\s*at\s|runwar\.context -[[:space:]]+at[[:space:]]' | tail -100
echo "=== /end filtered logs ==="
echo "=== Container logs (raw, last 200 lines) ==="
docker logs "$CONTAINER" 2>&1 | tail -200
echo "=== /end raw logs ==="
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
# Databases whose failures are logged but don't block CI.
# oracle: tracked in #2663 — datasource registration, DBMS_LOCK, and constraint cleanup.
SOFT_FAIL_DBS="oracle"
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
# 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="oracle"
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/
#############################################
# RustCFML (experimental, JVM-free engine)
#############################################
# Informational lane, never a merge gate. The engine is pinned in
# tools/rustcfml/ENGINE_VERSION (upstream ships multiple releases/day, so
# tracking latest would make this lane flake on engine churn). Pass criteria
# is "no NEW failures vs tools/rustcfml/baseline.json" — known residual
# errors (no-JVM limitations, open upstream issues) live in the baseline.
# To bump the pin: update ENGINE_VERSION, run
# bash tools/rustcfml/run-suite.sh --write-baseline
# locally, and commit both files together.
rustcfml:
name: "rustcfml (experimental)"
runs-on: ubuntu-latest
continue-on-error: true
steps:
- name: Checkout Repository
uses: actions/checkout@v5
- name: Read pinned engine version
id: engine
run: echo "version=$(tr -d '[:space:]' < tools/rustcfml/ENGINE_VERSION)" >> $GITHUB_OUTPUT
- name: Cache engine binary
uses: actions/cache@v4
with:
path: ~/.cache/wheels-rustcfml
key: rustcfml-${{ steps.engine.outputs.version }}-linux-x86_64
- name: Run core suite against pinned RustCFML
env:
GH_TOKEN: ${{ github.token }}
run: bash tools/rustcfml/run-suite.sh
#############################################
# 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