- JDK 25 for normal builds and tests. Any vendor works (Temurin, Zulu, GraalVM CE).
- GraalVM CE 25 for
nativeCompile. The build pinslanguageVersion=25+vendor=GRAAL_VMin qodana-cli/build.gradle.kts and the foojay toolchain resolver auto-downloads a matching JDK on first run ifJAVA_HOMEdoesn't already point at one. Any GraalVM CE 25 patch release is accepted; foojay picks one of the latest at the time of first build. ./gradlew— checked-in Gradle wrapper handles the rest.
If you prefer to install GraalVM yourself, the cleanest path is SDKMAN:
sdk install java 25-graalce
sdk use java 25-graalceThe Gradle daemon then picks up GraalVM via the toolchain pin in qodana-cli/build.gradle.kts.
./gradlew testTo run the Docker-touching tests (gated on QODANA_TEST_CONTAINER=1):
QODANA_TEST_CONTAINER=1 ./gradlew parityTestRelease/version logic lives in the release-tools module as unit-tested Kotlin cores
(./gradlew :release-tools:test). Thin *.main.kts wrappers in release-tools/scripts/ drive the CI
workflows and are runnable by hand. They need the pinned Kotlin compiler (version in
gradle/libs.versions.toml's kotlin = "…") — the same one CI installs via
.github/actions/setup-kotlin:
sdk install kotlin 2.3.20 # matches the pin; mirrors `sdk install java 25-graalce`
kotlin release-tools/scripts/normalize-version.main.kts 2026.3.1
kotlin release-tools/scripts/cleanup-old-nightlies.main.kts --keep 7 --dry-run
The pre-push checkVersion guard is a Gradle task (./gradlew :release-tools:checkVersion), so pushing
needs no Kotlin compiler — only running the scripts does.
The qodana-cli executable can be compiled ahead-of-time into a self-contained native binary via GraalVM Native Image. The binary runs without a JVM dependency.
./gradlew :qodana-cli:nativeCompileProduces qodana-cli/build/native/nativeCompile/qodana-cli (or .exe on Windows). Cold builds take 3–6 minutes on Apple Silicon; subsequent builds are faster thanks to Gradle's build cache.
native-image cannot cross-compile — to ship Linux/Windows binaries you must build on each target OS. Phase B (QD-14720) wires this into a GitHub Actions matrix.
GraalVM static analysis can't see code reached only via reflection, Class.forName, ServiceLoader, Jackson annotations, etc. Such usages must be declared in JSON metadata files committed under:
qodana-cli/src/main/resources/META-INF/native-image/org.jetbrains.qodana/qodana-cli/
Two sources contribute:
- The upstream GraalVM Reachability Metadata Repository — covers Jackson, OkHttp 4.x, slf4j, kotlin-stdlib, kotlinx-coroutines. Enabled in build-logic/src/main/kotlin/graalvm-native.gradle.kts via
metadataRepository { enabled.set(true) }. Downloads on the firstnativeCompileinvocation. - The tracing agent run over our own tests — captures qodana-specific reflection (
QodanaYaml,IdeProductInfoJson, etc.). Committed JSON regenerated on demand by the executor.
Run after bumping any dependency that touches reflection, or after adding code that uses reflection / ServiceLoader / classpath resources. Requires GraalVM CE 25 and a running Docker daemon (the agent has to drive the Docker-tagged tests to capture docker-java DTOs):
# 1) Generate metadata under the agent for BOTH the regular `test` task
# (non-Docker reflection: Clikt, Jackson, InitCommand file IO, send via
# MockQDCloudHttpClient) and the `parityTest` task (Docker-tagged tests).
# Both runs are merged into the committed JSON via mergeWithExisting.
./gradlew -Pagent :qodana-cli:test :qodana-cli:parityTest --rerun-tasks
# 2) Copy captured JSON into src/main/resources. The
# `stripTestEntriesFromMetadata` task runs as a finalizer of `metadataCopy`
# and removes JUnit / kotlin-test / scan-smoke-fixture entries using the
# canonical list in qodana-cli/src/test/resources/banned-metadata-patterns.txt.
./gradlew :qodana-cli:metadataCopy
# 3) Verify hygiene. The test enforces that no test-infrastructure entries
# landed in the committed JSON; if it fails, the failure message names
# exactly which entries to remove from which file (regenerate via Step 2).
./gradlew :qodana-cli:test --tests 'org.jetbrains.qodana.cli.MetadataHygieneTest'
# 4) Diff the result and rebuild the native image.
git diff qodana-cli/src/main/resources/META-INF/native-image/
./gradlew :qodana-cli:nativeCompileIf nativeCompile reports Classes that should be initialized at run time got initialized during image building, add the named class as --initialize-at-run-time=<class-or-package> in build-logic/src/main/kotlin/graalvm-native.gradle.kts and re-run. Likely candidates: org.slf4j.simple, okhttp3.internal.platform.
If a runtime command fails with MissingReflectionRegistrationError, the agent didn't see that code path. Extend NativeSmokeTest.kt to exercise it, then re-run the cycle.
Both the scan smoke test and the CI native-e2e job pin jetbrains/qodana-jvm-community via qodana-jvm-community-tag in gradle/libs.versions.toml. When bumping the tag:
- Update
qodana-jvm-community-taginlibs.versions.toml. - Update the matching
image:line inqodana-cli/src/test/resources/scan-smoke-fixture/qodana.yaml. - Re-run agent capture (steps 1–4 above) — new linter versions can rename rules; the
StringEqualityassertion inNativeSmokeTest.ktand the matchinggrepin.github/workflows/ci.yaml'sAssert SARIF (native)step will surface a rename clearly.
The native binary supports the full runtime command set: --help, --version, init (Phase A, QD-14643), plus scan, view, send, pull, show execution (added in QD-14728). The CI native-e2e job exercises every command end-to-end against a real Docker daemon and a local mock cloud on each supported platform.
Windows (the windows-latest runner): GitHub-hosted Windows runners are nested VMs whose hypervisor blocks the additional virtualisation Docker Desktop would need to run Linux containers (see community/discussions/25491). The runner's bundled Docker engine is in Windows-containers mode, and the jetbrains/qodana-jvm-community image is linux/amd64-only — so on this runner the native-e2e job exercises only the binary-only commands (--version, --help, view, send via the local mock cloud, show --dir-only). Docker-dependent steps (scan, pull, SARIF parity) are gated off via matrix.platform.docker: false.
Windows on ARM: There is no native arm64 build of qodana-cli. ARM Windows users are expected to run the amd64 binary under Windows 11's Prism x86 emulation, but that binary does not currently run on Windows ARM — it exits immediately with:
The current machine does not support all of the following CPU features that
are required by the image: [CX8, CMOV, FXSR, MMX, SSE, ..., AVX, AVX2,
BMI1, BMI2, FMA]. Please rebuild the executable with an appropriate setting
of the -march option.
The fix is to add a -march=compatibility (or x86-64) variant of the amd64 build that drops the high-end CPU-feature requirements. This is tracked separately in QD-14819; until that's done, the CI native-e2e matrix does NOT include a windows-amd64-on-arm entry, since it would always fail at the first binary invocation regardless of metadata changes. Docker Desktop is also not preinstalled on the windows-11-arm runner (see actions/partner-runner-images), so the re-added entry will need the same Docker-less treatment as windows-amd64.
macOS (Intel — dropped, QD-14862): darwin-amd64 was removed from both the native-build and native-e2e matrices. The Qodana JVM linter container (IntelliJ-based) can't bind its DirectoryLock Unix-domain socket on the Lima+QEMU+containerd-snapshotter overlayfs stack that GitHub-hosted macOS Intel runners provide — tracked upstream as JetBrains IJPL-161337 and IJPL-34916. Seven CI iterations during QD-14728 unblocked every other layer (colima → setup-docker-action, DOCKER_HOST export, Lima writable mount for /private/tmp/lima, symlink canonicalization); the IDE-bootstrap UDS bind is unfixable from the CI side. Intel Mac users should use the JVM qodana-cli distribution or upgrade to Apple Silicon.
macOS (Apple Silicon — macos-15): GitHub's hosted M1 arm64 runner has no working Docker path. Both colima VM backends fail at VM creation: --vm-type qemu panics in lima 2.1.1's hostagent (panic: send on closed channel, qemu_driver.go:382) because Hypervisor.framework returns HV_UNSUPPORTED; --vm-type vz refuses with "Virtualization is not available on this hardware" because the runner is itself a guest VM and M1 hardware lacks nested-virt support. Until GitHub exposes nested virtualisation on macos-15 arm64 (which requires M3+ host hardware) — or we provision a self-hosted M3+ runner — the native-e2e darwin-arm64 entry runs with docker: false and exercises only the binary-only commands (--version, --help, view, send via the local mock cloud, show --dir-only). Tracked in QD-14821. Related upstream tickets: actions/runner-images#9460, abiosoft/colima#1427.
The release pipeline is documented in docs/release.md. Key points for contributors:
- Version source of truth: gradle.properties's
version=line. Default isdev(development state —SystemUtils.checkForUpdatesskips network calls). To start a release cycle, bump to a numeric version that satisfies the bump rule (matches the most recent stablev*tag, or is exactly one semantic bump ahead). - Pre-push enforcement:
./gradlew :release-tools:checkVersionis wired into .pre-commit-config.yaml'spre-pushstage. Pushes with a version that skips a segment are rejected. - Runtime version overrides (
-Dqodana.version=…,QODANA_VERSION=…) remain supported for local JVM dev only — see QodanaCommand.ktcompanion object. Native binaries bake the version in viaBuildInfo.VERSION(generated fromproject.versionat build time) and ignore the runtime overrides under--initialize-at-build-time. - The pre-push hook also guards against accidental reintroduction of
qodana.version/QODANA_VERSIONreads outside the two authorized locations (QodanaCommand.ktandSystemUtilsTest.kt).
-Pagent runs trigger a post-test merge step that needs the GraalVM toolchain's native-image-configure binary. If foojay's downloaded toolchain is incomplete (zero-byte bin/native-image* placeholders), the binaries can be symlinked from lib/svm/bin/:
cd ~/.gradle/jdks/graalvm_community-25-aarch64-os_x.2/graalvm-community-openjdk-25.0.2+10.1/Contents/Home/bin # +build suffix varies by patch
rm native-image native-image-configure
ln -s ../lib/svm/bin/native-image native-image
ln -s ../lib/svm/bin/native-image-configure native-image-configureThen re-run with GRAALVM_HOME and JAVA_HOME pointing at the toolchain.
Gradle's OS-level JDK auto-detection may find another JVM (e.g. Amazon Corretto or JetBrains Runtime) and select it over the foojay-downloaded GraalVM CE. The symptom is:
/path/to/corretto-25.../bin/native-image wasn't found. This probably means that JDK isn't a GraalVM distribution.
Fix: install GraalVM CE 25 via SDKMAN and point JAVA_HOME at it before running Gradle:
sdk install java 25-graalce
sdk use java 25-graalce
./gradlew --stop
./gradlew :qodana-cli:nativeCompileThis is reliable across machines because it removes the ambiguity Gradle's auto-detection runs into when multiple JDK vendors are installed side by side.
Improving auto-detection so this workaround is unnecessary is tracked separately in QD-14818. If you have to pin manually with org.gradle.java.installations.paths=... in a per-machine gradle.properties while that ticket is open, that file is gitignored and stays local to your machine.
metadataRepository { enabled.set(true) } fetches from the public GraalVM mirror. Behind a proxy, set the standard Gradle proxy properties (-Dhttps.proxyHost=...) or pin a local mirror. See Gradle's HTTP settings.