diff --git a/CLAUDE.md b/CLAUDE.md
index 037761ced..d2faaed9a 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -2,6 +2,10 @@
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+## Requirements
+
+The build runs on Gradle 9.x, which requires a JVM of **17 or higher** to run the Gradle daemon itself. Compilation targets Java 17 via the toolchain declared in `bdk.java-common-conventions` — the daemon JVM and the toolchain JVM are independent, so contributors on a newer daemon JDK still produce Java 17 bytecode.
+
## Build & Test Commands
```bash
@@ -49,7 +53,7 @@ OBO (On-Behalf-Of) flows are surfaced through `OboServices` / `OboService`, whic
Four Groovy convention plugins used by sub-modules:
-- `bdk.java-common-conventions` — Java 17, UTF-8, JaCoCo, JUnit Platform, sources+javadoc jars, BOM platform import
+- `bdk.java-common-conventions` — Java 17 toolchain, UTF-8, JaCoCo, JUnit Platform, sources+javadoc jars, BOM platform import
- `bdk.java-library-conventions` — extends common + `java-library` plugin (used by all published libs)
- `bdk.java-publish-conventions` — `maven-publish` + `signing`; signing is **only required for release versions** (`isReleaseVersion = !version.endsWith('SNAPSHOT')`)
- `bdk.java-codegen-conventions` — OpenAPI Generator (Jersey2, Java 8 date library) reading `src/main/resources/api.yaml`; generated sources land in `build/generated/openapi`
diff --git a/allow-list.xml b/allow-list.xml
index e186c559e..fd9cc3038 100644
--- a/allow-list.xml
+++ b/allow-list.xml
@@ -32,6 +32,15 @@
^pkg:maven/io\.netty/netty.*@.*$
CVE-2026-42582
+
+
+ ^pkg:maven/io\.netty/netty.*@.*$
+ CVE-2026-56816
+
/dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -114,7 +114,6 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
-CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
@@ -133,10 +132,13 @@ location of your Java installation."
fi
else
JAVACMD=java
- which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
+ fi
fi
# Increase the maximum file descriptors if we can.
@@ -144,7 +146,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
- # shellcheck disable=SC3045
+ # shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
@@ -152,7 +154,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
- # shellcheck disable=SC3045
+ # shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@@ -169,7 +171,6 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
- CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
@@ -197,16 +198,19 @@ if "$cygwin" || "$msys" ; then
done
fi
-# Collect all arguments for the java command;
-# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
-# shell script including quotes and variable substitutions, so put them in
-# double quotes to make sure that they get re-expanded; and
-# * put everything else in single quotes, so that it's not re-expanded.
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
- -classpath "$CLASSPATH" \
- org.gradle.wrapper.GradleWrapperMain \
+ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
diff --git a/gradlew.bat b/gradlew.bat
index 6689b85be..8508ef684 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -13,16 +13,18 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
-@rem Gradle startup script for Windows
+@rem gradlew startup script for Windows
@rem
@rem ##########################################################################
-@rem Set local scope for the variables with windows NT shell
-if "%OS%"=="Windows_NT" setlocal
+@rem Set local scope for the variables, and ensure extensions are enabled
+setlocal EnableExtensions
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@@ -43,13 +45,13 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
-echo.
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
-goto fail
+"%COMSPEC%" /c exit 1
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
@@ -57,36 +59,24 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
-echo.
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
-goto fail
+"%COMSPEC%" /c exit 1
:execute
@rem Setup the command line
-set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
-
-
-@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
-
-:end
-@rem End local scope for the variables with windows NT shell
-if %ERRORLEVEL% equ 0 goto mainEnd
-:fail
-rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
-rem the _cmd.exe /c_ return code!
-set EXIT_CODE=%ERRORLEVEL%
-if %EXIT_CODE% equ 0 set EXIT_CODE=1
-if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
-exit /b %EXIT_CODE%
-:mainEnd
-if "%OS%"=="Windows_NT" endlocal
+@rem Execute gradlew
+@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
+@rem which allows us to clear the local environment before executing the java command
+endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
-:omega
+:exitWithErrorLevel
+@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
+"%COMSPEC%" /c exit %ERRORLEVEL%
diff --git a/openspec/changes/modernize-build-toolchain/design.md b/openspec/changes/archive/2026-08-05-modernize-build-toolchain/design.md
similarity index 100%
rename from openspec/changes/modernize-build-toolchain/design.md
rename to openspec/changes/archive/2026-08-05-modernize-build-toolchain/design.md
diff --git a/openspec/changes/modernize-build-toolchain/proposal.md b/openspec/changes/archive/2026-08-05-modernize-build-toolchain/proposal.md
similarity index 100%
rename from openspec/changes/modernize-build-toolchain/proposal.md
rename to openspec/changes/archive/2026-08-05-modernize-build-toolchain/proposal.md
diff --git a/openspec/changes/modernize-build-toolchain/specs/build-toolchain/spec.md b/openspec/changes/archive/2026-08-05-modernize-build-toolchain/specs/build-toolchain/spec.md
similarity index 100%
rename from openspec/changes/modernize-build-toolchain/specs/build-toolchain/spec.md
rename to openspec/changes/archive/2026-08-05-modernize-build-toolchain/specs/build-toolchain/spec.md
diff --git a/openspec/changes/archive/2026-08-05-modernize-build-toolchain/tasks.md b/openspec/changes/archive/2026-08-05-modernize-build-toolchain/tasks.md
new file mode 100644
index 000000000..70ed2a259
--- /dev/null
+++ b/openspec/changes/archive/2026-08-05-modernize-build-toolchain/tasks.md
@@ -0,0 +1,106 @@
+## 1. Gradle 9 Compatibility Spike (blocking — resolves D1)
+
+- [x] 1.1 Bump `gradle/wrapper/gradle-wrapper.properties` to the chosen Gradle 9.x on a throwaway branch and run `./gradlew :symphony-bdk-core:openApiGenerate` — record whether `openapi-generator-gradle-plugin:6.6.0` works
+- [x] 1.2 Run `./gradlew build --warning-mode all` and capture the full deprecation list; this is the authoritative task list for section 2, superseding the statically-found call sites
+- [x] 1.3 Verify `owasp-dependencycheck:12.2.2` and `com.github.ben-manes.versions` still resolve under Gradle 9 (`buildSrc/build.gradle` already carries a manual override block for a 6.6.0-vs-dependencycheck conflict)
+- [x] 1.4 Per D1, choose and record the branch: proceed as scoped / (a) pull generator bump forward / (b) defer Gradle 9 to `adopt-java-25-baseline`. Update this change's proposal scope if (a) or (b)
+
+ **D1 spike result — proceed as scoped.** `openapi-generator-gradle-plugin:6.6.0`'s `GenerateTask` runs cleanly under Gradle 9.6.1 (verified via `:symphony-bdk-core:generateAgent`); the default no-arg `openApiGenerate` task fails with "generator name must be specified" but that is pre-existing, unconfigured-task behaviour unrelated to Gradle 9. `com.github.ben-manes.versions` needed a bump from 0.42.0 → 0.54.0 (0.42.0 calls a `LenientConfiguration` API removed in Gradle 9); `org.owasp.dependencycheck:12.2.2` resolves and applies with no changes needed. Two additional Gradle-9 interlocks were found beyond D1's scope and fixed as part of section 2: cross-project `project(':x').sourceSets` access in two Spring Boot starter modules (dynamic property forwarding on `ProjectDependency` was removed — fixed via a top-level `project(':symphony-bdk-core')` accessor + `evaluationDependsOn`), and the `org.springframework.boot` Gradle plugin versions pinned in the two example modules (3.2.2, 3.5.4) predated Gradle 9 support and were bumped to 3.5.16 to match the BOM's existing `spring-boot-dependencies` constraint — no Spring Boot runtime version change.
+
+## 2. Gradle 9 Migration
+
+- [x] 2.1 Bump the wrapper to Gradle 9.x (`gradle/wrapper/gradle-wrapper.properties`, and commit the regenerated wrapper scripts)
+- [x] 2.2 Replace `sourceCompatibility = JavaVersion.VERSION_17` in `bdk.java-common-conventions.gradle` with a `java { toolchain { languageVersion = JavaLanguageVersion.of(17) } }` block (D2)
+- [x] 2.3 Confirm `options.compilerArgs << '-parameters'` and `options.encoding = 'UTF-8'` survive the conventions rewrite — both are load-bearing (Spring constructor binding, Jackson parameter names)
+- [x] 2.4 Replace `project.buildDir` with `layout.buildDirectory` in `bdk.java-codegen-conventions.gradle`, `symphony-bdk-core/build.gradle`, and `symphony-bdk-extensions/symphony-group-extension/build.gradle`
+- [x] 2.5 Convert the two `tasks.create(...)` calls in `symphony-bdk-core/build.gradle`'s `apisToGenerate` loop (`download$api`, `generate$api`) to `tasks.register(...)`, preserving the `compileJava.dependsOn` / `sourcesJar.dependsOn` wiring
+- [x] 2.6 Work through the remaining deprecations from 1.2 until `./gradlew build --warning-mode all` is clean
+
+ Additional deprecations found beyond the statically-known 5: `Project.getProperties()` (root `build.gradle`, 4 call sites → `findProperty`), implicit parent-project property lookup of `projectVersion` in the `allprojects` block (→ `rootProject.ext.projectVersion`), and Groovy space-assignment syntax for `username`/`password`/`url`/`required` in the two publishing blocks (`bdk.java-publish-conventions.gradle` and `symphony-bdk-bom/build.gradle`). `./gradlew clean build --warning-mode all` is now warning-free.
+- [x] 2.7 Add an explicit `jacoco { toolVersion = '...' }` to `bdk.java-common-conventions.gradle` rather than relying on the Gradle-bundled default (pinned to `0.8.15`, latest stable)
+
+## 3. Bytecode-Processing Tooling Bumps
+
+- [x] 3.1 Bump the explicit `net.bytebuddy:byte-buddy` pin in `symphony-bdk-core/build.gradle` (currently 1.12.19) to a release supporting class file version 69
+
+ Bumped to `1.18.11`, the latest release.
+- [x] 3.2 Bump `org.mapstruct:mapstruct` and `org.mapstruct:mapstruct-processor` in `symphony-bdk-core/build.gradle` (currently 1.4.2.Final) to 1.6.x
+
+ Bumped both to `1.6.3`, the latest 1.6.x patch.
+- [x] 3.3 Diff the generated `UserDetailMapperImpl` before and after the MapStruct bump; investigate any change in null-handling or unmapped-property behaviour (D5)
+
+ No behaviour change. The only diff is cosmetic: fully-qualified `com.symphony.bdk.gen.api.model.V2UserAttributes.AccountTypeEnum` references became simple `V2UserAttributes.AccountTypeEnum` (import already present), plus the `@Generated` comment's version/date. Null-handling, the `AccountTypeEnum` switch, and every field mapping are byte-for-byte identical in logic.
+- [x] 3.4 Bump `com.tngtech.archunit:archunit-junit5` in the BOM (currently 1.2.1); confirm the 3 architecture tests still pass — `CoreArchitectureTest`, `SymphonyGroupExtensionArchitectureTest`, `bdk-spring-boot-example/ArchitectureTest`
+
+ Bumped to `1.5.0`, the latest release. All 3 architecture tests pass (`CoreArchitectureTest`: 5/5, `SymphonyGroupExtensionArchitectureTest`: 1/1, `bdk-spring-boot-example/ArchitectureTest`: 2/2).
+- [x] 3.5 Bump `org.assertj:assertj-core` in the BOM if a newer release is available for the JaCoCo/JDK combination
+
+ No change — `3.27.7` is already the latest stable 3.x release (`4.0.0-M1` is a milestone, not stable).
+- [x] 3.6 Verify `jacocoTestCoverageVerification` still passes in every module after the JaCoCo toolVersion bump; if thresholds shift because of JaCoCo counting changes rather than test changes, adjust them in a dedicated commit naming the JaCoCo version
+
+ `./gradlew build jacocoTestReport jacocoTestCoverageVerification` passes in every module with no threshold adjustments needed.
+
+## 4. Lombok Version Ownership
+
+- [x] 4.1 Add an explicit `org.projectlombok:lombok` constraint to `symphony-bdk-bom/build.gradle`, at the version currently resolved from `spring-boot-dependencies:3.5.16` or newer
+- [x] 4.2 Verify all 13 Lombok-consuming modules still compile with the pinned version
+- [x] 4.3 Confirm the pinned version has published JDK 25 support (needed later by `adopt-java-25-baseline`, recorded here so the check isn't repeated)
+
+ Pinned at `1.18.46` — the same version already resolved from `spring-boot-dependencies:3.5.16`, so the constraint changes nothing today; it just stops future Spring Boot bumps from moving Lombok implicitly. 1.18.46 shipped JDK 26 support and had JDK 25 support since 1.18.40 (2025-09-04).
+
+## 5. MockServer Replacement (D3)
+
+- [x] 5.1 Check whether a current MockServer release tracks Netty 4.2; if so, reduce this section to a version bump and skip 5.2–5.4
+- [x] ~~5.2 Choose the replacement (WireMock or OkHttp MockWebServer) and add it to the BOM~~ — not needed, D3 resolved to a bump
+- [x] ~~5.3 Migrate the 4 test files using `mockserver` off it, preserving assertion coverage~~ — not needed
+- [x] ~~5.4 Remove `org.mock-server:mockserver-netty` from `symphony-bdk-bom` and from `symphony-bdk-core` / `symphony-bdk-http-webclient` test dependencies~~ — not needed
+
+ **D3 result — bump, not replace.** `mockserver-netty:7.5.0`'s own POM explicitly documents it now depends on Netty 4.2 (comment: "MockServer's Netty 4.2"). Bumped `symphony-bdk-bom` straight from `5.15.0` to `7.5.0` — a 2-major-version jump — and ran the full test suite in `symphony-bdk-core` and `symphony-bdk-http-webclient` unmodified: all tests pass. The only `org.mockserver.*` surface in use (`BdkMockServer`/`BdkMockServerExtension` test helpers in both modules) is limited to `ClientAndServer.startClientAndServer()`, `HttpRequest.request()`, `HttpResponse.response()`, and basic builder methods — all stable across the 5.x→7.x line. No test files needed changes.
+
+## 6. Dead Dependency Removal (D4 — own commit)
+
+- [x] 6.1 Remove `javax.xml.bind:jaxb-api:2.3.1` from `symphony-bdk-core/build.gradle` (verified: zero source usage anywhere in the tree)
+- [x] 6.2 ~~Replace `javax.annotation:jsr250-api:1.0` with `jakarta.annotation:jakarta.annotation-api` in `bdk.java-codegen-conventions.gradle`, and confirm the generated sources still compile~~ — **does not compile, kept as-is**
+
+ Tested empirically per the task's own instruction. `openapi-generator` 6.6.0's `java`/`jersey2` generator emits `@javax.annotation.Generated(...)` fully-qualified (335 occurrences in `symphony-bdk-core`'s generated sources alone, plus all of `symphony-group-extension`'s). `jakarta.annotation-api` only provides `jakarta.annotation.Generated` (different package, verified via its jar contents) — swapping the dependency produced 36 "cannot find symbol" compile errors. `javax.annotation:jsr250-api:1.0` is **not dead** — it is a required compile-time dependency for every module using `bdk.java-codegen-conventions`, and the proposal's classification of it as a dead dependency was incorrect. Making the generator emit `jakarta.annotation.Generated` instead would require an `openapi-generator` config change affecting all 335+ generated classes' consumer-visible imports — out of scope for this build-only change (belongs with an eventual openapi-generator/Jakarta migration). Left unchanged in both `bdk.java-codegen-conventions.gradle` and `symphony-bdk-core/build.gradle` (which carries its own separate copy, also verified required — same `@javax.annotation.Generated` usage, ~335 files).
+- [x] 6.3 Remove `org.projectreactor:reactor-spring:1.0.1.RELEASE` from `symphony-bdk-bom` after confirming no module resolves it
+- [x] 6.4 Land 6.1 and 6.3 plus the Lombok constraint from 4.1 as a single commit whose message names each removed coordinate and why (jsr250-api excluded — see 6.2)
+
+## 7. Forward-Compatibility Verification (does not change committed config)
+
+- [x] 7.1 On a throwaway branch, flip the toolchain to `JavaLanguageVersion.of(25)` and run `./gradlew build` — record every failure as a task in `adopt-java-25-baseline`, then discard the branch
+- [x] 7.2 Specifically record: whether Mockito's dynamic agent attach warns or fails, whether ArchUnit reads class file 69, whether Lombok compiles, and whether `openApiGenerate` runs
+- [x] 7.3 Confirm the chosen Gradle 9.x can provision a JDK 25 toolchain (needed by `adopt-java-25-baseline`)
+
+ **Result — zero failures.** `./gradlew clean build jacocoTestReport jacocoTestCoverageVerification` passes entirely on `JavaLanguageVersion.of(25)`, with every tooling bump from sections 3–6 already in place. Specifics:
+ - Gradle 9.6.1 auto-detected the SDKMAN-installed Temurin JDK 25 (`./gradlew javaToolchains`) with no `foojay-resolver` plugin needed — answers 7.3.
+ - Compiled classes carry major version `69` (verified via `javap -v` on `SymphonyBdk.class`), confirming the toolchain actually drove compilation, not just the daemon.
+ - No Mockito dynamic-agent-attach warnings or failures anywhere in the build log.
+ - All 3 architecture tests pass under JDK 25 (`CoreArchitectureTest`: 5/5, `SymphonyGroupExtensionArchitectureTest`: 1/1, `bdk-spring-boot-example/ArchitectureTest`: 2/2) — ArchUnit reads class file 69 fine.
+ - Lombok (1.18.46, pinned in task 4.1) compiles cleanly across all 13 consuming modules.
+ - `openApiGenerate`/the custom `generate$api` tasks run and produce output identically to JDK 17.
+ - `jacocoTestCoverageVerification` passes with no threshold changes needed.
+
+ **Nothing to record in `adopt-java-25-baseline` task 1.2** — the residual failure list it expects as its "authoritative starting inventory" is empty. The bytecode-tooling floor raised in sections 3–4 of this change appears sufficient on its own; `adopt-java-25-baseline` section 2 should still re-run this check for a fresh confirmation, since dependency resolution can shift between now and when that change lands.
+ - The toolchain-flip edit was made and tested on a throwaway branch (`spike/jdk25-forward-compat`) and discarded without merging, per the task; `bdk.java-common-conventions.gradle` stays pinned at `JavaLanguageVersion.of(17)` on this branch.
+
+## 8. Release
+
+- [x] 8.1 Confirm `./gradlew build jacocoTestReport jacocoTestCoverageVerification` is green on JDK 17 (the CI command)
+- [x] 8.2 Confirm `./gradlew publishToMavenLocal` produces the same module coordinates as 3.5.x
+
+ Same `groupId`/`artifactId` set as before this change (verified against `~/.m2/repository/org/finos/symphony/bdk`, matching `symphony-bdk-bom`'s constraint list) — no module added, removed, or renamed. The BOM's published pom confirms the `lombok` constraint is present and `reactor-spring` is absent.
+- [x] 8.3 Draft 3.6.0 release notes covering the two consumer-visible BOM changes from D4's migration plan
+
+ Draft, ready to paste into the GitHub Release description when `v3.6.0` is cut:
+
+ > ## 3.6.0
+ >
+ > Build-tooling modernization: Gradle 8 → 9, and a floor raise across every bytecode-processing tool in the build (Byte Buddy, MapStruct, ArchUnit, JaCoCo, MockServer) ahead of a future Java 25 baseline. No source changes, no public API changes, no Java or Spring Boot version change.
+ >
+ > **Consumer-visible changes to `symphony-bdk-bom`:**
+ > - `org.projectreactor:reactor-spring` is no longer constrained by the BOM. It was a 2017 artifact that no BDK module depends on; if you relied on the BOM to pin its version, declare it yourself.
+ > - Lombok's version is now constrained directly by `symphony-bdk-bom` (`1.18.46`) instead of being inherited from `spring-boot-dependencies`. The resolved version is unchanged today — this just means a future Spring Boot bump will no longer move it implicitly.
+ >
+ > **Also updated:** `mockserver-netty` 5.15.0 → 7.5.0 (test-scope only, not consumer-visible).
+- [x] 8.4 Update `CLAUDE.md` with the new Gradle version and the minimum daemon JDK for contributors
diff --git a/openspec/changes/modernize-build-toolchain/tasks.md b/openspec/changes/modernize-build-toolchain/tasks.md
deleted file mode 100644
index 034adb528..000000000
--- a/openspec/changes/modernize-build-toolchain/tasks.md
+++ /dev/null
@@ -1,58 +0,0 @@
-## 1. Gradle 9 Compatibility Spike (blocking — resolves D1)
-
-- [ ] 1.1 Bump `gradle/wrapper/gradle-wrapper.properties` to the chosen Gradle 9.x on a throwaway branch and run `./gradlew :symphony-bdk-core:openApiGenerate` — record whether `openapi-generator-gradle-plugin:6.6.0` works
-- [ ] 1.2 Run `./gradlew build --warning-mode all` and capture the full deprecation list; this is the authoritative task list for section 2, superseding the statically-found call sites
-- [ ] 1.3 Verify `owasp-dependencycheck:12.2.2` and `com.github.ben-manes.versions` still resolve under Gradle 9 (`buildSrc/build.gradle` already carries a manual override block for a 6.6.0-vs-dependencycheck conflict)
-- [ ] 1.4 Per D1, choose and record the branch: proceed as scoped / (a) pull generator bump forward / (b) defer Gradle 9 to `adopt-java-25-baseline`. Update this change's proposal scope if (a) or (b)
-
-## 2. Gradle 9 Migration
-
-- [ ] 2.1 Bump the wrapper to Gradle 9.x (`gradle/wrapper/gradle-wrapper.properties`, and commit the regenerated wrapper scripts)
-- [ ] 2.2 Replace `sourceCompatibility = JavaVersion.VERSION_17` in `bdk.java-common-conventions.gradle` with a `java { toolchain { languageVersion = JavaLanguageVersion.of(17) } }` block (D2)
-- [ ] 2.3 Confirm `options.compilerArgs << '-parameters'` and `options.encoding = 'UTF-8'` survive the conventions rewrite — both are load-bearing (Spring constructor binding, Jackson parameter names)
-- [ ] 2.4 Replace `project.buildDir` with `layout.buildDirectory` in `bdk.java-codegen-conventions.gradle`, `symphony-bdk-core/build.gradle`, and `symphony-bdk-extensions/symphony-group-extension/build.gradle`
-- [ ] 2.5 Convert the two `tasks.create(...)` calls in `symphony-bdk-core/build.gradle`'s `apisToGenerate` loop (`download$api`, `generate$api`) to `tasks.register(...)`, preserving the `compileJava.dependsOn` / `sourcesJar.dependsOn` wiring
-- [ ] 2.6 Work through the remaining deprecations from 1.2 until `./gradlew build --warning-mode all` is clean
-- [ ] 2.7 Add an explicit `jacoco { toolVersion = '...' }` to `bdk.java-common-conventions.gradle` rather than relying on the Gradle-bundled default
-
-## 3. Bytecode-Processing Tooling Bumps
-
-- [ ] 3.1 Bump the explicit `net.bytebuddy:byte-buddy` pin in `symphony-bdk-core/build.gradle` (currently 1.12.19) to a release supporting class file version 69
-- [ ] 3.2 Bump `org.mapstruct:mapstruct` and `org.mapstruct:mapstruct-processor` in `symphony-bdk-core/build.gradle` (currently 1.4.2.Final) to 1.6.x
-- [ ] 3.3 Diff the generated `UserDetailMapperImpl` before and after the MapStruct bump; investigate any change in null-handling or unmapped-property behaviour (D5)
-- [ ] 3.4 Bump `com.tngtech.archunit:archunit-junit5` in the BOM (currently 1.2.1); confirm the 3 architecture tests still pass — `CoreArchitectureTest`, `SymphonyGroupExtensionArchitectureTest`, `bdk-spring-boot-example/ArchitectureTest`
-- [ ] 3.5 Bump `org.assertj:assertj-core` in the BOM if a newer release is available for the JaCoCo/JDK combination
-- [ ] 3.6 Verify `jacocoTestCoverageVerification` still passes in every module after the JaCoCo toolVersion bump; if thresholds shift because of JaCoCo counting changes rather than test changes, adjust them in a dedicated commit naming the JaCoCo version
-
-## 4. Lombok Version Ownership
-
-- [ ] 4.1 Add an explicit `org.projectlombok:lombok` constraint to `symphony-bdk-bom/build.gradle`, at the version currently resolved from `spring-boot-dependencies:3.5.16` or newer
-- [ ] 4.2 Verify all 13 Lombok-consuming modules still compile with the pinned version
-- [ ] 4.3 Confirm the pinned version has published JDK 25 support (needed later by `adopt-java-25-baseline`, recorded here so the check isn't repeated)
-
-## 5. MockServer Replacement (D3)
-
-- [ ] 5.1 Check whether a current MockServer release tracks Netty 4.2; if so, reduce this section to a version bump and skip 5.2–5.4
-- [ ] 5.2 Choose the replacement (WireMock or OkHttp MockWebServer) and add it to the BOM
-- [ ] 5.3 Migrate the 4 test files using `mockserver` off it, preserving assertion coverage
-- [ ] 5.4 Remove `org.mock-server:mockserver-netty` from `symphony-bdk-bom` and from `symphony-bdk-core` / `symphony-bdk-http-webclient` test dependencies
-
-## 6. Dead Dependency Removal (D4 — own commit)
-
-- [ ] 6.1 Remove `javax.xml.bind:jaxb-api:2.3.1` from `symphony-bdk-core/build.gradle` (verified: zero source usage anywhere in the tree)
-- [ ] 6.2 Replace `javax.annotation:jsr250-api:1.0` with `jakarta.annotation:jakarta.annotation-api` in `bdk.java-codegen-conventions.gradle`, and confirm the generated sources still compile
-- [ ] 6.3 Remove `org.projectreactor:reactor-spring:1.0.1.RELEASE` from `symphony-bdk-bom` after confirming no module resolves it
-- [ ] 6.4 Land 6.1–6.3 plus the Lombok constraint from 4.1 as a single commit whose message names each removed coordinate and why
-
-## 7. Forward-Compatibility Verification (does not change committed config)
-
-- [ ] 7.1 On a throwaway branch, flip the toolchain to `JavaLanguageVersion.of(25)` and run `./gradlew build` — record every failure as a task in `adopt-java-25-baseline`, then discard the branch
-- [ ] 7.2 Specifically record: whether Mockito's dynamic agent attach warns or fails, whether ArchUnit reads class file 69, whether Lombok compiles, and whether `openApiGenerate` runs
-- [ ] 7.3 Confirm the chosen Gradle 9.x can provision a JDK 25 toolchain (needed by `adopt-java-25-baseline`)
-
-## 8. Release
-
-- [ ] 8.1 Confirm `./gradlew build jacocoTestReport jacocoTestCoverageVerification` is green on JDK 17 (the CI command)
-- [ ] 8.2 Confirm `./gradlew publishToMavenLocal` produces the same module coordinates as 3.5.x
-- [ ] 8.3 Draft 3.6.0 release notes covering the two consumer-visible BOM changes from D4's migration plan
-- [ ] 8.4 Update `CLAUDE.md` with the new Gradle version and the minimum daemon JDK for contributors
diff --git a/openspec/specs/build-toolchain/spec.md b/openspec/specs/build-toolchain/spec.md
new file mode 100644
index 000000000..e28d0f123
--- /dev/null
+++ b/openspec/specs/build-toolchain/spec.md
@@ -0,0 +1,64 @@
+# Build-Toolchain Specification
+
+### Requirement: Target Java version declared via a Gradle toolchain
+The build SHALL declare the target Java version through a Gradle Java toolchain in `bdk.java-common-conventions`, not through the project-level `sourceCompatibility` convention. The declared language version SHALL be the single source of truth for every module's target, so that changing the baseline is a one-line change in one file. The JDK running the Gradle daemon SHALL be independent of the declared toolchain version.
+
+#### Scenario: Baseline declared in exactly one place
+- **WHEN** the target Java version needs to change
+- **THEN** exactly one `languageVersion` declaration in `bdk.java-common-conventions` requires editing, and no module build file declares its own `sourceCompatibility` or `targetCompatibility`
+
+#### Scenario: Compilation target is independent of the daemon JDK
+- **WHEN** the build is run with a Gradle daemon JDK newer than the declared toolchain version
+- **THEN** all modules still compile to the declared toolchain version, and the produced class files carry that version
+
+#### Scenario: Parameter names and encoding are preserved
+- **WHEN** any module is compiled
+- **THEN** `-parameters` is passed to `javac` and source encoding is UTF-8, so that Spring constructor binding and Jackson parameter-name resolution continue to work
+
+### Requirement: Bytecode-processing tooling supports the target class file version
+Every tool in the build that reads, writes, or instruments bytecode — the Gradle distribution, Byte Buddy, MapStruct's annotation processor, Lombok, JaCoCo, ArchUnit, and the Mockito mock maker — SHALL be at a version that supports the class file version emitted by the declared toolchain. Versions SHALL be pinned explicitly where the build already pins them directly, rather than inherited implicitly from a third-party platform whose upgrade cadence the BDK does not control.
+
+#### Scenario: Build succeeds when the toolchain is raised to a newer JDK
+- **WHEN** the declared toolchain `languageVersion` is temporarily raised to the next Java LTS and `./gradlew build` is run
+- **THEN** no task fails with an unsupported-class-file-version, unknown-constant-pool, or unsupported-source-release error
+
+#### Scenario: Lombok version is owned by this repository
+- **WHEN** the Spring Boot platform version imported by `symphony-bdk-bom` changes
+- **THEN** the resolved Lombok version does not change, because `symphony-bdk-bom` constrains it explicitly
+
+#### Scenario: JaCoCo version is explicit
+- **WHEN** the Gradle distribution version changes
+- **THEN** the JaCoCo tool version used for coverage does not change, because it is declared via `jacoco { toolVersion }`
+
+### Requirement: Build is free of deprecation warnings that block the next Gradle major
+`./gradlew build --warning-mode all` SHALL complete without emitting Gradle deprecation warnings. This applies to build scripts in every module and to the convention plugins in `buildSrc`.
+
+#### Scenario: No deprecation warnings on a full build
+- **WHEN** `./gradlew build --warning-mode all` is run on a clean checkout
+- **THEN** no deprecation warnings are emitted, including from `buildSrc` convention plugins at configuration time
+
+#### Scenario: Build directory and task registration use current APIs
+- **WHEN** any build script references the build output directory or registers a task
+- **THEN** it uses `layout.buildDirectory` and `tasks.register`, not `project.buildDir` or `tasks.create`
+
+### Requirement: Published BOM constrains only dependencies the project actually uses
+`symphony-bdk-bom` SHALL NOT carry version constraints for artifacts that no module in the project resolves. Removing a constraint from the published BOM is a consumer-visible change to dependency resolution and SHALL be released in a minor or major version, never a patch, and SHALL be recorded in release notes.
+
+#### Scenario: Dead constraint is removed
+- **WHEN** a constraint in `symphony-bdk-bom` names an artifact that no module resolves
+- **THEN** the constraint is removed, and the removal is listed in the release notes for the version that drops it
+
+#### Scenario: Constraint removal is not shipped as a patch
+- **WHEN** a release removes one or more constraints from `symphony-bdk-bom`
+- **THEN** that release increments at least the minor version
+
+### Requirement: Generated and processor-derived output is diffed across tooling upgrades
+When an annotation processor or code generator used by the build is upgraded, the output it produces SHALL be compared before and after the upgrade, and any change to a consumer-visible type or to null/default-value handling SHALL be reviewed before release. A tooling upgrade SHALL NOT be treated as build-internal if it changes generated output.
+
+#### Scenario: MapStruct processor upgrade
+- **WHEN** the MapStruct processor version changes
+- **THEN** the generated mapper implementations are diffed, and any change in unmapped-property or null handling is reviewed as a behaviour change rather than a build change
+
+#### Scenario: OpenAPI generator upgrade
+- **WHEN** the OpenAPI generator version changes
+- **THEN** the generated sources under `com.symphony.bdk.gen.api` are diffed in full, because those types are part of the published API surface
diff --git a/symphony-bdk-bom/build.gradle b/symphony-bdk-bom/build.gradle
index ec3c1d208..c47a8601a 100644
--- a/symphony-bdk-bom/build.gradle
+++ b/symphony-bdk-bom/build.gradle
@@ -55,6 +55,11 @@ dependencies {
api 'org.apache.tomcat.embed:tomcat-embed-websocket:10.1.57'
api 'org.apache.tomcat.embed:tomcat-embed-el:10.1.57'
+ // Owned by this repo rather than inherited from spring-boot-dependencies: Lombok lags
+ // JDK releases and is used in 13 modules, so its version needs to move independently
+ // of the Spring Boot version.
+ api 'org.projectlombok:lombok:1.18.46'
+
api 'org.apiguardian:apiguardian-api:1.1.2'
api 'org.slf4j:slf4j-api:2.0.9'
@@ -78,16 +83,14 @@ dependencies {
api 'io.swagger:swagger-annotations:1.6.14'
api 'org.openapitools:jackson-databind-nullable:0.2.6'
- api 'org.projectreactor:reactor-spring:1.0.1.RELEASE'
-
api 'org.freemarker:freemarker:2.3.33'
api 'com.github.jknack:handlebars:4.5.1'
api 'org.reflections:reflections:0.10.2'
api 'info.picocli:picocli:4.7.6'
- api 'com.tngtech.archunit:archunit-junit5:1.2.1'
- api 'org.mock-server:mockserver-netty:5.15.0'
+ api 'com.tngtech.archunit:archunit-junit5:1.5.0'
+ api 'org.mock-server:mockserver-netty:7.5.0'
api 'org.assertj:assertj-core:3.27.7'
api 'jakarta.ws.rs:jakarta.ws.rs-api:3.1.0'
@@ -110,15 +113,15 @@ publishing {
repositories {
maven {
credentials {
- username rootProject.ext.mavenRepoUsername
- password rootProject.ext.mavenRepoPassword
+ username = rootProject.ext.mavenRepoUsername
+ password = rootProject.ext.mavenRepoPassword
}
- url rootProject.ext.mavenRepoUrl
+ url = rootProject.ext.mavenRepoUrl
}
}
}
signing {
- required { rootProject.isReleaseVersion }
+ required = { rootProject.isReleaseVersion }
sign publishing.publications.bom
}
diff --git a/symphony-bdk-core/build.gradle b/symphony-bdk-core/build.gradle
index f1d4d4bfe..56a87f99e 100644
--- a/symphony-bdk-core/build.gradle
+++ b/symphony-bdk-core/build.gradle
@@ -37,9 +37,9 @@ dependencies {
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
- implementation 'org.mapstruct:mapstruct:1.4.2.Final'
- implementation 'net.bytebuddy:byte-buddy:1.12.19'
- annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final'
+ implementation 'org.mapstruct:mapstruct:1.6.3'
+ implementation 'net.bytebuddy:byte-buddy:1.18.11'
+ annotationProcessor 'org.mapstruct:mapstruct-processor:1.6.3'
api 'org.apiguardian:apiguardian-api'
implementation 'org.slf4j:slf4j-api'
@@ -56,7 +56,6 @@ dependencies {
implementation 'io.swagger:swagger-annotations'
implementation 'com.google.code.findbugs:jsr305'
implementation 'javax.annotation:jsr250-api:1.0'
- implementation 'javax.xml.bind:jaxb-api:2.3.1'
implementation 'jakarta.ws.rs:jakarta.ws.rs-api'
testImplementation project(':symphony-bdk-http:symphony-bdk-http-jersey2')
@@ -79,7 +78,7 @@ dependencies {
// OpenAPI code generation
def apiBaseUrl = "https://raw.githubusercontent.com/finos/symphony-api-spec/baf01bee3bd00612d57d2df481ea54aab9404017"
-def generatedFolder = "$buildDir/generated/openapi"
+def generatedFolder = "${layout.buildDirectory.get()}/generated/openapi"
def apisToGenerate = [
Agent: 'agent/agent-api-public-deprecated.yaml',
Pod : 'pod/pod-api-public-deprecated.yaml',
@@ -91,20 +90,19 @@ def apisToGenerate = [
sourceSets.main.java.srcDirs += "$generatedFolder/src/main/java"
apisToGenerate.each { api, path ->
- def downloadTask = tasks.create(name: "download$api", type: Download) {
+ def downloadTask = tasks.register("download$api", Download) {
src "$apiBaseUrl/$path"
- dest "$buildDir/openapi/$api/${path.split("/")[1]}"
+ dest "${layout.buildDirectory.get()}/openapi/$api/${path.split("/")[1]}"
overwrite false
useETag "all"
}
- def generateTask = tasks.create(name: "generate$api",
- type: org.openapitools.generator.gradle.plugin.tasks.GenerateTask,
- dependsOn: downloadTask) {
+ def generateTask = tasks.register("generate$api", org.openapitools.generator.gradle.plugin.tasks.GenerateTask) {
+ dependsOn downloadTask
generatorName = 'java'
library = 'jersey2'
outputDir = generatedFolder
- inputSpec = "$buildDir/openapi/$api/${path.split("/")[1]}"
+ inputSpec = "${layout.buildDirectory.get()}/openapi/$api/${path.split("/")[1]}"
skipOverwrite = true
generateApiTests = false
generateModelTests = false
diff --git a/symphony-bdk-examples/bdk-app-spring-boot-example/build.gradle b/symphony-bdk-examples/bdk-app-spring-boot-example/build.gradle
index f95e9360a..de581a135 100644
--- a/symphony-bdk-examples/bdk-app-spring-boot-example/build.gradle
+++ b/symphony-bdk-examples/bdk-app-spring-boot-example/build.gradle
@@ -1,6 +1,6 @@
plugins {
id 'bdk.java-common-conventions'
- id 'org.springframework.boot' version "3.2.2"
+ id 'org.springframework.boot' version "3.5.16"
}
description = 'Symphony Java BDK Examples for the SpringBoot integration'
diff --git a/symphony-bdk-examples/bdk-spring-boot-example/build.gradle b/symphony-bdk-examples/bdk-spring-boot-example/build.gradle
index 0e258991c..069012687 100644
--- a/symphony-bdk-examples/bdk-spring-boot-example/build.gradle
+++ b/symphony-bdk-examples/bdk-spring-boot-example/build.gradle
@@ -1,6 +1,6 @@
plugins {
id 'bdk.java-common-conventions'
- id 'org.springframework.boot' version "3.5.4"
+ id 'org.springframework.boot' version "3.5.16"
}
description = 'Symphony Java BDK Examples for the SpringBoot integration'
diff --git a/symphony-bdk-extensions/symphony-group-extension/build.gradle b/symphony-bdk-extensions/symphony-group-extension/build.gradle
index 2384fe6ec..60dee2186 100644
--- a/symphony-bdk-extensions/symphony-group-extension/build.gradle
+++ b/symphony-bdk-extensions/symphony-group-extension/build.gradle
@@ -34,13 +34,13 @@ task downloadFile(type: Download) {
"$baseSpecsUrl/profile-manager-api.yaml",
"$baseSpecsUrl/symphony-common-definitions.yaml"
])
- dest buildDir
+ dest layout.buildDirectory.get().asFile
onlyIfModified true
useETag "all"
}
openApiGenerate {
- inputSpec = "$buildDir/profile-manager-api.yaml"
+ inputSpec = "${layout.buildDirectory.get()}/profile-manager-api.yaml"
apiPackage = 'com.symphony.bdk.ext.group.gen.api'
modelPackage = 'com.symphony.bdk.ext.group.gen.api.model'
globalProperties = [
diff --git a/symphony-bdk-spring/symphony-bdk-app-spring-boot-starter/build.gradle b/symphony-bdk-spring/symphony-bdk-app-spring-boot-starter/build.gradle
index 54837378b..419424ca4 100644
--- a/symphony-bdk-spring/symphony-bdk-app-spring-boot-starter/build.gradle
+++ b/symphony-bdk-spring/symphony-bdk-app-spring-boot-starter/build.gradle
@@ -5,6 +5,9 @@ plugins {
description = 'Spring Boot Starter that ease Application developments'
+def coreProject = project(':symphony-bdk-core')
+evaluationDependsOn(coreProject.path)
+
jacocoTestCoverageVerification {
violationRules {
rule {
@@ -41,7 +44,7 @@ dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
- testImplementation project(':symphony-bdk-core').sourceSets.test.output
+ testImplementation coreProject.sourceSets.test.output
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
exclude group: 'junit', module: 'junit'
diff --git a/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/build.gradle b/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/build.gradle
index 1b3fdb203..f73a4131f 100644
--- a/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/build.gradle
+++ b/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/build.gradle
@@ -5,6 +5,9 @@ plugins {
description = 'Spring Boot Wrapper for the Symphony BDK Core module'
+def coreProject = project(':symphony-bdk-core')
+evaluationDependsOn(coreProject.path)
+
jacocoTestCoverageVerification {
violationRules {
rule {
@@ -45,7 +48,7 @@ dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
- testImplementation project(':symphony-bdk-core').sourceSets.test.output
+ testImplementation coreProject.sourceSets.test.output
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
exclude group: 'junit', module: 'junit'