Skip to content

Migrate all modules to Kora 2.0 - #49

Merged
GoodforGod merged 69 commits into
kora-projects:migration/2.0from
dsudomoin:migration/2.0
Aug 13, 2026
Merged

GoodforGod merged 69 commits into
kora-projects:migration/2.0from
dsudomoin:migration/2.0

Conversation

@dsudomoin

Copy link
Copy Markdown

Migrates every Gradle module in the repository from Kora 1.x to Kora 2.0 — Java, Kotlin and GraalVM.

Built against the framework from master published locally as io.koraframework:*:2.0.0-SNAPSHOT, with the 27 fixes listed below applied.

Result

Group Modules Tests passed
examples/java/* 29 164
examples/graalvm/* 3 15
guides/java/* 33 112
examples/kotlin/* 29 162
guides/kotlin/* 33 118
Total 127 571

All 127 modules are MIGRATED, 0 tests failing. Numbers come from an actual run — ./gradlew <all modules>:test --max-workers=1 --continue — not from an estimate. --max-workers=1 is required: KSP processors race under parallel execution (JacksonIOException: Stream closed).

The Gradle JVM itself must be JDK 25, not just the toolchain: io.koraframework:openapi-generator lands on the buildscript classpath, which resolves against Gradle's own JVM.

GraalVM

All three native images build and were verified running against real dependencies:

Module Size Verified against
kora-java-graalvm-crud-jdbc 51 MiB Postgres
kora-java-graalvm-crud-cassandra 76 MiB Scylla + Redis
kora-java-graalvm-kafka 80 MiB Kafka

Each is built both ways — nativeCompile and native-image inside ghcr.io/graalvm/native-image-community:25 — and BlackBoxTests exercise the image built from the Dockerfile. kora-java-graalvm-crud-cassandra was rewritten from reactive Mono to synchronous code.

Framework defects found and fixed

27 defects, each as a separate branch off master with a regression test where one is possible: #791–#816, #818. The ones worth knowing about did not show up as compilation errors:

  • no span is ever exported — OpentelemetryContext.with loses the Kora wrapper (#809)
  • the metrics endpoint always answers "Metric Scraper disabled" (#810)
  • every log event outside a request scope is silently dropped (#816)
  • a gRPC-only application exits immediately after startup with code 0 (#807)
  • reachability metadata in three modules is named reflection-config.json, which native-image does not read at all (#814)

Modules on removed functionality

The six R2DBC/Vert.x modules are not deleted — they are commented out of settings.gradle with the reason and marked BLOCKED_BY_REMOVED_FUNCTIONALITY. Whether to rewrite them on synchronous JDBC, keep them as legacy or delete them is a repository decision, not a migration one.

Documentation

Everything is under migration/:

  • KORA_2_JAVA_MIGRATION_GUIDE.md, KORA_2_KOTLIN_MIGRATION_GUIDE.md — self-contained per-language guides; shared rules are duplicated in both on purpose
  • KORA_2_MIGRATION_STATUS.md — per-module status from real runs
  • KORA_2_FRAMEWORK_ISSUES.md — 29 entries, including three hypotheses that were disproved and left in with the disproof
  • KORA_2_PULL_REQUESTS.md — all 27 upstream PRs
  • KORA_2_FINAL_REPORT.md — summary, including what was not validated
  • openrewrite/, scripts/ — automation, with its limits documented

Naming note: kora-java-s3-client-minio and kora-kotlin-s3-client-minio now demonstrate the declarative s3-client-kora against Minio as S3-compatible storage, since 2.0 has no Minio SDK implementation. The directories were deliberately left unrenamed — that touches four modules plus README links and is your call.

… neuro docs

- KORA_2_MIGRATION_GUIDE.md разложен на два самодостаточных языковых руководства
- KORA_2_MIGRATION_STATUS.md: инвентарь всех 133 модулей по данным реального прогона сборки
- KORA_2_FRAMEWORK_ISSUES.md: журнал дефектов фреймворка и опровергнутых гипотез
- KORA_MIGRATION_NEURO.md: инструкции для агентов на недетерминированные трансформации
- ссылки на несуществующий agents-md/kora-2 заменены на ../kora и agents-md/kora-docs
- зафиксированы ограничения migrate_kora_2.py
- @HttpClient(configPath = "x") -> @HttpClient("x") во всех 22 файлах (атрибут configPath удалён в 2.0)
- kora-java-http-server: маппер запроса помечен @component (генерируемый модуль инжектит его)
- kora-java-http-server: убран catch (IOException) вокруг JsonWriter.toByteArray — метод больше не бросает checked
- kora-java-http-server: удалён ReactorController с тестом и зависимость reactor-core;
  реактивные возвраты в 2.0 не поддерживаются, синхронный аналог дублировал бы SyncController
…rceptor tag

- httpServer.publicApiHttpPort -> httpServer.port, privateApiHttpPort -> httpServer.system.port
  (+ readiness/liveness/metrics paths) в 53 конфигах; SystemHttpServerConfig наследует port()=8080,
  поэтому старые ключи молча приводили к конфликту портов и падению приложения на старте
- @tag(HttpServerModule) -> @tag(HttpServer) в 14 файлах: по старому тегу глобальные интерцепторы
  не собираются, компиляция при этом зелёная, интерцептор молча не вызывается
- добавлен migration/scripts/migrate_http_server_config.py (dry-run по умолчанию, идемпотентный)
- kora-java-http-server: все 16 тестов проходят
…tracts

- ru.tinkoff.grpc.* -> io.koraframework.grpc.* в 6 файлах (пакет gRPC-клиента не начинался
  с ru.tinkoff.kora, поэтому массовая автозамена его не затронула)
- kora-java-http-client: HttpClientInterceptor.processRequest(InterceptChain, HttpClientRequest)
  вместо (Context, InterceptChain, ...) с CompletionStage; Context удалён в 2.0
- kora-java-http-client: HttpClientRequestMapper.apply(T) без Context
- kora-java-http-client: @component на интерцепторы и маппер запроса (генерируемый клиент их инжектит);
  мапперы из @ResponseCodeMapper компонентами быть НЕ должны — иначе неоднозначность в графе
- kora-java-http-client: удалён ReactorHttpClient с тестом, метод VoidHttpClient.reactor()
  и зависимость reactor-core; реактивные возвраты в 2.0 не поддерживаются
- FormHttpClientTests: templateParam -> pathParam, content-type части multipart без пробела
- migration: запись о подтверждённом и исправленном дефекте фреймворка

kora-java-http-client: 9/9 тестов проходят
…d JDBC/Cassandra repositories

- JsonCommonModule -> JsonModule; toStringUnchecked/readUnchecked/toByteArrayUnchecked -> toString/read/toByteArray
- JSpecify type-use: @nullable Outer.Inner -> Outer.@nullable Inner (32 файла)
- добавлен migration/scripts/migrate_api_renames.py (dry-run, идемпотентный)
- database-jdbc: удалён @tag(JdbcDatabase) Executor (БД в 2.0 синхронна, тег отсутствует),
  удалены async/reactor репозитории с тестами и зависимость reactor-core
- database-cassandra: удалены reactor-репозиторий и reactive ResultSet-маппер,
  мапперы из @mapping помечены @component, сущности требуют @EntityCassandra
- migration: зафиксирован подтверждённый дефект генератора Cassandra для CompletableFuture<T>
- удалён AutoCommitRecordsTelemetryListener с тестом: KafkaConsumerTelemetry в 2.0 переработан,
  per-record телеметрический контекст в параметрах листенера отсутствует
- убраны catch (IOException) вокруг JsonReader.read / JsonWriter.toByteArray в 3 файлах:
  методы больше не бросают checked-исключений
- migration: добавлен KORA_2_PULL_REQUESTS.md с готовыми описаниями двух PR во фреймворк

kora-java-kafka компилируется (main + test)
JdbcDatabaseModule в 2.0 создаёт JdbcDatabaseFactoryModule("jdbc"), поэтому секция db
оставляла все значения незаполненными: компиляция зелёная, а приложение и тесты падали
с ConfigValueException 'ROOT.jdbc.username'.

- 12 конфигов мигрированы
- migrate_http_server_config.py -> migrate_config_keys.py: скрипт теперь покрывает и секции,
  переименование делается только для верхнеуровневого db-блока в файлах с jdbcUrl
- правило добавлено в оба языковых гайда
PetV3Delegate возвращал Mono<...>: генератор java-server в 2.0 создаёт синхронный интерфейс,
поэтому делегат не реализовывал ни одного метода супертипа.

Осталось в модуле: HttpServerPrincipalExtractor стал двухпараметрическим <V, P>,
а генерируемый ApiSecurity.* не находится — требует отдельного разбора генерации OpenAPI-сервера.
- Application: добавлены HttpClientTokenProvider под тегами ApiSecurity.bearerAuth/apiKeyAuth/oAuth
  (сгенерированный ApiSecurity требует провайдер на каждую схему; basicAuth он собирает сам из конфига)
- тесты: убраны .block() — сгенерированный клиент в 2.0 синхронный
- openapi-generator-http-server: делегат и security-теги приведены к 2.0, тесты проходят

Потребовало фикса генератора во фреймворке: ветка fix/openapi-client-api-interface-public
… S3 split

- httpClient.<prefix>.PetApi -> httpClient.<prefix>.petApi: генератор 2.0 добавляет имя клиента
  в lowerCamelCase (см. тест clientConfigPrefixAppendsLowerCamelClientName во фреймворке)
- openapiAuth.apiKeyAuth заменён на httpClient.<prefix>.apiKeyAuth + basicAuth.username/password:
  сгенерированный ApiSecurity читает их именно под путём клиента
- убран ручной провайдер apiKeyAuth: генератор отдаёт его сам из конфига (@DefaultComponent)
- гайды: исправлено неверное утверждение про s3-client-aws. В 2.0 это ДВА разных артефакта:
  io.koraframework:s3-client-aws (только AWS SDK-обёртка, без @s3 и моделей) и
  io.koraframework.experimental:s3-client-kora (декларативный клиент, пакет ...s3.client.kora.annotation)
s3-client-aws in 2.0 is a plain AWS SDK wrapper: the module exposes
software.amazon.awssdk.services.s3.S3Client as a component, and the declarative
@S3.Client contracts are not part of it anymore. The example now uses the SDK
directly and reads the bucket name through @ConfigSource.

The declarative client moved to io.koraframework.experimental:s3-client-kora with
a new API (io.koraframework.s3.client.kora.annotation.S3, @S3.Head instead of
metadata-returning @S3.Get, GetObjectResult/HeadObjectResult/ListBucketResult
models). The Minio example now demonstrates that client against a Minio container;
the Minio SDK backed implementation no longer exists in 2.0.

Batch @S3.Delete over List<String> and the async/reactive declarative clients are
gone in 2.0, so those methods and modules were dropped.
Camunda 8.8 renamed the client, so ZeebeClient and its response types become
CamundaClient and io.camunda.client.api.response.*, and JobWorkerException moved into
the .exception subpackage. ZeebeClientConfig#rest() is no longer optional and
ZeebeWorkerModule dereferences it unconditionally, so zeebe.client.rest.url is now a
required key; added it to the config, the run environment and the test.

The test keeps the legacy ZeebeClient: zeebe-process-test still exposes only that client
and BpmnAssert accepts only its response types. Bumped the extension to 8.9.14 to match
the client version Kora builds against.

Dropped the ActivatedJob overload of WorkerUtils.logJob: in 2.0 a worker method is handed
a JobContext, never an ActivatedJob, so the overload was dead code pointing at an API the
example never uses.

Getting this module to build and pass required two framework fixes, both on local
branches in ../kora with regression tests:
  - fix/zeebe-worker-annotations-not-aop
  - fix/zeebe-worker-exception-throws-bpmn-error
Kora 2.0 selects a resilient aspect by type rather than by name, and the type is an
interface that declares where its configuration lives. @CIRCUITBREAKER and @Retry were
also renamed to @CircuitBreakable and @retryable.

Added migration/scripts/migrate_resilient_specs.py, which generates the specification
interface next to each annotated file and rewrites the annotation. It points the spec at
resilient.<kind>.<name> - the path the configuration already uses - so no configuration
changes are needed. @fallback is deliberately out of scope: 2.0 has no FallbackSpec, so
those call sites need a decision rather than a rewrite.

Also fixed PetWithCategory in the pet-api submodule: @column is type-use in 2.0 and cannot
sit in front of a qualified nested type, so Pet.Status is imported the way the reference
module does it.
Tests configure the graph with HOCON inside a text block passed to
KoraConfigModification.ofString, and the config scripts only ever looked at .conf and
.yaml files, so those keys stayed on 1.x while the resource files were already migrated.
That is invisible at compile time and only shows up as a graph initialization failure.

migrate_config_keys.py now also scans .java and .kt, rewriting strictly inside the
ofString text block - applying config rules to arbitrary source would corrupt real code.
The db -> jdbc rule also had to accept leading whitespace, since the block is indented.

Added the circuit breaker rule while at it: 1.x had a single slidingWindowSize, 2.0 has
several implementations and moved the window into countBased. The block is nullable in
the config interface but StripedApproxKoraCircuitBreaker dereferences it without a check,
so it is required in practice.
… sources

Also corrected the circuit breaker note: the failure mode of a missing countBased block is
an NPE inside StripedApproxKoraCircuitBreaker, not a config validation error, and the type
itself is optional.
…ining type-use annotations

ConfigValueExtractor<T> became ConfigValueMapper<T> and moved from config.common.extractor
to config.common.mapper, and extract(value) became mapOrThrow(value). The tricky part is
that 2.0 also has a @ConfigMapper annotation with the same simple name in
config.common.annotation, so the rule in migrate_api_renames.py keys off the old package
and off the generic parameter rather than on the name alone.

@column is type-use in 2.0 like the JSpecify annotations, so on a qualified nested type it
has to sit before the simple name - fixed in the three graalvm crud modules.

Also ran the resilient specification script over guides, which was missed earlier.
… graalvm interceptors

CircuitBreakerPredicate is a functional interface in 2.0 - name() is gone and test() became
isCircuitBreakerFailure(). The predicate is no longer selected by the failurePredicateName
config key but bound with @tag(<spec>.class), so that key was dropped from the config.

@fallback lost its value attribute along with named fallback configuration; added the rule
to migrate_api_renames.py.

The graalvm crud modules carried the same reactive HttpServerInterceptor as the other
examples; brought them in line with the synchronous contract of the reference module.

Added @Valid to the validation guide DTO - without it no Validator<T> is generated and the
controller proxy fails to resolve its dependency.
… contract

Interceptors, request mappers and the manual HttpClient call lose Context and
CompletionStage; HttpServerRequestHandlerImpl handlers now take just the request and return
the response directly.

Two things that are not a mechanical rewrite:

A response mapper needs @component exactly when it has constructor dependencies - Kora
instantiates a dependency-free mapper itself, and declaring that one as a component gives
'Multiple components match'. The mappers in the advanced client read a JsonReader, so they
have to be components.

HttpResponseEntity<Void> has no mapper in 2.0 - only String and byte[] ship by default - so
the client declares HttpClientResponseMapper<Void> itself. It stays unreferenced by
@mapping on purpose: @mapping would make the mapper produce the whole return type, while
the framework template wraps a mapper of the payload type into the entity.
…ules

UndertowModule split into UndertowPublicHttpServerModule and
UndertowSystemHttpServerModule; the graalvm kafka app only ever served probes and metrics,
so it takes the system one.

<db>Repository#get<Db>ConnectionFactory() became executor(), returning the executor that
still carries inTx(...); added the rename to migrate_api_renames.py.

EmailConfig needs @ConfigMapper for its ConfigValueMapper to be generated, and the library
module that declares it needs the annotation processor of its own - otherwise the mapper is
never generated and the application module fails with 'Generated dependency class was not
found'.
…ponse mapper resolution issue

The interceptor moves to the synchronous contract, HttpServerPrincipalExtractor gained its
token type parameter and returns the principal directly, ApiSecurity now exposes
SecurityRequirementTagN instead of scheme-named tags, and the generated ErrorResponseTO
wraps details in JsonNullable.

The module still does not build: the sealed response mapper resolves
HttpServerResponseMapper<HttpResponseEntity<T>> through the generic json template instead
of the entity one, so it asks for a JsonWriter of the entity type. Both templates carry
@JSON here, so unlike the client-side defect this is not a missing tag - recorded in
KORA_2_FRAMEWORK_ISSUES.md with what has been ruled out.
The guide used one artifact for both the declarative @S3.Client and the AWS SDK client it
needs for bucket administration. In 2.0 those are separate: s3-client-kora carries the
declarative contract, s3-client-aws publishes the SDK client, so the app includes both
modules and the configuration gains a section per client.

The bucket used to come from an injectable S3ClientConfig; the declarative client now reads
it through @S3.Bucket, which produces a generated class rather than a component, so bucket
administration reads the same configuration path through @ConfigSource.

Multipart handling changed too: MultipartFileStream carries an HttpBodyOutput that writes
itself out, instead of a publisher wrapped into an InputStream, and MultipartFile exposes
its content type while the stream variant takes it from the body.
Nothing depends on a Lifecycle component that only prepares external state, so without
@root it is pruned from the graph - taking the AWS SDK S3Client it depends on with it, and
the test fails with 'S3Client wasn't found in graph'. Both tests pass now.
HttpServerRequest/HttpServerResponse/HttpServerInterceptor moved into the request,
response and interceptor subpackages, so the shared `http.server.common.*` wildcard
no longer covered them. Interceptors and mappers are now synchronous — Context and
CompletionStage are gone — and the error handler turns `.exceptionally {}` into a
try/catch, returning HttpServerResponseException directly since it is a response.

Mappers and interceptors referenced by @Mapping/@InterceptWith are injected by the
generated module in 2.0, so they must be @component.

HttpServerResponseMapper declares its result as @nullable, which Kotlin enforces:
the override takes `HelloWorldResponse?`.

The suspend route is kept — 2.0 still accepts it and bridges it with runBlocking on
the Virtual Thread that runs the handler.

Verified: 17/17 tests pass.
The library module holds a config DTO but had no symbol processor at all, so no
ConfigValueMapper<EmailConfig> was generated and the application graph could not resolve it.
@ConfigMapper marks the DTO and KSP is wired into the module the same way as in the submodule.

Verified: the app graph starts (1/1 test).
…nt guide

2.0 ships response mappers for String and ByteArray only, so a body-less DELETE that still needs
its status code has to say how Void is produced; the framework wraps it into
HttpResponseEntity<Void> with its own template factory, which is why the component is declared
but never referenced with @mapping.

Verified: 5/5 tests pass against the containerized http-server app.
The generator rejects suspend methods outright — "Suspend methods are not supported by the HTTP
client generator" — while HTTP server routes still accept them. The coroutine entry points are
therefore plain default functions that bridge to the generated blocking calls on Dispatchers.IO,
which keeps the example's suspend API and the tests unchanged.

Interceptors and request/response mappers drop Context and CompletionStage and must be
@component, since 2.0 injects the classes named by @InterceptWith/@Mapping/@ResponseCodeMapper.

The multipart expectation matched "text/plain; charset=utf-8"; 2.0 emits it without the space.

Verified: 11/11 tests pass against mockserver.
…or and mapper contracts

The repository exposes executor() instead of jdbcConnectionFactory, and its inTx overloads are
ambiguous for a Kotlin lambda, so the SAM constructor is named explicitly — SqlSupplier where the
block returns a value, SqlRunnable where it does not. JdbcParameterColumnMapper.set declares the
value @nullable, which Kotlin enforces on the override; both mappers now handle null the way the
Java twin does.

An @EntityJdbc was also missing on the macros entity of the JDBC example.

Verified: 7/7 tests pass across the two JDBC guides.
… mapper

Row mappers are generated only for entities marked @EntityJdbc, which two of them were missing.
JdbcParameterColumnMapper.set declares the value @nullable, which Kotlin enforces on the override.

Verified: 18/18 tests pass against a Postgres container.
…ud example

OpenApiManagementConfig#files is a required list in 2.0 — the 1.x key was `file` — and RapiDoc
was replaced by Scalar. HOCON silently accepts the unknown `rapidoc` block, but the missing
`files` aborts graph initialization, which is what made every blackbox container exit before it
could answer /system/readiness. That also resolves the open blocker in
examples/java/kora-java-crud-submodule: its BlackBoxTests now pass 5/5.

migrate_config_keys.py gained both rules and applied them to 11 configurations, GraalVM ones
included.

The Kotlin crud example additionally needed the typed resilient specifications, the synchronous
interceptor contract, named arguments for the generated TO constructors — 2.0 orders them by
optionality — and the koraBom on kspTest.

Verified: kora-kotlin-crud 9/9 and kora-java-crud-submodule-app 7/7 tests pass.
AutoCommitRecordsTelemetryListener is removed together with its test, matching the Java twin:
KafkaConsumerTelemetry was redesigned in 2.0 and no longer exposes a per-record telemetry
context as a listener parameter. JsonReader.read is nullable in Kotlin, so the two custom
deserializers assert the payload instead of returning a platform type.

Publishing needed a framework fix — the publisher observation forked an unbound MDC and threw
before the record reached the broker, in both languages.

Verified: 48/48 tests pass across the Kotlin and Java kafka examples.
Typed resilient specifications per submodule, the synchronous interceptor contract, and named
arguments for the generated TO constructors, which 2.0 orders by optionality. VetUpdateTO no
longer exists: the generator collapses two structurally identical named schemas into one, so the
update operation takes VetCreateTO exactly as in the Java twin — recorded as an open framework
issue rather than worked around further.

Verified: 7/7 tests pass, blackbox container included.
…server guide

The generated client config path is the lower-camel client name — httpClient.petV2.petApi — and
the configuration still used PetApi, so the client got no url and every request waited out its
timeout instead of failing. ValidationModule declares an HTTP server interceptor and therefore
drags http-server-common into a client-only application, which KSP rejects as an unresolvable
type; ValidatorModule is the part this example needs. The generated ApiSecurity requires an
HttpClientTokenProvider per scheme tag, as in the Java twin.

The advanced server guide follows the same 2.0 contracts as the other guides: a synchronous
interceptor, HttpServerPrincipalExtractor<T, P> with two type parameters, security tags named by
requirement order, and JsonNullable for an optional array of the specification.

Generated models expose .value on enums and order constructor parameters by optionality, so the
tests name their arguments.
HttpClientPetV3Tests expected an X-API-KEY header and got Authorization. This
was recorded as an open question about the framework's auth ordering; it was a
defect in the example itself.

The generated interceptor walks the schemes in order (bearer, apiKey, basic,
oAuth) and takes the first whose HttpClientTokenProvider returns a token. A
null return means "this scheme has no credentials, try the next one". Both
examples stubbed the bearer and oAuth providers with constants, so bearer
always won and apiKey was never reached. They now return null.

The Kotlin module additionally kept its security section under `openapiAuth`,
while the generator reads it from clientConfigPrefix
(`httpClient.petV3.apiKeyAuth`); `securityConfigPrefix` only ends up in a
@ConfigSource annotation on a nested record that nothing resolves. Its basic
auth credentials were missing entirely. The configuration now matches what the
generated code actually reads.

4/4 in both languages. The run takes ~12 seconds; before
fix/test-junit5-graph-init-lock-leak the first failed graph initialization
blocked the rest of the module and the same run took 22 minutes, reporting
only one of the two failures.
Five distinct causes behind the failures left after the framework fixes, all of
them runtime rather than compile-time:

gRPC. Every module pinned io.grpc artifacts at 1.74.0 while Kora 2.0 builds on
1.83.1, so grpc-inprocess and grpc-netty in tests ran against a newer
grpc-core: AbstractMethodError on buildClientTransportServers. All 12 modules
now use 1.83.1.

Flyway. Kora exposes only flyway-core 13, and since Flyway 10 the per-database
support lives in separate artifacts. Every module using database-flyway with
PostgreSQL failed at startup with "Unsupported Database: PostgreSQL 16.14" --
which the black-box guide surfaced as a container exiting with code 255. Added
flyway-database-postgresql where the dialect is actually needed.

Mockito. mockito-kotlin 5.4.0 pins an older mockito-core whose Byte Buddy
rejects Java 25 class files, so mock creation failed inside graph
initialization and the Kotlin crud submodules, camunda-engine and the junit
guide all failed while their Java twins passed. The Kotlin modules now declare
mockito-core explicitly, and the Kotlin testing-junit guide moves from 5.12.0
to the 5.23.0 its Java twin already used.

kspTest. kora-kotlin-crud-submodule-{pet,vet}-api declare a @koraapp in test
sources but had no processor in kspTest, so no graph was generated; their Java
twins declare testAnnotationProcessor per module. Added once in the submodule
parent, which also removes the duplicate from the app module.

Metrics. TelemetryConfig.MetricsConfig.enabled() defaults to false in 2.0, so
the observability guides exposed only JVM metrics. Enabled explicitly.

Plus two config paths the 2.0 generator changed: the openapi client section is
keyed by the generated @HttpClient path, whose first letter is lower case
(httpClient.usersApi, not httpClient.UsersApi).

Both migration guides gain a section on third-party version alignment and one
on the processors needing a full recompile -- Gradle's incremental compilation
can make the database processor read a repository interface from a class file
and report ":arg0" as the only available parameter.
…ion run

The per-module table is now generated from the actual sweep log rather than
maintained by hand: 127 Gradle modules, 116 of them with tests, 556 tests
passing and none failing.

The table records what the prompt asks for per module -- path, language,
runtime, the Kora integrations it exercises, migration status, compilation,
code generation, test and native-image results.

The prose sections drop the stale "first pass" narrative and record instead
what was actually resolved, including three earlier conclusions that turned out
to be wrong and were refuted by experiment: the HttpResponseEntity template
selection (a cascade of the multipart converter defect), the gRPC lifetime
(already an established framework contract, see XnioLifecycle), and the
openapi client auth ordering (a defect in the example, not the framework).

The one failure the sweep still reports, kora-java-crud:compileJava,
reproduces only under incremental compilation and is documented as such; the
module builds and passes 9/9 with --rerun.
…k fixes

Four entries were stale: the gRPC ForwardingServerBuilder resolution (closed by
fix/ksp-template-match-star-projection), the gRPC UNAVAILABLE test failure
(a duplicate of the process-lifetime defect), and the umbrella entry on KSP
processors dying with internal exceptions, every reproduced case of which now
has its own fix.

Of the twenty recorded issues: fifteen fixed, two closed as not-a-defect or
duplicate, two confirmed but deliberately not fixed (GraphBuilder diagnostics
and the parallel KSP race, both needing a framework design decision), one
observed and not investigated.
All twenty framework fixes are now open against kora-projects/kora as
#791-#810, pushed from the dsudomoin/kora fork. Each branch was rebased onto
the current origin/master first; the two upstream commits since the original
base touch no file any of them changes, and all twenty rebased without
conflict. The test-junit5 branch was re-verified afterwards because an upstream
commit touched another file in the same module.

The documents claimed throughout that nothing was pushed and no PR existed.
That is no longer true, so every such marker is replaced by the actual PR link:
the summary table in KORA_2_PULL_REQUESTS.md, the per-fix "Related fix branch"
lines, and the "Related PR: prepared, not submitted" entries in
KORA_2_FRAMEWORK_ISSUES.md.

Recorded for the maintainers: #805 and #808 both add a test to
HttpServerJavaOpenapiTest, so whichever merges second needs a trivial rebase
keeping both tests.
…VM examples

- GraalVM CE 25.0.4 via toolchain; native-build-tools 0.11.5 -> 1.1.7 (the older plugin
  registers one build-scoped metadata service for the whole build, which Gradle 9 rejects
  as a cross-project configuration resolution)
- keep the plain jar enabled: nativeCompile takes the project's own artifact as a classpath entry
- pass application.mainClass as a provider; interpolating it yielded the provider's description
- migrate kora-java-graalvm-crud-cassandra off the reactive repository, service, delegate and
  tests that were still on Kora 1.x, and drop the reactor dependency
- repair that module's docker-compose: Scylla was published on the Postgres port, the contact
  points held a JDBC URL, and the redis service it depends on did not exist
- Dockerfile base image 21 -> 25, README links to the JDK 25 release notes
- move the native-image metadata directories off the ru.tinkoff group id
- kafka example: correct the logging config syntax and the log line BlackBoxTests waits for

Verified in operation, not just built: each binary runs against its real dependencies and
answers /system/readiness, /metrics and its own scenario. kora-java-graalvm-crud-jdbc also
passes BlackBoxTests, which builds the image inside Docker from the Dockerfile.
The black box test hung waiting for a startup message that never reached the log: events
logged outside a request scope were dropped by KoraAsyncAppender (fixed upstream in #816),
and the wording it waited for came from Kora 1.x anyway. Log wording is not part of Kora's
contract, so the container now waits on /system/readiness like the other examples do.

The bootstrap address keeps pointing at the broker's in-network listener - the testcontainers
broker advertises :9092 to the host and :9093 inside the network - now with a comment saying
why, and the debug System.out.println calls left over from the migration are gone.
Audit of the deliverables against the migration brief turned up four things
that were required and missing, plus stale facts in two documents.

- GraalVM/Native Image sections in both language guides. The brief lists
  "GraalVM migration" as a mandatory topic in each guide and asks for the
  rules to be documented in both; neither guide mentioned native-image at
  all. Java §17 and Kotlin §15 now carry the plugin upgrade, the JDK 25
  move, the imageName/mainClass provider rule, the jar.enabled removal, the
  metadata story and the readiness criteria. The Kotlin-specific part is
  explicitly marked as reasoning rather than measurement -- the repository
  has no Kotlin native module to measure on.
- A native-image diagnostic pattern and a metadata-relocation pattern in
  KORA_MIGRATION_NEURO.md, which the brief asks for by name.
- The OpenRewrite recipe is split into five composable recipes plus the
  aggregate, moved to the canonical META-INF/rewrite location, and covered
  by before/after tests (5/5).
- The 24-point final report is now a document rather than a chat message.

Two corrections while I was in there. The recipe module could never have
run: org.openrewrite.recipe:rewrite-gradle:4.27.0 does not exist, and
rewriteRun inside a build whose settings.gradle covers only itself sees no
example sources. The docs presented it as step 1 of the procedure; the
script is what actually migrated this repository, and both READMEs and both
guides now say so. Separately, the status document still claimed 25 fixes
and listed ConfigWatcher as unfixed, and the PR document carried the old
title, the pre-rebase commit and a reference to a test that was deleted when
that fix was redesigned.
#807 was rejected in review and reworked. The original fix put a non-daemon
thread in GrpcServer on the reasoning that XnioLifecycle already did the same,
so holding the process was an established per-server contract. Checking that
claim after the review showed it was wrong: the same trick is duplicated in
five modules, so it was a repeated workaround, not a contract. The thread now
lives once in KoraApplication#run and both GrpcServer and XnioLifecycle are
back to not caring about process lifetime.

#801 keeps its fix but loses an inaccurate claim. The note said a record may be
published from a scheduled job with no MDC bound; scheduled jobs do bind one --
AbstractJob, CronJob and KoraQuartzJob all wrap the job body. The contexts that
genuinely have none are graph initialization, shutdown hooks, self-started
threads and tests.

The reviewer also asked for an MDC scope at startup. That cannot go where it
belongs: MDC lives in logging-common, which sits above application-graph
through core:common, so KoraApplication cannot reach it without a dependency
cycle. Options are written up in the PR for the maintainers to choose from.

Also syncs every commit hash in the PR document with what is actually pushed --
fifteen were stale from the rebase onto current master.
@GoodforGod

Copy link
Copy Markdown
Contributor

@GoodforGod GoodforGod added the enhancement New feature or request label Aug 11, 2026
- switch from kora-parent to kora-bom
- simplify Kotlin BOM and KSP configuration
- remove unsupported R2DBC, Vert.x, and Minio examples
- fix generated OpenAPI API usage
- update migration guides and automation
- migrate builds to kora-bom and JUnit 6.1.3
- remove suspend HTTP, repository, and server APIs
- remove obsolete coroutine dependencies
- add OpenRewrite and Python migration rules
- document Java Structured Concurrency migration

BREAKING CHANGE: Kotlin framework contracts are now synchronous;
structured concurrency requires the latest GA JDK preview API.
Remove empty Gradle environment variable names that prevent test JVM
startup on Windows. Fix GraalVM Kafka readiness probe to use the system
server port and automate cleanup in migration recipes.
@GoodforGod
GoodforGod merged commit 5dd1254 into kora-projects:migration/2.0 Aug 13, 2026
29 of 42 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants