jamesfredley opened a new pull request, #16011: URL: https://github.com/apache/grails-core/pull/16011
## Summary [#15948](https://github.com/apache/grails-core/pull/15948) split the CLI command tier off the application runtime classpath. As part of that split the command contract moved from `grails.dev.commands.*` to `org.apache.grails.core.cli.*` (with no forwarding alias) and command registration moved from `META-INF/grails.factories` (key `grails.dev.commands.ApplicationCommand`) to `META-INF/grails-cli.factories`. The classpath split is good and is preserved here. The side effect is that an **unchanged, already‑published Grails 7 command plugin silently loses all of its application commands on Grails 8**: the command classes no longer link (their `grails.dev.commands.*` supertypes are gone) and their factory entries are never read. Every plugin that ships an application command would otherwise have to be re‑released just to work on Grails 8, and Grails 8 is arriving late enough that the plugin ecosystem will not have caught up when users start upgrading. This PR adds a **deprecated backwards‑compatibility layer**, entirely inside the `grails-core-cli` tier (so it stays off the application runtime classpath and the split is preserved), so unchanged Grails 7 command plugins keep working **without a re‑release**, while nudging authors to migrate. ## The full picture: every way a plugin provides a command, and what happens on Grails 8 There are several distinct mechanisms for authoring/shipping a command in a Grails plugin. This PR audited all of them so nothing is silently left broken: | # | Mechanism (how authored) | Packaged as | Discovered by | Broken by #15948? | This PR | |---|---|---|---|---|---| | 1 | `create-command` → `grails.dev.commands.ApplicationCommand` / `GrailsApplicationCommand` (runs with an application context) | class + `META-INF/grails.factories` (key `grails.dev.commands.ApplicationCommand`) | `ApplicationContextCommandRegistry` (now reads `grails-cli.factories` / new key) | **Yes — dead** (types removed, key + file changed, no alias) | **Fixed** — restored deprecated contract, adapter, registry dual‑load, runner unwrap, legacy per‑command Gradle tasks | | 2 | `create-script` → codegen script in `src/main/scripts/*.groovy` (runs without an app context) | `META-INF/commands/*.groovy` | `GroovyScriptCommandFactory` (compiled from source at runtime vs `GroovyScriptCommand`) | No (packaging + resolver unchanged) — **but** a separate Groovy 5 name regression broke hyphenated scripts | **Fixed** the hyphenated‑name regression (see below) | | 3 | YAML / multi‑step commands | `META-INF/commands/*.yml` | `YamlCommandFactory` | No | Unaffected — verified, documented | | 4 | `ServiceLoader` `CommandFactory` / profile commands | `META-INF/services` / profile jar | `ServiceCommandFactory` / profile mechanism | No | Unaffected — verified, documented | | 5 | Legacy Gant `scripts/*.groovy` (pre‑Grails 3) | — | removed in Grails 3 | Already gone pre‑8 | Out of scope — noted | So the only mechanism actually broken by the CLI split is **#1 (application commands)**; that is the bulk of this PR. **#2 (codegen scripts)** are not broken by the split, but testing them surfaced a separate, real Grails 8 regression (below), which is also fixed here because it otherwise breaks every hyphenated `*-quickstart`‑style script command without a re‑release. ## What this PR does **Application‑command compatibility (mechanism #1):** - **Restore** the deprecated `grails.dev.commands.*` command contract (`ApplicationCommand`, `GrailsApplicationCommand`, `ExecutionContext`, and the `io` / `template` helpers) into the `grails-core-cli` tier so pre‑compiled Grails 7 command classes link again. The restored public signatures were confirmed against Grails 7.1.1 bytecode. - **Adapt** legacy commands to the new contract via `LegacyApplicationCommandAdapter` / `LegacyApplicationCommandAware`, and discover them from the legacy `META-INF/grails.factories` key in `ApplicationContextCommandRegistry`. Each legacy command is instantiated **in isolation**, so one faulty command cannot suppress the rest; a real new‑contract command always wins a name clash. A one‑time deprecation warning nudges migration. - **Unwrap** the adapter in `GrailsApplicationContextCommandRunner` so Spring autowiring and the `skipBootstrap` lookup target the real legacy command that is subsequently executed. - **Register per‑command Gradle tasks** for legacy commands found on the runtime classpath (so `grails <command>`, which routes through a Gradle task, keeps working), degrading gracefully when the classpath cannot be resolved at configuration time (the generic `runCommand` task always works). **Codegen‑script fix (mechanism #2):** a hyphenated script filename (e.g. `audit-quickstart`) produced a class name without the hyphen under Groovy 5, so the command resolved under the wrong name (`auditquickstart`). `GroovyScriptCommandFactory` now uses the filename‑derived command name (a no‑op for non‑hyphenated commands). This restores `audit-quickstart`, `s2-quickstart`, `dbm-*`, `create-job`, etc. ## Testing - **Unit:** `LegacyApplicationCommandAdapterSpec`, `LegacyCommandRegistryLoadingSpec` (incl. an exception‑isolation regression where one throwing legacy command does not suppress valid legacy/new commands), `GrailsApplicationContextCommandRunnerSpec` (adapter unwrap), `LegacyPluginScriptCompatSpec` (hyphenated‑script name resolution). - **End‑to‑end functional example** under `grails-test-examples/legacy-commands` (+ `-plugin`): a plugin that ships a legacy `ApplicationCommand`, a legacy `GrailsApplicationCommand`, and a hyphenated codegen script; an `@Integration` spec boots a real Grails 8 application and executes them through the registry. Verified running (`Grails: 8.0.0-SNAPSHOT | Groovy: 5.0.7 | Spring Boot: 4.1.0`). - `codeStyle` passes on the changed modules. ## Migration path The compatibility layer is `@Deprecated` and logs a one‑time warning. Plugin authors should migrate to the `org.apache.grails.core.cli.*` command API and publish a `-cli` companion artifact for full support (classpath split, first‑class per‑command task discovery). The layer is intended to be removed in a future major release. ## Follow‑up (minor, non‑blocking) Reviewers flagged one minor gap: there is no dedicated TestKit test asserting the Gradle legacy per‑command task registration in isolation (its behavior is covered by the runtime functional test and confirmed via a task dry‑run). A focused TestKit fixture could be added later. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
