blob: 9445daa831321cea203a6abb6e954896964791ac [file]
name: Regression Safety - Database Changes
on:
workflow_call:
permissions:
contents: read
jobs:
detect-db-changes:
runs-on: ubuntu-24.04
timeout-minutes: 5
outputs:
has_liquibase_changes: ${{ steps.check.outputs.has_changes }}
steps:
- name: Download workspace
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: fineract-workspace-${{ github.run_id }}
path: .
- name: Extract workspace
run: tar -xf fineract-workspace.tar
- name: Detect Liquibase changes
id: check
run: |
if [ "${{ github.event_name }}" != "pull_request" ]; then
echo "has_changes=false" >> "$GITHUB_OUTPUT"
echo "Not a pull request event."
exit 0
fi
git fetch origin "${{ github.event.pull_request.base.ref }}" --no-tags
MERGE_BASE=$(git merge-base "origin/${{ github.event.pull_request.base.ref }}" HEAD)
CHANGES=$(git diff --name-only "$MERGE_BASE"..HEAD -- '**/db/changelog/**/*.xml' || true)
if [ -n "$CHANGES" ]; then
echo "has_changes=true" >> "$GITHUB_OUTPUT"
echo "Liquibase changes detected:"
echo "$CHANGES"
else
echo "has_changes=false" >> "$GITHUB_OUTPUT"
echo "No Liquibase changes detected."
fi
regression-safety:
needs: detect-db-changes
if: needs.detect-db-changes.outputs.has_liquibase_changes == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 60
services:
postgresql:
image: postgres:18.3
ports:
- 5432:5432
env:
POSTGRES_USER: root
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd="pg_isready -q -d postgres -U root"
--health-interval=5s
--health-timeout=2s
--health-retries=5
env:
DB_USER: root
DB_PASSWORD: postgres
DB_NAME: fineract_default
TZ: Asia/Kolkata
DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
# E2E test configuration
BASE_URL: https://localhost:8443
TEST_USERNAME: mifos
TEST_PASSWORD: password
TEST_STRONG_PASSWORD: A1b2c3d4e5f$
TEST_TENANT_ID: default
INITIALIZATION_ENABLED: true
EVENT_VERIFICATION_ENABLED: false
# devRun connection args (reused across instances)
DEVRUN_ARGS: >-
--spring.datasource.hikari.driverClassName=org.postgresql.Driver
--spring.datasource.hikari.jdbcUrl=jdbc:postgresql://localhost:5432/fineract_tenants
--spring.datasource.hikari.username=root
--spring.datasource.hikari.password=postgres
--fineract.tenant.host=localhost
--fineract.tenant.port=5432
--fineract.tenant.username=root
--fineract.tenant.password=postgres
steps:
# ============================================================
# SETUP
# ============================================================
- name: Checkout base commit (baseline)
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: ${{ github.event.pull_request.base.repo.full_name }}
ref: ${{ github.event.pull_request.base.sha }}
fetch-depth: 0
path: baseline
- name: Set up JDK 21
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
with:
distribution: 'zulu'
java-version: '21'
- name: Download workspace
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: fineract-workspace-${{ github.run_id }}
path: .
- name: Extract workspace
run: tar -xf fineract-workspace.tar
- name: Setup Gradle
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
- name: Wait for PostgreSQL
run: |
until pg_isready -h localhost -U $DB_USER; do
echo "Waiting for postgres..."
sleep 2
done
- name: Print checked out revisions
run: |
echo "Baseline revision:"
git -C baseline rev-parse HEAD
git -C baseline log -1 --oneline
echo "PR revision:"
git rev-parse HEAD
git log -1 --oneline
# ============================================================
# INSTANCE #1: BASELINE (develop code + fresh DB)
# Proves: golden path works before the PR
# ============================================================
- name: "Instance #1: Create fresh databases"
working-directory: baseline
run: |
./gradlew --no-daemon createPGDB -PdbName=fineract_tenants
./gradlew --no-daemon createPGDB -PdbName=$DB_NAME
- name: "Instance #1: Start baseline backend"
working-directory: baseline
run: |
./gradlew :fineract-provider:devRun --args="${DEVRUN_ARGS}" &
BACKEND_PID=$!
echo $BACKEND_PID > backend.pid
ACTUATOR_URL="https://localhost:8443/fineract-provider/actuator/health"
TIMEOUT_SECONDS=600
INTERVAL_SECONDS=2
echo "Waiting for baseline backend Actuator: $ACTUATOR_URL (timeout ${TIMEOUT_SECONDS}s)..."
start_ts=$(date +%s)
while true; do
if ! kill -0 "$BACKEND_PID" 2>/dev/null; then
echo "Backend process exited before Actuator became available."
exit 1
fi
if curl -kfsS "$ACTUATOR_URL" >/dev/null 2>&1; then
echo "Actuator is up."
break
fi
now_ts=$(date +%s)
if [ $((now_ts - start_ts)) -ge "$TIMEOUT_SECONDS" ]; then
echo "Timed out waiting for Actuator."
exit 1
fi
sleep "$INTERVAL_SECONDS"
done
- name: "Instance #1: Run regression safety E2E test"
id: instance1
continue-on-error: true
run: |
./gradlew --no-daemon --console=plain \
:fineract-e2e-tests-runner:cucumber \
-Pcucumber.tags="@RegressionSafety" \
-x spotlessCheck
- name: "Instance #1: Stop backend"
if: always()
working-directory: baseline
run: |
kill $(cat backend.pid) 2>/dev/null || true
sleep 10
- name: "Instance #1: Drop databases"
if: always()
run: |
PGPASSWORD=postgres psql -h localhost -U root -d postgres \
-c "DROP DATABASE IF EXISTS fineract_tenants;" \
-c "DROP DATABASE IF EXISTS $DB_NAME;"
# ============================================================
# INSTANCE #2: HYBRID (baseline code + PR schema)
# Proves: old code survives the PR's schema changes
# This is the KEY test for rolling deployment safety
# ============================================================
- name: "Instance #2: Create fresh databases"
run: |
PGPASSWORD=postgres psql -h localhost -U root -d postgres \
-c "CREATE DATABASE fineract_tenants;" \
-c "CREATE DATABASE $DB_NAME;"
- name: "Instance #2: Apply PR Liquibase migrations (schema only)"
run: |
echo "Applying all Liquibase changesets (base + PR) using PR code..."
# The liquibase-only profile sets web-application-type=none, so Spring Boot
# exits naturally after migrations complete. The 600s timeout is a safety net
# that FAILS the step if migrations hang — it is NOT treated as success.
timeout 600 ./gradlew --no-daemon :fineract-provider:bootRun --args="\
--spring.profiles.active=liquibase-only \
--spring.datasource.hikari.driverClassName=org.postgresql.Driver \
--spring.datasource.hikari.jdbcUrl=jdbc:postgresql://localhost:5432/fineract_tenants \
--spring.datasource.hikari.username=root \
--spring.datasource.hikari.password=postgres \
--fineract.tenant.host=localhost \
--fineract.tenant.port=5432 \
--fineract.tenant.username=root \
--fineract.tenant.password=postgres" || {
EXIT_CODE=$?
if [ $EXIT_CODE -eq 124 ]; then
echo "::error::Liquibase migrations timed out after 600s — migrations may be incomplete."
exit 1
else
echo "bootRun failed with exit code $EXIT_CODE"
exit 1
fi
}
echo "Liquibase migrations completed successfully (natural exit)."
- name: "Instance #2: Start baseline backend (old code, new schema)"
working-directory: baseline
run: |
echo "Starting baseline code against DB with PR schema (Liquibase disabled)..."
./gradlew :fineract-provider:devRun --args="\
${DEVRUN_ARGS} \
--spring.liquibase.enabled=false" &
BACKEND_PID=$!
echo $BACKEND_PID > backend.pid
ACTUATOR_URL="https://localhost:8443/fineract-provider/actuator/health"
TIMEOUT_SECONDS=600
INTERVAL_SECONDS=2
echo "Waiting for hybrid backend Actuator: $ACTUATOR_URL (timeout ${TIMEOUT_SECONDS}s)..."
start_ts=$(date +%s)
while true; do
if ! kill -0 "$BACKEND_PID" 2>/dev/null; then
echo "::error::Instance #2 FAILED: Baseline code could not start against PR schema."
echo "This likely means the PR's Liquibase changes break backward compatibility."
exit 1
fi
if curl -kfsS "$ACTUATOR_URL" >/dev/null 2>&1; then
echo "Actuator is up."
break
fi
now_ts=$(date +%s)
if [ $((now_ts - start_ts)) -ge "$TIMEOUT_SECONDS" ]; then
echo "Timed out waiting for Actuator."
exit 1
fi
sleep "$INTERVAL_SECONDS"
done
- name: "Instance #2: Run regression safety E2E test"
id: instance2
continue-on-error: true
run: |
./gradlew --no-daemon --console=plain \
:fineract-e2e-tests-runner:cucumber \
-Pcucumber.tags="@RegressionSafety" \
-x spotlessCheck
- name: "Instance #2: Stop backend"
if: always()
working-directory: baseline
run: |
kill $(cat backend.pid) 2>/dev/null || true
sleep 10
- name: "Instance #2: Drop databases"
if: always()
run: |
PGPASSWORD=postgres psql -h localhost -U root -d postgres \
-c "DROP DATABASE IF EXISTS fineract_tenants;" \
-c "DROP DATABASE IF EXISTS $DB_NAME;"
# ============================================================
# INSTANCE #3: FULL PR (PR code + PR schema)
# Proves: golden path works with all PR changes
# ============================================================
- name: "Instance #3: Create fresh databases"
run: |
PGPASSWORD=postgres psql -h localhost -U root -d postgres \
-c "CREATE DATABASE fineract_tenants;" \
-c "CREATE DATABASE $DB_NAME;"
- name: "Instance #3: Start PR backend"
run: |
./gradlew :fineract-provider:devRun --args="${DEVRUN_ARGS}" &
BACKEND_PID=$!
echo $BACKEND_PID > backend.pid
ACTUATOR_URL="https://localhost:8443/fineract-provider/actuator/health"
TIMEOUT_SECONDS=600
INTERVAL_SECONDS=2
echo "Waiting for PR backend Actuator: $ACTUATOR_URL (timeout ${TIMEOUT_SECONDS}s)..."
start_ts=$(date +%s)
while true; do
if ! kill -0 "$BACKEND_PID" 2>/dev/null; then
echo "Backend process exited before Actuator became available."
exit 1
fi
if curl -kfsS "$ACTUATOR_URL" >/dev/null 2>&1; then
echo "Actuator is up."
break
fi
now_ts=$(date +%s)
if [ $((now_ts - start_ts)) -ge "$TIMEOUT_SECONDS" ]; then
echo "Timed out waiting for Actuator."
exit 1
fi
sleep "$INTERVAL_SECONDS"
done
- name: "Instance #3: Run regression safety E2E test"
id: instance3
continue-on-error: true
run: |
./gradlew --no-daemon --console=plain \
:fineract-e2e-tests-runner:cucumber \
-Pcucumber.tags="@RegressionSafety" \
-x spotlessCheck
- name: "Instance #3: Stop backend"
if: always()
run: |
kill $(cat backend.pid) 2>/dev/null || true
sleep 10
# ============================================================
# RESULTS
# ============================================================
- name: Generate results summary
if: always()
run: |
echo "## Regression Safety - Database Changes" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Instance | Description | Result |" >> $GITHUB_STEP_SUMMARY
echo "|----------|-------------|--------|" >> $GITHUB_STEP_SUMMARY
if [ "${{ steps.instance1.outcome }}" == "success" ]; then
echo "| #1 Baseline | develop code + fresh DB | PASSED |" >> $GITHUB_STEP_SUMMARY
else
echo "| #1 Baseline | develop code + fresh DB | FAILED |" >> $GITHUB_STEP_SUMMARY
fi
if [ "${{ steps.instance2.outcome }}" == "success" ]; then
echo "| #2 Hybrid | develop code + PR schema | PASSED |" >> $GITHUB_STEP_SUMMARY
else
echo "| #2 Hybrid | develop code + PR schema | **FAILED** — PR schema breaks old code |" >> $GITHUB_STEP_SUMMARY
fi
if [ "${{ steps.instance3.outcome }}" == "success" ]; then
echo "| #3 Full PR | PR code + PR schema | PASSED |" >> $GITHUB_STEP_SUMMARY
else
echo "| #3 Full PR | PR code + PR schema | FAILED |" >> $GITHUB_STEP_SUMMARY
fi
- name: Upload test results
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: regression-safety-results
path: |
fineract-e2e-tests-runner/build/reports/
retention-days: 1
- name: Fail if any instance failed
if: always()
run: |
FAILED=0
if [ "${{ steps.instance1.outcome }}" != "success" ]; then
echo "::error::Instance #1 (baseline) FAILED — golden path broken on develop"
FAILED=1
fi
if [ "${{ steps.instance2.outcome }}" != "success" ]; then
echo "::error::Instance #2 (hybrid) FAILED — PR's Liquibase changes break backward compatibility!"
FAILED=1
fi
if [ "${{ steps.instance3.outcome }}" != "success" ]; then
echo "::error::Instance #3 (full PR) FAILED — functional regression in the PR"
FAILED=1
fi
if [ $FAILED -ne 0 ]; then
exit 1
fi
echo "All instances passed."