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'