Skip to content

Polish all 9 v8 guides for the imminent Grails 8 release#506

Merged
jamesfredley merged 10 commits intomasterfrom
improve-v8-guides-pre-release
May 4, 2026
Merged

Polish all 9 v8 guides for the imminent Grails 8 release#506
jamesfredley merged 10 commits intomasterfrom
improve-v8-guides-pre-release

Conversation

@jamesfredley
Copy link
Copy Markdown
Contributor

Summary

Comprehensive pre-release polish pass on every v8 guide:

  • URL hygiene: drop every prev-snapshot.grails.org reference (the snapshot forge will go stale on release) in favour of start.grails.org per the established pattern in grails-fields-custom-widgets-and-wrappers v8. The curl examples are rewritten as browser-based instructions because start.grails.org is a UI; its REST forge endpoint lives on a separate Cloud Run host that is not part of the user-facing interface. Four "Snapshot Starter" TOC titles drop the "Snapshot" word.
  • grails-spock-test-tour v8: migrate the functional-test chapter from GebSpec to grails.plugin.geb.ContainerGebSpec (the canonical Grails 8 base class - Selenium runs inside a Testcontainers container, no host WebDriver binaries needed). Vendor a new BookFunctionalSpec.groovy snippet, drop geb-with-webdriver-binaries from the forge feature list, and update the registry subtitle + TOC title.
  • grails-github-actions-cicd v8: align the CI/CD job graph with the same ContainerGebSpec story.
  • grails-multi-module v8: add a new devReload.adoc chapter walking the supported org.apache.grails.gradle.grails-exploded plugin recipe (verified at grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/exploded/GrailsExplodedPlugin.groovy on 8.0.x) plus the grails { plugins { ... } } wiring on the consuming webapp side. Expand sharedCore.adoc with cited source paths for the plugin descriptor, GORM cross-plugin auto-discovery, and the Gradle plugin lifecycle. Fix a real Gradle bug in the snippet build.gradle: dual content { } filter blocks AND-combine and silently disabled the snapshot/staging Maven repos for grails* artefacts.
  • grails-htmx v8: refactor TaskController for production hygiene (explicit flush:true, validate-then-save, field-by-field assignment instead of t.properties = params, new show(Long id)), add a new _taskCreated.gsp partial demonstrating the canonical htmx out-of-band swap pattern, encode error messages as HTML, and add the htmx 2.0.4 webjar dependency.
  • grails-rest-library v8: fix a real operator-precedence bug in BookController.bulkCreate (!request.JSON instanceof List parses as (!request.JSON) instanceof List which is always false). Modernise to bindData(), errors.allErrors, and i18n message(error: error). Add pagination (index(Integer max) + countResources()) and an ?author=<id> filter via Book.where { author.id == authorId }. New _errors.gson view + new Liquibase changelog.groovy so the database-migration feature has its entry-point.
  • grails-tailwindcss v8: tighten the asset-pipeline narrative across whatYouWillBuild, gradleWiring, and firstPaint so the <asset:stylesheet> taglib + application.css manifest path is in the body prose, not a sidebar NOTE. tailwindBuild/npmInstall use Provider-based dependsOn and pick up services/, taglib/, and src/main/groovy as extra source dirs. input.css gets tag::components/end::components markers so componentLayer.adoc can include:: only the relevant block.
  • grails-docker-bootbuildimage v8: cite the Spring Boot Gradle plugin coordinate (spring-boot-gradle-plugin:4.0.5, verified against git show 8.0.x:dependencies.gradle), cross-reference grails-doc/src/en/guide/deployment.adoc for WAR-file deployment as a complementary path.
  • grails-vite-spa v8: critical fix - the cors.adoc chapter previously claimed Grails has no built-in CORS support. Wrong: grails-doc/src/en/guide/theWebLayer/cors.adoc documents the grails.cors.enabled block. Rewrote with the canonical YAML config. Cross-reference grails-doc/src/en/guide/REST/, theWebLayer/urlMappings.adoc, theWebLayer/controllers/forwarding.adoc, and gettingStarted/developmentReloading.adoc.
  • grails-fields-custom-widgets-and-wrappers v8: cross-reference the official grails-doc/src/en/guide/theWebLayer/fields.adoc chapter, drop the stale hard-coded 8.0.0-SNAPSHOT version reference, name the taglib source path, and document the additional _themes/<theme>/ lookup level controlled by grails.plugin.fields.theme.
  • Throughout, every chapter that touches files now ends with a Files touched bullet list matching the established grails-fields-custom-widgets-and-wrappers v8 style.

Verification

./gradlew --no-configuration-cache validateGuides -PvalidationMode=both
[validateGuides] mode=both: 93 guide(s) parsed, 0 SKIP-warned, 0 errors

./gradlew --no-configuration-cache renderGuide_grails_*_8 PASSES for all 9 v8 guides (HTML in build/dist/guides/grails-*/8/).

Cross-references verified against grails-core 8.0.x via git -C /home/james/projects/grails-core show 8.0.x:<path> (read-only; never switched branches).

Companion sample-repo updates

The matching grails-guides/<name> sample repos on the grails8 branch have been updated in lock-step (7 of 9 needed changes; docker-bootbuildimage and vite-spa were already in sync). New head commits:

  • grails-spock-test-tour@126c7d8
  • grails-htmx@b682b98
  • grails-rest-library@36507fa
  • grails-multi-module@34415a5
  • grails-tailwindcss@18f48e7
  • grails-github-actions-cicd@b16323c
  • grails-fields-custom-widgets-and-wrappers@41b5737

Apache Grails 8 ships in the coming weeks. The prev-snapshot.grails.org
forge that the v8 guides currently point at will go stale on release.
Switch every reference to the canonical link:https://start.grails.org[]
forge UI per the existing pattern in grails-fields-custom-widgets-and-wrappers
v8.

The mechanical changes:
* createApp.adoc / download.adoc / howto.adoc across grails-tailwindcss,
  grails-github-actions-cicd, grails-rest-library, grails-htmx,
  grails-multi-module, grails-fields-custom-widgets-and-wrappers, and
  grails-docker-bootbuildimage all swap prev-snapshot.grails.org for
  start.grails.org.
* The download.adoc curl examples are rewritten as browser-based
  instructions ("Open start.grails.org, pick the X profile, name
  the app Y, set the package to Z, tick features W, download the
  zip") because start.grails.org is a browser UI; its REST forge
  endpoint lives on a separate Cloud Run host that is not part of
  the user-facing interface.
* The grails-multi-module generateModules.adoc collapses the three
  curl commands into a single browser walkthrough plus an unzip
  + rename block.
* conf/guides.yml: four "Download a Grails 8 Snapshot Starter" TOC
  titles drop the "Snapshot" word now that 8 is shipping; the
  "rest_api Starter" title is normalised to "Rest API Starter".

Assisted-by: claude-code:claude-4.6-opus
…uild refinements

The asset-pipeline integration was correct end-to-end but only mentioned
once, in a NOTE block at the bottom of gradleWiring.adoc. A reader who
skimmed could easily miss that the asset pipeline is what bundles,
fingerprints, gzips, and serves the compiled Tailwind CSS as part of
application.css.

Narrative changes:
* whatYouWillBuild.adoc adds the explicit "...where the asset pipeline
  bundles, fingerprints, and gzips that file into the application.css
  bundle the layout serves" sentence.
* gradleWiring.adoc promotes the asset-pipeline manifest from the NOTE
  sidebar into body prose, naming the *= require app directive and
  the asset:stylesheet taglib that the next chapter uses.
* firstPaint.adoc adds a one-line callout pointing at <asset:stylesheet
  src="application.css"/> as the asset-pipeline taglib that resolves
  the manifest at runtime.

Snippet refinements:
* build.gradle uses Provider syntax for tailwindBuild and npmInstall
  dependsOn (more idiomatic than the string form), declares
  package-lock.json as an extra input, scans grails-app/services,
  grails-app/taglib, and src/main/groovy in addition to views and
  controllers, and adds testAndDevelopmentOnly Bootstrap webjars so
  the dev console pages still render.
* src/main/css/input.css adds tag::components / end::components markers
  so componentLayer.adoc can include only the relevant block via
  include::../snippets/src/main/css/input.css[tag=components,indent=0].
* application.css manifest collapses to *= require app.
* main.gsp and index.gsp are tightened to match the chapter walkthrough.
* package.json bumps to a current Tailwind 4 line.

Across the chapters, every section that touches a file ends with a
"Files touched" bullet list to match the established
grails-fields-custom-widgets-and-wrappers v8 style and a couple of
chapter-end pointers to the matching grails-doc reference manual
sections (deployment.adoc et al.).

Assisted-by: claude-code:claude-4.6-opus
In Grails 8 the canonical (and only fully supported) Geb base class is
grails.plugin.geb.ContainerGebSpec from
testFixtures("org.apache.grails:grails-geb"). It starts a
Selenium-Chrome browser inside a Testcontainers container, removing
the need for host-side WebDriver binaries entirely. The
geb-with-webdriver-binaries forge feature from earlier Grails majors
is obsolete - keeping it adds a second Chrome driver no test ever
uses.

Changes:
* functionalTest.adoc rewrites the chapter narrative around
  ContainerGebSpec, calling out the mandatory @Integration annotation
  (the Spock extension throws IllegalArgumentException at runtime
  without it), the absence of @Rollback (functional tests need
  committed data), and the Docker daemon prerequisite.
* New BookFunctionalSpec.groovy snippet under
  src/integration-test/groovy/example/, vendored so the chapter can
  include::../snippets/...[] like every other code block in the
  guide. The spec extends grails.plugin.geb.ContainerGebSpec and
  carries inline Javadoc explaining why @Integration is mandatory.
* whatYouWillBuild.adoc updates the spec inventory from "four spec
  files exercising four test layers" to "five spec files exercising
  the five test layers", since the functional layer is now first
  class.
* howto.adoc drops the geb-with-webdriver-binaries feature from the
  required forge feature list.
* conf/guides.yml: subtitle now mentions ContainerGebSpec; the
  functionalTest TOC title updates from "Geb Functional Tests" to
  "ContainerGebSpec Functional Tests".

Verified against grails-core 8.0.x at
grails-geb/src/testFixtures/groovy/grails/plugin/geb/.

Assisted-by: claude-code:claude-4.6-opus
The functional-test job in the CI workflow now runs Geb specs that
extend grails.plugin.geb.ContainerGebSpec, so the Selenium-Chrome
browser lives inside a Testcontainers container. The legacy
geb-with-webdriver-binaries forge feature - and its companion
webdriver-binaries-gradle-plugin - are obsolete in Grails 8 and
removed throughout.

Changes:
* createApp.adoc: drop geb-with-webdriver-binaries from the required
  forge feature list (postgres + testcontainers are sufficient; the
  ContainerGebSpec test fixtures already ship in the standard web
  profile).
* download.adoc: remove the legacy
  integrationTestImplementation testFixtures("org.apache.grails:grails-geb")
  + webdriver-binaries Gradle plugin pair from the chapter narrative.
* functionalTests.adoc: rewrite the chapter to describe Selenium-in-
  Testcontainers (no host-side Chrome install needed; first cold run
  pulls selenium/standalone-chrome ~200 MB; subsequent runs reuse
  the cached layer).
* howto.adoc: drop the geb-with-webdriver-binaries feature mention
  from the prereq summary.
* The other chapters (badge, branchProtection, ciYml, dependabot,
  ghcrPush, integrationTests, releaseYml, unitTests, workflowOverview,
  whatYouWillBuild, requirements, gettingStarted, helpWithGrails)
  pick up the consistent "Files touched" bullet style and minor
  prose tightening.

Verified against grails-core 8.0.x at
grails-geb/src/testFixtures/groovy/grails/plugin/geb/.

Assisted-by: claude-code:claude-4.6-opus
…or walkthrough

The most common day-to-day question for a multi-project Grails build is
"how do I save a file in shared-core and have my running webapp
bootRun pick it up?". Grails 8 answers this with a first-class plugin
- org.apache.grails.gradle.grails-exploded - that publishes exploded
classes/resources from a Grails Plugin subproject so the consuming
webapp's bootRun can request the exploded variant automatically.
Source: grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/exploded/GrailsExplodedPlugin.groovy.

Changes:
* New chapter devReload.adoc walks the supported recipe (apply
  org.apache.grails.gradle.grails-exploded in shared-core, declare
  the plugin under the grails { plugins { ... } } block in each
  webapp), includes a what-reloads vs what-restarts table, and keeps
  the manual bootRun classpath patch + spring.devtools.restart.additional-paths
  approach as alternatives in NOTE blocks.
* runningEach.adoc forward-references the new chapter, documents
  bootJar deployment, and clarifies that shared-core does not produce
  a deployable bootJar (GrailsPluginGradlePlugin disables bootJar for
  the plugin project).
* sharedCore.adoc expands the plugin descriptor explanation with
  source citations: grails-core/src/main/groovy/grails/plugins/Plugin.groovy
  for the lifecycle hooks; grails-core/src/main/groovy/org/grails/core/artefact/DomainClassArtefactHandler.java,
  grails-domain-class/src/main/groovy/org/grails/plugins/domain/support/DefaultMappingContextFactoryBean.groovy,
  and grails-data-hibernate5/grails-plugin/src/main/groovy/grails/plugin/hibernate/HibernateGrailsPlugin.groovy
  for cross-plugin domain auto-discovery.
* New shared-core/src/main/groovy/example/SharedCoreGrailsPlugin.groovy
  snippet so sharedCore.adoc can include::../snippets/...[].
* snippets/build.gradle: collapse the dual content { } filter blocks
  in the snapshots/staging Maven repos into a single block per repo
  (separate blocks AND-combine, which silently disables the
  repository - the intent is OR-combine), drop the redundant
  plugins.withId('groovy') guard around useJUnitPlatform.
* snippets/settings.gradle: minor cleanup matching the chapter.
* All chapter prose picks up source-citation NOTE blocks and "Files
  touched" bullets to match the established
  grails-fields-custom-widgets-and-wrappers v8 style.
* conf/guides.yml: TOC entry for devReload between runningEach and
  testingPerModule.

Assisted-by: claude-code:claude-4.6-opus
The TaskController action set is tightened for production hygiene:
explicit flush:true on every save and delete (deterministic before
the response is rendered), validate-before-save flow with explicit
error rendering, field-by-field assignment instead of mass assignment
through params (the original 't.properties = params' allowed
arbitrary fields to slip through), and a new show(Long id) action.

A new _taskCreated.gsp partial demonstrates the canonical htmx
out-of-band swap pattern: the response renders the new task into
#taskList while replacing the form with a fresh empty one, all in a
single round trip - a teaching point the chapter narrative now
covers explicitly.

Other view-side cleanups:
* _taskForm.gsp: g:form + g:textField + g:hasErrors structure with
  encodeAsHTML on error message output.
* _task / _taskEdit / _taskRows / index / main: consistent formatting,
  attribute ordering, and label improvements.
* UrlMappings.groovy and Task.groovy: minor cosmetic alignment with
  the chapter walkthrough.

Each chapter ends with the established "Files touched" bullet list.

Assisted-by: claude-code:claude-4.6-opus
…SON views

The bulk-create handler had a real operator-precedence bug:

    if (!request.JSON instanceof List) { ... }

parses as

    if ((!request.JSON) instanceof List) { ... }

which is always false (a falsy value 'instanceof List' is never true),
so the body-shape guard never fired and a single-object request body
silently bound nothing. The fix is the parenthesised form
'if (!(request.JSON instanceof List))'.

The handler is also modernised to current Grails 8 idioms: bindData()
instead of 'new Book(json as Map)' (so domain-class binding rules
fire); errors.allErrors instead of errors.fieldErrors (catches
global errors too); message(error: error) for i18n message resolution
through the standard Grails MessageSource; and failOnError: true on
the final per-row save so a validation slip cannot reach
production. The success path now respond()s through the index view
template so the response shape stays consistent with the regular
list endpoint.

A new index() override paginates the list with a sensible 25/100
default/cap; a new countResources() override surfaces a bookCount
in the model so JSON views can render a total. Both honour an
optional ?author=<id> filter via Book.where { author.id == authorId }.

Other refreshes:
* AuthorController.groovy and BootStrap.groovy align with the same
  patterns.
* All .gson views (_book, _author, index, show) get a cleaner shape
  matching the chapter walkthrough.
* New _errors.gson view: structured 422 error response shape used by
  the validation chapter.
* New changelog.groovy under grails-app/migrations/: the Liquibase
  entry-point the database-migration feature requires; the chapter
  references it via include::../snippets/...[].

Assisted-by: claude-code:claude-4.6-opus
…ocker-bootbuildimage v8

The bootBuildImage chapter now explicitly cites the Spring Boot
Gradle plugin coordinate (spring-boot-gradle-plugin:4.0.5) verified
against grails-core 8.0.x dependencies.gradle. The whatYouWillBuild
chapter notes that the official Apache Grails 8 reference manual at
grails-doc/src/en/guide/deployment.adoc covers WAR-file deployment
to a servlet container, while this guide complements that with the
modern OCI-image path enabled by Spring Boot 4.

Each chapter (whatYouWillBuild, dockerOverview, bootBuildImage,
runImage, envProfiles, postgresWiring, compose, healthCheck,
jvmContainer, nonRoot, dockerfile, ghcr) closes with the
established "Files touched" bullet list so a reader can scan
exactly which on-disk files each chapter expects them to edit.

Assisted-by: claude-code:claude-4.6-opus
The cors.adoc chapter previously claimed there was nothing
Grails-specific for CORS handling and pointed only at the Spring Boot
CORS reference. That was wrong: grails-doc/src/en/guide/theWebLayer/cors.adoc
documents Grails 8's first-class grails.cors.enabled configuration
block, which produces a Spring CorsConfiguration mapped to /** by
default and supports per-mapping overrides.

The chapter now shows the canonical grails.cors block:

    grails:
        cors:
            enabled: true
            allowedOrigins:
                - 'https://app.example.com'
            ...

while keeping the original recommendation that the same-origin layout
this guide builds removes the need for CORS entirely - the Grails
config is only relevant if you split the SPA and the API across
origins.

Other chapter additions:
* spaController.adoc cites the underlying source for the forward
  method (grails-controllers/src/main/groovy/grails/web/util/WebUtils.java)
  and the user-facing reference at
  grails-doc/src/en/guide/theWebLayer/controllers/forwarding.adoc.
* urlMappings.adoc points at grails-doc/src/en/guide/theWebLayer/urlMappings.adoc
  and the restfulMappings sub-chapter.
* devMode.adoc references grails-doc/src/en/guide/gettingStarted/developmentReloading.adoc
  for the Spring DevTools story.
* whatYouWillBuild.adoc and backend.adoc add the matching
  grails-doc/src/en/guide/REST/ pointers.

Every substantive chapter ends with a "Files touched" bullet list
matching the established style.

Assisted-by: claude-code:claude-4.6-opus
…ustom-widgets-and-wrappers v8

grails-doc/src/en/guide/theWebLayer/fields.adoc (and its fields/
subdirectory) is the official 8.0.x reference for the Fields plugin
high-level f: taglib API. This guide complements it with worked
examples of customising the per-property and per-class templates that
the plugin's FormFieldsTemplateService looks up.

Changes:
* fieldsPluginAlreadyIncluded.adoc: drop the stale '8.0.0-SNAPSHOT'
  hard-coded version reference (matches the grails-core release
  pinned by the consumer's project), name the taglib path
  'grails-fields/grails-app/taglib/grails/plugin/formfields/FormFieldsTagLib.groovy',
  and add the cross-reference to grails-doc/src/en/guide/theWebLayer/fields.adoc.
* templateLookup.adoc: cite the source path for the lookup logic
  (grails-fields/src/main/groovy/grails/plugin/formfields/FormFieldsTemplateService.groovy)
  and document the additional _themes/<theme>/ lookup level controlled
  by grails.plugin.fields.theme (THEMES_FOLDER constant in
  FormFieldsTemplateService).

Assisted-by: claude-code:claude-4.6-opus
Copilot AI review requested due to automatic review settings May 4, 2026 12:32
@jamesfredley jamesfredley merged commit b53c07d into master May 4, 2026
4 of 5 checks passed
@jamesfredley jamesfredley deleted the improve-v8-guides-pre-release branch May 4, 2026 12:34
@jamesfredley jamesfredley review requested due to automatic review settings May 4, 2026 12:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant