Skip to content

ci: http4s-vs-Lift per-test speed report after every CI run #64

ci: http4s-vs-Lift per-test speed report after every CI run

ci: http4s-vs-Lift per-test speed report after every CI run #64

name: Build and publish container develop
# read-write repo token
# access to secrets
on: [push]
env:
DOCKER_HUB_ORGANIZATION: ${{ vars.DOCKER_HUB_ORGANIZATION }}
DOCKER_HUB_REPOSITORY: obp-api
# ---------------------------------------------------------------------------
# compile — compiles everything once, packages the JAR, uploads classes
# test — 3-way matrix downloads compiled output and runs a shard of tests
# docker — downloads compiled output, builds and pushes the container image
#
# Wall-clock target:
# compile ~10 min (parallel with setup of test shards)
# tests ~8 min (3 shards in parallel after compile finishes)
# docker ~3 min (after all shards pass)
# total ~21 min (vs ~30 min single-job)
# ---------------------------------------------------------------------------
jobs:
# --------------------------------------------------------------------------
# Job 1: compile
# --------------------------------------------------------------------------
compile:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 11
uses: actions/setup-java@v4
with:
java-version: "11"
distribution: "adopt"
cache: maven # caches ~/.m2/repository keyed on pom.xml hash
- name: Setup production props
run: |
cp obp-api/src/main/resources/props/sample.props.template \
obp-api/src/main/resources/props/production.default.props
- name: Compile and install (skip test execution)
run: |
# -DskipTests — compile test sources but do NOT run them
# Test classes must be in target/test-classes for the test shards
MAVEN_OPTS="-Xmx3G -Xss2m -XX:MaxMetaspaceSize=1G" \
mvn clean install -T 4 -Pprod -DskipTests
- name: Upload compiled output
uses: actions/upload-artifact@v4
with:
name: compiled-output
retention-days: 1
# Upload full target dirs — test shards and docker job download these
path: |
obp-api/target/
obp-commons/target/
- name: Save .jar artifact
run: mkdir -p ./push && cp obp-api/target/obp-api.jar ./push/
- uses: actions/upload-artifact@v4
with:
name: ${{ github.sha }}
path: push/
# --------------------------------------------------------------------------
# Job 2: test (3-way matrix)
#
# Shard assignment (based on actual build #48 timings):
# Shard 1 ~440s v4_0_0(292) v5_0_0(47) v3_0_0(42) v2_1_0(37) v2_2_0(11) …
# Shard 2 ~460s v1_2_1(175) v6_0_0(162) ResourceDocs(82) util(15) berlin(41) …
# Shard 3 ~420s v5_1_0(156) v3_1_0(124) http4sbridge(53) v7_0_0(27) code.api(26) …
# --------------------------------------------------------------------------
test:
needs: compile
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- shard: 1
name: "v4 + v5_0 + v3_0 + v2 + small"
# ~440s of test work
# Space-separated package prefixes for scalatest wildcardSuites (-w)
test_filter: >-
code.api.v4_0_0
code.api.v5_0_0
code.api.v3_0_0
code.api.v2_1_0
code.api.v2_2_0
code.api.v2_0_0
code.api.v1_4_0
code.api.v1_3_0
code.api.UKOpenBanking
code.atms
code.branches
code.products
code.crm
code.accountHolder
code.entitlement
code.bankaccountcreation
code.bankconnectors
code.container
- shard: 2
name: "v1_2_1 + v6 + ResourceDocs + util + berlin + small"
# ~460s of test work
test_filter: >-
code.api.v1_2_1
code.api.v6_0_0
code.api.ResourceDocs1_4_0
code.api.util
code.api.berlin
code.management
code.metrics
code.model
code.views
code.usercustomerlinks
code.customer
code.errormessages
- shard: 3
name: "v5_1 + v3_1 + http4sbridge + v7 + code.api + util + connector"
# ~420s of test work
# Root-level code.api tests use class-name prefix matching (lowercase classes)
test_filter: >-
code.api.v5_1_0
code.api.v3_1_0
code.api.http4sbridge
code.api.v7_0_0
code.api.Authentication
code.api.dauthTest
code.api.DirectLoginTest
code.api.gateWayloginTest
code.api.OBPRestHelperTest
code.util
code.connector
services:
redis:
image: redis
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Set up JDK 11
uses: actions/setup-java@v4
with:
java-version: "11"
distribution: "adopt"
cache: maven
- name: Download compiled output
uses: actions/download-artifact@v4
with:
name: compiled-output
- name: Touch artifact files (prevent Zinc recompilation)
run: |
# actions/download-artifact preserves original compile-job timestamps.
# actions/checkout gives source files the current (later) time.
# Zinc sees sources newer than classes → full recompile (~215 s wasted).
# Touching everything in target/ makes all artifact files appear
# just-downloaded (current time) → newer than sources → Zinc skips.
find obp-api/target obp-commons/target -type f -exec touch {} + 2>/dev/null || true
echo "Touched $(find obp-api/target obp-commons/target -type f 2>/dev/null | wc -l) files"
- name: Install local artifacts into Maven repo
run: |
# The compile runner's ~/.m2 is discarded after that job completes.
# Install the two local multi-module artifacts so scalatest:test can
# resolve com.tesobe:* without hitting remote repos.
#
# 1. Parent POM — obp-commons' pom.xml declares obp-parent as its
# <parent>; Maven fetches it when reading transitive deps.
mvn install:install-file \
-Dfile=pom.xml \
-DgroupId=com.tesobe \
-DartifactId=obp-parent \
-Dversion=1.10.1 \
-Dpackaging=pom \
-DgeneratePom=false
# 2. obp-commons JAR with its full POM (lists compile deps inherited
# by obp-api at test classpath resolution time).
mvn install:install-file \
-Dfile=obp-commons/target/obp-commons-1.10.1.jar \
-DpomFile=obp-commons/pom.xml
- name: Setup props
run: |
cp obp-api/src/main/resources/props/sample.props.template \
obp-api/src/main/resources/props/production.default.props
echo connector=star > obp-api/src/main/resources/props/test.default.props
echo starConnector_supported_types=mapped,internal >> obp-api/src/main/resources/props/test.default.props
echo hostname=http://localhost:8016 >> obp-api/src/main/resources/props/test.default.props
echo tests.port=8016 >> obp-api/src/main/resources/props/test.default.props
echo End of minimum settings >> obp-api/src/main/resources/props/test.default.props
echo payments_enabled=false >> obp-api/src/main/resources/props/test.default.props
echo importer_secret=change_me >> obp-api/src/main/resources/props/test.default.props
echo messageQueue.updateBankAccountsTransaction=false >> obp-api/src/main/resources/props/test.default.props
echo messageQueue.createBankAccounts=false >> obp-api/src/main/resources/props/test.default.props
echo allow_sandbox_account_creation=true >> obp-api/src/main/resources/props/test.default.props
echo allow_sandbox_data_import=true >> obp-api/src/main/resources/props/test.default.props
echo sandbox_data_import_secret=change_me >> obp-api/src/main/resources/props/test.default.props
echo allow_account_deletion=true >> obp-api/src/main/resources/props/test.default.props
echo allowed_internal_redirect_urls = /,/oauth/authorize >> obp-api/src/main/resources/props/test.default.props
echo transactionRequests_enabled=true >> obp-api/src/main/resources/props/test.default.props
echo transactionRequests_supported_types=SEPA,SANDBOX_TAN,FREE_FORM,COUNTERPARTY,ACCOUNT,SIMPLE >> obp-api/src/main/resources/props/test.default.props
echo SIMPLE_OTP_INSTRUCTION_TRANSPORT=dummy >> obp-api/src/main/resources/props/test.default.props
echo openredirects.hostname.whitlelist=http://127.0.0.1,http://localhost >> obp-api/src/main/resources/props/test.default.props
echo remotedata.secret = foobarbaz >> obp-api/src/main/resources/props/test.default.props
echo allow_public_views=true >> obp-api/src/main/resources/props/test.default.props
echo SIMPLE_OTP_INSTRUCTION_TRANSPORT=dummy >> obp-api/src/main/resources/props/test.default.props
echo ACCOUNT_OTP_INSTRUCTION_TRANSPORT=dummy >> obp-api/src/main/resources/props/test.default.props
echo SEPA_OTP_INSTRUCTION_TRANSPORT=dummy >> obp-api/src/main/resources/props/test.default.props
echo FREE_FORM_OTP_INSTRUCTION_TRANSPORT=dummy >> obp-api/src/main/resources/props/test.default.props
echo COUNTERPARTY_OTP_INSTRUCTION_TRANSPORT=dummy >> obp-api/src/main/resources/props/test.default.props
echo SEPA_CREDIT_TRANSFERS_OTP_INSTRUCTION_TRANSPORT=dummy >> obp-api/src/main/resources/props/test.default.props
echo allow_oauth2_login=true >> obp-api/src/main/resources/props/test.default.props
echo oauth2.jwk_set.url=https://www.googleapis.com/oauth2/v3/certs >> obp-api/src/main/resources/props/test.default.props
echo ResetPasswordUrlEnabled=true >> obp-api/src/main/resources/props/test.default.props
echo consents.allowed=true >> obp-api/src/main/resources/props/test.default.props
echo hikari.maximumPoolSize=20 >> obp-api/src/main/resources/props/test.default.props
echo write_metrics=false >> obp-api/src/main/resources/props/test.default.props
- name: Run tests — shard ${{ matrix.shard }} (${{ matrix.name }})
run: |
# wildcardSuites requires comma-separated package prefixes (-w per entry).
# The YAML >- scalar collapses newlines to spaces, so we convert here.
FILTER=$(echo "${{ matrix.test_filter }}" | tr ' ' ',')
# Shard 3 is the catch-all: append any test package not explicitly
# assigned to shard 1 or shard 2, so new packages are never silently skipped.
if [ "${{ matrix.shard }}" = "3" ]; then
SHARD1="code.api.v4_0_0 code.api.v5_0_0 code.api.v3_0_0 code.api.v2_1_0 \
code.api.v2_2_0 code.api.v2_0_0 code.api.v1_4_0 code.api.v1_3_0 \
code.api.UKOpenBanking code.atms code.branches code.products code.crm \
code.accountHolder code.entitlement code.bankaccountcreation \
code.bankconnectors code.container"
SHARD2="code.api.v1_2_1 code.api.v6_0_0 code.api.ResourceDocs1_4_0 \
code.api.util code.api.berlin code.management code.metrics \
code.model code.views code.usercustomerlinks code.customer \
code.errormessages"
ASSIGNED="$SHARD1 $SHARD2 ${{ matrix.test_filter }}"
# Discover all packages that contain at least one .scala test file
ALL_PKGS=$(find obp-api/src/test/scala obp-commons/src/test/scala \
-name "*.scala" 2>/dev/null \
| sed 's|.*/test/scala/||; s|/[^/]*\.scala$||; s|/|.|g' \
| sort -u)
EXTRAS=""
for pkg in $ALL_PKGS; do
covered=false
for prefix in $ASSIGNED; do
if [[ "$pkg" == "$prefix" || "$pkg" == "$prefix."* ]]; then
covered=true; break
fi
done
[ "$covered" = "false" ] && EXTRAS="$EXTRAS,$pkg"
done
[ -n "$EXTRAS" ] && echo "Catch-all extras added to shard 3:$EXTRAS"
FILTER="${FILTER}${EXTRAS}"
fi
MAVEN_OPTS="-Xmx3G -Xss2m -XX:MaxMetaspaceSize=1G" \
mvn test \
-DwildcardSuites="$FILTER" \
> maven-build-shard${{ matrix.shard }}.log 2>&1
- name: Report failing tests — shard ${{ matrix.shard }}
if: always()
run: |
echo "Checking shard ${{ matrix.shard }} log for failing tests..."
if [ ! -f maven-build-shard${{ matrix.shard }}.log ]; then
echo "No build log found."; exit 0
fi
echo "=== RECOMPILATION CHECK ==="
if grep -c "Compiling " maven-build-shard${{ matrix.shard }}.log > /dev/null 2>&1; then
echo "WARNING: Scala recompilation occurred on this shard:"
grep "Compiling " maven-build-shard${{ matrix.shard }}.log | head -10
else
echo "OK: no recompilation (Zinc used pre-compiled classes)"
fi
echo ""
echo "=== BRIDGE / UNCAUGHT EXCEPTIONS ==="
grep -n "\[BRIDGE\] Exception\|Uncaught exception in dispatch\|requestScopeProxy=" \
maven-build-shard${{ matrix.shard }}.log | head -200 || true
echo ""
echo "=== FAILING TEST SCENARIOS (with 30 lines context) ==="
if grep -C 30 -n "\*\*\* FAILED \*\*\*" maven-build-shard${{ matrix.shard }}.log; then
echo "Failing tests detected in shard ${{ matrix.shard }}."
exit 1
else
echo "No failing tests detected in shard ${{ matrix.shard }}."
fi
- name: Upload Maven build log — shard ${{ matrix.shard }}
if: always()
uses: actions/upload-artifact@v4
with:
name: maven-build-log-shard${{ matrix.shard }}
if-no-files-found: ignore
path: maven-build-shard${{ matrix.shard }}.log
- name: Upload test reports — shard ${{ matrix.shard }}
if: always()
uses: actions/upload-artifact@v4
with:
name: test-reports-shard${{ matrix.shard }}
if-no-files-found: ignore
path: |
obp-api/target/surefire-reports/**
obp-commons/target/surefire-reports/**
**/target/scalatest-reports/**
**/target/site/surefire-report.html
**/target/site/surefire-report/*
# --------------------------------------------------------------------------
# Job 3: report — http4s v7 vs Lift per-test speed table
# --------------------------------------------------------------------------
report:
needs: test
runs-on: ubuntu-latest
if: always()
steps:
- uses: actions/checkout@v4
- name: Download test reports — all shards
uses: actions/download-artifact@v4
with:
pattern: test-reports-shard*
path: all-reports
merge-multiple: true
- name: http4s v7 vs Lift — per-test speed
run: python3 .github/scripts/test_speed_report.py all-reports
# --------------------------------------------------------------------------
# Job 4: docker — build and push container image (runs after all shards pass)
# --------------------------------------------------------------------------
docker:
needs: test
runs-on: ubuntu-latest
if: vars.ENABLE_CONTAINER_BUILDING == 'true'
steps:
- uses: actions/checkout@v4
- name: Download compiled output
uses: actions/download-artifact@v4
with:
name: compiled-output
- name: Build the Docker image
run: |
echo "${{ secrets.DOCKER_HUB_TOKEN }}" | docker login -u "${{ secrets.DOCKER_HUB_USERNAME }}" --password-stdin docker.io
if [ "${{ github.ref }}" == "refs/heads/develop" ]; then
docker build . --file .github/Dockerfile_PreBuild \
--tag docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:$GITHUB_SHA \
--tag docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:latest \
--tag docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:${GITHUB_REF##*/}
else
docker build . --file .github/Dockerfile_PreBuild \
--tag docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:$GITHUB_SHA \
--tag docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:${GITHUB_REF##*/}
fi
docker push docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }} --all-tags
echo docker done
- uses: sigstore/cosign-installer@4d14d7f17e7112af04ea6108fbb4bfc714c00390
- name: Write signing key to disk (only needed for `cosign sign --key`)
run: echo "${{ secrets.COSIGN_PRIVATE_KEY }}" > cosign.key
- name: Sign container image
run: |
cosign sign -y --key cosign.key \
docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:${GITHUB_REF##*/}
cosign sign -y --key cosign.key \
docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:$GITHUB_SHA
if [ "${{ github.ref }}" == "refs/heads/develop" ]; then
cosign sign -y --key cosign.key \
docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:latest
fi
env:
COSIGN_PASSWORD: "${{secrets.COSIGN_PASSWORD}}"