Skip to content

fix: route plugin src/main/scripts into companion -cli jars - #16082

Merged
jdaugherty merged 6 commits into
8.0.xfrom
fix/move-plugin-scripts-to-cli-companion
Aug 2, 2026
Merged

fix: route plugin src/main/scripts into companion -cli jars#16082
jdaugherty merged 6 commits into
8.0.xfrom
fix/move-plugin-scripts-to-cli-companion

Conversation

@jamesfredley

Copy link
Copy Markdown
Contributor

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/scripts command resources (*.groovy, *.yml, *.json) are packaged into the companion -cli jar as META-INF/commands instead of the runtime plugin jar. That keeps command-only resources off runtimeClasspath / bootJar / bootWar while remaining available through grailsCliClasspath.

Backwards compatibility

Plugins without a companion keep the historical behavior: scripts still ship in the runtime jar so unmigrated Grails 7 plugins and legacyCommandSupport consumers continue to discover them (including the end-to-end legacy-commands fixture).

Incremental migration

After enabling grails-plugin-cli, a cleanStaleRuntimeCommandResources task wipes META-INF/commands under the configured processResources destination before main resources are processed. That removes prior src/main/scripts copies left by non-companion builds; processResources then restores any hand-authored src/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)
  • Related CLI specs: CliAutoDiscoverySpec, CliCompanionPublishingSpec, LegacyCommandTaskDiscoverySpec, BomCliMultiprojectRaceFunctionalSpec

Docs

  • providingBasicArtefacts.adoc
  • creatingCustomCommands.adoc
  • upgrading80x.adoc (CLI companion section)

Fixes #16035

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
Copilot AI review requested due to automatic review settings August 2, 2026 14:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 GrailsPluginGradlePlugin resource packaging so src/main/scripts is 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.

@bito-code-review

Copy link
Copy Markdown

The cleanStaleRuntimeCommandResources task is indeed configured with outputs.upToDateWhen { false }, which forces it to run every build. This task is wired into processResources and jar tasks, causing them to be invalidated and re-run unnecessarily. To improve this, you can make the cleanup conditional by checking for the existence of non-hand-authored files before deletion, or by using a more granular task input/output definition that only triggers when stale files are actually present.

grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsPluginGradlePlugin.groovy

TaskProvider cleanStaleRuntimeCommands = project.tasks
                        .register('cleanStaleRuntimeCommandResources') { Task cleanTask ->
                            File commandsDir = new File(processResources.destinationDir, 'META-INF/commands')
                            cleanTask.outputs.upToDateWhen { !commandsDir.exists() || commandsDir.list().length == 0 }
                            cleanTask.doLast {
                                project.delete(commandsDir)
                            }
                        }

@jdaugherty jdaugherty left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
@jamesfredley

Copy link
Copy Markdown
Contributor Author

Review feedback addressed (7a3049d)

Responding to @jdaugherty, Copilot, and Bito on the forced-clean regression:

Removed

  • cleanStaleRuntimeCommandResources with outputs.upToDateWhen { false } wired into processResources / jar

Replaced with Gradle-owned unique outputs

  • copyCommands / copyTemplates are Sync tasks writing only to build/tmp/grails-plugin-commands and build/tmp/grails-plugin-templates
  • Companion: processCliResources.from(copyCommands) { into 'META-INF/commands' }
  • No companion: processResources.from(copyCommands) { into 'META-INF/commands' }
  • Templates always: processResources.from(copyTemplates) { into 'META-INF/templates' }

Migration leftovers (no forced clean)

When a companion is present, the runtime jar excludes META-INF/commands/* entries that are not hand-authored under src/main/resources/META-INF/commands. That drops old side-written script copies without disabling incremental builds.

Verification

:grails-gradle-plugins:test --tests PluginScriptCommandPackagingSpec green (companion routing, non-companion legacy packaging, unique Sync dir, seeded stale leftovers excluded, hand-authored kept).

@jamesfredley

Copy link
Copy Markdown
Contributor Author

@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.

@jdaugherty

Copy link
Copy Markdown
Contributor

@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.

@jamesfredley

Copy link
Copy Markdown
Contributor Author

@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.

@jdaugherty

Copy link
Copy Markdown
Contributor

@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
@jdaugherty

jdaugherty commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

@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.

@jamesfredley

Copy link
Copy Markdown
Contributor Author

Let's do #15497, but that might be 5-10% of Gradle build maintenance pain.

@jdaugherty jdaugherty left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
@jamesfredley

Copy link
Copy Markdown
Contributor Author

Design aligned with jdaugherty review + gradle skill principles

Latest commit applies the unique task output composition model (not Sync, not forced clean, not packaging-time filters):

Concern Resolution
Forced clean / always-run tasks Never added back
Sync breaks end apps / asset plugin Copy with unique build/tmp/... dirs
DuplicatesStrategy.INCLUDE smell Removed - fail on overlap
Jar eachFile filter masks stale build/ Removed - document one-time clean
seedStaleRuntimeCommands side-writes Removed

processResources / processCliResources are only a combination of unique upstream Copy outputs via from(...). That is the Gradle design point: ownership via unique outputs, not cleanup theatre.

Tests: PluginScriptCommandPackagingSpec green. :grails-spring-security-ui:copyTemplates configures as Copy again.

@testlens-app

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 jdaugherty left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created #16086 to implement the feedback on this PR

@jamesfredley

Copy link
Copy Markdown
Contributor Author

Merged #16086 (82ebbd775e) into this branch.

Review summary (acceptable): jdaugherty's follow-up correctly fixes a real regression from routing templates through processResources - the **/*.gsp exclusion was silently emptying GSP templates from scaffolding and spring-security-ui jars.

Design matches the unique-output model we converged on, with the right refinements:

  • Staging dirs attached via sourceSet.output.dir(builtBy:) so they never re-enter processResources filters
  • Still Copy (not Sync) for consumer BC
  • doFirst wipe of own destination only when the task runs (up-to-date preserved)
  • Stronger TestKit coverage (GSP survival, staging outside process*Resources, delete-without-clean)

Local: all 5 PluginScriptCommandPackagingSpec features green.

@jdaugherty jdaugherty left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assuming no major test failures, I'm good to merge this.

@codecov

codecov Bot commented Aug 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 33 lines in your changes missing coverage. Please review.
✅ Project coverage is 51.8801%. Comparing base (485ca0f) to head (492b3c5).
⚠️ Report is 8 commits behind head on 8.0.x.

Files with missing lines Patch % Lines
...gradle/plugin/core/GrailsPluginGradlePlugin.groovy 0.0000% 27 Missing ⚠️
...rails/gradle/plugin/core/GrailsGradlePlugin.groovy 0.0000% 6 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@                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     
Files with missing lines Coverage Δ
...ugin/commands/GrailsCliArtifactGradlePlugin.groovy 0.0000% <ø> (ø)
...rails/gradle/plugin/core/GrailsGradlePlugin.groovy 0.0000% <0.0000%> (ø)
...gradle/plugin/core/GrailsPluginGradlePlugin.groovy 0.0000% <0.0000%> (ø)

... and 4 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jdaugherty
jdaugherty merged commit 3d129b7 into 8.0.x Aug 2, 2026
62 of 63 checks passed
@jdaugherty
jdaugherty deleted the fix/move-plugin-scripts-to-cli-companion branch August 2, 2026 19:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Move src/main/scripts command resources out of runtime plugin artifacts

3 participants