Skip to content

Commit ea64d62

Browse files
authored
build: raise scalac's -release cap from 17 to 25 and guard the build JDK (#78)
scalac's -release was pinned to 17 on the stated grounds that "the Scala 2.12.21 compiler only accepts -release up to 17". The ceiling is actually the spec version of the JDK running scalac, independent of the Scala version - the same 2.12.21 jar advertises up to 17 on JDK 17, 21 on JDK 21 and 25 on JDK 25 - so on JDK 25 build hosts the cap was pure configuration, and it blocked our Scala sources from using any JDK 18+ API. - Raise the cap to 25, aligned with java.version, as a scalac.release property. - Add a maven-enforcer rule at validate so a too-old build JDK fails immediately with the detected version and required range, instead of failing minutes later inside scalac with a message that reads like a Scala-version limit. - Make scripts/java_env.sh read the required version from pom.xml instead of a hardcoded floor that had already gone stale at >= 17, require it exactly, and abort when absent; run_tests_parallel.sh drops its duplicate resolver and the remaining runners are pinned too, so build, server and tests cannot diverge. - Correct the documents that still stated an older Java level, server backend or concurrency library, including one that had already restated the same misdiagnosis (Blaze -> Ember, Akka -> Pekko, Liftweb -> http4s where it applies). - Fix three defects a review of the branch found in its own code: JAVA_HOME not re-exported when already correct, the pom-version fallback made unreachable under set -e/pipefail, and the offline-by-default fast build being unable to resolve the newly added plugin. On Scala 2.12 this widens the visible API surface only; class files stay at major 52, so a call into a newer JDK API would fail at run time rather than compile time. No main source uses any Java 18+ API today - the same sources still compile at -release 17 - so artifacts are behaviourally unchanged. javac does honour the flag, so the one .java file under src/main/scala moves from class file 61 to 69. Verified: full build, 3395 local tests with 0 failures, 26/26 CI checks, class-file levels spot-checked, and the enforcer exercised on JDK 17/21/25/26.
1 parent 4aac90a commit ea64d62

11 files changed

Lines changed: 267 additions & 176 deletions

File tree

‎development/docker/README.md‎

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ This Docker Compose setup provides a complete **live development environment** f
66

77
### 🏦 **obp-api-app**
88
- Main OBP-API application with **live development mode**
9-
- Built with Maven 3.9.6 + OpenJDK 17
10-
- Runs with Jetty Maven Plugin (`mvn jetty:run`)
9+
- Built with Maven + Eclipse Temurin 25 (see `Dockerfile` / `Dockerfile.dev`)
10+
- Runs the packaged jar via `entrypoint.sh` (`java -jar obp-api.jar`)
1111
- Port: `8080`
1212
- **Features**: Hot reloading, incremental compilation, live props changes
1313

@@ -105,11 +105,11 @@ Environment variables take precedence over props files using OBP's built-in syst
105105

106106
### Live Development Features
107107

108-
**🔥 Hot Reloading**: `Dockerfile.dev` uses `mvn jetty:run` for automatic recompilation and reloading:
109-
- ✅ **Scala code changes** - Automatic recompilation and reload
110-
- ✅ **Props file changes** - Live configuration updates via volume mount
111-
- ✅ **Resource changes** - Instant refresh without container restart
112-
- ✅ **Incremental builds** - Only changed files are recompiled
108+
**Live configuration**: `Dockerfile.dev` builds in the container and starts the jar through
109+
`entrypoint.sh`. Props and resources are volume-mounted from the host:
110+
- ✅ **Props file changes** - picked up from the mount (restart the container to apply)
111+
- ✅ **Incremental builds** - Maven reuses the container's local repository between builds
112+
- ⚠️ **Scala code changes** - require a rebuild/restart; there is no in-place reload
113113

114114
**Volume Mounts for Development**:
115115
```yaml
@@ -207,7 +207,6 @@ Host Machine
207207
208208
### ⚡ **Live Development Mode** (`Dockerfile.dev`)
209209
- **Single-stage build** optimized for development speed
210-
- **Hot reloading** with `mvn jetty:run` - code changes are reflected instantly
211210
- **Incremental compilation** - only changed files are rebuilt
212211
- **Live props updates** - configuration changes without container restart
213212
- **Security compliant** - selective file copying (SonarQube approved)
@@ -221,7 +220,7 @@ Host Machine
221220
- Redis data persists in `obp-api-redis-data` volume
222221
- Props files are live-mounted from host for instant updates
223222
- Environment variables override props file values automatically
224-
- Java 17 with proper module system compatibility
223+
- Java 25, with the `--add-opens` flags the runtime needs (see `entrypoint.sh`)
225224
- All containers restart automatically unless stopped manually
226225
227226
---

‎docs/MTLS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,7 @@ implemented — it says which.
424424
| `code/api/util/APIUtil.scala` (`tppCertificateForStandard`) | Per-standard certificate source: Berlin Group → `TPP-Signature-Certificate`, UK / OBP → `PSD2-CERT`. |
425425
| `scripts/generate_dev_certs.sh` | Regenerates the role-named development certificate set (CA, server, TPP, proxy, expired fixture). |
426426
| `scripts/mtls_env.sh` | The `OBP_MTLS_*` environment overrides behind the `--mtls` flag of both `flushall_*build_and_run.sh` scripts. Sourceable on its own. |
427-
| `scripts/java_env.sh` | Selects a JDK >= 17 for the run scripts. The build compiles with `-release 17`; on a default JDK 11 it otherwise fails with the misleading `'17' is not a valid choice for '-release'`. |
427+
| `scripts/java_env.sh` | Selects the project's JDK (pom.xml `<java.version>`) for the build, run and test scripts, and aborts if it is absent. Without it a stale `java`/`mvn` silently builds on a JDK that CI never used, and one too old for `-release` fails with the misleading `'NN' is not a valid choice for '-release'`. |
428428
| `obp-api/src/test/scala/bootstrap/http4s/Http4sMtlsTest.scala` | Unit tests: PEM encoding, SSLContext from the checked-in keystores, the dev-store digest guard. |
429429
| `obp-api/src/test/scala/bootstrap/http4s/DevCertificateSetTest.scala` | Guards the role-named set over a real handshake: names, EKUs, SAN, CA-only truststore, expired certificate rejected. |
430430
| `obp-api/src/test/scala/bootstrap/http4s/Http4sMtlsHandshakeTest.scala` | End-to-end: real Ember server + real mTLS handshake; proves the verified client cert surfaces as `PSD2-CERT` and certless handshakes are rejected. |

‎flushall_build_and_run.sh‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ for arg in "$@"; do
4141
esac
4242
done
4343

44-
# Select a JDK >= 17 before any Maven work (the build compiles with -release 17).
44+
# Pin the JDK before any Maven work: the project builds and runs on one JDK only
45+
# (pom.xml <java.version>), and scripts/java_env.sh aborts if it is not available.
4546
. "$(dirname "${BASH_SOURCE[0]}")/scripts/java_env.sh"
4647

4748
################################################################################

‎flushall_fast_build_and_run.sh‎

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ for arg in "$@"; do
5656
esac
5757
done
5858

59-
# Select a JDK >= 17 before any Maven work (the build compiles with -release 17).
59+
# Pin the JDK before any Maven work: the project builds and runs on one JDK only
60+
# (pom.xml <java.version>), and scripts/java_env.sh aborts if it is not available.
6061
. "$(dirname "${BASH_SOURCE[0]}")/scripts/java_env.sh"
6162

6263
# Show offline mode status if not explicitly set
@@ -213,6 +214,34 @@ BUILD_DURATION_MS=$((BUILD_DURATION % 1000))
213214
echo "================================================================================"
214215
} >> fast_build.log
215216

217+
# Auto-retry online if the offline default could not resolve something. This build
218+
# runs with -o by default, so any artifact that is not in the local repository yet
219+
# fails the whole build - including a plugin newly added to pom.xml, which no
220+
# amount of cleaning or rebuilding can fix. Retrying once online is the only thing
221+
# that helps, and it leaves the local repository warm for subsequent offline runs.
222+
if [ $BUILD_EXIT_CODE -ne 0 ] && [ -n "$OFFLINE_FLAG" ] \
223+
&& grep -q "in offline mode" fast_build.log; then
224+
echo ""
225+
echo "⚠️ Offline build could not resolve an artifact. Retrying once with network access..."
226+
{ grep -oE "The following artifacts could not be resolved: [^ ]+|Plugin [^ ]+ or one of its dependencies could not be resolved" fast_build.log | head -1 | sed 's/^/ /'; } || true
227+
mv fast_build.log fast_build_offline_failed.log
228+
echo " Previous build log saved to: fast_build_offline_failed.log"
229+
OFFLINE_FLAG=""
230+
set +e
231+
mvn -pl obp-api -am \
232+
$DO_CLEAN \
233+
package \
234+
-T 1C \
235+
-DskipTests=true \
236+
-Dmaven.test.skip=true \
237+
-Dcheckstyle.skip=true \
238+
-Dspotbugs.skip=true \
239+
-Dpmd.skip=true >> fast_build.log 2>&1
240+
BUILD_EXIT_CODE=$?
241+
set -e
242+
echo " Online retry: $([ $BUILD_EXIT_CODE -eq 0 ] && echo 'SUCCESS' || echo 'FAILED')"
243+
fi
244+
216245
# Auto-retry with clean build if incremental build fails
217246
if [ $BUILD_EXIT_CODE -ne 0 ] && [ -z "$DO_CLEAN" ]; then
218247
echo ""

‎obp-api/src/main/resources/docs/glossary/Run_via_IntelliJ_IDEA.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
### Prerequisites
44

5-
* **JDK 25.** `pom.xml` sets `<java.version>25</java.version>`, so anything older will not compile. Any OpenJDK 25 distribution works (Eclipse Temurin, Azul Zulu, …).
5+
* **JDK 25.** `pom.xml` sets `<java.version>25</java.version>`, so anything older will not compile. Any OpenJDK 25 distribution works (Eclipse Temurin, Azul Zulu, …). `<scalac.release>` is 25 as well, so the IDE SDK, the command-line build and CI all sit on the same Java level — there is no second, lower level to account for.
66

7-
Note that `pom.xml` also pins `<release>17</release>` for *scalac only* — the Scala 2.12.21 compiler accepts `-release` no higher than 17. That is not a project-wide Java level; do not set the IDE SDK to 17 because of it.
7+
If a build ever fails with `'25' is not a valid choice for '-release'`, it means scalac is running on a JDK older than 25, not that the Scala version is too old: the highest `-release` scalac accepts is the version of the JDK running it. `maven-enforcer-plugin` catches that up front, so from the command line you would see `Detected JDK version … is not in the allowed range [25,)` first.
88

99
* **Scala 2.12.21** (`<scala.compiler>` in `pom.xml`).
1010

‎obp-api/src/main/resources/docs/introductory_system_documentation.md‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ The Open Bank Project (OBP) is an open-source RESTful API platform for banks tha
174174
- **Caching**: Multi-layer caching strategy with ETags
175175
- **Metrics & Monitoring**: API usage metrics, performance tracking
176176
- **Database Support**: PostgreSQL, Oracle, MySQL, MS SQL Server, H2
177-
- **Akka Integration**: Actor-based concurrency model
177+
- **Pekko Integration**: Actor-based concurrency model (Pekko is the renamed Akka fork; the connector and props names still read `akka`)
178178
- **Connection Pooling**: Efficient database connection management
179179

180180
#### 1.2.13 Developer Experience
@@ -354,12 +354,12 @@ The Open Bank Project (OBP) is an open-source RESTful API platform for banks tha
354354

355355
**Backend (OBP-API):**
356356

357-
- Language: Scala 2.12/2.13
358-
- Framework: Liftweb (API logic), HTTP4S (runtime server)
357+
- Language: Scala 2.12.21
358+
- Framework: HTTP4S (API logic and runtime server); Lift survives only as the Mapper ORM
359359
- Build Tool: Maven 3
360-
- Server: HTTP4S (Blaze) - standalone executable JAR, no application server required
361-
- Concurrency: Akka
362-
- JDK: OpenJDK 11+
360+
- Server: HTTP4S (Ember) - standalone executable JAR, no application server required
361+
- Concurrency: Pekko (the renamed Akka fork; connector names and props keep the `akka` spelling)
362+
- JDK: OpenJDK 25 (pinned; see `<java.version>` in `pom.xml`)
363363
- Modules: `obp-commons` + `obp-api` (2-module structure)
364364

365365
**Frontend (API Explorer II):**
@@ -1522,7 +1522,7 @@ POST /open-banking/v3.1/cbpii/funds-confirmation-consents
15221522

15231523
**Software Requirements:**
15241524

1525-
- Java: OpenJDK 11+ or Oracle JDK 1.8/13
1525+
- Java: OpenJDK 25 (pinned; older JDKs are rejected by the build)
15261526
- Maven: 3.6+
15271527
- Node.js: 18+ (for frontend components)
15281528
- PostgreSQL: 12+ (production)
@@ -1597,7 +1597,7 @@ mvn install -pl .,obp-commons -DskipTests && mvn package -pl obp-api -DskipTests
15971597
java -Xss128m -jar obp-api/target/obp-api.jar
15981598
```
15991599

1600-
**For Java 11+ (if needed):**
1600+
**The `--add-opens` flags (required on the pinned JDK 25, as on any Java 11+):**
16011601

16021602
```bash
16031603
mkdir -p .mvn
@@ -3976,7 +3976,7 @@ sudo systemctl start obp-api
39763976

39773977
```bash
39783978
# Install Java
3979-
sdk install java 11.0.2-open
3979+
sdk install java 25-tem
39803980

39813981
# Install Maven
39823982
sdk install maven 3.8.6

‎pom.xml‎

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,32 @@
4141
driver loads on JDK 25. Revisit only if a release appears with a jre suffix at or above
4242
java.version. -->
4343
<mssql.jre.version>11</mssql.jre.version>
44+
45+
<!-- The -release scala-maven-plugin passes to scalac, and on to javac for the .java sources
46+
that live under src/main/scala. It bounds which JDK API our own sources may reference.
47+
48+
Kept aligned with java.version: there is no supported configuration below it. The test
49+
runner aborts unless it finds exactly that JDK, every Dockerfile is eclipse-temurin:25,
50+
and every CI job sets java-version 25 - so a build host on an older JDK does not exist,
51+
and capping this lower would only hide APIs we are entitled to use.
52+
53+
Still a separate property rather than a direct ${java.version} reference, because the two
54+
express different constraints. java.version is the JDK we target and ship on; this is the
55+
highest -release the *compiler* will accept, which is the spec version of the JDK actually
56+
running scalac - not a property of the Scala version. The same scalac 2.12.21 accepts up
57+
to 17 on JDK 17, up to 21 on JDK 21 and up to 25 on JDK 25. So whenever java.version moves
58+
ahead of the JDK on the build host, the two must be able to come apart; wiring this to
59+
${java.version} would make that impossible to express. That is exactly what happened on
60+
2026-07-03: java.version had just gone to 25 while a build host was still on 21, scalac
61+
rejected -release 25 with "'25' is not a valid choice for '-release'", and the symptom was
62+
read as a Scala-version limit. maven-enforcer-plugin now fails fast on that case instead.
63+
64+
Caveat: on Scala 2.12 this only widens the visible API surface, it does not raise the
65+
class-file version. 2.12 emits Java 8 class files whatever -release says, so a Scala class
66+
that calls a Java 25 method fails at run time with NoSuchMethodError instead of at load
67+
time with UnsupportedClassVersionError. Getting that check back needs Scala 2.13+. javac
68+
does honour it, so the .java sources under src/main/scala track this value. -->
69+
<scalac.release>25</scalac.release>
4470
</properties>
4571

4672
<modules>
@@ -192,16 +218,10 @@
192218
<configuration>
193219
<scalaVersion>${scala.compiler}</scalaVersion>
194220
<charset>${project.build.sourceEncoding}</charset>
195-
<!-- Pin scalac's -release independently of ${java.version}. javac needs -release 25
196-
for JDK 25, but the Scala 2.12.21 compiler only accepts -release up to 17 (its
197-
max supported Java API/target). Without this, scala-maven-plugin derives -release
198-
from maven.compiler.target (=${java.version}=25) and passes an unsupported
199-
-release 25 to scalac, failing the build with
200-
"'25' is not a valid choice for '-release'". Scala 2.12 emits Java 8 bytecode
201-
regardless, so this only bounds the visible JDK API surface; the classes still run
202-
on JDK 25. Bump this only when moving to a Scala version that supports a higher
203-
-release. -->
204-
<release>17</release>
221+
<!-- Set explicitly because scala-maven-plugin otherwise derives -release from
222+
maven.compiler.target (= ${java.version}), which would demand a JDK 25 build host.
223+
See the scalac.release property for the ceiling rule and its caveats. -->
224+
<release>${scalac.release}</release>
205225
<displayCmd>true</displayCmd>
206226
<!-- Optimized JVM settings for faster Scala compilation -->
207227
<jvmArgs>
@@ -298,6 +318,38 @@
298318
</execution>
299319
</executions>
300320
</plugin>
321+
<!-- Fail fast when the build JDK is older than scalac's -release. scalac cannot target a
322+
release newer than the JDK running it, so without this check the build dies minutes
323+
later inside scala-maven-plugin with "'25' is not a valid choice for '-release'" -
324+
which reads as a Scala-version limit rather than a stale build host, and was misread
325+
that way on 2026-07-03. The bound is ${scalac.release} itself, not a literal, so the
326+
rule can never drift from the actual constraint.
327+
328+
No custom <message>: the rule's default text is strictly more useful here, naming the
329+
detected version, its JAVA_HOME and the required range, e.g.
330+
Detected JDK version 17.0.5 (JAVA_HOME=...) is not in the allowed range [25,).
331+
A custom message would replace that rather than add to it. -->
332+
<plugin>
333+
<groupId>org.apache.maven.plugins</groupId>
334+
<artifactId>maven-enforcer-plugin</artifactId>
335+
<version>3.5.0</version>
336+
<executions>
337+
<execution>
338+
<id>enforce-build-jdk</id>
339+
<phase>validate</phase>
340+
<goals>
341+
<goal>enforce</goal>
342+
</goals>
343+
<configuration>
344+
<rules>
345+
<requireJavaVersion>
346+
<version>[${scalac.release},)</version>
347+
</requireJavaVersion>
348+
</rules>
349+
</configuration>
350+
</execution>
351+
</executions>
352+
</plugin>
301353

302354

303355

‎run_specific_tests.sh‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@
3939

4040
set -eo pipefail
4141

42+
# Pin the JDK before any Maven work, exactly as the other runners do — the suite must
43+
# run on the project's JDK, because a different one produces different results.
44+
. "$(dirname "${BASH_SOURCE[0]}")/scripts/java_env.sh"
45+
4246
################################################################################
4347
# CONFIGURATION
4448
################################################################################

0 commit comments

Comments
 (0)