Do we plan to revert every breaking API change then? Deprecations have already broken multiple plugins (I recently fixed the grails web console in one example). Why is changing a single flag to enable compatibility not acceptable while removing an entire API is? This inconsistently does not make sense to me.
On 2026/07/21 19:52:25 James Fredley wrote: > I am taking another spin through > https://github.com/apache/grails-core/pull/16011 in an attempt to get this > closer to a compromise. > > But I am strongly of the opinion that existing, already published Grails 7 > plugins should work in Grails 8, by default with warnings. And I think we > should keep the compatibility shims for an extended period of time. > > If we do not have to break end apps, I would like to keep them working, while > still doing the split and letting teams move to the modern plugin approach, > at their own pace. > > On the 1 of 4, it seems like the other 3 could also have similar classpath > issues, based on what they import. > > James > > On 2026/07/21 16:52:21 James Daugherty wrote: > > 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. > > >
