Hi all,
Following James Fredley's -1 on the CLI split, I want to lay out where
we actually agree, correct a couple of points in the record, and
propose a concrete path so we can settle this on the list rather than
in PR or voting threads.
Where we agree
Nobody is proposing that users wait on plugin re-releases. We all
agree on the goal: an application upgrading to Grails 8 must be able
to keep using commands from already-published, unchanged Grails 7
plugins. #16011 provides a mechanism for that, and after the review
iterations its core design is right: the deprecated
grails.dev.commands.* contract lives in a separate, execution-only
grails-core-cli-legacy artifact, off every compile classpath,
discovered through a neutral SPI, so grails-core-cli carries zero
legacy references and retiring the window later means dropping one
artifact. I support that design and want it to land.
Correcting the record
1. "We only addressed 1 of 4 paths that provide commands." #16011's
own audit table reaches the opposite conclusion: of the four
mechanisms (application commands, codegen scripts, YAML commands,
ServiceLoader/profile commands), only application commands were broken
by the split. Codegen scripts, YAML, and ServiceLoader commands were
verified unaffected. The hyphenated-script issue is a separate Groovy
5 regression that exists independently of #15948 and is fixed in
#16011. So the compatibility gap is one mechanism, not four — and it's
the one both PRs are focused on.
2. "#15948 was merged before a path was identified." It was merged
into a milestone, explicitly with the note that the design would
iterate before an RC. Milestones exist precisely so breaking changes
can land and the compatibility story can be worked out before anything
users depend on ships. Nothing has GA'd; no user is currently
stranded. Framing a milestone merge as the final word — and a -1 as
the response — escalates a design discussion we were already having.
The actual open disagreement: default-on vs. explicit opt-in
I am firmly for shipping the compatibility layer, and firmly against
provisioning it by default. My proposal keeps the upgrade
zero-confusion without committing the framework to zero-touch support
of a deprecated ABI:
- Detection always-on, provisioning opt-in. When the build or registry
sees a grails.dev.commands.ApplicationCommand entry on the classpath
and grails { legacyCommandSupport = true } is not set, it fails loudly
with exact remediation: "Plugin X ships Grails 7 commands; set grails
{ legacyCommandSupport = true } or upgrade the plugin." Nothing is
silent. The discoverability concern behind default-on is fully
answered. The user flips one documented switch during an upgrade they
are already performing.
- Precedent. This is how we've handled every comparable transition:
indy, and the BOM-handling change in this very release, shipped behind
explicit settings. The wider ecosystem does the same: Spring Boot did
not keep Jackson 2 on the classpath by default next to Jackson 3; you
add it explicitly if you need it. I know of no precedent for
default-provisioning a deprecated contract onto every build, test
runtime included.
- Costs of default-on fall on people who don't need it. Every Grails 8
application would carry grails-core-cli-legacy and the
dual-classloader factories scan on its command and test-runtime
classpaths for the lifetime of the window.
- Convergence. Default-on removes the only pressure that makes the
deprecation window converge. A one-time runtime warning doesn't reach
plugin authors — they never see their users' logs. And when the
artifact is eventually dropped, plugins that never migrated break at
that point with no action ever having been asked of anyone — a silent
break outside the visible opt-in contract. A visible switch is the
signal, to users and authors both, that migration is actually
expected.
What must be true before we advertise the compat contract (regardless
of the default)
These are in my review of #16011 and I consider them merge/RC gates,
not follow-ups:
1. Prove the headline claim. The current fixture recompiles legacy
sources with Groovy 5, which answers a different question than whether
a genuine Grails 7/Groovy 4 trait-consumer binary links against the
restored traits. The repo's existing composite-build pattern
(includeBuild) gives us a pinned grails-core:7.1.1 fixture without any
toolchain fight. Until that two-app test is green, "unchanged plugins
keep working without a re-release" is a hope, not a contract — and
it's the exact claim the -1 rests on.
2. No nondeterministic task graphs. The legacy per-command task
discovery resolves runtimeClasspath eagerly in afterEvaluate under
catch (Throwable), making the task set scheduling-dependent. Either
drop the legacy per-command tasks (the generic runCommand -Pargs=...
already works) or source names from the lenient withDependencies
mechanism the companion discovery uses. We spent Grails 7 removing
exactly this pattern.
3. Loud linkage failures. A LinkageError loading a legacy command is
the compat layer failing its contract, not a faulty plugin; it must
log at error level with clear framework-bug wording, distinct from
ordinary per-command isolation.
Proposal
Land #16011 with: the separate-artifact design as-is;
detection-always-on + legacyCommandSupport opt-in; the precompiled
7.1.1 fixture via composite build; legacy per-command tasks dropped in
favor of runCommand; the LinkageError split. Document the switch in
the upgrade notes as a one-line step.
To be clear, I'm not asking James to carry these items alone — I'm
offering to do the heavy lifting on them. Specifically, I'll take the
precompiled 7.1.1 fixture (the composite-build wiring is the pattern I
set up for grails-gradle and build-logic, so it's fastest for me to
stand up), and I'm happy to do the opt-in gating and detection wiring
in the Gradle plugin as well, either as commits into #16011 or as a
stacked PR on top of it, whichever James prefers. The LinkageError
split is a few lines either of us can do. None of this needs to slow
the PR down.
If the community consensus after discussion is genuinely default-on,
I'll accept that outcome — but let's decide it here, deliberately,
with the trade-offs on the table. With the gates above shared between
us, I think we can have this landed well before the RC either way.