Skip to content

Publish a Java 11 main artifact and a jdk21 classifier - #68

Merged
ghedwards merged 2 commits into
masterfrom
claude/jdk11-jdk21-artifacts
Sep 7, 2026
Merged

ghedwards merged 2 commits into
masterfrom
claude/jdk11-jdk21-artifacts

Conversation

@ghedwards

@ghedwards ghedwards commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Mirrors the split cfparser now publishes (cfmleditor/cfparser#73). One source tree, three jars:

Artifact Class file Runs on
cflint-VERSION.jar 55 (Java 11) 11, 17, 21, 25
cflint-VERSION-all.jar 55 (Java 11) 11, 17, 21, 25 — shaded
cflint-VERSION-jdk21.jar 65 (Java 21) 21+

The default is the one that loads everywhere; the classifier is there for consumers who specifically want a 21-targeted build.

Both variants depend on the Java 11 parser

Maven cannot pair main→main and jdk21→jdk21 — a pom has one dependency set, so both CFLint variants compile against whichever cfparser artifact is declared. Declaring the Java 11 one is the only correct choice: a Java 11 CFLint linked against a Java 21 parser would fail at runtime on 11, which is the exact case this change exists to support. The jdk21 classifier is a bytecode-level variant, not a separate dependency graph.

release rather than source/target

maven.compiler.source/target only set the language level — they will happily compile a call to a Java 17 method into class file 55 and hand you a jar that throws NoSuchMethodError on 11. --release checks the API surface too, so that fails the build instead.

The resource copy stays at process-classes

maven-jar-plugin takes a single classesDirectory, and classes-jdk21 holds only .class files, so the classified jar needs resources copied in.

That copy cannot move later. Setting outputDirectory on a compiler execution leaks into what the reactor hands dependent modules — in cfparser, moving this to prepare-package made all 326 tests fail with Problem loading dictionaryconfig.xml, which reads like a missing resource rather than a phase-ordering problem. The comment sits at the line where someone would otherwise move it.

CI runs on both runtimes

gradle.yml gains a java-version axis of [11, 21] across all three operating systems, with fail-fast: false so one platform failing does not mask the others.

Compiling to 11 on a 21 JDK cannot catch a problem that only appears on an 11 runtime, so the matrix is the only thing that actually exercises Java 11.

Both JDKs are installed with 21 last, so it becomes JAVA_HOME and Gradle's daemon runs there; the matrix JDK is picked up by the toolchain to compile and run the tests. That separation is load-bearing on the cfparser side, where the bnd OSGi plugin is class file 61 and throws UnsupportedClassVersionError if the daemon is on 11. Keeping the two builds symmetric is worth more than shaving a step here.

Gradle builds the Java 11 line only. Maven is what publishes, so the classifier is produced there.

The second commit: always re-resolve the cfparser SNAPSHOT

The first CI run failed on all three Java 11 jobs, identically:

bad class file: .../cfml.parsing-2.16.1-SNAPSHOT.jar(/cfml/parsing/cfscript/CFExpression.class)
  class file has wrong version 65.0, should be 55.0

Not a defect in this change — the published 2.16.1-SNAPSHOT was still the Java 21 build. cfparser#73 merging to master publishes nothing; that repository's publish workflow fires only on a tag push, a release, or a dispatch. This ordering is now permanent: a cfparser change that CFLint's Java 11 leg depends on has to be published before CFLint's CI can go green. Merging cfparser first is not enough.

Republishing the parser fixed it, but exposed a second problem worth closing while it was visible. Gradle treats a -SNAPSHOT as a changing module and caches it for 24 hours, and the CI workflows restore a Gradle cache — so a green run shortly after a cfparser republish can have tested the previous artifact, and here the re-run could have resolved the same stale jar and failed for a reason that no longer existed. resolutionStrategy.cacheChangingModulesFor 0, 'seconds' makes CFLint always take the newest parser. It is one small jar; a stale one costs far more than the download.

Verification

  • All 6 CI jobs green — Ubuntu, macOS and Windows × Java 11 and 21. This is the first time CFLint's suite has actually executed on a Java 11 JVM rather than merely compiled to it
  • 675 Maven tests, mvn clean test — 0 failures, 0 errors
  • ./gradlew build -PjavaTestVersion=21 — BUILD SUCCESSFUL, and the Gradle jar is class file 55
  • mvn clean package produces the three jars at the class file versions above, with matching class counts between the main and jdk21 variants (128 each; the shaded -all is 9,437)

Follow-up

CFLint's cfparser.version is a SNAPSHOT pin. When cfparser next cuts a fixed version, this should move to it — at which point the cacheChangingModulesFor setting stops mattering for that dependency, though it stays correct.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GzpZFd4rnE1Yi2sVHAji35

Mirrors the split cfparser now publishes. One source tree, two jars:

  cflint-VERSION.jar        class file 55 (Java 11) -- 11, 17, 21, 25
  cflint-VERSION-all.jar    class file 55 (Java 11) -- shaded, same
  cflint-VERSION-jdk21.jar  class file 65 (Java 21) -- 21+

The default is the one that loads everywhere; the classifier is for
consumers who specifically want a 21-targeted build. Both variants
compile against the Java 11 cfparser artifact -- a pom has one dependency
set, and a Java 11 CFLint linked against a Java 21 parser would fail at
runtime on 11.

maven.compiler.source/target become release, which checks the API surface
rather than only the language level, so a Java 12+ method call fails the
build instead of producing a jar that NoSuchMethodErrors on 11.

The resource copy into classes-jdk21 stays at process-classes for the
reason cfparser's pom documents: setting outputDirectory on a compiler
execution leaks into what dependents resolve, and running the copy later
breaks the test run with an error that reads like a missing resource.

CI gains a java-version axis of [11, 21] across all three operating
systems, with fail-fast: false. Both JDKs are installed with 21 last, so
Gradle's daemon stays on 21 while the toolchain moves to the matrix JDK
for compiling and running the tests -- compiling to 11 on a 21 JDK cannot
see a problem that only appears on an 11 runtime.

Verified: 675 Maven tests, ./gradlew build, and the packaged jars carry
the expected class file versions with matching class counts between the
main and jdk21 variants.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GzpZFd4rnE1Yi2sVHAji35
Gradle treats a -SNAPSHOT as a changing module and caches it for 24 hours,
and the CI workflows restore a Gradle cache. A green run shortly after a
cfparser republish can therefore have tested the previous artifact --
which reads as a pass rather than as a stale dependency.

This is not hypothetical here: the Java 11 CI jobs on this branch failed
with "class file has wrong version 65.0, should be 55.0" against the
cfparser SNAPSHOT published before the Java 11 split landed, and without
this the re-run after republishing could have resolved the same cached
jar again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GzpZFd4rnE1Yi2sVHAji35
@ghedwards
ghedwards merged commit e55d16a into master Sep 7, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants