fix: route plugin src/main/scripts into companion -cli jars - #16082
Conversation
When a plugin applies grails-plugin-cli, copyCommands now packages src/main/scripts under the cli source set as META-INF/commands instead of the runtime jar. Plugins without a companion keep the historical runtime packaging so Grails 7 / legacyCommandSupport consumers still discover scripts on the application classpath. Also clear stale META-INF/commands under the configured processResources destination on incremental builds after migration, so prior script copies do not remain on the runtime classpath while hand-authored src/main/resources/META-INF/commands are restored by processResources. Fixes #16035 Assisted-by: Sisyphus:xai/grok-4.5
There was a problem hiding this comment.
Pull request overview
Routes plugin src/main/scripts command resources into the companion -cli JAR (when org.apache.grails.gradle.grails-plugin-cli is applied) so command-only resources stay off the application runtime classpath while remaining discoverable via grailsCliClasspath. This aligns plugin script-command packaging with the CLI split work and preserves legacy behavior for plugins that do not publish a companion.
Changes:
- Update
GrailsPluginGradlePluginresource packaging sosrc/main/scriptsis copied to the CLI source set’s resources output when a companion exists; otherwise keep historical runtime-JAR packaging. - Add a stale-output cleanup task to prevent incremental builds from retaining previously-copied runtime command resources after migration.
- Add Gradle TestKit coverage (fixture projects + spec) and update user docs to reflect the new packaging behavior and migration path.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsPluginGradlePlugin.groovy | Routes src/main/scripts into CLI vs runtime resources depending on companion presence; adds stale cleanup task. |
| grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/commands/GrailsCliArtifactGradlePlugin.groovy | Exposes a plugin id constant used to detect the companion CLI plugin. |
| grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/commands/PluginScriptCommandPackagingSpec.groovy | TestKit spec asserting correct runtime vs -cli jar contents for script commands and templates. |
| grails-gradle/plugins/src/test/resources/test-projects/plugin-script-commands/settings.gradle | New multi-project TestKit fixture settings for “with-cli” vs “without-cli” plugins. |
| grails-gradle/plugins/src/test/resources/test-projects/plugin-script-commands/gradle.properties | Fixture properties (version + injected Grails version). |
| grails-gradle/plugins/src/test/resources/test-projects/plugin-script-commands/plugin-with-cli/build.gradle | Fixture plugin that applies grails-plugin-cli and inspects runtime vs CLI jar entries (incl. stale-seeding scenario). |
| grails-gradle/plugins/src/test/resources/test-projects/plugin-script-commands/plugin-with-cli/src/main/templates/example.txt | Fixture template to verify templates remain runtime-packaged. |
| grails-gradle/plugins/src/test/resources/test-projects/plugin-script-commands/plugin-with-cli/src/main/scripts/example-script.groovy | Fixture script-command resource expected to be CLI-packaged. |
| grails-gradle/plugins/src/test/resources/test-projects/plugin-script-commands/plugin-with-cli/src/main/scripts/example-multi-step.yml | Fixture YAML multi-step command expected to be CLI-packaged. |
| grails-gradle/plugins/src/test/resources/test-projects/plugin-script-commands/plugin-with-cli/src/main/resources/META-INF/commands/hand-authored.yml | Fixture “hand-authored” runtime command resource expected to remain in runtime jar. |
| grails-gradle/plugins/src/test/resources/test-projects/plugin-script-commands/plugin-without-cli/build.gradle | Fixture plugin without companion; asserts legacy runtime packaging for scripts/templates. |
| grails-gradle/plugins/src/test/resources/test-projects/plugin-script-commands/plugin-without-cli/src/main/templates/legacy-only.txt | Fixture template for non-companion path. |
| grails-gradle/plugins/src/test/resources/test-projects/plugin-script-commands/plugin-without-cli/src/main/scripts/legacy-only-script.groovy | Fixture script-command resource expected to remain runtime-packaged without companion. |
| grails-doc/src/en/guide/upgrading/upgrading80x.adoc | Upgrade notes clarifying script-command packaging with/without companions. |
| grails-doc/src/en/guide/plugins/providingBasicArtefacts.adoc | Plugin docs describing companion routing for src/main/scripts and legacy behavior. |
| grails-doc/src/en/guide/commandLine/creatingCustomCommands.adoc | Command authoring docs updated to mention script/YAML/JSON routing into companions. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
The grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsPluginGradlePlugin.groovy |
jdaugherty
left a comment
There was a problem hiding this comment.
My only concern with this is the regression first found by copilot. Clean tasks can't be forced out of date and always wired
Address review feedback on #16082 (Copilot, Bito, jdaugherty): - Remove cleanStaleRuntimeCommandResources with outputs.upToDateWhen { false } wired into processResources/jar (broke incremental builds). - copyCommands / copyTemplates are Sync tasks with unique dirs under build/tmp/, consumed via process*Resources.from(...) so Gradle owns each path and drops removed sources without a forced clean task. - When a companion is present, filter the runtime jar so META-INF/commands entries that are not hand-authored under src/main/resources are excluded (covers leftover files from prior non-companion packaging without a clean). Assisted-by: Sisyphus:xai/grok-4.5
Review feedback addressed (7a3049d)Responding to @jdaugherty, Copilot, and Bito on the forced-clean regression: Removed
Replaced with Gradle-owned unique outputs
Migration leftovers (no forced clean)When a companion is present, the runtime Verification
|
|
@jdaugherty I am also creating a PR with a Gradle skill based on the last 200 PRs, our gradle code standards and then lastly the Gradle 9 docs, so we can avoid gradle syntax concerns, hopefully, going forward. |
To be clear, this isn't a syntax issue. It's a design issue. I support such a skill though. |
|
@jdaugherty "tomato, tomato", if Gradle had backported fixes, instead of only moving forward and made all these optimizations automatic, without syntax changes, we would have saved ~1,000 hrs on grails-core. The build system should take maybe 5% of our time, it currently takes 25-33%, which is crazy. Hopefully the skill can solve this, since I see all of this as completely wasted time, that should be spent on real code in the framework. I still want the build to be 99% as fast as possible, just can't stand how hard Gradle make it to get there. |
I'm skeptical the skill will solve it. The crux of this issue is Gradle decided to maintain backwards compatibility so it worked, even when the threading model changed. Worse, most of the examples are mixed so it's never clear. Gradle did put together this link though: https://docs.gradle.org/current/userguide/lazy_configuration.html We can try it though. |
copyTemplates is now a Sync task (unique output dirs). Configure it with Sync instead of Copy so project configuration succeeds. Assisted-by: Sisyphus:xai/grok-4.5
|
@jamesfredley if you really wanted to fix this, we should adopt the fixes mentioned here: #15483 (comment) Having gated validations is better than instruction when it comes to AI's doing the right thing. #15497 is the ticket I created to track this. |
|
Let's do #15497, but that might be 5-10% of Gradle build maintenance pain. |
jdaugherty
left a comment
There was a problem hiding this comment.
Switching to sync is not a valid solution because it will break end apps (including the asset plugin usage). Copy will ensure a clean base as long as gradle understands that each task has a unique output - the end result is the processResources task should be only a combination of unique outputs of other tasks. The AI did a better job pointing the details out here so I'm posting it's review.
Address jdaugherty review on #16082: - Keep copyCommands/copyTemplates as Copy (not Sync) with unique build/tmp output dirs so end apps and asset-pipeline typing stay valid - processResources/processCliResources only compose those unique outputs via from(...) - no side-writes into processResources.destinationDir - Drop DuplicatesStrategy.INCLUDE (overlapping paths are config errors) - Drop runtime jar eachFile filter and seedStaleRuntimeCommands fixture - Document one-time ./gradlew clean after first applying grails-plugin-cli on a previously built tree (upgrading80x) Assisted-by: Sisyphus:xai/grok-4.5
Design aligned with jdaugherty review + gradle skill principlesLatest commit applies the unique task output composition model (not Sync, not forced clean, not packaging-time filters):
Tests: |
This comment has been minimized.
This comment has been minimized.
Routing src/main/templates through processResources subjected it to the
`**/*.gsp` exclusion that keeps compiled views out of build/resources/main,
because copy patterns set on a task apply to every spec composed into it.
Plugin templates are frequently GSPs, so this silently emptied them: the
grails-scaffolding jar lost all four META-INF/templates/scaffolding/*.gsp that
generate-views renders from, and grails-spring-security-ui lost all 45
META-INF/templates/views/**/*.gsp behind s2ui-override. Both shipped bare
directory entries instead.
copyCommands and copyTemplates now each own one directory that nothing else
writes, laid out as the archive sees it, attached to a source set output rather
than copied again by process*Resources. They stay Copy tasks, so build scripts
keep using tasks.named('copyTemplates', Copy), and they are registered when the
plugin is applied instead of in afterEvaluate - only the companion routing has
to defer, since grails-plugin-cli is applied after this plugin.
A plain Copy only ever adds, and a private staging directory gets no stale-state
cleanup from Gradle - not even when registered through SourceSetOutput.dir. Each
task therefore clears its own destination in a task action, so a deleted script
or template stops being packaged. Being an action rather than a separate clean
task, it runs only when the task executes and leaves up-to-date checks intact.
Sync would give the same result but would change a task type build scripts
depend on.
src/main/templates was also being contributed twice, once by enableNative2Ascii
and once by copyTemplates, an overlap that DuplicatesStrategy.INCLUDE was
hiding. That wiring moves out of enableNative2Ascii, which it never belonged to,
into an overridable configureTemplateResources that Grails plugins turn off
because copyTemplates already packages them.
PluginScriptCommandPackagingSpec covers companion and non-companion packaging,
GSP templates surviving alongside excluded plugin views, neither copy task
writing into a process*Resources output, and deletions taking effect without a
clean.
jdaugherty
left a comment
There was a problem hiding this comment.
I created #16086 to implement the feedback on this PR
|
Merged #16086 ( Review summary (acceptable): jdaugherty's follow-up correctly fixes a real regression from routing templates through Design matches the unique-output model we converged on, with the right refinements:
Local: all 5 |
jdaugherty
left a comment
There was a problem hiding this comment.
assuming no major test failures, I'm good to merge this.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## 8.0.x #16082 +/- ##
==================================================
- Coverage 51.8882% 51.8801% -0.0082%
- Complexity 18127 18128 +1
==================================================
Files 2046 2046
Lines 96280 96299 +19
Branches 16730 16731 +1
==================================================
+ Hits 49958 49960 +2
- Misses 38948 38967 +19
+ Partials 7374 7372 -2
🚀 New features to boost your workflow:
|
Summary
Addresses #16035 on top of the CLI split (#15948) and legacy command compatibility (#16011).
When a plugin applies
org.apache.grails.gradle.grails-plugin-cli,src/main/scriptscommand resources (*.groovy,*.yml,*.json) are packaged into the companion-clijar asMETA-INF/commandsinstead of the runtime plugin jar. That keeps command-only resources offruntimeClasspath/bootJar/bootWarwhile remaining available throughgrailsCliClasspath.Backwards compatibility
Plugins without a companion keep the historical behavior: scripts still ship in the runtime jar so unmigrated Grails 7 plugins and
legacyCommandSupportconsumers continue to discover them (including the end-to-endlegacy-commandsfixture).Incremental migration
After enabling
grails-plugin-cli, acleanStaleRuntimeCommandResourcestask wipesMETA-INF/commandsunder the configuredprocessResourcesdestination before main resources are processed. That removes priorsrc/main/scriptscopies left by non-companion builds;processResourcesthen restores any hand-authoredsrc/main/resources/META-INF/commands.Test plan
:grails-gradle-plugins:test --tests PluginScriptCommandPackagingSpec(companion + non-companion jar contents, stale cleanup, hand-authored main resources, templates)CliAutoDiscoverySpec,CliCompanionPublishingSpec,LegacyCommandTaskDiscoverySpec,BomCliMultiprojectRaceFunctionalSpecDocs
providingBasicArtefacts.adoccreatingCustomCommands.adocupgrading80x.adoc(CLI companion section)Fixes #16035