| 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." |