Native image and testing
GraalVM native image is the production artifact
The Docker image ships a GraalVM native image (./gradlew nativeCompile, Dockerfile.native), and
that constrains how code is written:
- Never use
@ConditionalOnPropertyfor a runtime-toggleable feature. Bean conditions are evaluated and frozen at native-image build time; a conditional bean is baked out of the production image and no property can bring it back. Check the flag inside the bean instead — the Typesense handlers are the reference example (chapter 6). - Reflection and resource hints are hand-maintained per module under
src/main/resources/META-INF/native-image/. Nothing generates them; forgetting one surfaces only in the native image, not in JVM runs. - A properties file imported via
spring.config.importneeds an entry in its module'sresource-config.json(seesearch/src/main/resources/META-INF/native-image/). - AWT works in the native image, but only with hand-fed JNI hints. PDFBox touches AWT even for
a plain page count (
PDDocument's static init warms up the color subsystem), and the JDK's AWT native libraries look up classes, fields and methods through JNI at run time — every one of them must be listed indisk/.../native-image/jni-config.jsonor the lookup fails and the class initializer dies for the lifetime of the process. The charset flag-H:+AddAllCharsets(serverbuild.gradle) is part of the same story: PDFBox'sBaseParserwants Windows-1252, which the image drops by default. The generatedlibawt*.sofiles land next to the binary inbuild/native/nativeCompileand ship with it (Dockerfile.nativecopies the whole directory).
Database schema discipline
Prod runs hibernate.ddl-auto=validate: an entity change without a matching Flyway migration fails
startup. Migrations live in database/src/main/resources/db/migration/V<n>__<name>.sql (latest:
V23), are forward-only, and are also shipped as a separate image (Dockerfile.migrations).
Never edit a migration that has been applied anywhere.
Testing
-
Unit tests are JUnit 5 + Mockito; Mockito runs as a
-javaagent, wired in the rootbuild.gradle. -
jimfs(in-memory filesystem) backs the disk/file-path tests. -
ffmpeg must be on
PATH— the transcoder tests shell out to it, and CI installs it before the build. -
Integration tests are not a separate source set or task. They run under the normal
testtask, are named*IntegrationTest, and use Testcontainers PostgreSQL (and RabbitMQ for the dead-letter flow). They are annotated@Testcontainers(disabledWithoutDocker = true), so they silently skip when no container runtime is reachable — a green./gradlew testdoes not prove they ran. Locally:systemctl --user start podman.socketDOCKER_HOST=unix:///run/user/$UID/podman/podman.sock ./gradlew test
Useful commands:
./gradlew build # build all modules
./gradlew :core:test # one module
./gradlew :worker:test --tests "app.ister.worker.…" # one class
CI and quality gate
CI runs ./gradlew build check jacocoTestReport sonar. There is no separate linter or formatter —
quality gating is SonarCloud (org.sonarqube) plus Jacoco coverage, both part of check.
Commits and releases
Commit subjects must be Conventional Commits (feat(scope): …, fix: …, !/BREAKING CHANGE: for breaking); a commit-lint job fails the PR otherwise, and
.github/workflows/release.yml derives both the version bump and the release notes from them — see
CONTRIBUTING.md.
Releases are cut nightly and automatically. Never bump version in build.gradle by hand:
main always carries a -SNAPSHOT, and only the tagged release commit carries a clean version.
docker-publish.yml tags every main push with the gradle project version next to main; the server
and migrations images get the same tag so downstream pins stay in lockstep.