build: migrate obp-api and obp-commons to Scala 2.13 - #2890
Open
hongwei1 wants to merge 37 commits into
Open
Conversation
First step of the Scala 2.12 -> 2.13 migration: pay the language-level debt while still on 2.12, so the version flip carries as little as possible. -Xsource:2.13 makes the 2.12 compiler apply 2.13 language semantics. It reports zero errors on this tree and 110 new warnings, all fixed here: - 108 sites (one per file) of `new Inject(buildOne _)`. 2.13 no longer converts a method without a parameter list to a function via `m _`; the replacement is the explicit literal `() => buildOne`. Two further occurrences live in commented-out code and were converted as well, so uncommenting them later cannot reintroduce the construct. - API1_2_1Test: the wildcard import of the commons model brought in an ErrorMessage that the same-package code.api.v1_2_1.ErrorMessage shadows. Both are case class ErrorMessage(code: Int, message: String), so resolution never changed behaviour; the import now excludes the name to say which one is meant. - AccountTagTest: `tags.exists(...) equals true` computed a Boolean and threw it away, so the assertion never asserted anything. It is now `should equal(true)` and passes, meaning the condition held all along. Note the flag's boundary: it covers language semantics only. It does not report uses of collection types that 2.13 removes outright, which are handled separately. The flag has to be listed in both poms. obp-api declares its own <args>, and Maven replaces rather than merges that list, so an entry in the parent pom alone would not reach this module. Verification: mvn clean compile test-compile is BUILD SUCCESS, and the three warning categories above go from 110 to 0. Measuring that needed -Xmaxwarns raised for the run: scalac defaults to 100 warnings and obp-api's main sources sit above that, so the default output silently truncates and undercounts. The full local suite passes with no FAILED, RUN ABORTED or SUITE ABORTED markers.
cglib 3.3.0 bundles ASM 7.1, which reads class files only up to major version 57. Scala 2.13 with -release 25 emits major 69, so all three generated Connector proxies would fail the moment the compiler is switched. byte-buddy 1.18.11 knows class file versions up to JAVA_V27. Doing the swap before the flip keeps the two failures separable. None of the three interceptors calls the superclass implementation, so cglib's MethodProxy argument was unused and java.lang.reflect.InvocationHandler is a direct replacement via InvocationHandlerAdapter. The generation itself is now in ConnectorProxy rather than written out three times. Two details that the rewrite turns on: - All three interceptors forward with `method.invoke(target, args: _*)`, which throws on a null array. java.lang.reflect.Proxy passes null for a method that declares no parameters; cglib passes an empty array. InvocationHandlerAdapter builds the array from the method's parameter list, so no-argument methods get a zero-length array and the forwarding stays valid. - The interceptors are anonymous classes, not lambdas: Scala does not apply SAM conversion to InvocationHandler and types the lambda as a Function3 instead. That moves what `this` means, so InternalConnector's `this.callableMethods` is now qualified as `InternalConnector.this.callableMethods` - unqualified it would have resolved to the handler. ProxyConnectorTest is new. ConnectorUtils.proxyConnector is registered as the "proxy" connector and its own comment says it exists for unit tests, but neither the string "proxy" nor proxyConnector appeared anywhere under src/test and no props file selects it, so the one interceptor of the three with no coverage was also the one whose $default$ handling differs. The test pins delegation, the no-argument path, a $default$ accessor, and a call whose result carries an InBound DTO through the field-stripping branch. It was written and made to pass against cglib first, then rerun unchanged against byte-buddy. The other two proxies are covered by the existing suite: test.default.props sets connector=star, so every integration test goes through the package-object proxy, and starConnector_supported_types includes internal. cglib names survive in dynamic_code_sandbox_permissions, and that is deliberate: those entries grant reads of cglib system properties to dynamically compiled code, the proxies are built in lazy val initialisers rather than inside the sandbox, and System.setSecurityManager has been permanently disabled since JDK 24, so the list has no effect on this change either way. Verification: ProxyConnectorTest passes 4/4 under both libraries, and the full local suite passes with no FAILED, RUN ABORTED or SUITE ABORTED markers.
…acts
Wires in the two prerequisite releases. Both still resolve to their 2.12
artifacts here, so this is verifiable now rather than only at the version flip.
lift.version v1.0.4 -> v1.0.5. v1.0.5 is the first tag of that project published
for both Scala versions: JitPack builds sbt projects with `sbt clean publishM2`,
without the `+`, so every earlier tag shipped lift-persistence_2.12 and nothing
else, whatever its number. Fetching the pom and jar for both suffixes confirms
v1.0.5 has them; JitPack's maven-metadata.xml cannot be used for that check
because it lists versions that do not exist.
scala-macros moves from the aggregate com.github.OpenBankProject:scala-macros to
the com.github.OpenBankProject.scala-macros:macros_${scala.version} module at
v1.0.0-alpha.4. Only com.tesobe.CacheKeyFromArguments and com.tesobe.CacheKeyOmit
are used from that project and both are in the macros module, so the aggregate
was also pulling in a core module nothing references. More to the point, the
aggregate cannot survive the flip: the v1.0.0-alpha.3 pom hardcodes the _2.12
artifacts, and an already-published pom cannot be changed, while v1.0.0-alpha.4's
lists both _2.12 and _2.13 and would put two Scala versions of the same classes
on the classpath. Naming the module directly makes the suffix follow
${scala.version} like every other Scala dependency in this pom.
Verification: `mvn dependency:tree` resolves lift-persistence_2.12:v1.0.5 and
macros_2.12:v1.0.0-alpha.4 with no core module, and every one of the 88
Scala-suffixed artifacts in the tree is _2.12 - no second Scala version anywhere.
The full local suite passes with no FAILED, RUN ABORTED or SUITE ABORTED markers.
Both constructs still compile on 2.13 - they are deprecated there and removed only in Scala 3 - so this is debt paid down while the tree is quiet rather than a blocker for the flip. 51 procedure definitions across 28 files gain an explicit `: Unit =`. A body in braces with no `=` is the same method either way, so nothing changes but the spelling. The seven view bounds become the implicit parameter they desugar to: `[T, D <% T: TypeTag]` is `[T, D: TypeTag](implicit ev: D => T)`. This is the riskier half, because Converter's whole purpose is implicit conversion and its own comment warns about clashing with Predef.$conforms - the concern being that writing the evidence out could change which implicit is found. It does not: the tree compiles with no errors and not one of the 62 Converter use sites needed a change, because subclasses already had to supply exactly this implicit for the view bound to be satisfied. Verification: mvn clean compile test-compile is BUILD SUCCESS, and the full local suite passes with no FAILED, RUN ABORTED or SUITE ABORTED markers.
…ction scala.collection.JavaConverters is deprecated in 2.13 in favour of scala.jdk.CollectionConverters. 31 imports change, in obp-api only, and nothing else: every conversion at the 59 call sites is the extension-method form (.asScala / .asJava), and there is not one use of the old named conversions such as asScalaBuffer, so the new package covers all of them under the same names. scala.jdk is a 2.13 package, but scala-collection-compat back-ports it to 2.12 and obp-api already declares that dependency - for this very reason, per the comment on it. obp-commons has no converter imports and so needs nothing. The list of .asScala call sites matters again at the flip and is worth stating here: 2.13 makes a bare Seq mean immutable.Seq, while .asScala yields a mutable Buffer, so these are the places where passing the result to a Seq parameter, or splatting it with `: _*`, turns into a compile error. Nothing to do about that now - on 2.12 both spellings behave identically - but that is the population to look at when the flip produces Seq type errors. Verification: mvn clean compile test-compile is BUILD SUCCESS, and the full local suite passes with no FAILED, RUN ABORTED or SUITE ABORTED markers.
mapValues and filterKeys return a view on 2.13 rather than a Map, and the Left and Right projections' get is deprecated. Each site is rewritten to the strict, version-neutral form. Most are mechanical, but two are not. DynamicEntityInfo built a fieldNameToType map and then only ever read its key set. mapValues is lazy, so jsonTypeMap was never actually applied; forcing it with a .toMap would start throwing NoSuchElementException for any entity field whose declared type is not one of the DynamicEntityFieldType names - a failure that does not exist today and would first appear after the flip, far from its cause. mapValues cannot change the key set, so the filter now reads the keys off the source map and the derived map is gone. That leaves jsonTypeMap unused, so it goes too; it was a val in the case class body, not a constructor parameter, so it is absent from copy, equals and the JSON encoding. The other is the dependency map that DynamicUtil.Validation builds as a source string and compiles at class-initialisation time. It needs the .toMap for 2.13, but the props default is an empty list, which makes the generated code `Map()`, whose type parameters stay undetermined - and .toMap then cannot prove the elements are pairs. The generated snippet now names the type, `Map[String, String](`, which types on both the empty and the populated case. That last one is worth spelling out, because the failure was expensive to read. A compile error in that string surfaces as ExceptionInInitializerError once and NoClassDefFoundError forever after, so every dynamic-code endpoint 500s or hangs, the shard's embedded server goes down with it, and what the report shows is eleven failures spread over ten suites, all of them Read timed out, connection refused, or "Looking for Connection Identifier ConnectionIdentifier(lift) but failed" - not one of which points at the line. The compiler cannot see this code; only running it can. Verification: mvn clean compile test-compile is BUILD SUCCESS, DynamicUtilTest passes 6/6, and the full local suite passes with no FAILED, RUN ABORTED or SUITE ABORTED markers.
The gRPC server is a running production path with no test of any kind: nothing under src/test mentioned grpc, and no shard's test_filter names the package. That is worth fixing on its own, and it is a prerequisite for upgrading scalapb, since everything under code/obp/grpc/api is generated code checked into the repository and "it still compiles" is not evidence that regenerating it kept working. ObpGrpcServerSmokeTest starts the server, talks to it over a real socket, and covers both branches of the auth interceptor - which had no coverage either, and is what decides whether the port is open to the world. The authenticated call goes through the service binding, the generated stub, and protobuf serialisation in both directions. Writing it turned up two defects in getBanks, both on the untested path: - The handler destructured with `val BankJson400(id, ..., None) = bank`, a refutable pattern in a val definition. Any bank whose attributes are Some - including Some(List()), which is what the JSON factory actually produces - threw a MatchError that reached the client as INTERNAL. The attributes are not carried over the wire, so the fields are now read rather than matched on. - logo and website are nullable, and protobuf string fields reject null, so a bank without either would have failed serialisation right behind the MatchError. Neither is caused by the migration; both were simply invisible without a test. The package is covered by the catch-all rather than by any explicit filter, so the run was checked for it: "code.obp.grpc" appears in the catch-all extras line, confirming the suite is actually executed rather than silently skipped. Verification: the suite passes 2/2, and the full local run passes with no FAILED, RUN ABORTED or SUITE ABORTED markers.
scalapb-runtime-grpc_2.13 does not exist below 0.9.0 - 0.8.4 returns 404 for that suffix - so this is a hard prerequisite rather than a version refresh, and 0.9.0 is the smallest step that clears it. 0.9.0 narrows two members of the generated companions from GeneratedMessageCompanion[_] to GeneratedMessageCompanion[_ <: GeneratedMessage], which the 0.8.4-generated sources do not satisfy: bumping the runtime alone fails with 45 errors. All 41 declarations are narrowed here, in the five directories that hold generated code - not just code/obp/grpc, but also code/bankconnectors/grpc/connector and the vendored well-known types under com/google/protobuf. Locating them by the "Generated by the Scala Plugin" header rather than by directory is what found the last two. Patching in place rather than regenerating is deliberate. The checked-in sources are not a clean output of the generator: - The packages do not match the protos. api.proto declares `package code.obp.grpc` and chat.proto declares `code.obp.grpc.chat.g1`, while the checked-in code lives in code.obp.grpc.api and code.obp.grpc.chat.api. Regenerating adds a parallel set of packages - 41 files become 71 - instead of updating the existing ones. - They have been edited by hand: ObpServiceGrpc.scala and ApiProto.scala carry a "Temporarily disabled ... javaDescriptor filter" change, with matching edits in Client.scala and ObpGrpcServer.scala. Regenerating discards all of it. Reconciling those is real work with its own risk, and it is not what a runtime upgrade should drag in. scripts/regenerate_grpc.sh still lands, because the generated code having no recorded provenance is a problem either way. It pins protoc and scalapb, caches them under target/, and has a --check mode that regenerates into a temp directory and diffs without touching the tree. Its header states plainly that the checked-in sources are not reproducible from it today and why, so nobody reads a large --check diff as breakage. It also fetches protoc directly instead of going through scalapbc's bundled protoc-jar, which pins protoc 3.7.1 - a release with no osx-aarch_64 build, so scalapbc cannot run on Apple Silicon at all. Verification: BUILD SUCCESS with no errors, ObpGrpcServerSmokeTest passes 2/2 against the new runtime - a real socket, a real RPC, protobuf encoded and decoded - and the full local suite passes with no FAILED, RUN ABORTED or SUITE ABORTED markers.
scalameta_2.13 does not exist below 4.x - 3.7.4 returns 404 for that suffix - so this is a prerequisite, not a refresh. No source change was needed: the ten org.scalameta.logger.elem calls and the two `import scala.meta._` blocks compile unchanged. Compiling is not enough to conclude anything here, though. Both scalameta uses parse a .scala file and walk the AST at run time, so a 3.x-to-4.x change in node types would show up as a collect that quietly matches less - fewer results, or none - rather than as an error. The compiler cannot see that, and one of the two tools has no test at all: ApiRole's Util.checkWrongDefinedNames is called only from its own main. So it was run on both versions and the output compared: 40 lines under 3.7.4, the same 40 lines byte for byte under 4.1.12. The other tool, ErrorMessages.getDuplicatedMessageNumbers, is covered by code.errormessages.DuplicatedMessages, which asserts the count is zero and runs in the suite. Verification: BUILD SUCCESS with no errors, the self-check output is identical across both scalameta versions, and the full local suite passes with no FAILED, RUN ABORTED or SUITE ABORTED markers.
None of the three has a 2.13 artifact at its current version - scala-nameof 1.0.3, chill 0.9.3 and chill-bijection 0.9.1 all return 404 for that suffix - so each is a prerequisite rather than a refresh. scala-nameof 1.0.3 -> 2.0.0 needs no source change at all: NameOf.scala is byte-for-byte identical between the two releases and the package path is unchanged, which is what lets 3,326 call sites stay as they are. chill and chill-bijection -> 0.9.5 touch one place, the Kryo codec in Redis.scala that serialises memoized values. That codec is worth a note for whoever deploys this. A chill upgrade carries a Kryo upgrade, and the entries already in Redis were written by the old one, so some of them will fail to decode after the rollout. A test environment never shows this, because it starts from an empty cache. It is not an outage: the codec rethrows on a failed decode, scalacache treats a throwing read as a miss, recomputes from the source block and repopulates the key with a valid serialisation. The cost is a cold cache on the first request per key, not wrong data - RedisDeserializeMissTest exists to keep that behaviour from regressing to the old sentinel-value approach, which did serve wrong data for a whole TTL. Verification: BUILD SUCCESS with no errors, RedisDeserializeMissTest and MethodRoutingCacheInvalidationTest pass 5/5, and the full local suite passes with no FAILED, RUN ABORTED or SUITE ABORTED markers.
scalacache-redis_2.13 and scalacache-guava_2.13 do not exist below 0.28.0, and
that release is also the first one published for 2.13 at all, so there is no
gentler intermediate step to take.
0.28 types its Cache by the value type, while all four wrappers are generic in A
and the old ScalaCache instance was untyped. The cache is therefore built per
call rather than held as a field. RedisCache and GuavaCache are thin wrappers
over the pool and the Guava instance built at startup - neither opens anything of
its own - so the connection pool, its authentication and its SSL configuration
stay shared, which is the property the comment on the pool exists to protect.
Guava's underlying store is declared at Entry[Any] and narrowed per call; the
cast is erased at run time, and a key always holds the type its own call site
wrote, which is what the untyped ScalaCache already assumed.
The codec API changed shape too: deserialize threw, decode returns
Left(FailedToDecode). The self-healing contract is unchanged but its machinery
moved, and the move is easy to misread. RedisCacheBase.doGet raises the Left,
which on its own would mean a corrupt entry fails the caller for a whole TTL.
AbstractCache._caching - the path memoize takes - wraps that read in
handleNonFatal and substitutes None, so a failed decode is still a miss: the
source block runs and the key is rewritten. Reading only doGet gives the opposite
answer, so the reasoning is recorded next to the codec.
That mattered more than usual here because the chill upgrade in the previous
commit guarantees some entries written by the old Kryo will not decode after a
rollout. Cold cache, not errors - but only because of the layer above.
InMemoryCachingTest is new, and was written and made to pass against 0.9.3 before
any of this. The Guava backend had no test at all while six business call sites
went through it. Two of its scenarios pin the derived key's shape rather than
hit/miss behaviour: scalacache builds the key from the enclosing method and its
non-excluded arguments, which is why the caller's string ends up inside it and
why NewStyle's deleteKeysByPattern("*getMethodRoutings*") matches anything. A
change in key derivation would leave the cache caching and the invalidation
silently matching nothing.
RedisDeserializeMissTest is rewritten against decode/encode, asserting the same
intent: a corrupt entry must never be served as a valid hit.
Verification: BUILD SUCCESS with no errors; InMemoryCachingTest,
RedisDeserializeMissTest and MethodRoutingCacheInvalidationTest pass 11/11 -
including both key-shape scenarios and the getMethodRoutings pattern - and the
full local suite passes with no FAILED, RUN ABORTED or SUITE ABORTED markers.
2.13 removes TraversableLike, SeqLike, GenSetLike, GenTraversableOnce, GenTraversableLike, CanBuildFrom and breakOut outright. -Xsource:2.13 does not report any of it - that flag covers language semantics, not the library - so these sites would first surface as a wall of errors during the version flip, mixed in with everything else it produces. Rewritten here instead, on 2.12, where each one can be judged on its own. Functions.Implicits.RichCollection is the substantial one. It moves from TraversableLike + CanBuildFrom to the collection type itself plus scala.collection.Factory, which 2.13 has natively and scala-collection-compat back-ports to 2.12, so one source compiles on both. obp-commons now declares that dependency; obp-api already had it. The result type narrows with it. CanBuildFrom[Repr, A, That] let the result be a different kind of collection from the source, and no call site used that: distinctBy returns the List it was given, classify splits a Seq into two Seqs, ?+ returns the List it was given. Fixing the result at C[A] keeps every call compiling and drops a degree of freedom that only made the rewrite harder. Elsewhere the removed types are replaced by Iterable, deliberately rather than by IterableOnce. IterableOnce is the closer match to GenTraversableOnce, but the branches in bankconnectors call isEmpty and nonEmpty, and 2.13 puts those on IterableOps and not on IterableOnce - so IterableOnce would compile here and fail at the flip, which is the exact failure this step exists to prevent. Nothing reaching those branches is an Iterator: connector methods return List, Seq or Set, and the neighbouring branch already matched on Traversable. SwaggerJSONFactory's Coll alias feeds reflective subtype tests against declared field types, all of which are Iterables, so the tests keep selecting the same fields. Its breakOut becomes a collected sequence handed to ListMap - .to(ListMap) was tried first and does not preserve the target type through compat on 2.12. Order is unchanged: the sortBy still fixes it and ListMap preserves insertion order. FunctionsTest gains six scenarios, written and made to pass against the old implementation first. It covered ?+, ?-, distinctBy, findByType, deepFlatten and BinaryOp but not classify - which is what validateRequiredFields in code.bankconnectors uses to split validation results - nor toMapByKey, toMapByValue or notExists. Verification: BUILD SUCCESS with no errors, FunctionsTest passes 12/12 both before and after the rewrite, and the full local suite passes with no FAILED, RUN ABORTED or SUITE ABORTED markers.
First half of the version flip. It cannot stand on its own: com.tesobe:obp-commons carries no Scala suffix, so a 2.13 build of it is not consumable by a 2.12 obp-api, and the version properties both modules read live in the parent pom. The next commit moves obp-api and the pair is what has to be reviewed and reverted together. scala.version 2.12 -> 2.13, scala.compiler 2.12.21 -> 2.13.18. Alongside that: -Ypartial-unification goes, since 2.13 rejects it as an unknown option and does that work by default; the paradise compiler plugin goes, replaced by the -Ymacro-annotations flag it became. scalac.release stays at 25, against the migration plan, which wanted it held at 8 so the language version and the bytecode version moved in separate revertible commits. That is not achievable. On 2.12 the flag only widened the visible API surface; on 2.13 it is enforced, and the sources call String.isBlank (Java 11) in five places and java.io.ObjectInputFilter (Java 9) in BankAccountCreationDispatcher, where it filters deserialization. At -release 8 neither is visible and the build fails. Trading a deserialization filter for a tidier commit split is not a trade worth making, so the axes move together and the pom records why. Eight source errors, in three shapes: - Six postfix operators - `exists(ele ==)`, `filterNot(null ==)`, `} toList` - which 2.12 reported as warnings under -feature and 2.13 rejects. They are now ordinary lambdas and dotted calls. One knock-on: with `toList` no longer postfix, an infix `collect` bound the call to the block rather than to its result, so that one is dotted too. - BigInt.intValue() twice: parameterless on 2.13, so the parens applied () to an Int. Verification: obp-commons compiles clean, and the full suite result is recorded on the obp-api commit that follows, since neither half runs alone.
Second half of the flip, and the half that has to be read together with the first: obp-commons has no Scala suffix in its coordinates, so the two modules cannot be on different Scala versions even for one commit. Fifty compile errors, and the ones worth naming are the ones that were already wrong before 2.13 refused them. List no longer extends Product. Six ResourceDocs passed a bare List as the example body, and the docs machinery walks that value with productIterator - so those endpoints have been documenting a List's product elements, `head` and `tl`, as if they were API fields. APIUtil.jArrayBodyOf now wraps them in the JArrayBody that already existed for array-shaped bodies, which keeps the rendered example an array and gives the field table something real. Widening the parameter to Any was tried first and reverted: getAllFields genuinely calls productIterator, so the bound is load-bearing rather than nominal. The runtime-compiled connector methods all failed with "identifier expected but ':' found". InternalConnector builds each method's signature by string surgery on `methodSymbol.typeSignature.toString`, which renders as "(params)ReturnType" on 2.12 and "(params): ReturnType" on 2.13, so appending a colon produced two. It is render-agnostic now, and deliberately emits no space after the colon because the regexes below it match ")\s*:" immediately followed by the type name - the first fix added a space and broke the pattern matching instead. SwaggerJSONFactory's Coll alias became IterableOnce rather than Iterable. With Iterable, the reflective subtype tests walked 2.13's much deeper collection base graph and SwaggerFactoryUnitTest died with a StackOverflowError inside scala-reflect's FindMembers. Runtime matches that call head or nonEmpty use Iterable directly, since IterableOnce has neither. translateEntity no longer reflects into collections. 2.13's Nil carries a static EmptyUnzip of (Nil, Nil), so walking a List's fields arrives back at Nil for ever; JArray was already excluded for the same kind of reason. Two guards that never guarded: `case Unit` matched the Unit companion object, which reflectively invoking a Unit-returning method never produces, so that arm was dead - it matches BoxedUnit now. And `Objects.nonNull(exampleValue)` returns a Boolean and discards it, so a null example reached JsonUtils.getType and its own requireNonNull threw; it is a real check now, describing an unknown example as a plain object. The rest are mechanical: 13 breakOut calls in the checked-in generated gRPC code, map's second type parameter, Java's protected childValue which no Scala source can name, `Unit` used as a value, more postfix operators, and an implicit needing an explicit type. Two test fixtures are regenerated rather than edited. They are Java-serialized Scala collections written under 2.12 and unreadable on 2.13, which left the frozen connector contract null and failing with NPEs. Regenerating captures the same contract: every phase-1 step ran the full suite green on 2.12 with the old fixtures, including these two, and nothing since has touched a connector DTO. run_tests_parallel.sh installs obp-commons with -am so the parent pom goes in with it. Without that the local repository holds a 2.13 obp-commons beside a 2.12 parent, obp-api resolves its dependencies through the stale parent, and _2.12 artifacts land on the classpath next to 2.13 classes. Nothing detects it - the build succeeds and the suite dies at run time with ClassNotFoundException: scala.Serializable. Verification: the full local suite passes with no FAILED, RUN ABORTED or SUITE ABORTED markers - 581 unit/pure and 2813 integration, with every per-version count matching the 2.12 baseline.
regenerate_grpc.sh fetches protoc and scalapbc with curl -L and then runs them. -L follows a redirect to http:// just as readily as to https://, so a downgraded redirect could substitute what the script executes. --proto '=https' restricts the transfer and its redirects to https, and --tlsv1.2 sets a floor. Raised by SonarCloud on this PR (shell:S6506, two mediums), and worth fixing rather than waiving given the fetched files are executables.
… first Every one was written as a failing test before it was fixed, and two of those tests changed what the fix had to be. ConnectorProxy intercepted with ElementMatchers.any(), which covers Object's methods as well as Connector's. For two of the three handlers that was harmless, because they forward with method.invoke(delegate, ...). InternalConnector's reads any unrecognised name as a dynamic connector method to look up and compile, so toString, hashCode and equals on that proxy all threw IllegalStateException - logging the connector, interpolating it into a string or using it as a map key blew up. Object's methods are excluded now, which also gives the proxies ordinary identity semantics. ConnectorProxyObjectMethodsTest covers all of it, including that ConnectorUtils.proxyConnector was never affected. ObpGrpcServerSmokeTest bound the single configured gRPC port while every other server-starting suite here takes its port from the shard, because shards are parallel JVMs. The first attempt to reproduce this was wrong: occupying the port from another process did not stop the server, since the occupying socket had SO_REUSEADDR and macOS allowed the second bind. Running two copies of the suite concurrently did reproduce it - one aborts with BindException. The port is a constructor parameter now, defaulted to the configured one, and the test asks the OS for a free one. Two concurrent runs both pass. jArrayBodyOf is removed, and this is the one where the test showed the original diagnosis was wrong. The claim was that a bare List documented `head` and `tl` as API fields because 2.12's List was a Product. getAllFields has always had a branch for a root-level list, and although List is not a Product in 2.13, a non-empty one is a `::` - a case class, so a Product at run time. Lists were being handled correctly; those two names came from a nested list field. The replacement made things worse: the array body described eight json4s internals - MODULE$, EmptyUnzip, arr, obj - instead of the entity's four fields. The six ResourceDocs pass their lists again, and getAllFields takes Any and unwraps a root-level collection through .head rather than productIterator.toSet.head, which picked from a two-element set by hash and could return the tail. One knock-on: a complexNestedClass assertion expected a field declaration with a trailing ", ", which asserts that field is not last in the rendered list. The reordering broke it. It joins the assertions rewritten earlier in this branch to name the fields rather than pin the rendering. Verification: the full local suite passes with no FAILED, RUN ABORTED or SUITE ABORTED markers - 585 unit/pure and 2818 integration, the increase being the new tests.
…futed ConnectorProxy's earlier fix excluded Object's methods, which was the shape of the problem rather than its extent. Connector extends Helper.MdcLoggable, and that contributes public abstract interface methods - logger, clazzName, a default initiate - declared by MdcLoggable and so untouched by an Object exclusion. They still reached InternalConnector's handler, which reads an unrecognised name as a dynamic connector method to look up and compile, and threw. Narrowing the matcher to Connector's own methods, which was the obvious next step, does not work: those members are abstract, so something must implement them or the generated class cannot be instantiated. The fix belongs in the handler, which now sends anything outside the Connector API to the empty Connector object that already backs the $default$ accessors. ConnectorProxyObjectMethodsTest gains a shape check over every zero-argument method the interface inherits, so the next trait mixed into Connector is covered without anyone having to remember. The gRPC smoke test asked the OS for a free port, closed it, then bound it. That window is the flaw of the idiom rather than a fix for it. It passes 0 to grpc-java and reads ObpGrpcServer.boundPort back instead; start() logs the bound port too, which was previously the requested one. getAllFields folds every element of a root-level collection rather than describing only the head, so a heterogeneous list documents all of it. JSONFactory1_4_0_LightTest gets its list scenario back. It was removed on the theory that a List documented head and tl, which the previous round disproved - getAllFields has always had a collection branch, and a non-empty List is a `::`, a Product at run time. The comment left in its place asserted that refuted diagnosis and pointed at jArrayBodyOf, deleted two commits ago. code/bankconnectors/grpc/connector - four files, 360 lines - is deleted. It arrived with the scalapb upgrade as a second copy of the connector service under connector.proto's package name, and nothing referenced it: GrpcUtils imports the same types from grpc.api. It compiled into the jar carrying its own bindService. One reported defect is not fixed, because writing the test showed the report was wrong. getAllFields answering Nil for a value it cannot describe was called a silent failure, and rejecting one looked like the fix; it took out five suites. The method recurses, so it legitimately reaches scalars - the elements of a List[String] field, a null - which have no fields to report. Nil is the correct answer there. What is true is that widening the parameter to Any gave up a compile-time check across every ResourceDoc, and that belongs at the call site rather than inside a field walker; the comment now says so instead of claiming the bound never bought anything. Verification: the full local suite passes with no FAILED, RUN ABORTED or SUITE ABORTED markers - 586 unit/pure and 2820 integration - and two concurrent runs of the gRPC suite both pass with no bind error.
boundPort read server.getPort and fell back to the constructor argument once stop() nulled the field, which is 0 for a server given an ephemeral port - so after shutdown it reported a port the server had never listened on. The port is now recorded at start() and held in a volatile field, which also publishes it to the threads that read it. The smoke test asserts it across stop(); it failed with "0 did not equal 53257" before the change. regenerate_grpc.sh still fed connector.proto to protoc. That proto declares package code.bankconnectors.grpc and scalapb appends the file name, so generating from it writes code.bankconnectors.grpc.connector - the 360-line duplicate of the connector service that came in with the scalapb upgrade and was removed again, while GrpcUtils takes those types from the hand-written .api package. Excluded, with the reconciliation it needs written down. InternalConnector decided what dynamic code may implement by asking methodNameToSignature, a map built by filtering out vals and vars - so besides the inherited members it also excludes Connector's own vals, messageDocs among them, and routes them to the empty stub. The criterion now has a name and states that; a scenario pins that messageDocs answers, and answers stably. getAllFields folded the root-collection branch with acc ++ getAllFields(e), rebuilding the accumulator once per element. It flatMaps over the iterator now, which is linear and keeps first-seen order. The proxy shape check invoked every zero-arg inherited method, MdcLoggable's initiate() among them - a lifecycle hook Boot overrides with real work. Connector leaves it a no-op, so the invoke is safe today; that is now asserted before the loop rather than assumed.
StarConnector threw NullPointerException on every member Connector inherits rather than declares. Its handler recognised the $default$ accessors and sent everything else into MethodRouting resolution, where it zips parameter names with args - and InvocationHandlerAdapter passes null, not an empty array, for a method that declares no parameters. So logger, clazzName and initiate all threw, the same hole InternalConnector had for the same reason. The rule now lives once, as ConnectorProxy.isInheritedMember, and both handlers ask it; it was being rediscovered one proxy at a time. The scaladoc that claimed InvocationHandlerAdapter hands over a zero-length array is corrected: forwarding survives the null because Method.invoke reads a null array as no arguments, which is why the proxy connector was never affected. ObpGrpcServer.stop() shut down ChatEventBus, LogCacheEventBus and MetricsEventBus whether or not this instance had started them. Each is an object holding one subscriber connection for the process and start() is a no-op once one is running, so a second server's stop() closed the pub/sub the first was still serving from. start() now records which buses it actually started and stop() takes down only those. stop() also removes the shutdown hook start() registered, which was accumulating one per instance and calling stop() again at exit. Redis and InMemory built a cache wrapper on every memoize call. Both wrappers are stateless and their value type is erased, so one instance now serves every A - these are the per-request caching paths, and the allocations sat in front of every read. Also: the commented-out Lift imports in APIMethods400 and APIMethods600 are restored to what Lift imported, a global rename having swept them up; the scaladoc for classify, ?+ and ?- no longer documents a type parameter they lost; and the ArrayBuffer alternative in validateRequiredFields, which Iterable already covers, is gone.
…' own work
Two comments claimed things the code does not do. APIUtil said a wrong-typed
ResourceDoc body still surfaces because "getAllFields now throws for a body it
cannot describe" - that throw was tried, fired on legitimate scalar input and was
reverted, so the widening from scala.Product to Any has no replacement check and
the comment now says so. Redis carried two adjacent blocks, one describing the
cache as built per call and the next as built once; they are one block again.
The gRPC server's bus-ownership flags are volatile, like actualPort above them
and for the same reason: stop() also runs on the shutdown-hook thread, which
never synchronised with whoever ran start(). Ownership is also read after each
start() rather than before, since a disabled bus makes start() a no-op and "was
not running beforehand" alone claimed one this instance never started.
RedisDeserializeMissTest asserted `outcome.right.toOption should not be
Some("NONE")` directly after asserting the outcome is a Left, so the projection
was always None and the assertion could not fail - in the one suite whose purpose
is to keep a sentinel value from being cached as a hit. It asserts on the Either
now. The StarConnector scenario gained the initiate() guard its sibling already
had, and a try/finally whose body and finally were the same call is a plain call.
Both childValue overrides now say why they are public: TransmittableThreadLocal
declares the method public and 2.13, unlike 2.12, rejects narrowing it.
…unds Two files still described work that had been reverted or refuted. ProxyConnectorTest's scaladoc kept the account ConnectorProxy now contradicts - that forwarding throws when the proxy library passes a null argument array. It does not: `args: _*` compiles to Java varargs and Method.invoke reads null as no arguments, which is why StarConnector broke on zipping args and this proxy never did. SwaggerPathOrderAndArrayBodyTest credited jArrayBodyOf, a helper that was tried, refuted by its own test and removed; the root-collection branch in getAllFields is what actually replaced the leak. ObpGrpcServer.start() is guarded against a second call on the same instance. It would have recomputed the bus-ownership flags against buses it had already started, reading them as somebody else's, and overwritten both `server` and `shutdownHook` - leaking the first server with its port bound and orphaning its hook with no reference left to remove it. RequiredInfo.flatten matched ArrayBuffer immediately before Iterable with the same body; ArrayBuffer is an Iterable, so only the second arm could ever run. The comment beside it called this production method a test. The first scenario in JSONFactory1_4_0_LightTest asserted the whole rendering of a reflected field list, $outer included, while the three below it had been rewritten away from exactly that - their comments naming $outer as the example of what not to pin. It asserts on field names now, like its siblings.
…efects The two frozen fixtures are Java-serialized blobs that exist to fail when a frozen type drifts - which only works if a human can see what changed, and a binary diff shows nothing. This branch had to regenerate both, because collections written under 2.12 do not deserialize under 2.13, and it went in as two unreadable blobs whose contents nobody could compare. The stated justification, that no connector DTO changed, does not cover what they actually store: reflection renderings, which this branch already showed differ between 2.12 and 2.13. Extracting the serialized strings from both revisions settles it. frozen_type_meta_data lost nothing and gained three entries - APIUtil.JArrayBody, org.json4s.JArray and PostAccountTagJSON. RestConnector's differences are the json4s alias rendering alone, org.json4s.JsonAST.JValue becoming org.json4s.JValue, which a normalizeTypeName predating this branch already absorbs. Benign, but it should not have taken a hex dump. Each blob now has a checked-in text sibling, and FrozenMetaDataTextTest fails when the two disagree - so the next regeneration lands in the diff as readable text. release_notes.md records the migration. The artifact's class files are Java 25 now, where 2.12 emitted Java 8 whatever -release said, so anything loading them on an older JVM fails at class load rather than at first call; the byte-buddy and scalacache swaps are noted with it. run_tests_parallel.sh armed its lock's EXIT trap at startup rather than where the lock is taken, so an interrupted run deleted the lock a different run was holding and let a third in beside it - with -am the protected section covers two artifacts now, which lengthens the window. ObpGrpcServer.start() rolls back on failure. Its guard keys off `server`, set last, so a bind failure left the buses started and the guard open; the retry then recorded them as somebody else's and stop() would leave them running. Also: the sandbox permission lists no longer grant three cglib properties, cglib having been removed from the build; the -Xsource:2.13 comment no longer describes the sources as 2.12; and a blank line left by a removed import is gone.
…wn work The frozen-fixture text mechanism was built wrong in three ways. Its documented regeneration flow did not regenerate: the test only wrote a .txt when one was absent, so after a real regeneration it failed on a 200KB diff with no way forward. It wrote into the source tree at all, which is a test repairing the thing it exists to check. And it rendered RestConnector's connector method names as endpoints of a version called "methods" - in the one artifact a reviewer reads to judge a regeneration. Rendering and writing move to FrozenMetaDataText, which reads the blobs and needs no server; the test only compares. -Xsource:2.13 is removed from both poms. scalac describes the flag as enabling "warnings and features for a future version", and 2.13 is not a future version of a 2.13 compiler - it did nothing after the flip, and last round's comment justified keeping it with a behaviour the flag does not have. The lock in run_tests_parallel.sh records its holder's PID and the waiter reclaims a lock whose PID is gone, with a ten-minute ceiling. Moving the trap to where the lock is taken stopped a run from deleting somebody else's lock, but left the reverse: a run killed before the trap was armed orphaned the directory, and the wait loop had neither timeout nor staleness check, so every later run on that machine hung silently. ObpGrpcServer's start rollback keeps the failure that caused it - a throwing stop() is suppressed onto it rather than replacing it - and catches NonFatal rather than Throwable. The release note names the flip commit rather than a branch that will not exist after merge.
The pid the lock records is written one statement after the mkdir that takes it, so a run killed in between left a directory with no pid inside - and the waiter only reclaimed a lock whose recorded pid was dead. Nothing removed that one, so every later run waited out the full ten minutes and exited. A lock with no recorded holder is now reclaimed too, after a grace period, since a live holder is only momentarily in that state. The stale branch also ran rm -rf and continued without sleeping or advancing the timer, so a removal that could not take - a lock left in /tmp by another user - became a busy loop that the new timeout could never end, because only the sleep path advanced the counter. Every path through the loop now sleeps and advances it, and a removal that does not take fails with the reason rather than spinning. The EXIT trap is armed once at the top again, but checks the recorded pid before removing anything. Armed unconditionally it deleted locks other runs held; armed after the mkdir it missed signals in between; and disarming it after release left an instant where an exit deleted a lock somebody else had just taken. Checking ownership covers all three, and needs no disarm - once the directory is gone the check cannot match. Verified against the three cases: a dead pid is reclaimed in one poll, a lock with no pid after the grace period, and a lock held by a live process is left alone.
…dead scalatest-maven-plugin runs with forkMode=once, so a shard is two JVMs: mvn, and the test JVM it forks. Pekko's non-daemon threads keep that fork alive after the tests finish, mvn exits anyway, and the fork is reparented with nothing owning it. The timeout wrapper never sees this - it fires only when mvn itself overruns, and on the ordinary path mvn returns 0. They accumulate. Five were found alive six to ten hours after their runs, one of them holding port 8080, where it answered a verification probe with a build eight commits old and reported no error at all. A reaper now runs once, after every shard has been waited on, matching on the plugin's own -Drun.mode=test together with this checkout's path - a dev server started by hand carries no run.mode=test and another checkout has another basedir. It is called from neither branch's shard loop: the shards run in parallel and share the matcher, so reaping from inside one would kill the JVMs the others are still using. It did not fire on the run that followed, which left one straggler that then exited by itself, so the orphan condition is not reproduced here - the reaper is a safety net whose match scope is verified rather than a fix demonstrated to trigger. The lock's staleness test is ps -p rather than kill -0. kill -0 fails with EPERM for a live process owned by another user as well as with ESRCH for one that is gone - `kill -0 1` fails for root's launchd while `ps -p 1` succeeds - so on a shared machine a running build of another user was reported as a dead holder.
The orphan reaper selected what to kill with `pgrep -f "Drun.mode=test"` and `grep -q -- "$CHECKOUT_ROOT"`, both of which read their argument as a regular expression - and a checkout path contains a literal dot, in .claude, which a regex takes as any character. The selection ends in kill -9, so it should say what it means. Verified against a real forked test JVM that the fixed-string form still matches it.
…p lacks The comment added with the previous fix said "-F on both". Only grep took -F: pgrep has no fixed-string option, so its pattern stayed an extended regex whose dots match any character - the thing that change was made to remove, still in place two lines under a comment saying otherwise. `pgrep -f "sleep 3.0"` matching a running `sleep 300` is the demonstration. The dots are escaped now, and the comment says what each half actually does.
The reaper ran straight after the shards were waited on, which is before the authoritative verdict reads the surefire XMLs and before the speed report reads them again. This script already counts a report truncated by a JVM killed mid-write as a broken suite - so a fork still flushing when the reaper reached it would have failed a run whose tests all passed, manufacturing the exact failure mode that audit exists to catch. Moved below both reads, where nothing left alive can change the verdict.
…swallowing .gitignore excludes obp-api/src/test/resources/** and re-includes fixtures one by one; the blob predates that rule and is grandfathered in, but the text rendering added beside it got no negation line and was never committed. git status stayed clean throughout, so nothing said so. The consequence is a test that passes only for whoever generated the file: FrozenMetaDataTextTest asserts the text exists before comparing it, and on a clean checkout - CI included - it does not. Confirmed by moving the file aside and running the suite: "frozen_type_meta_data.txt is missing; run code.util.FrozenMetaDataText to write it". The negation follows the convention already in that file, whose neighbouring comment records the same lesson about the dev certificate set: without an explicit line, a regenerated fixture is silently untracked and the tests that need it fail everywhere but the machine that made it.
…of it type Coll[T] backs every collection test in SwaggerJSONFactory's type dispatch. It was GenTraversableLike on 2.12 and is IterableOnce here - and 2.13's Option implements IterableOnce where 2.12's did not, which javap on both libraries confirms. So Coll[X] answers true for Option[X], and wherever a Coll case is reached before the matching Option case, a scalar field is published as an array of itself. That reaches clients: the swagger definitions are what they generate code from. Two cases reach it first. The String block tests Coll[String] before Option[String] - the one scalar block written in that order - so every optional string became an array of strings. And the generic List-or-Array fallback at the end catches every Option the named cases did not: an Option of a case class, of a JValue, published as an array of it. The alias itself cannot go back to Iterable: that is what made scala-reflect's FindMembers recurse until it overflowed, which is why IterableOnce was chosen. So the fix is per case. String's Option case moves above its Coll case, matching every other scalar block. The generic fallback excludes Option, which drops those types onto the Option case already sitting below it - it unwraps and recurses, which is what should have happened. Deliberately not applied to the other Coll cases: they are safe by ordering, and `isOneOfType[Coll[Date], Option[Coll[Date]]]` is a mixed test that a blanket !isTypeOf[Option[_]] would break, taking Option[List[Date]] with it. SwaggerOptionFieldTypeTest covers both directions - two scenarios that were red before the change, and four that must stay green through it, Option[List[Date]] among them.
…case The previous commit moved the whole Option[String] case above Coll[String], but these cases carry two independent clauses: a type test, and an isNestEnumeration test. The enumeration clauses have an order among themselves that matters - isNestEnumeration digs to the innermost type argument, so Option[List[Colour]] satisfies isNestEnumeration[Option[_]] exactly as well as isNestEnumeration[Option[List[_]]], and whichever is tested first wins. Only the second is right for it. Hoisting the Option[_] enumeration clause to the top of the block therefore claimed every optional list of enumerations and published it as a string instead of an array of strings. The full suite passed with that in place: nothing covered Option[List[SomeEnumeration]], including the guards added with the fix, which use String, Date and a case class. Only the type test moves now. Four enumeration shapes are pinned - a bare enumeration, a list of them, an optional list of them, an optional one - so the next change to this block's order fails here rather than in a published document.
… nothing
translateEntity returns early for a JArray and builds the schema from its first
element. A bare Scala collection had no such branch: it fell through to the field
map, was answered with Map.empty, and the endpoint published "properties": {}.
Three endpoints return that shape - getSystemLevelEndpointTags,
getBankLevelEndpointTags, createUserWithAccountAccessById - across five API
versions each, so fifteen published response bodies described nothing.
2.12 reflected over the cons cell and leaked head and tl. That was wrong, but the
element's real schema came out under head, so the migration made these strictly
less informative than they had been.
The Map.empty is there to stop reflection following Nil's static EmptyUnzip,
which is (Nil, Nil) and leads back to Nil for ever - the comment beside it then
claimed "the value cases below already take the first one", which is true of a
JArray and not of a List. A collection is now answered the way a JArray already
was: an array whose items are the translated head, or an empty array when there
is no head. Neither path reflects over the collection, so the recursion stays
impossible. Map is excluded deliberately - an Iterable, but not a JSON array.
Found by diffing the published surface against a pre-migration baseline rather
than by reading the code, which had been read past this comment several times.
The swagger fixes change how 62 typed response bodies are described - the three bare-List endpoints across five versions, plus 47 nested collection fields that 2.12 rendered as empty objects. The responses themselves are unchanged, so the release note's "shapes are the same" claim stays true, but a reader diffing the published documents would see 62 differences with nothing explaining them. Verified against the 2.12 baseline: 0 breaking, 62 corrections, 0 regressions.
createOpenAPI31Json serves GET /obp/vX/resource-docs/API_VERSION/openapi and
reads typed_success_response_body structurally, walking it for type, properties
and items. No test file in the repository referenced OpenAPI31 at all, and this
branch changed that field for 62 (version, endpoint) pairs - handing it a
root-level array where it used to get an object, which is the one shape an
object-oriented conversion drops.
It handles them correctly. Checked against the live endpoint first: 2.9MB, 421
paths, the three corrected endpoints emit {"type":"array","items":{...}} with the
element's four real fields, and no array schema anywhere lacks items. The 67
endpoints with no 200 schema are DELETEs whose success body is null or a bare
true, unchanged by this branch.
That was a hand check. These five pin it: an object body, a root array, a nested
array field, the declared 3.1.0 version, and an absent typed body.
The path key is looked up rather than spelled - createOpenAPI31Json runs the url
through convertPathToOpenAPI, and asserting the result would test that transform
instead of the schema conversion.
The root-collection branch described its element with translateEntity, which
only knows how to reflect over an object's constructor arguments. Hand it a
scalar and it answers {"properties":{},"type":"object"} - the element's real
type is gone.
Twelve published request bodies are lists of enumeration values:
createAuthenticationTypeValidation and updateAuthenticationTypeValidation across
five API versions. 2.12 reflected over the cons cell, so `head` was a *field*,
and a field holding an EnumValue goes through the case that emits
{"type":"string","enum":[DirectLogin, GatewayLogin, ...]}. The members survived
by accident. Describing the list as an array of its head dropped them, which is
strictly less than 2.12 published.
Reproduced across four element kinds - enum, string, integer, boolean all came
back as an empty object - so this is a scalar vocabulary, not an enum special
case. elementSchema mirrors the cases the per-field loop already has and sits in
front of the translateEntity fallback, which still handles object elements.
Found by diffing typed_request_body against the pre-migration baseline. The
contract suite records that field but only ever diffs the response side, so
every schema change on this branch altered both surfaces while one went
unchecked - which is how the regression passed a clean contract run.
build: migrate to Scala 2.13
Brings in the nine commits upstream added after this branch was cut - chat message constraints and email digest, the password-policy endpoint, signal channel sanitizing, and the Sonar annotations - so the branch is tested as it will merge rather than as it stands alone. They were written on 2.12. Compiling and testing them under 2.13 is the point of merging here rather than leaving it to the merge button: a long-lived branch being green on its own says nothing about the merge, which is what the pull_request build actually compiles.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Moves obp-api and obp-commons from Scala 2.12.21 to 2.13.18.
What changes for anyone deploying this
The artifacts now require JDK 25 at runtime. On 2.12 the compiler emitted Java 8 class files whatever
-releasesaid, so the setting only widened the visible API surface. 2.13 honours it, and this build targets 25. Anything loading these jars on an older JVM fails at class load withUnsupportedClassVersionErrorrather than at first call. The Dockerfiles and CI are already oneclipse-temurin:25; a deployment running the jars on its own JVM has to be too.Two dependency swaps are visible to anyone assembling their own artifact:
Connectorproxies. cglib 3.3.0 bundles ASM 7.1, which reads class files only up to major 57; 2.13 with-release 25emits major 69, so every proxy would fail to generate.Also: scalapb 0.8.4 → 0.9.0 (first release with a
_2.13artifact), scalameta 3.7.4 → 4.1.12,-Ypartial-unificationremoved (on by default in 2.13), and the paradise compiler plugin replaced by-Ymacro-annotations.External artifacts.
lift-persistenceandscala-macrosare consumed as_2.13builds from JitPack; both resolve publicly today (verified).scala-macrosis now pinned to themacrosmodule rather than the aggregate — the aggregate's pom hardcodes_2.12, which would put two Scala versions of the same classes on the classpath.No API change
Endpoint behaviour, request and response shapes, and error codes are unchanged. Per-version test counts match the 2.12 baseline.
The documentation of response shapes is corrected in 74 typed bodies (resource-docs/swagger). 2.12 described a bare-
Listbody by reflecting over the cons cell — publishing itshead/tlinternals — described nested collection fields as empty objects, and lost enumeration members. All three now read{"type":"array", ...}with the element's real schema. The responses themselves did not move; only their published description did, from wrong to right.Verification
develop(9 commits: chat, password-policy, signal) into this branch compiles under 2.13 with 0 errorsNotes for review
The migration surfaced defects that predate it and are fixed here — a refutable pattern in a
valdefinition that threwMatchErroron any non-Nonebank attributes, protobufnullstring fields, a discarded assertion, and a no-opObjects.nonNullguard. Each is a separate commit with its reasoning.Three of the schema fixes are worth a careful look, because the failure mode is subtle:
type Coll[T]moved fromGenTraversableLiketoIterableOnce, and 2.13'sOptionimplementsIterableOncewhere 2.12's did not — so everyColl[X]type test also answered true forOption[X], and every optional field whoseCollcase was reached first was published as an array of itself.