)]}'
{
  "log": [
    {
      "commit": "b53c07d6a5ef672ce4f2db07ef6a332067881ece",
      "tree": "b1e8b2735cbcc01bbb97c0185cfac1911b5fa160",
      "parents": [
        "18daf93d43fa48d0dc1674849ed65cf297cf91bd",
        "681bbc4472689ce2474cf286bc87858c03f87891"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Mon May 04 08:34:02 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon May 04 08:34:02 2026 -0400"
      },
      "message": "Merge pull request #506 from apache/improve-v8-guides-pre-release\n\nPolish all 9 v8 guides for the imminent Grails 8 release"
    },
    {
      "commit": "681bbc4472689ce2474cf286bc87858c03f87891",
      "tree": "b1e8b2735cbcc01bbb97c0185cfac1911b5fa160",
      "parents": [
        "aa1526d3205d73185c5510fd2a542297d07642b2"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Mon May 04 08:31:04 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Mon May 04 08:31:04 2026 -0400"
      },
      "message": "Cross-reference grails-doc Fields plugin chapter from grails-fields-custom-widgets-and-wrappers v8\n\ngrails-doc/src/en/guide/theWebLayer/fields.adoc (and its fields/\nsubdirectory) is the official 8.0.x reference for the Fields plugin\nhigh-level f: taglib API. This guide complements it with worked\nexamples of customising the per-property and per-class templates that\nthe plugin\u0027s FormFieldsTemplateService looks up.\n\nChanges:\n* fieldsPluginAlreadyIncluded.adoc: drop the stale \u00278.0.0-SNAPSHOT\u0027\n  hard-coded version reference (matches the grails-core release\n  pinned by the consumer\u0027s project), name the taglib path\n  \u0027grails-fields/grails-app/taglib/grails/plugin/formfields/FormFieldsTagLib.groovy\u0027,\n  and add the cross-reference to grails-doc/src/en/guide/theWebLayer/fields.adoc.\n* templateLookup.adoc: cite the source path for the lookup logic\n  (grails-fields/src/main/groovy/grails/plugin/formfields/FormFieldsTemplateService.groovy)\n  and document the additional _themes/\u003ctheme\u003e/ lookup level controlled\n  by grails.plugin.fields.theme (THEMES_FOLDER constant in\n  FormFieldsTemplateService).\n\nAssisted-by: claude-code:claude-4.6-opus\n"
    },
    {
      "commit": "aa1526d3205d73185c5510fd2a542297d07642b2",
      "tree": "29178ca588337bf929b9d206d1c2e58e7805c2b0",
      "parents": [
        "0abf497504a3cd8fa19853de73653ed5d142ed42"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Mon May 04 08:30:54 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Mon May 04 08:30:54 2026 -0400"
      },
      "message": "Correct grails-vite-spa v8 CORS chapter + cross-reference grails-doc\n\nThe cors.adoc chapter previously claimed there was nothing\nGrails-specific for CORS handling and pointed only at the Spring Boot\nCORS reference. That was wrong: grails-doc/src/en/guide/theWebLayer/cors.adoc\ndocuments Grails 8\u0027s first-class grails.cors.enabled configuration\nblock, which produces a Spring CorsConfiguration mapped to /** by\ndefault and supports per-mapping overrides.\n\nThe chapter now shows the canonical grails.cors block:\n\n    grails:\n        cors:\n            enabled: true\n            allowedOrigins:\n                - \u0027https://app.example.com\u0027\n            ...\n\nwhile keeping the original recommendation that the same-origin layout\nthis guide builds removes the need for CORS entirely - the Grails\nconfig is only relevant if you split the SPA and the API across\norigins.\n\nOther chapter additions:\n* spaController.adoc cites the underlying source for the forward\n  method (grails-controllers/src/main/groovy/grails/web/util/WebUtils.java)\n  and the user-facing reference at\n  grails-doc/src/en/guide/theWebLayer/controllers/forwarding.adoc.\n* urlMappings.adoc points at grails-doc/src/en/guide/theWebLayer/urlMappings.adoc\n  and the restfulMappings sub-chapter.\n* devMode.adoc references grails-doc/src/en/guide/gettingStarted/developmentReloading.adoc\n  for the Spring DevTools story.\n* whatYouWillBuild.adoc and backend.adoc add the matching\n  grails-doc/src/en/guide/REST/ pointers.\n\nEvery substantive chapter ends with a \"Files touched\" bullet list\nmatching the established style.\n\nAssisted-by: claude-code:claude-4.6-opus\n"
    },
    {
      "commit": "0abf497504a3cd8fa19853de73653ed5d142ed42",
      "tree": "dd118cba086304bc367862e0ea30b6b7c2852966",
      "parents": [
        "4cf2039e2d4a61f40c0178e3a54f1c0ce9e5227b"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Mon May 04 08:30:34 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Mon May 04 08:30:34 2026 -0400"
      },
      "message": "Add Files-touched bullets and grails-doc cross-references to grails-docker-bootbuildimage v8\n\nThe bootBuildImage chapter now explicitly cites the Spring Boot\nGradle plugin coordinate (spring-boot-gradle-plugin:4.0.5) verified\nagainst grails-core 8.0.x dependencies.gradle. The whatYouWillBuild\nchapter notes that the official Apache Grails 8 reference manual at\ngrails-doc/src/en/guide/deployment.adoc covers WAR-file deployment\nto a servlet container, while this guide complements that with the\nmodern OCI-image path enabled by Spring Boot 4.\n\nEach chapter (whatYouWillBuild, dockerOverview, bootBuildImage,\nrunImage, envProfiles, postgresWiring, compose, healthCheck,\njvmContainer, nonRoot, dockerfile, ghcr) closes with the\nestablished \"Files touched\" bullet list so a reader can scan\nexactly which on-disk files each chapter expects them to edit.\n\nAssisted-by: claude-code:claude-4.6-opus\n"
    },
    {
      "commit": "4cf2039e2d4a61f40c0178e3a54f1c0ce9e5227b",
      "tree": "bbb414f4b55003dbaee3eba45b9425d0468146c0",
      "parents": [
        "b8fbed4120fb19366e3b98734d82d6f0b36e9913"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Mon May 04 08:30:26 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Mon May 04 08:30:26 2026 -0400"
      },
      "message": "Fix grails-rest-library v8 BookController bulk-create bug + refresh JSON views\n\nThe bulk-create handler had a real operator-precedence bug:\n\n    if (!request.JSON instanceof List) { ... }\n\nparses as\n\n    if ((!request.JSON) instanceof List) { ... }\n\nwhich is always false (a falsy value \u0027instanceof List\u0027 is never true),\nso the body-shape guard never fired and a single-object request body\nsilently bound nothing. The fix is the parenthesised form\n\u0027if (!(request.JSON instanceof List))\u0027.\n\nThe handler is also modernised to current Grails 8 idioms: bindData()\ninstead of \u0027new Book(json as Map)\u0027 (so domain-class binding rules\nfire); errors.allErrors instead of errors.fieldErrors (catches\nglobal errors too); message(error: error) for i18n message resolution\nthrough the standard Grails MessageSource; and failOnError: true on\nthe final per-row save so a validation slip cannot reach\nproduction. The success path now respond()s through the index view\ntemplate so the response shape stays consistent with the regular\nlist endpoint.\n\nA new index() override paginates the list with a sensible 25/100\ndefault/cap; a new countResources() override surfaces a bookCount\nin the model so JSON views can render a total. Both honour an\noptional ?author\u003d\u003cid\u003e filter via Book.where { author.id \u003d\u003d authorId }.\n\nOther refreshes:\n* AuthorController.groovy and BootStrap.groovy align with the same\n  patterns.\n* All .gson views (_book, _author, index, show) get a cleaner shape\n  matching the chapter walkthrough.\n* New _errors.gson view: structured 422 error response shape used by\n  the validation chapter.\n* New changelog.groovy under grails-app/migrations/: the Liquibase\n  entry-point the database-migration feature requires; the chapter\n  references it via include::../snippets/...[].\n\nAssisted-by: claude-code:claude-4.6-opus\n"
    },
    {
      "commit": "b8fbed4120fb19366e3b98734d82d6f0b36e9913",
      "tree": "79f550180ee1850b1ccfdb0e135e98af99df3149",
      "parents": [
        "9f7e442e3a8beb87086ceddf28b2181913a66e1f"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Mon May 04 08:30:09 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Mon May 04 08:30:09 2026 -0400"
      },
      "message": "Refactor grails-htmx v8 TaskController + add OOB-swap created partial\n\nThe TaskController action set is tightened for production hygiene:\nexplicit flush:true on every save and delete (deterministic before\nthe response is rendered), validate-before-save flow with explicit\nerror rendering, field-by-field assignment instead of mass assignment\nthrough params (the original \u0027t.properties \u003d params\u0027 allowed\narbitrary fields to slip through), and a new show(Long id) action.\n\nA new _taskCreated.gsp partial demonstrates the canonical htmx\nout-of-band swap pattern: the response renders the new task into\n#taskList while replacing the form with a fresh empty one, all in a\nsingle round trip - a teaching point the chapter narrative now\ncovers explicitly.\n\nOther view-side cleanups:\n* _taskForm.gsp: g:form + g:textField + g:hasErrors structure with\n  encodeAsHTML on error message output.\n* _task / _taskEdit / _taskRows / index / main: consistent formatting,\n  attribute ordering, and label improvements.\n* UrlMappings.groovy and Task.groovy: minor cosmetic alignment with\n  the chapter walkthrough.\n\nEach chapter ends with the established \"Files touched\" bullet list.\n\nAssisted-by: claude-code:claude-4.6-opus\n"
    },
    {
      "commit": "9f7e442e3a8beb87086ceddf28b2181913a66e1f",
      "tree": "7105416e5e9e5e0d0d7a631e0662c01663503725",
      "parents": [
        "1150b5a0e78ef0a04f74860739b3cd983067c5e5"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Mon May 04 08:29:51 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Mon May 04 08:29:51 2026 -0400"
      },
      "message": "Add grails-multi-module v8 dev-reload chapter + source-cited descriptor walkthrough\n\nThe most common day-to-day question for a multi-project Grails build is\n\"how do I save a file in shared-core and have my running webapp\nbootRun pick it up?\". Grails 8 answers this with a first-class plugin\n- org.apache.grails.gradle.grails-exploded - that publishes exploded\nclasses/resources from a Grails Plugin subproject so the consuming\nwebapp\u0027s bootRun can request the exploded variant automatically.\nSource: grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/exploded/GrailsExplodedPlugin.groovy.\n\nChanges:\n* New chapter devReload.adoc walks the supported recipe (apply\n  org.apache.grails.gradle.grails-exploded in shared-core, declare\n  the plugin under the grails { plugins { ... } } block in each\n  webapp), includes a what-reloads vs what-restarts table, and keeps\n  the manual bootRun classpath patch + spring.devtools.restart.additional-paths\n  approach as alternatives in NOTE blocks.\n* runningEach.adoc forward-references the new chapter, documents\n  bootJar deployment, and clarifies that shared-core does not produce\n  a deployable bootJar (GrailsPluginGradlePlugin disables bootJar for\n  the plugin project).\n* sharedCore.adoc expands the plugin descriptor explanation with\n  source citations: grails-core/src/main/groovy/grails/plugins/Plugin.groovy\n  for the lifecycle hooks; grails-core/src/main/groovy/org/grails/core/artefact/DomainClassArtefactHandler.java,\n  grails-domain-class/src/main/groovy/org/grails/plugins/domain/support/DefaultMappingContextFactoryBean.groovy,\n  and grails-data-hibernate5/grails-plugin/src/main/groovy/grails/plugin/hibernate/HibernateGrailsPlugin.groovy\n  for cross-plugin domain auto-discovery.\n* New shared-core/src/main/groovy/example/SharedCoreGrailsPlugin.groovy\n  snippet so sharedCore.adoc can include::../snippets/...[].\n* snippets/build.gradle: collapse the dual content { } filter blocks\n  in the snapshots/staging Maven repos into a single block per repo\n  (separate blocks AND-combine, which silently disables the\n  repository - the intent is OR-combine), drop the redundant\n  plugins.withId(\u0027groovy\u0027) guard around useJUnitPlatform.\n* snippets/settings.gradle: minor cleanup matching the chapter.\n* All chapter prose picks up source-citation NOTE blocks and \"Files\n  touched\" bullets to match the established\n  grails-fields-custom-widgets-and-wrappers v8 style.\n* conf/guides.yml: TOC entry for devReload between runningEach and\n  testingPerModule.\n\nAssisted-by: claude-code:claude-4.6-opus\n"
    },
    {
      "commit": "1150b5a0e78ef0a04f74860739b3cd983067c5e5",
      "tree": "e707563914c03db3ca0711b21ddcd0967443ee3c",
      "parents": [
        "0b9571310bdb9b0e1e0efbf1f7baec9159032099"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Mon May 04 08:29:16 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Mon May 04 08:29:16 2026 -0400"
      },
      "message": "Align grails-github-actions-cicd v8 with ContainerGebSpec story\n\nThe functional-test job in the CI workflow now runs Geb specs that\nextend grails.plugin.geb.ContainerGebSpec, so the Selenium-Chrome\nbrowser lives inside a Testcontainers container. The legacy\ngeb-with-webdriver-binaries forge feature - and its companion\nwebdriver-binaries-gradle-plugin - are obsolete in Grails 8 and\nremoved throughout.\n\nChanges:\n* createApp.adoc: drop geb-with-webdriver-binaries from the required\n  forge feature list (postgres + testcontainers are sufficient; the\n  ContainerGebSpec test fixtures already ship in the standard web\n  profile).\n* download.adoc: remove the legacy\n  integrationTestImplementation testFixtures(\"org.apache.grails:grails-geb\")\n  + webdriver-binaries Gradle plugin pair from the chapter narrative.\n* functionalTests.adoc: rewrite the chapter to describe Selenium-in-\n  Testcontainers (no host-side Chrome install needed; first cold run\n  pulls selenium/standalone-chrome ~200 MB; subsequent runs reuse\n  the cached layer).\n* howto.adoc: drop the geb-with-webdriver-binaries feature mention\n  from the prereq summary.\n* The other chapters (badge, branchProtection, ciYml, dependabot,\n  ghcrPush, integrationTests, releaseYml, unitTests, workflowOverview,\n  whatYouWillBuild, requirements, gettingStarted, helpWithGrails)\n  pick up the consistent \"Files touched\" bullet style and minor\n  prose tightening.\n\nVerified against grails-core 8.0.x at\ngrails-geb/src/testFixtures/groovy/grails/plugin/geb/.\n\nAssisted-by: claude-code:claude-4.6-opus\n"
    },
    {
      "commit": "0b9571310bdb9b0e1e0efbf1f7baec9159032099",
      "tree": "5eac91b6113ef312e7f91b757747f6b86f1c3f41",
      "parents": [
        "784043aab23b2bf89cc35cd60ff99d6be9aae5ab"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Mon May 04 08:28:56 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Mon May 04 08:28:56 2026 -0400"
      },
      "message": "Migrate grails-spock-test-tour v8 functional tests to ContainerGebSpec\n\nIn Grails 8 the canonical (and only fully supported) Geb base class is\ngrails.plugin.geb.ContainerGebSpec from\ntestFixtures(\"org.apache.grails:grails-geb\"). It starts a\nSelenium-Chrome browser inside a Testcontainers container, removing\nthe need for host-side WebDriver binaries entirely. The\ngeb-with-webdriver-binaries forge feature from earlier Grails majors\nis obsolete - keeping it adds a second Chrome driver no test ever\nuses.\n\nChanges:\n* functionalTest.adoc rewrites the chapter narrative around\n  ContainerGebSpec, calling out the mandatory @Integration annotation\n  (the Spock extension throws IllegalArgumentException at runtime\n  without it), the absence of @Rollback (functional tests need\n  committed data), and the Docker daemon prerequisite.\n* New BookFunctionalSpec.groovy snippet under\n  src/integration-test/groovy/example/, vendored so the chapter can\n  include::../snippets/...[] like every other code block in the\n  guide. The spec extends grails.plugin.geb.ContainerGebSpec and\n  carries inline Javadoc explaining why @Integration is mandatory.\n* whatYouWillBuild.adoc updates the spec inventory from \"four spec\n  files exercising four test layers\" to \"five spec files exercising\n  the five test layers\", since the functional layer is now first\n  class.\n* howto.adoc drops the geb-with-webdriver-binaries feature from the\n  required forge feature list.\n* conf/guides.yml: subtitle now mentions ContainerGebSpec; the\n  functionalTest TOC title updates from \"Geb Functional Tests\" to\n  \"ContainerGebSpec Functional Tests\".\n\nVerified against grails-core 8.0.x at\ngrails-geb/src/testFixtures/groovy/grails/plugin/geb/.\n\nAssisted-by: claude-code:claude-4.6-opus\n"
    },
    {
      "commit": "784043aab23b2bf89cc35cd60ff99d6be9aae5ab",
      "tree": "588e775b741ffc554608ae61877e398010d76550",
      "parents": [
        "8a2067c775516e1acede1a9bcee760efb3bfcb3a"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Mon May 04 08:28:26 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Mon May 04 08:28:26 2026 -0400"
      },
      "message": "Tighten grails-tailwindcss v8 asset-pipeline narrative + Tailwind 4 build refinements\n\nThe asset-pipeline integration was correct end-to-end but only mentioned\nonce, in a NOTE block at the bottom of gradleWiring.adoc. A reader who\nskimmed could easily miss that the asset pipeline is what bundles,\nfingerprints, gzips, and serves the compiled Tailwind CSS as part of\napplication.css.\n\nNarrative changes:\n* whatYouWillBuild.adoc adds the explicit \"...where the asset pipeline\n  bundles, fingerprints, and gzips that file into the application.css\n  bundle the layout serves\" sentence.\n* gradleWiring.adoc promotes the asset-pipeline manifest from the NOTE\n  sidebar into body prose, naming the *\u003d require app directive and\n  the asset:stylesheet taglib that the next chapter uses.\n* firstPaint.adoc adds a one-line callout pointing at \u003casset:stylesheet\n  src\u003d\"application.css\"/\u003e as the asset-pipeline taglib that resolves\n  the manifest at runtime.\n\nSnippet refinements:\n* build.gradle uses Provider syntax for tailwindBuild and npmInstall\n  dependsOn (more idiomatic than the string form), declares\n  package-lock.json as an extra input, scans grails-app/services,\n  grails-app/taglib, and src/main/groovy in addition to views and\n  controllers, and adds testAndDevelopmentOnly Bootstrap webjars so\n  the dev console pages still render.\n* src/main/css/input.css adds tag::components / end::components markers\n  so componentLayer.adoc can include only the relevant block via\n  include::../snippets/src/main/css/input.css[tag\u003dcomponents,indent\u003d0].\n* application.css manifest collapses to *\u003d require app.\n* main.gsp and index.gsp are tightened to match the chapter walkthrough.\n* package.json bumps to a current Tailwind 4 line.\n\nAcross the chapters, every section that touches a file ends with a\n\"Files touched\" bullet list to match the established\ngrails-fields-custom-widgets-and-wrappers v8 style and a couple of\nchapter-end pointers to the matching grails-doc reference manual\nsections (deployment.adoc et al.).\n\nAssisted-by: claude-code:claude-4.6-opus\n"
    },
    {
      "commit": "8a2067c775516e1acede1a9bcee760efb3bfcb3a",
      "tree": "4c7d43c3563fe9a18c87826db37c9611afd1a050",
      "parents": [
        "18daf93d43fa48d0dc1674849ed65cf297cf91bd"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Mon May 04 08:28:01 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Mon May 04 08:28:01 2026 -0400"
      },
      "message": "Replace prev-snapshot.grails.org with start.grails.org across v8 guides\n\nApache Grails 8 ships in the coming weeks. The prev-snapshot.grails.org\nforge that the v8 guides currently point at will go stale on release.\nSwitch every reference to the canonical link:https://start.grails.org[]\nforge UI per the existing pattern in grails-fields-custom-widgets-and-wrappers\nv8.\n\nThe mechanical changes:\n* createApp.adoc / download.adoc / howto.adoc across grails-tailwindcss,\n  grails-github-actions-cicd, grails-rest-library, grails-htmx,\n  grails-multi-module, grails-fields-custom-widgets-and-wrappers, and\n  grails-docker-bootbuildimage all swap prev-snapshot.grails.org for\n  start.grails.org.\n* The download.adoc curl examples are rewritten as browser-based\n  instructions (\"Open start.grails.org, pick the X profile, name\n  the app Y, set the package to Z, tick features W, download the\n  zip\") because start.grails.org is a browser UI; its REST forge\n  endpoint lives on a separate Cloud Run host that is not part of\n  the user-facing interface.\n* The grails-multi-module generateModules.adoc collapses the three\n  curl commands into a single browser walkthrough plus an unzip\n  + rename block.\n* conf/guides.yml: four \"Download a Grails 8 Snapshot Starter\" TOC\n  titles drop the \"Snapshot\" word now that 8 is shipping; the\n  \"rest_api Starter\" title is normalised to \"Rest API Starter\".\n\nAssisted-by: claude-code:claude-4.6-opus\n"
    },
    {
      "commit": "18daf93d43fa48d0dc1674849ed65cf297cf91bd",
      "tree": "ce27e22c7807a30249e14999c18dcbe7051f217a",
      "parents": [
        "18dfc5120c927ea0e5440879421bdb778cff0266",
        "6fde52319b3ce7d291732d4cff81b8cc55675ffe"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Mon May 04 07:02:38 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon May 04 07:02:38 2026 -0400"
      },
      "message": "Merge pull request #505 from apache/retag-guides-canonical-taxonomy\n\nRetag every guide with a generous canonical taxonomy and reshape categories"
    },
    {
      "commit": "6fde52319b3ce7d291732d4cff81b8cc55675ffe",
      "tree": "ce27e22c7807a30249e14999c18dcbe7051f217a",
      "parents": [
        "11e8c0a7c48de1b715cac529a869cc1c846557e3"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Mon May 04 06:31:31 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Mon May 04 06:31:31 2026 -0400"
      },
      "message": "Drop the inaccurate \u0027~70 canonical tags\u0027 figure from tagCloud Javadoc\n\nPer Copilot review on PR #505: the actual taxonomy ended up at ~290\nunique tags (driven by per-guide enrichment, not by a top-down cap),\nso the docstring\u0027s \u0027~70\u0027 figure is misleading. The taxonomy is\nhand-curated but unbounded - the docstring now just says so.\n\nAssisted-by: claude-code:claude-opus-4-7\n"
    },
    {
      "commit": "11e8c0a7c48de1b715cac529a869cc1c846557e3",
      "tree": "f1de996c90568b6843671c7fa68473a50bc0de3f",
      "parents": [
        "526da09eb9e4c25fbcab0dfe3ff05348a6f2c6e3"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Mon May 04 06:19:59 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Mon May 04 06:19:59 2026 -0400"
      },
      "message": "Retag every guide with a generous canonical taxonomy and reshape categories\n\nTags are the primary browse mechanism on the guides index - clicking a\ntag lands on a curated page listing every guide carrying that tag - so\nit is worth investing in a serious taxonomy. The previous YAML had 195\nunique tags from a decade of ad-hoc tagging, with 73% of them appearing\nin just 1-2 guides (typos like aws-elasticbeanstlak, synonyms like\nrest+rest-api+restful-controller, version labels like grails3..grails8,\nbrowser-specific noise like firefox+chrome, and one-off dead-ends).\nAfter this change the registry has 289 thoughtfully assigned tags from\na curated taxonomy, every guide carries 8-15 generous tags reflecting\nwhat it teaches, and the bottom-of-page category grid is reshaped\naround the tracks Apache Grails 7/8 actually publishes today.\n\n\u003d\u003d conf/guides.yml: hand-curated tag list per guide (93 entries) \u003d\u003d\n\nEvery guide now has an explicit, generous tag set covering web stack,\npersistence, testing, security, devops, frontend, libraries, and\npatterns. Tags were derived from each guide\u0027s intro chapter, title,\nsubtitle, and category so they actually describe the guide content.\n\nTop tags after the retag (by occurrence):\n\n  47  rest-api          21  unit-tests        17  gradle        13  spa\n  32  gorm              20  domain-classes    17  json-views    13  hibernate\n  21  testing           19  controllers       16  geb           12  spring-boot\n  21  unit-tests        18  devops            16  gsp           12  authentication\n                        18  functional-tests  15  integration-tests\n\nDistribution:\n   93 tags in 1 guide         60 tags in 3-5 guides     24 tags in 11+ guides\n   92 tags in 2 guides        20 tags in 6-10 guides\n\nSynonym normalisation, typo fixes, and version-label removal applied:\n  rest, restful-controller       -\u003e rest-api\n  unit-test                      -\u003e unit-tests\n  integration-test               -\u003e integration-tests\n  functional-test                -\u003e functional-tests\n  test                           -\u003e testing\n  mock                           -\u003e mocking\n  json views, json-view          -\u003e json-views\n  compose                        -\u003e docker-compose\n  paketo                         -\u003e buildpacks\n  health                         -\u003e actuator\n  fields                         -\u003e fields-plugin\n  log, logs                      -\u003e logging\n  locale                         -\u003e i18n\n  tvml, tvmljs, tvos             -\u003e apple-tv (canonical)\n  aws-elasticbeanstlak (typo)    -\u003e elasticbeanstalk\n  schwatz (typo)                 -\u003e schwartz\n  rabbitMQ (case)                -\u003e rabbitmq\n  jpq-ql (typo)                  -\u003e jpa-ql\n  spring boot (space)            -\u003e spring-boot\n  vue-profile, react-profile     -\u003e vue, react\n\nDropped entirely:\n  grails3..grails8     - version is implicit in the guide URL\n  firefox, chrome      - covered by the geb tag\n  backend, frontend    - vague (replaced by category placement)\n  framework, language  - vague\n  task, execution      - vague when standalone\n  github, git, google  - too generic\n  java                 - all guides are java, no signal\n  ide                  - vague\n  test                 - merged into testing\n  phantomjs, htmlunit  - dead tech\n  spock-spring         - merged into spock\n  web-profile          - profile choices are not navigational\n  profile, profiles    - kept only where actually meaningful\n\n\u003d\u003d conf/guides.yml: 28 category reassignments \u003d\u003d\n\nThe bottom grid now surfaces the tracks Grails 7/8 actually publishes:\n\n  Security (NEW, 7 guides)\n    grails-basicauth, grails-spring-security-core-plugin-custom-authentication,\n    grails-oauth-google, grails-oauth-twitter,\n    grails-custom-security-tenant-resolver, react-spring-security,\n    grails-test-security\n    Previously buried under \"Advanced Grails\" alongside multi-tenancy\n    and SOAP, even though spring-security-* alone covers ~10 guides.\n\n  Frontend SPA (renamed/merged, 14 guides)\n    Combines the previous Grails + React (6) + Grails + Vue.js (3) +\n    Grails + Angular (1) + Grails + AngularJS (2) + iOS/Android/RIA\n    legacy categories. The integration patterns overlap heavily and\n    none of the individual sub-tracks justified its own column on the\n    index. Mobile-client guides (Android, iOS Objective-C, iOS Swift,\n    tvOS) join here since they\u0027re \"frontend client consuming Grails\".\n\n  Grails + Cloud (renamed/broadened, 3 guides)\n    Was Grails + Google Cloud. Now also includes grails-elasticbeanstalk\n    so all cloud-deployment guides browse together regardless of which\n    cloud provider they target.\n\n  Grails + DevOps (gained 3 guides)\n    grails-as-docker-container, adding-commit-info,\n    grails-docker-external-services moved out of \"Advanced Grails\"\n    where they were thematically miscategorised.\n\n  Web Layer (gained 1 guide)\n    vaadin-grails moved here - it\u0027s a server-rendered rich-UI framework,\n    not a SPA.\n\n\u003d\u003d buildSrc/src/main/groovy/website/model/guides/GuidesPage.groovy \u003d\u003d\n\ncategories map: dropped legacy `android`, `angular`, `angularjs`, `ios`,\n`ria` keys (they had 1-2 guides each and have been folded into Frontend\nSPA). Dropped `react` and `vue` keys (now Frontend SPA). Renamed\n`googlecloud` to `cloud`. Added `security`, `spa`. The map now lists\nexactly the 10 tracks rendered on the index, in the order they appear:\napprentice, advanced, weblayer, devops, gorm, testing, security, spa,\nasync, cloud.\n\nmainContent: regenerated the two-column grid as 5 paired rows:\n  Apprentice         | Advanced Grails\n  Web Layer          | Grails + DevOps\n  GORM               | Grails Testing\n  Security           | Frontend SPA\n  Grails Async       | Grails + Cloud\n\nTag cloud: removed the TAG_CLOUD_LIMIT cap. After retagging the\nregistry to a curated taxonomy, every survivor is worth showing - 289\ntags total, sorted alphabetically for display, with occurrence still\ndriving the tagN CSS class so popular tags render larger. The version-\nlabel filter (~/^grails\\d+$/) is kept defensively so any future YAML\ndrift cannot resurrect grails3..grails8 in the cloud.\n\n\u003d\u003d Verified \u003d\u003d\n\n  ./gradlew :buildSrc:test --tests \u0027website.model.guides.GuidesFetcherSpec\u0027\n    -\u003e all 7 pass\n\n  ./gradlew build validateGuides --no-configuration-cache\n    -\u003e BUILD SUCCESSFUL, validateGuides 93 guide(s) parsed, 0 errors\n\nbuild/dist/guides/index.html section headers (in render order):\n  Grails Apprentice, Advanced Grails, Web Layer, Grails + DevOps,\n  GORM, Grails Testing, Security, Frontend SPA, Grails Async,\n  Grails + Cloud\n\nTag cloud: 289 entries (was capped at 50, before that was 195 noisy\nentries - now 289 curated entries with the long tail providing useful\ndeep-link navigation per guide).\n\nAssisted-by: claude-code:claude-opus-4-7\n"
    },
    {
      "commit": "18dfc5120c927ea0e5440879421bdb778cff0266",
      "tree": "83f041c6712bd39f8afcad9c373e7715ecc2d479",
      "parents": [
        "328cd266e027b231de3fe5c69b17926e9423e6e4",
        "526da09eb9e4c25fbcab0dfe3ff05348a6f2c6e3"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 20:47:12 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun May 03 20:47:12 2026 -0400"
      },
      "message": "Merge pull request #504 from apache/fix-guides-index-page\n\nFix guides index: surface every YAML entry, drop legacy categories, curate tag cloud"
    },
    {
      "commit": "526da09eb9e4c25fbcab0dfe3ff05348a6f2c6e3",
      "tree": "83f041c6712bd39f8afcad9c373e7715ecc2d479",
      "parents": [
        "328cd266e027b231de3fe5c69b17926e9423e6e4"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 20:42:29 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 20:42:29 2026 -0400"
      },
      "message": "Fix guides index: surface every YAML entry, drop legacy categories, curate tag cloud\n\nFive recently-added Grails 8 guides (multi-module, rest-library, github-actions-cicd)\nplus three older entries (grails-as-docker-container, grails-mock-basics v4,\ncreating-your-first-grails-app v6) were silently absent from\nhttps://grails.apache.org/guides/ even though their per-guide HTML was\nbuilt and uploaded correctly. Three independent bugs combined to make\nthis happen, and the bottom-of-page category grid was simultaneously\noverweight on dead/legacy stacks while missing the modern Web Layer\nsection that every flagship Grails 8 guide lives in.\n\n\u003d\u003d conf/guides.yml: 5 sampleRef.repo collisions \u003d\u003d\n\nThe fetcher used to group guide-versions by sampleRef.repo, so any two\nYAML entries that happened to point at the same external repo merged\ninto a single rendered guide and clobbered each other. Five YAML entries\nwere pointing at the wrong repo (the new Grails 8 sample repo instead of\ntheir own legacy repo):\n\n  - grails-as-docker-container v3, v4 -\u003e grails-as-docker-container\n    (was grails-docker-bootbuildimage; collided with grails-docker-bootbuildimage v8)\n  - grails-multi-project-build v4 -\u003e grails-multi-project-build\n    (was grails-multi-module; collided with grails-multi-module v8)\n  - rest-hibernate v3, v4 -\u003e rest-hibernate\n    (was grails-rest-library; collided with grails-rest-library v8)\n  - grails-on-github-actions v4 -\u003e grails-on-github-actions\n    (was grails-github-actions-cicd; collided with grails-github-actions-cicd v8)\n\nThese look like the residue of repo renames that were applied in the\nsample registry but never propagated to the new entries that needed\ndistinct slugs.\n\n\u003d\u003d GuidesFetcher: identify guides by YAML name, not sampleRef.repo \u003d\u003d\n\nEven with the YAML cleaned up, the fetcher was structurally fragile.\nIt also had two latent bugs:\n\n1. BRANCH_TO_MAJOR map hardcoded master-\u003e4 and was missing grails7,\n   grails8. creating-your-first-grails-app/v6 (whose sampleRef.branch\n   is master) was silently mapped to major version 4, overwriting the\n   real v4 entry in grailsMayorVersionTags. The v6 build was reachable\n   only via direct URL.\n2. grails-mock-basics had v3 and v4 both with branch\u003dmaster under one\n   guide. The slug+branch grouping reduced to {master}, so size\u003d\u003d1\n   triggered toSingleGuide and silently returned the first match (v3).\n\nRefactor parseGuides to walk one Guide per top-level YAML entry:\n\n  - 1 versions: child  -\u003e SingleGuide\n  - 2+ versions: child -\u003e GrailsVersionedGuide, with one rendered link\n                          per version. The YAML version key is used\n                          DIRECTLY as the integer major version, which\n                          eliminates BRANCH_TO_MAJOR entirely.\n\nsampleRef.repo is now treated only as the \"Get the Code\" target on the\nrendered chrome - it is never used to identify or group guides. Two\nYAML entries that happen to point at the same external repo are still\nrendered as two separate guides.\n\nAdds GuidesFetcherSpec with 7 unit tests pinning the regression cases:\n\n  - SingleGuide for 1-version entries\n  - GrailsVersionedGuide for 2+-version entries with the YAML version\n    key preserved as the int major version (covers the master-\u003e4\n    overwrite + missing grails7/grails8)\n  - Two YAML entries sharing sampleRef.repo render as two guides\n    (covers all 4 collision pairs above)\n  - Two YAML versions sharing the same branch are both kept when they\n    belong to the same guide (covers grails-mock-basics)\n  - Future-dated guides are filtered when skipFuture is true\n  - Empty versions: blocks are silently skipped\n  - Sort order is publication-date-descending\n\n\u003d\u003d GuidesPage: trim dead categories, add Web Layer, curate tag cloud \u003d\u003d\n\ncategories map: dropped Grails + Android, Grails + Angular,\nGrails + AngularJS, Grails + iOS, Grails + RIA. Each had 1-2 stale\nguides and was eating equal real estate to Advanced Grails (22 guides).\nTheir guides remain built and reachable via tags, search, and direct\nURL; only the index-page section is gone.\n\nAdded Web Layer (5 guides today: htmx, tailwindcss, vite-spa,\nfields-custom-widgets-and-wrappers, rest-library). Without this\nsection, rest-library was the only one of those guides that didn\u0027t\neven fit in the top-8 Latest Guides sidebar, so it was completely\ninvisible from the index.\n\nReshuffled the two-column grid so the modern stacks (Apprentice,\nAdvanced, Web Layer, DevOps) sit above the older ones (GORM, Testing,\nVue, React, Google Cloud). DevOps in particular was previously buried\nunder the legacy iOS / Android / RIA cluster despite having more guides\nthan any of them.\n\ntagCloud: filters out version-label tags (grails3..grails8) - they\nartificially dominated the cloud by occurrence count without adding\nnavigational value (the version is implicit in the guide URL anyway).\nCaps the visible set to TAG_CLOUD_LIMIT\u003d50 most-used tags before\nre-sorting alphabetically for display, so the long tail of one-shot\nlegacy tags (apple-tv, tvml, aws-elasticbeanstlak, ...) no longer\nclutters the sidebar.\n\n\u003d\u003d Verified \u003d\u003d\n\n  ./gradlew :buildSrc:test --tests \u0027website.model.guides.GuidesFetcherSpec\u0027\n    -\u003e all 7 pass\n\n  ./gradlew build validateGuides\n    -\u003e BUILD SUCCESSFUL, validateGuides 93 guide(s) parsed, 0 errors\n\nLocal build/dist/guides/index.html before-vs-after:\n  - multi-module      0 -\u003e 2 hits\n  - rest-library      0 -\u003e 2 hits\n  - github-actions-cicd                 0 -\u003e 2 hits\n  - grails-as-docker-container          0 -\u003e 2 hits\n  - grails-mock-basics  1 (v3 only) -\u003e 2 (v3 + v4)\n  - creating-your-first-grails-app  2 (v3,v4) -\u003e 3 (v3,v4,v6)\n  - rest-hibernate / grails-on-github-actions / grails-multi-project-build:\n    each one stops being mis-rendered as the wrong v8 guide\n\nLatest Guides sidebar now contains all 8 new Grails 8 guides\n(was 5 of 8: docker-bootbuildimage, htmx, spock-test-tour, tailwindcss,\nvite-spa). Tag cloud is now 50 entries (was 200+).\n\nAssisted-by: claude-code:claude-opus-4-7\n"
    },
    {
      "commit": "328cd266e027b231de3fe5c69b17926e9423e6e4",
      "tree": "4654c1275bfafc6b3c17d7c6329a6eabefef4471",
      "parents": [
        "584dbff3e1f92a3e01fa5dceeedfdaaef97d17c7",
        "85293a7e45410f78b7ca49226b56fc88725ee844"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 19:59:13 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun May 03 19:59:13 2026 -0400"
      },
      "message": "Merge pull request #503 from apache/remove-parity-migration-scaffolding\n\nRemove vestigial parity / structural-diff migration scaffolding"
    },
    {
      "commit": "85293a7e45410f78b7ca49226b56fc88725ee844",
      "tree": "4654c1275bfafc6b3c17d7c6329a6eabefef4471",
      "parents": [
        "584dbff3e1f92a3e01fa5dceeedfdaaef97d17c7"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 19:52:36 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 19:52:36 2026 -0400"
      },
      "message": "Remove vestigial parity / structural-diff migration scaffolding\n\nParityCheckGuideTask, StructuralDiffGuidesTask and their AdHocFixtureDiff\nhelper were one-shot migration scaffolding: they compared HTML rendered\nby our local renderer against vendored snapshots of the legacy\nguides.grails.org output to verify the new pipeline reproduced the same\nshape. With the migration complete and the guides themselves\nsubstantially rewritten on grails.apache.org, structural parity with the\nlegacy site is no longer the design goal - \"matches the old HTML\" is now\nnoise instead of signal.\n\nConcrete signals that this code has finished its job:\n\n- Only one parity-baseline snapshot was ever vendored\n  (creating-your-first-grails-app-v6/index.html, ~2900 lines), so the\n  parity check could only run for a single guide out of 93.\n- parityCheckAllGuides was registered but NEVER added to verifyAllGuides\n  (or any other aggregate). It only ran when invoked by hand.\n- StructuralDiffGuidesTask.groovy carried a TODO (\"Integrate captured\n  production baseline snapshots from https://guides.grails.org/\u003cguide\u003e/\n  \u003cversion\u003e/ once they are committed.\") - the additional baselines that\n  would have made it useful were never landed.\n\nThis change deletes:\n- buildSrc/src/main/groovy/website/qa/AdHocFixtureDiff.groovy\n- buildSrc/src/main/groovy/website/gradle/tasks/ParityCheckGuideTask.groovy\n- buildSrc/src/main/groovy/website/gradle/tasks/StructuralDiffGuidesTask.groovy\n- buildSrc/src/test/groovy/website/gradle/tasks/StructuralDiffGuidesTaskSpec.groovy\n- buildSrc/src/test/resources/parity-baseline/creating-your-first-grails-app-v6/index.html\n\nAnd updates the wire-up:\n- RenderGuidesPlugin: drop PARITY_AGGREGATE_TASK / PARITY_BASELINE_ROOT\n  constants, the per-guide parityCheckGuide_* registration loop, the\n  parityTaskNames field on Wiring, and the parityCheckAllGuides aggregate\n  registration. Render-only configuration remains intact.\n- GrailsWebsitePlugin: drop the StructuralDiffGuidesTask import,\n  registration, and verifyAllGuides dependency.\n- AcceptanceReportTask: drop the structuralReportFile input, the\n  StructuralDiffGuidesTask dependsOn wiring, and the structuralDiffGuides\n  column from build/reports/acceptance.csv (header, row, AcceptanceRow,\n  parsing, verdict merging, and detail summarization). The remaining\n  gates (asciidoctorWarningGate, crawlBuiltGuides, cspScan) are unchanged.\n- AcceptanceReportTaskSpec: drop the per-test structural-diff CSV writes\n  and adjust the expected acceptance.csv header / row strings.\n- README: update the \"verifyAllGuides\" description so it no longer claims\n  to run a structural diff step.\n\nKept on purpose:\n- GenerateRedirectStubsTask and GenerateRedirectsManifestTask remain\n  untouched. They are NOT migration scaffolding - they generate the\n  meta-refresh stubs deployed to the legacy guides.grails.org gh-pages\n  branch and the _redirects.json manifest published with the main site,\n  both of which are still load-bearing for legacy URL traffic.\n\nVerified:\n- ./gradlew :buildSrc:test --tests AcceptanceReportTaskSpec -\u003e all 4 pass\n- ./gradlew build validateGuides -x :buildSrc:test -\u003e BUILD SUCCESSFUL,\n  validateGuides 93 guide(s) parsed, 0 errors\n- ./gradlew help --task verifyAllGuides -\u003e task graph still resolves\n- The 5 unrelated :buildSrc:test failures in RecordCompanionReleaseTaskSpec\n  and ValidateGuidesTaskSpec are pre-existing on master (584dbff3e1) and\n  are not caused by this change.\n\nNet diff: 10 files changed, +10 / -3763 lines.\n\nAssisted-by: claude-code:claude-opus-4-7\n"
    },
    {
      "commit": "584dbff3e1f92a3e01fa5dceeedfdaaef97d17c7",
      "tree": "6af7a6785b15d71e19c47acb8099c74a5876f145",
      "parents": [
        "1606bad448f7544133623b7c10a1b269204d2703",
        "63786e5370061005ef11f2e80e401fcd9d56b1bd"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 19:44:59 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun May 03 19:44:59 2026 -0400"
      },
      "message": "Merge pull request #502 from apache/fix-redirect-links\n\nReplace legacy *.grails.org URLs with their canonical grails.apache.org targets"
    },
    {
      "commit": "63786e5370061005ef11f2e80e401fcd9d56b1bd",
      "tree": "6af7a6785b15d71e19c47acb8099c74a5876f145",
      "parents": [
        "1606bad448f7544133623b7c10a1b269204d2703"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 19:30:49 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 19:30:49 2026 -0400"
      },
      "message": "Replace legacy *.grails.org URLs with their canonical grails.apache.org targets\n\nThe Plugins link in the site header pointed at https://plugins.grails.org,\nwhich is kept alive only as a meta-refresh redirect back to\nhttps://grails.apache.org/plugins.html. Many other internal links in the\nguides, blog posts, and shared chrome had the same shape - they targeted a\nlegacy *.grails.org host that exists today only to bounce visitors to the\ncanonical grails.apache.org URL. Each of those redirects costs the user\nan extra hop, hurts SEO, and breaks if any of the legacy hosts ever go\naway.\n\nReplace each redirect-bound URL with its canonical destination so internal\nlinks resolve directly:\n\n- plugins.grails.org -\u003e grails.apache.org/plugins.html (paths under\n  /plugin/\u003cvendor\u003e/\u003cname\u003e are 404, so they collapse to the landing page)\n- guides.grails.org\u003cpath\u003e -\u003e grails.apache.org/guides\u003cpath\u003e (path-preserving)\n- docs.grails.org\u003cpath\u003e -\u003e grails.apache.org/docs\u003cpath\u003e (path-preserving)\n- views.grails.org\u003cpath\u003e -\u003e grails.apache.org/docs/latest/guide/theWebLayer.html#gson\n- async.grails.org\u003cpath\u003e -\u003e grails.apache.org/docs/latest/guide/async.html\n- testing.grails.org\u003cpath\u003e -\u003e grails.apache.org/docs/latest/guide/testing.html\n- gsp.grails.org\u003cpath\u003e -\u003e grails.apache.org/docs/latest/guide/theWebLayer.html#gsp\n  (the four subdomains above all redirect to the same fixed anchor on the\n  canonical site - the legacy path component is dropped at redirect time\n  anyway, so we point straight at the anchor)\n- grails.org/\u003cX\u003e.html -\u003e grails.apache.org/\u003cX\u003e.html\n- grails.org/learn.html -\u003e grails.apache.org/learning.html (page renamed)\n- grails.org/plugin/\u003cX\u003e -\u003e grails.apache.org/plugins.html (legacy 404)\n- grails.org/blog\u003cpath\u003e -\u003e grails.apache.org/blog\u003cpath\u003e\n- grails.org/ root and bare grails.org -\u003e grails.apache.org/\n\nIntentionally NOT touched:\n\n- gorm.grails.org/* - some paths (notably /latest/neo4j/manual/...) have\n  no canonical replacement on grails.apache.org because GORM for Neo4j\n  has not been updated for Grails 7/8. The gorm.grails.org URL is still\n  the current canonical destination for those readers.\n- start.grails.org, slack.grails.org, repo.grails.org,\n  prev-snapshot.grails.org - these are canonical hosts that serve content\n  directly (the Forge, the Slack signup, the JFrog Maven repo, and the\n  Grails 8 snapshot Forge).\n- grails.org/buildstatus.html - redirects to a GitHub README, not back\n  to grails.apache.org, so it falls outside the redirect-back-to-here\n  scope of this change.\n- buildSrc/src/test/resources/parity-baseline/ - golden HTML snapshot of\n  the legacy site, used by ParityCheckGuideTask as a regression-test\n  fixture; rewriting it would corrupt the source-of-truth the renderer\n  is diffed against.\n- buildSrc/src/main/groovy/website/qa/AdHocFixtureDiff.groovy,\n  buildSrc/src/main/groovy/website/gradle/tasks/Generate{Redirects,\n  RedirectStubs}{Manifest,}Task.groovy, ParityCheckGuideTask.groovy, and\n  StructuralDiffGuidesTask.groovy - these intentionally reference\n  guides.grails.org as the LEGACY_BASE for the redirect-stub generator\n  and parity baseline.\n- README.md and buildSrc/VENDOR.md - both document the migration itself,\n  so the legacy URLs need to remain readable.\n- minutes/20210321-tab.md - frozen historical record from the TAB.\n\nVerified locally with `./gradlew validateGuides -PvalidationMode\u003dboth`\n(93 guides parsed, 0 errors) and `./gradlew build` (renderSite succeeds;\nthe rendered Plugins link in build/dist/index.html is now\nhttps://grails.apache.org/plugins.html as expected).\n\nAssisted-by: claude-code:claude-opus-4-7\n"
    },
    {
      "commit": "1606bad448f7544133623b7c10a1b269204d2703",
      "tree": "1e87327e1332fbae320a335bdbb3d93c67fc3f72",
      "parents": [
        "e698300f4a22f9ffbb9aaa0021daabd1b6614541",
        "97acadbbcbd1cff7adf3177b401d2ed9ea43fdf2"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 19:08:49 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun May 03 19:08:49 2026 -0400"
      },
      "message": "Merge pull request #501 from apache/surface-orphan-companion-plugins-on-downloads-page\n\nSurface companion plugins on downloads page when target Grails major has not yet shipped"
    },
    {
      "commit": "97acadbbcbd1cff7adf3177b401d2ed9ea43fdf2",
      "tree": "1e87327e1332fbae320a335bdbb3d93c67fc3f72",
      "parents": [
        "e698300f4a22f9ffbb9aaa0021daabd1b6614541"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 19:07:11 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 19:07:11 2026 -0400"
      },
      "message": "Surface companion plugins on downloads page when target Grails major has not yet shipped\n\nApache-released companion plugins (e.g. grails-publish 1.0.0-M1 under\ncompanionArtifacts[\u00278\u0027]) used to be invisible on download.html until at\nleast one matching core release landed in coreReleases, because the page\niterates majors via activeMinorLines / latestPreReleasePerMajor, both of\nwhich read only coreReleases.\n\nAdd a new \u0027Plugins for upcoming Apache Grails releases\u0027 section between\nthe \u0027Pre-release (Apache-released)\u0027 grid and \u0027Older Versions\u0027. The section\nrenders one card per major present in companionArtifacts but absent from\ncoreReleases (stable or pre-release). The card lists each companion\u0027s\nSource / SHA512 / ASC verification links plus a release-notes link, and\nomits the core source / binary / wrapper block that doesn\u0027t yet exist.\n\nThe section is driven by a new SiteMap.orphanCompanionMajors helper that\nreturns those orphan majors in descending order, skipping empty companion\nlists. Once any release for an orphan major (including a milestone or RC)\nis appended to coreReleases, the major drops out of the helper and the\nexisting per-major card automatically picks up its companions through\ncompanionArtifactsFor - no YAML edits needed.\n\nVerified by 6 new SiteMapSpec cases covering empty, all-covered,\npre-release-covered, single orphan, multiple orphans (descending order),\nand empty-companion-list scenarios. Verified by full build that download.html\nrenders the new section with the existing companionArtifacts[\u00278\u0027] entry.\n\nAssisted-by: claude-code:claude-4-opus\n"
    },
    {
      "commit": "e698300f4a22f9ffbb9aaa0021daabd1b6614541",
      "tree": "a01d1bf99b2bae8b226d8cc291955ea92b24f594",
      "parents": [
        "b5c0c07d2a089b66f00d3335ce7dd49c6e0bfe5e",
        "c2dbac8a4f3d81907c9bf02091b3b353594f7f12"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 19:00:07 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun May 03 19:00:07 2026 -0400"
      },
      "message": "Merge pull request #500 from apache/add-grails-vite-spa-v8\n\nAdd grails-vite-spa v8 guide"
    },
    {
      "commit": "c2dbac8a4f3d81907c9bf02091b3b353594f7f12",
      "tree": "0154ad6e5f5d82568dec4a3cadd8936e04ab33fb",
      "parents": [
        "967175525b2dc9b2484c760e50867f0d857080a1"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 18:53:41 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 18:53:41 2026 -0400"
      },
      "message": "Add grails-vite-spa v8 guide\n\nTwo-tier integration guide: Grails 8 rest_api JSON backend plus a Vite/React SPA frontend in a single Gradle multi-project, packaged into one bootJar with the SPA served from the same origin as the API. Replaces the long-broken Webpack-era React/Vue profiles.\n\nGreenfield - new grails-guides/grails-vite-spa repo created public on the grails8 default branch.\n\nThe 14 chapters cover the multi-project layout, the JSON backend (Book + RestfulController + JSON views), the SpaController forwarder for client-side routes, the Vite/React frontend, vite.config.js with /api proxy, the dev mode (two servers behind one origin), the prod build (one bootJar), and why CORS never appears.\n\nVerified locally: validateGuides -PvalidationMode\u003dboth returns 86 guides 0 errors; renderGuide_grails_vite_spa_8 --rerun-tasks --no-configuration-cache renders all 14 chapter HTML pages, no Unresolved directive errors.\n\nAssisted-by: claude-code:claude-opus-4-7\n"
    },
    {
      "commit": "b5c0c07d2a089b66f00d3335ce7dd49c6e0bfe5e",
      "tree": "292d984435c67b99f39b7e544fa98fd49d988d1a",
      "parents": [
        "71523d0078098c260c8723de87a80b544fc8513b",
        "8c86731574649c4aa8e2c6f6bdce240514a67654"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 18:48:38 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun May 03 18:48:38 2026 -0400"
      },
      "message": "Merge pull request #499 from apache/add-grails-spock-test-tour-v8\n\nAdd grails-spock-test-tour v8 guide"
    },
    {
      "commit": "8c86731574649c4aa8e2c6f6bdce240514a67654",
      "tree": "33aae37be909d9acc04601ed620274506c1b6837",
      "parents": [
        "967175525b2dc9b2484c760e50867f0d857080a1"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 16:15:02 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 16:15:02 2026 -0400"
      },
      "message": "Add grails-spock-test-tour v8 guide\n\nWorking examples of every Spock test layer Grails 8 supports.\nGreenfield - new grails-guides/grails-spock-test-tour repo created public\non the grails8 default branch.\n\nThe 13 chapters demonstrate one canonical example per layer:\n\n- Getting Started (whatYouWillBuild, requirements, howto)\n- Five Test Layers (the speed/cost/coverage matrix)\n- DomainUnitTest for Constraints (BookSpec with @Unroll where: table\n  for pageCount)\n- ServiceUnitTest + DataTest for Queries (BookServiceSpec, in-memory\n  GORM, dynamic-finder testing)\n- ControllerUnitTest for Routing (BookControllerSpec, response status +\n  request.json shape)\n- @Integration With @Rollback (BookIntegrationSpec, real Spring\n  context, real datasource, transaction-isolated specs)\n- Geb Functional Tests (Testcontainers Postgres, browser session reuse,\n  cross-references the CI/CD guide for staging)\n- Parameterised where: Data Tables (block discipline, mock-call-count\n  assertions)\n- useJUnitPlatform and JaCoCo Coverage (the wiring most teams get wrong)\n\nVerified locally:\n\n  ./gradlew validateGuides -PvalidationMode\u003dboth\n    [validateGuides] mode\u003dboth: 86 guide(s) parsed, 0 SKIP-warned, 0 errors\n\n  ./gradlew renderGuide_grails_spock_test_tour_8 --rerun-tasks --no-configuration-cache\n    Renders all 13 chapter HTML pages, no Unresolved directive errors.\n\nAssisted-by: claude-code:claude-opus-4-7\n"
    },
    {
      "commit": "71523d0078098c260c8723de87a80b544fc8513b",
      "tree": "388f270ab4f0563aed6348171e5a293019101346",
      "parents": [
        "f6b8f76f73dc3ca9224a9b10798373d5bdfd58b7",
        "9a44ae9eeb401a9983bd5feaa3c3f6f8cb173e62"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 16:11:47 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun May 03 16:11:47 2026 -0400"
      },
      "message": "Merge pull request #498 from apache/add-grails-multi-module-v8\n\nAdd grails-multi-module v8 guide (rename of grails-multi-project-build)"
    },
    {
      "commit": "9a44ae9eeb401a9983bd5feaa3c3f6f8cb173e62",
      "tree": "a885051cb96e6d52ef93c1765ed70f771464c7c5",
      "parents": [
        "967175525b2dc9b2484c760e50867f0d857080a1"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 16:09:54 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 16:09:54 2026 -0400"
      },
      "message": "Add grails-multi-module v8 guide\n\nReal-world Grails 8 multi-project layout: one shared-core Grails Plugin\n(domain model + GORM data services), two web apps (customer + admin)\nthat depend on it via implementation project(\u0027:shared-core\u0027) and build\nto independent bootJars sharing the same data model.\n\nCompanion conf/guides.yml change repoints the existing v4 entry from\ngrails-guides/grails-multi-project-build to the freshly-renamed\ngrails-guides/grails-multi-module. The original master branch (and\nforensic/grails4 etc.) is preserved on the renamed repo.\n\nThe 12 chapters cover:\n\n- Getting Started (whatYouWillBuild, requirements, howto)\n- Generate the Three Module Starters (web_plugin + 2 web)\n- Root settings.gradle and build.gradle (subprojects { repositories,\n  useJUnitPlatform } pattern)\n- The shared-core Plugin (Book domain + @Service(Book) BookService data\n  service)\n- The Two Webapps Consume shared-core (single-line dependency, package\n  separation customer.* / admin.* / example.*)\n- Running and Packaging Each Webapp (independent bootRun + bootJar)\n- Per-Module Testing (each module owns its own test suite)\n- Extracting a Plugin From a Monolith (six-step migration recipe)\n- Failure Modes (circular deps, GORM mapping conflicts, schema\n  migration coordination)\n\nVerified locally:\n\n  ./gradlew validateGuides -PvalidationMode\u003dboth\n    [validateGuides] mode\u003dboth: 86 guide(s) parsed, 0 SKIP-warned, 0 errors\n\n  ./gradlew renderGuide_grails_multi_module_8 --rerun-tasks --no-configuration-cache\n    Renders all 12 chapter HTML pages, no Unresolved directive errors.\n\nThe mode\u003dboth pass also confirms the renamed repo + branch resolve.\n\nAssisted-by: claude-code:claude-opus-4-7\n"
    },
    {
      "commit": "f6b8f76f73dc3ca9224a9b10798373d5bdfd58b7",
      "tree": "8995171f80739292c068335cc2877987c37d879e",
      "parents": [
        "cba360aa9425e2a53b27bfbf00d76a8e9964d400",
        "d40bf5cc77d6a946faf5f33de8a2cd1bfaa9ea8e"
      ],
      "author": {
        "name": "James Daugherty",
        "email": "jdaugherty@jdresources.net",
        "time": "Sun May 03 16:08:54 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun May 03 16:08:54 2026 -0400"
      },
      "message": "Merge pull request #491 from apache/add-companion-major-bootstrap-and-grails-publish-1.0.0-M1\n\nBootstrap recordCompanionRelease for new majors and add Grails 8 grails-publish 1.0.0-M1"
    },
    {
      "commit": "cba360aa9425e2a53b27bfbf00d76a8e9964d400",
      "tree": "e9a488ab99efd46b934168f35e626f1556a4dc4f",
      "parents": [
        "d7cd9d7fb6917b0f43e75a7ba6d798f15c4ce258",
        "cebdcadaaf69553273c0ec50fe7ae602d7284b4e"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 16:02:02 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun May 03 16:02:02 2026 -0400"
      },
      "message": "Merge pull request #497 from apache/add-grails-htmx-v8\n\nAdd grails-htmx v8 guide"
    },
    {
      "commit": "cebdcadaaf69553273c0ec50fe7ae602d7284b4e",
      "tree": "38e68b9126efb3ca993eb59bd5a002c5880f8854",
      "parents": [
        "967175525b2dc9b2484c760e50867f0d857080a1"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 15:47:13 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 15:47:13 2026 -0400"
      },
      "message": "Add grails-htmx v8 guide\n\nWorked example for HTMX-driven interactive UI on a Grails 8 GSP backend.\nA small task tracker with server-rendered initial page, then HTMX-driven\ninline editing, live search, toggle, and optimistic delete with confirm.\n\nSample-app flavour. Greenfield (no upstream rename) - the new\ngrails-guides/grails-htmx repo was created public on the grails8\ndefault branch.\n\nThe 14 chapters walk through:\n\n- Getting Started (whatYouWillBuild, requirements, howto)\n- Creating the Application\n- The Task Domain Class\n- Adding HTMX to the Layout (script tag with SRI hash)\n- URL Mappings (explicit method per route)\n- The TaskController (7 actions, 6 of which return GSP partials)\n- The Initial Page (server-rendered first paint)\n- The Row Partial (one template, four call sites)\n- Inline Edit With Validation (422 returns the same edit fragment with\n  field errors rendered inline)\n- Live Search (hx-trigger\u003d\"keyup changed delay:300ms\")\n- CSRF With HTMX (htmx:configRequest hook reads token from a meta tag)\n- HTMX vs an SPA (when each fits, cross-references the upcoming\n  grails-vite-spa guide)\n\nVerified locally:\n\n  ./gradlew validateGuides -PvalidationMode\u003dboth\n    [validateGuides] mode\u003dboth: 86 guide(s) parsed, 0 SKIP-warned, 0 errors\n\n  ./gradlew renderGuide_grails_htmx_8 --rerun-tasks --no-configuration-cache\n    Renders all 14 chapter HTML pages, no Unresolved directive errors.\n\nAssisted-by: claude-code:claude-opus-4-7\n"
    },
    {
      "commit": "d7cd9d7fb6917b0f43e75a7ba6d798f15c4ce258",
      "tree": "493986aaef709004a3d630e7ed2f53c94c49db6c",
      "parents": [
        "3ce7cbd928a4d5ac05a2a93ef4445679892c8acf",
        "1db2c26401f187f461b55826b8a91fae144bbd34"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 15:42:35 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun May 03 15:42:35 2026 -0400"
      },
      "message": "Merge pull request #496 from apache/add-grails-rest-library-v8\n\nAdd grails-rest-library v8 guide (rename of rest-hibernate)"
    },
    {
      "commit": "1db2c26401f187f461b55826b8a91fae144bbd34",
      "tree": "f48c067f014b2be9bb97459100f72eba242e8e4e",
      "parents": [
        "967175525b2dc9b2484c760e50867f0d857080a1"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 15:41:09 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 15:41:09 2026 -0400"
      },
      "message": "Add grails-rest-library v8 guide\n\nWorked example for a Book + Author REST API on Grails 8: RestfulController-\nbased CRUD, JSON views (.gson) with HAL-style _links, structured 422\nvalidation responses with stable error codes, bounded pagination\n(max hard-capped at 100), URL-prefix versioning under /v1/, and an\nall-or-nothing bulk create endpoint.\n\nCompanion conf/guides.yml change repoints the existing v3 and v4 entries\nfrom grails-guides/rest-hibernate to grails-guides/grails-rest-library\n(rename of the upstream repo). The old grails3, grails4, grails-4, master\nbranches are preserved on the renamed repo so the historical guides\nkeep resolving. The grails-guides/rest-mongodb repo is left alone (it\ncovers a different stack).\n\nThe 17 chapters cover:\n\n- Getting Started (whatYouWillBuild, requirements, howto)\n- Creating the Application (download via the rest_api forge type)\n- Domain Model (Book + Author with realistic constraints, lazy loading\n  to head off N+1, bootstrap sample data)\n- URL Mappings and v1 Versioning (resources mapping + nested collection\n  routes for bulk create)\n- RestfulController with a Hard Page Cap (listAllResources override)\n- JSON Views (.gson partial + index + show pattern)\n- Structured 422 Validation Responses (stable field/code/message shape)\n- Pagination, Sorting, Filtering (max/offset/sort/order)\n- All-or-Nothing Bulk Create (transactionStatus.setRollbackOnly)\n- URL-Prefix vs Content-Negotiation Versioning\n- Running the Application (curl smoke tests for every endpoint)\n\nVerified locally:\n\n  ./gradlew validateGuides -PvalidationMode\u003dboth\n    [validateGuides] mode\u003dboth: 86 guide(s) parsed, 0 SKIP-warned, 0 errors\n\n  ./gradlew renderGuide_grails_rest_library_8 --rerun-tasks --no-configuration-cache\n    Renders all 17 chapter HTML pages, no Unresolved directive errors.\n\nThe mode\u003dboth pass also confirms grails-guides/grails-rest-library\nexists on grails3, grails4, and grails8 branches - i.e. the upstream\nrename and the new branch are intact.\n\nAssisted-by: claude-code:claude-opus-4-7\n"
    },
    {
      "commit": "3ce7cbd928a4d5ac05a2a93ef4445679892c8acf",
      "tree": "c464dcdc063220823e42b962a51faeb153cf9534",
      "parents": [
        "527a6271639fe73852c2e7d057e07360c6524cbd",
        "b96505759092033145dafa5e425b653f4e45480e"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 15:37:00 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun May 03 15:37:00 2026 -0400"
      },
      "message": "Merge pull request #494 from apache/add-grails-docker-bootbuildimage-v8\n\nAdd grails-docker-bootbuildimage v8 guide (rename of grails-as-docker-container)"
    },
    {
      "commit": "527a6271639fe73852c2e7d057e07360c6524cbd",
      "tree": "e4f497119a850ccc52e66b7ead88e69f3a92196e",
      "parents": [
        "23103ea616c06a2dffa388c6c46296c1ea680b62",
        "94b3d4f3790297dedaf72fc7d4eab2de2ac69b72"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 15:36:36 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun May 03 15:36:36 2026 -0400"
      },
      "message": "Merge pull request #495 from apache/add-grails-github-actions-cicd-v8\n\nAdd grails-github-actions-cicd v8 guide (rename of grails-on-github-actions)"
    },
    {
      "commit": "94b3d4f3790297dedaf72fc7d4eab2de2ac69b72",
      "tree": "2659d2a372e892db123764d7d0e26b7bfa4072ed",
      "parents": [
        "967175525b2dc9b2484c760e50867f0d857080a1"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 15:34:13 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 15:34:13 2026 -0400"
      },
      "message": "Add grails-github-actions-cicd v8 guide\n\nModern CI/CD pipeline for a Grails 8 application. Replaces the seven-\nyear-old grails-on-github-actions/v4 guide whose actions are all\ndeprecated.\n\nCompanion conf/guides.yml change repoints the existing v4 entry from\ngrails-guides/grails-on-github-actions to the renamed\ngrails-guides/grails-github-actions-cicd repo. The original master\nbranch is preserved on the renamed repo so the v4 guide continues to\nresolve.\n\nThe 17 chapters walk through a five-job graph (validation -\u003e unit -\nintegration -\u003e functional -\u003e coverage), a separate tag-triggered\nrelease.yml that builds the OCI image with bootBuildImage and pushes\nto GHCR, plus dependabot.yml with grouped Grails / Spring / testing\nPRs and required-status-check configuration.\n\nForward-references the just-landed grails-docker-bootbuildimage v8\nguide (release.yml consumes its bootBuildImage Gradle task).\n\nVerified locally:\n\n  ./gradlew validateGuides -PvalidationMode\u003dboth\n    [validateGuides] mode\u003dboth: 86 guide(s) parsed, 0 SKIP-warned, 0 errors\n\n  ./gradlew renderGuide_grails_github_actions_cicd_8 --rerun-tasks --no-configuration-cache\n    Renders all 17 chapter HTML pages, no Unresolved directive errors.\n\nThe mode\u003dboth pass also confirms grails-guides/grails-github-actions-cicd\nexists on master (where the v4 guide points) and grails8 (where this\nguide points), i.e. the upstream rename and new branch are intact.\n\nAssisted-by: claude-code:claude-opus-4-7\n"
    },
    {
      "commit": "b96505759092033145dafa5e425b653f4e45480e",
      "tree": "6fe3501e08f1f0d14781af9e36282237f15728e7",
      "parents": [
        "967175525b2dc9b2484c760e50867f0d857080a1"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 15:27:34 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 15:27:34 2026 -0400"
      },
      "message": "Add grails-docker-bootbuildimage v8 guide\n\nWorked example for packaging a Grails 8 app as a production-ready OCI\nimage and orchestrating it with Docker Compose + PostgreSQL.\n\nLeads with Spring Boot 4\u0027s bootBuildImage Gradle task (Paketo Buildpacks,\nno Dockerfile), then includes a hand-rolled multi-stage Dockerfile with\nnon-root user (UID 10001) as the alternative path. Both produce\nequivalent runtime behaviour with -XX:+UseContainerSupport and\n-XX:MaxRAMPercentage\u003d75.0 baked in.\n\nThe companion conf/guides.yml change repoints the existing v3 and v4\nentries on lines 1051 and 1079 from grails-as-docker-container to the\nfreshly-renamed grails-docker-bootbuildimage repo. GitHub auto-redirects\nmean the historical v3/v4 sample-app clones keep working via either\nname; the registry now points at the canonical name.\n\nSample-app flavour. Upstream is grails-guides/grails-docker-bootbuildimage\non the new grails8 branch (alongside the preserved grails3/grails4/grails5\nbranches). The complete/ tree generates a ghcr.io image, runs alongside\npostgres:16-alpine via compose.yml, and exposes Spring Boot Actuator\nliveness + readiness probes for the Compose HEALTHCHECK.\n\nThe 18 chapters walk through:\n\n- Getting Started (whatYouWillBuild, requirements, howto)\n- Creating the Application (download via the forge with\n  postgres+testcontainers+database-migration features)\n- Two Paths to a Container Image (Paketo vs Dockerfile mental model)\n- The Spring Boot bootBuildImage Task (canonical path)\n- Run the Image and Verify\n- The Hand-Rolled Multi-Stage Dockerfile (alternative)\n- Environment-Driven application.yml (SPRING_DATASOURCE_* env vars)\n- Container-Friendly JVM Flags\n- Spring Boot Actuator Health Probes (liveness + readiness)\n- Orchestrating the Stack with Docker Compose\n- Wiring the App to a Real Postgres\n- Pushing to GitHub Container Registry\n- Running as a Non-Root User\n\nVerified locally:\n\n  ./gradlew validateGuides -PvalidationMode\u003dboth\n    [validateGuides] mode\u003dboth: 86 guide(s) parsed, 0 SKIP-warned, 0 errors\n\n  ./gradlew renderGuide_grails_docker_bootbuildimage_8 --rerun-tasks --no-configuration-cache\n    Renders all 18 chapter HTML pages, no Unresolved directive errors.\n\nThe validateGuides mode\u003dboth pass also confirms that\ngrails-guides/grails-docker-bootbuildimage#grails3,\ngrails-guides/grails-docker-bootbuildimage#grails4, and\ngrails-guides/grails-docker-bootbuildimage#grails8 all exist - i.e. the\nupstream rename and the new branch are both intact.\n\nCross-references the upcoming grails-github-actions-cicd v8 guide\n(release.yml workflow consumes the bootBuildImage output).\n\nAssisted-by: claude-code:claude-opus-4-7\n"
    },
    {
      "commit": "23103ea616c06a2dffa388c6c46296c1ea680b62",
      "tree": "d431a63e482e686db166953faaab3dc6727fa81e",
      "parents": [
        "cf0be8da350c8defd9c1b2016faaf61771d81a6a",
        "2b7fe54ed30ab6c1f47bb9e7bcb9bea7a20eb027"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 15:22:18 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun May 03 15:22:18 2026 -0400"
      },
      "message": "Merge pull request #493 from apache/add-grails-tailwindcss-v8\n\nAdd grails-tailwindcss v8 guide"
    },
    {
      "commit": "2b7fe54ed30ab6c1f47bb9e7bcb9bea7a20eb027",
      "tree": "ad144cedbef2f853623f1bb0152491c4d5e63dbd",
      "parents": [
        "967175525b2dc9b2484c760e50867f0d857080a1"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 15:05:40 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 15:05:40 2026 -0400"
      },
      "message": "Add grails-tailwindcss v8 guide\n\nWorked example for wiring Tailwind CSS 4 into a Grails 8 GSP application\nvia a Gradle-driven\npx @tailwindcss/cli build step. Targets the same\naudience as the recently-landed grails-fields-custom-widgets-and-wrappers\nv8 guide: server-rendered Grails apps that want a modern utility-first\nCSS pipeline without abandoning GSP for an SPA.\n\nSample-app flavour. Upstream is grails-guides/grails-tailwindcss on the\ngrails8 branch, with the standard initial/ (vanilla Grails 8 starter\ngenerated from prev-snapshot.grails.org) + complete/ (Tailwind wiring\napplied) layout. The complete/ build was verified end-to-end:\n`npm install` + `npx @tailwindcss/cli` produces a 16KB minified\n`app.css` in 62ms against Tailwind 4.2.4.\n\nThe 14 chapters walk through the full integration:\n\n- Getting Started (whatYouWillBuild, requirements, howto)\n- Creating the Application (download via the forge create endpoint)\n- Installing Tailwind CSS 4\n- Configuring Tailwind for Grails (CSS-first config via @source +\n  @custom-variant; no tailwind.config.js needed in v4)\n- Wiring Tailwind into the Gradle Build (npmInstall + tailwindBuild\n  Exec tasks hooked into processResources and compileGroovy)\n- Applying Tailwind to the GSP Layout (rewritten main.gsp + index.gsp)\n- Class-Based Dark Mode (pre-paint script + localStorage toggle)\n- Reusable Component Classes with @apply (btn-primary, card, nav-link)\n\nVerified locally:\n\n  ./gradlew validateGuides -PvalidationMode\u003dboth\n    [validateGuides] mode\u003dboth: 86 guide(s) parsed, 0 SKIP-warned, 0 errors\n\n  ./gradlew renderGuide_grails_tailwindcss_8 --rerun-tasks --no-configuration-cache\n    Renders all 14 chapter HTML pages, no Unresolved directive errors,\n    clean prev/next navigation. (--no-configuration-cache mirrors the\n    existing publishMainSite workaround in 0a0201f898.)\n\nAssisted-by: claude-code:claude-opus-4-7\n"
    },
    {
      "commit": "cf0be8da350c8defd9c1b2016faaf61771d81a6a",
      "tree": "404892cccc10d1032db378dd3d0cfee15dfcafad",
      "parents": [
        "41851a5a5f38b8bece0f2745221069ca4f3f3b52",
        "97f47ade12a9f2d204b1b87ea96be8c7456104bb"
      ],
      "author": {
        "name": "James Daugherty",
        "email": "jdaugherty@jdresources.net",
        "time": "Sun May 03 14:49:39 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun May 03 14:49:39 2026 -0400"
      },
      "message": "Merge pull request #492 from apache/keys-downloads-fix\n\nFix Key URL"
    },
    {
      "commit": "97f47ade12a9f2d204b1b87ea96be8c7456104bb",
      "tree": "404892cccc10d1032db378dd3d0cfee15dfcafad",
      "parents": [
        "41851a5a5f38b8bece0f2745221069ca4f3f3b52"
      ],
      "author": {
        "name": "James Daugherty",
        "email": "jdaugherty@jdresources.net",
        "time": "Sun May 03 14:32:15 2026 -0400"
      },
      "committer": {
        "name": "James Daugherty",
        "email": "jdaugherty@jdresources.net",
        "time": "Sun May 03 14:32:15 2026 -0400"
      },
      "message": "All links to checksums, detached signatures and public keys must reference the Apache Distribution Directory (downloads.apache.org) and use https:// (SSL)\n"
    },
    {
      "commit": "d40bf5cc77d6a946faf5f33de8a2cd1bfaa9ea8e",
      "tree": "dad0165472f1dbc28cd30b7257905dc11016118a",
      "parents": [
        "07363e7a36126e21ebe39d2b2a112ccac6f5ab18"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 14:11:03 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 14:11:03 2026 -0400"
      },
      "message": "Add Grails 8 companionArtifacts block with grails-publish 1.0.0-M1\n\nThe grails-publish Gradle plugin (used to publish Grails artifacts to\nMaven Central) is the first Grails-8-targeted Apache release in the\ncompanion ecosystem. It ships ahead of Grails 8 core because the\ntoolchain has to be in place before the runtime can be cut.\n\nBootstraps the \u00278\u0027: block under companionArtifacts: with this single\nentry. mirrorDirectory, releaseNotesRepo, and displayName mirror the\n\u00277\u0027: entry for the same artifact since they are stable per-plugin\nidentifiers, not per-major. The renderer (DownloadPage,\nDocumentationPage, hero cards) only iterates over majors that have\na corresponding stable core release listed in coreReleases:, so this\nentry is dormant in the rendered site until 8.0.0-M1 core ships - at\nwhich point the plugin\u0027s mirror URL and release-notes link are\nalready populated.\n\nCompanion to the previous commit\u0027s recordCompanionRelease bootstrap\nsupport: this is exactly the kind of edit the new-major path now\nautomates, but doing it as a one-shot data PR here exercises the\nmanual schema before the workflow has a chance to run for an\n8-targeted plugin.\n\nAssisted-by: claude-code:claude-opus-4-7\n"
    },
    {
      "commit": "07363e7a36126e21ebe39d2b2a112ccac6f5ab18",
      "tree": "9a1150c06baaf5a99e79e90d540a34af906db2b0",
      "parents": [
        "41851a5a5f38b8bece0f2745221069ca4f3f3b52"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 14:10:49 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 14:10:49 2026 -0400"
      },
      "message": "Extend recordCompanionRelease to bootstrap new majors and new artifacts\n\nCompanion artifacts ship independently of Grails core, so each (grailsMajor,\nartifactId) pair has its own first-publish moment. Today the task can only\nbump the version: line of an entry that already exists - any first-time\nappearance forces a manual edit of conf/releases.yml because the task\ndoesn\u0027t know mirrorDirectory, releaseNotesRepo, or displayName.\n\nThis commit lets the task perform the first-time edit too when those three\ndescriptor flags are passed via -P. The decision is automatic based on what\nthe file already contains:\n\n  - existing artifactId entry under the major  -\u003e bump version: in place\n    (unchanged behaviour; descriptor flags ignored to prevent accidental\n    URL drift mid-major)\n  - \u0027N\u0027: major block exists but the artifactId is not listed\n    -\u003e append a new entry to the end of the block; descriptor flags required\n  - no \u0027N\u0027: block at all\n    -\u003e append a new major block at the end of companionArtifacts:,\n       preserving the blank-line separator between it and coreReleases:;\n       descriptor flags required\n\nscanLayout is a single-pass line scanner that records every index the\naction needs (companion-section header, target-major end, target-artifact\nversion line, companion-section end). The two insertion paths trim back\nover trailing blank lines so an inserted entry lands directly after the\nlast meaningful content line rather than after the blank-line separator.\n\nrelease-companion.yml gains three optional workflow_dispatch inputs\n(mirror_directory, release_notes_repo, display_name) that are forwarded\nto the task. Operators only fill them on the first publish for each\n(major, artifactId) pair; subsequent bumps leave them blank.\n\n11 new RecordCompanionReleaseTaskSpec scenarios cover all three paths\nplus the missing-flags, missing-core-flags, and no-companionArtifacts-\nsection error cases, plus the two cross-major insertion-point cases\nthat surfaced during development (target major is the last block in the\nsection vs target major is followed by a higher major).\n\nAssisted-by: claude-code:claude-opus-4-7\n"
    },
    {
      "commit": "41851a5a5f38b8bece0f2745221069ca4f3f3b52",
      "tree": "b90fb9aea6fcd881800f0bd866b50417defec8a7",
      "parents": [
        "967175525b2dc9b2484c760e50867f0d857080a1"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 13:40:26 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun May 03 13:40:26 2026 -0400"
      },
      "message": "Delete .playwright-mcp directory"
    },
    {
      "commit": "967175525b2dc9b2484c760e50867f0d857080a1",
      "tree": "441d60b3a68cf2c5d8efbb335397dd3920a9c24c",
      "parents": [
        "9adb0592a8aa63e3646f268122c42adb4c9bb489"
      ],
      "author": {
        "name": "grails-build",
        "email": "grails-build@users.noreply.github.com",
        "time": "Sun May 03 16:14:45 2026 +0000"
      },
      "committer": {
        "name": "grails-build",
        "email": "grails-build@users.noreply.github.com",
        "time": "Sun May 03 16:14:45 2026 +0000"
      },
      "message": "Update conf/releases.yml\n\nBump grails-publish to 0.0.5 for Grails 7\n"
    },
    {
      "commit": "9adb0592a8aa63e3646f268122c42adb4c9bb489",
      "tree": "739269a8f76382d358bbe5cb0faab13fd3760b55",
      "parents": [
        "16b8fd753c9dfff4be39c6f534179591076d7f13"
      ],
      "author": {
        "name": "grails-build",
        "email": "grails-build@users.noreply.github.com",
        "time": "Sun May 03 16:13:17 2026 +0000"
      },
      "committer": {
        "name": "grails-build",
        "email": "grails-build@users.noreply.github.com",
        "time": "Sun May 03 16:13:17 2026 +0000"
      },
      "message": "Update conf/releases.yml\n\nBump grails-github-actions to 1.0.2 for Grails 7\n"
    },
    {
      "commit": "16b8fd753c9dfff4be39c6f534179591076d7f13",
      "tree": "d5a5fa14f9984de1310d79d328795d04e8925ee6",
      "parents": [
        "d12eb09af31b9eb27c735f16492236ae1b9a7add"
      ],
      "author": {
        "name": "grails-build",
        "email": "grails-build@users.noreply.github.com",
        "time": "Sun May 03 16:06:20 2026 +0000"
      },
      "committer": {
        "name": "grails-build",
        "email": "grails-build@users.noreply.github.com",
        "time": "Sun May 03 16:06:20 2026 +0000"
      },
      "message": "Update conf/releases.yml\n\nBump grails-spring-security to 7.0.2 for Grails 7\n"
    },
    {
      "commit": "d12eb09af31b9eb27c735f16492236ae1b9a7add",
      "tree": "37b8c4dab574d0c19ba8167cf4fdaa7330658026",
      "parents": [
        "a5c5694b873ece2365d595d5ad118bdca3890ecf"
      ],
      "author": {
        "name": "grails-build",
        "email": "grails-build@users.noreply.github.com",
        "time": "Sun May 03 15:22:12 2026 +0000"
      },
      "committer": {
        "name": "grails-build",
        "email": "grails-build@users.noreply.github.com",
        "time": "Sun May 03 15:22:12 2026 +0000"
      },
      "message": "Update conf/releases.yml\n\nAdd 7.1.1 to Grails Versions list\n"
    },
    {
      "commit": "a5c5694b873ece2365d595d5ad118bdca3890ecf",
      "tree": "d9f385a56f084712e95eeb6e0f1bda5b690986d7",
      "parents": [
        "d7db046b4d4413b6b8b2006020482219fdd8373c"
      ],
      "author": {
        "name": "grails-build",
        "email": "grails-build@users.noreply.github.com",
        "time": "Sun May 03 15:20:23 2026 +0000"
      },
      "committer": {
        "name": "grails-build",
        "email": "grails-build@users.noreply.github.com",
        "time": "Sun May 03 15:20:23 2026 +0000"
      },
      "message": "Update conf/releases.yml\n\nAdd 7.0.11 to Grails Versions list\n"
    },
    {
      "commit": "d7db046b4d4413b6b8b2006020482219fdd8373c",
      "tree": "1c93b0fc6cf91a27671f0104acc050a2b05b6e5a",
      "parents": [
        "6e407043f45a7c9bca7491a501185d84aed5faac",
        "e64bca93f3b6f4b06a1bddeeee7589f85e1969d9"
      ],
      "author": {
        "name": "James Daugherty",
        "email": "jdaugherty@jdresources.net",
        "time": "Sun May 03 10:45:00 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun May 03 10:45:00 2026 -0400"
      },
      "message": "Merge pull request #489 from apache/multi-version-support\n\nSupport multiple concurrent Grails majors on the home, downloads, and documentation pages"
    },
    {
      "commit": "6e407043f45a7c9bca7491a501185d84aed5faac",
      "tree": "6be82196a5cc65f51d1adc9b6761a6cdc31fcdea",
      "parents": [
        "8ce3054190fac127d354be8f3fa88b21c2a47217",
        "644b6f48054263c6e968b52d37f1961374b5dbac"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 09:07:05 2026 -0500"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun May 03 09:07:05 2026 -0500"
      },
      "message": "Merge pull request #490 from apache/fix-improve-this-doc-button-always-visible\n\nRender the guides Improve this doc button as a button at all times"
    },
    {
      "commit": "644b6f48054263c6e968b52d37f1961374b5dbac",
      "tree": "6be82196a5cc65f51d1adc9b6761a6cdc31fcdea",
      "parents": [
        "8ce3054190fac127d354be8f3fa88b21c2a47217"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 10:06:12 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 10:06:12 2026 -0400"
      },
      "message": "Render the guides \"Improve this doc\" button as a button at all times\n\nThe recent guide-typography modernisation in PR #485 (commit bb0d09ff17)\ndemoted the AsciiDoc-emitted `\u003cdiv class\u003d\"contribute-btn\"\u003e\u003cbutton\u003e` from\nthe legacy orange CTA into a calm GitHub-style outlined button - which is\nthe right direction. The implementation, however, took it one step too\nfar: in the default state the button background, background-image, and\nborder were all set to `transparent`, so at rest the control reads as a\nplain inline link with a pencil glyph. The button-shape only appears on\n:hover, when the background switches to `#f6f8fa` and the border to\n`#d0d7de`.\n\nPer user feedback on the live guide pages\n(https://grails.apache.org/guides/...), the button should look like a\nbutton at all times, not only while the cursor is over it. Hover should\nprovide an interaction accent on top of an already-visible button, not\nreveal the button chrome itself.\n\nThis commit moves the rest-state surface up by one level so that the\nbutton is always visible:\n\n  - Default state now uses `background: #f6f8fa` and\n    `border: 1px solid #d0d7de` (the values previously reserved for\n    :hover), keeping the existing `#6b7280` text colour, 12px Inter\n    font, 4/10px padding, 6px corner radius, and the no-shadow flat\n    treatment introduced in #485.\n  - :hover state keeps its `#0969da` GitHub-blue text accent but now\n    deepens the surface to `background-color: #eaeef2` and the border\n    to `#afb8c1` so there is still a clear, subtle interaction signal\n    sitting on top of the rest state.\n  - The block comment above the rule is updated to describe the new\n    intent (\"visible at rest, gains a colour accent on hover\") instead\n    of the old \"subtle right-aligned link\" framing.\n\nOnly `guides/resources/css/guide.css` is touched - the file actually\nloaded by `guides/resources/style/layout.html` for every rendered guide\npage. The legacy orange `.contribute-btn button` rules in\n`guides/resources/css/screen.css` and `assets/stylesheets/screen.css`\nremain on disk but are not loaded by guide pages and are already\noverridden everywhere they would matter via the `!important` selectors\nin `guide.css`, so no change is needed there.\n\nVerification (locally):\n\n  - `./gradlew renderGuide_creating_your_first_grails_app_6` produces\n    `build/dist/guides/creating-your-first-grails-app/6/` with the\n    updated `css/guide.css` (37 `.contribute-btn` buttons across the\n    rendered chapters).\n  - Served `build/dist/` via `jwebserver` and inspected the rendered\n    page with Playwright. `getComputedStyle(.contribute-btn button)`\n    in the rest state now reports\n    `background-color: rgb(246, 248, 250)` (#f6f8fa) and\n    `border-color: rgb(208, 215, 222)` (#d0d7de), confirming the\n    button surface and border are present without a hover - matching\n    the values previously reserved for :hover.\n\nAssisted-by: claude-code:claude-opus-4-7\n"
    },
    {
      "commit": "e64bca93f3b6f4b06a1bddeeee7589f85e1969d9",
      "tree": "048c28feeabd70ce4494e9487c16901c47af9647",
      "parents": [
        "f5d9214daba633447033327c306afbf840e66654"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 22:25:09 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 07:34:02 2026 -0400"
      },
      "message": "Add recordCompanionRelease Gradle task and matching release-companion workflow\n\nCompanion artifacts (grails-spring-security, grails-redis, grails-quartz, grails-github-actions, grails-publish, ...) ship independently of Grails core, so each one has its own release cadence and needs its own bump path now that their versions live in conf/releases.yml\u0027s companionArtifacts: section.\n\nRecordCompanionReleaseTask performs a line-based update of the existing entry\u0027s \u0027version:\u0027 field so comments and indentation in releases.yml are preserved (a SnakeYAML round-trip would erase them). It locates the right block by walking the file looking for the \u0027companionArtifacts:\u0027 key, then the matching \u0027N\u0027:\u0027 major key, then the matching \u0027artifactId: \u003cname\u003e\u0027 entry, then rewrites the next \u0027version:\u0027 line. Smoke-tested by bumping grails-redis 5.0.1 -\u003e 5.0.2 and confirming the diff is exactly one line.\n\nFirst-time addition of a brand-new companion artifact still requires a manual edit of releases.yml (mirrorDirectory, releaseNotesRepo, and displayName all need PMC-supplied values that vary per plugin) - the task fails with a clear error message in that case rather than guessing.\n\nrelease-companion.yml is a parallel GitHub Actions workflow modelled on release.yml: workflow_dispatch with grails_major + artifact_id + artifact_version inputs, runs the new task, then commits and pushes the single-line diff. The existing release.yml workflow is unchanged and still drives Grails core releases.\n\nAssisted-by: claude-code:claude-opus-4-7\n"
    },
    {
      "commit": "f5d9214daba633447033327c306afbf840e66654",
      "tree": "9c89625cbd82d511d51e552a32047ded1c37667d",
      "parents": [
        "e65b57de523ea92e1fdb7fb18fa1d90dfb7169aa"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 22:21:41 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 07:34:02 2026 -0400"
      },
      "message": "Render multi-version hero cards on the home page\n\nReplaces the home page\u0027s two hard-coded \u0027Download / Documentation\u0027 call-to-action boxes (which both pointed at the single [%latest] version) with a [%hero_cards] placeholder that RenderSiteTask fills with one card per active stable minor line at build time. Today this renders side-by-side cards for 7.1.0 and 7.0.10; once 8.0.0 ships it renders 8.0.0, 7.1.0, 7.0.10 with no edit to pages/index.html required.\n\nAdoption rules:\n\n  - Stable only. Pre-releases / milestones never appear in the hero per Apache release-policy guidance from this PR\u0027s pre-implementation discussion - those still surface on /download.html and /documentation.html.\n\n  - Apache-distributed only (major \u003e\u003d 7). Older majors used GitHub releases that don\u0027t follow the predictable apache.org/dyn/closer.lua URL pattern the hero links rely on, so they\u0027re skipped if they ever surface in activeMinorLines.\n\n  - Driven entirely by SiteMap.activeMinorLines + latestStablePerMinorLine. No new config, no per-major hand-coding in the template.\n\nRenderSiteTask.siteMeta gains a hero_cards key wired through to the template substitution. Existing [%latest] placeholders elsewhere in the site continue to resolve to the single highest-stable version (unchanged).\n\nCSS additions are scoped under .hero-cards / .hero-card / .hero-card-label / .hero-card-actions / .hero-action with .hero-action-download (orange call-to-action) and .hero-action-docs (outlined secondary) variants. Cards stack to full-width on viewports under 768px.\n\nVerified: rendered build/dist/index.html contains exactly one .hero-cards container, two .hero-card boxes (one each for 7.0.10 and 7.1.0), and one Download + one Documentation hero-action per card.\n\nAssisted-by: claude-code:claude-opus-4-7\n"
    },
    {
      "commit": "e65b57de523ea92e1fdb7fb18fa1d90dfb7169aa",
      "tree": "1a67b899b8210189fbbc65ed442511aaafa15e89",
      "parents": [
        "c07578d5704dfa68df59314f11f53ef51205a38d"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 22:19:02 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 07:34:02 2026 -0400"
      },
      "message": "Mirror Downloads-page card grid on the Documentation page\n\nRestructures documentation.html with the same multi-version layout the previous commit applied to download.html. The legacy two-column-stack collapsed every doc reference into a single column with one prominent \u0027Latest\u0027 card and the rest in a stack of dropdowns; the redesigned page features:\n\n  Current Versions: one card per active minor line linking to its User Guide + API Reference. Driven by SiteMap.activeMinorLines, so today this renders 7.1.0 + 7.0.10, and after 8.0.0 ships it renders 8.0 + 7.1 + 7.0.\n\n  Pre-release (Apache-released): cards for any milestone / RC documentation that hasn\u0027t been superseded. Apache release policy requires the M/RC docs to be linked even though those versions aren\u0027t featured on the home page. Empty today (7.1.0-RC1 is superseded by 7.1.0 stable).\n\n  Snapshot: a single card pointing at https://grails.apache.org/docs/snapshot/.\n\n  Older Versions: the three legacy dropdowns (Single-page, User Guide, API Reference) for every stable docs URL since Grails 1.2.0, unchanged.\n\n  Modules: the GORM, Security, Upgrade, Testing, Views, Async, Database, Redis category boxes, in a two-column footer.\n\nVerified: rendered build/temp/documentation.html contains 11 .guide-group cards inside 2 .release-grid containers, 4 .release-section-header section titles, and per-version /docs/X.Y.Z/ + /docs/X.Y.Z/api/ links for 7.0.10, 7.1.0, and snapshot.\n\nAssisted-by: claude-code:claude-opus-4-7\n"
    },
    {
      "commit": "c07578d5704dfa68df59314f11f53ef51205a38d",
      "tree": "d50b01046abdeec761d046614816fa4d89720f90",
      "parents": [
        "794b1605123e7573e8b05c4e457899a88c23614c"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 22:16:58 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 07:34:02 2026 -0400"
      },
      "message": "Redesign Downloads page with multi-version card grid for active minor lines\n\nRestructures the Downloads page from the legacy two-column-stack layout (which collapsed every release into the left column with one prominent card and the rest in dropdowns) into a multi-version card grid:\n\n  Current Releases: one .guide-group card per active minor line in a flex .release-grid container. Today this renders 7.1.0 + 7.0.10 side-by-side; once 8.0.0 ships, 8.0 + 7.1 + 7.0 \u003d three cards.\n\n  Pre-release (Apache-released): a parallel grid for milestone / RC versions that haven\u0027t been superseded by a stable release at or above them. Empty today because 7.1.0-RC1 was superseded by 7.1.0 stable, but Apache release policy requires links to remain available when an unsuperseded M/RC is current (e.g. 8.0.0-M1).\n\n  Older Versions: dropdown of every stable since 0.1, unchanged.\n\n  Get Started: Application Forge + SDKMAN install instructions, moved from a parallel right column into a full-width two-column footer.\n\nCSS additions are additive: new .release-grid (flex container, 380-480px card width with 24px gap, full-width stack on screens narrower than 768px), .release-section-header (matches the existing .column-header palette), and .release-page-intro for the verification preamble paragraph. The per-card visual treatment continues to use the existing .guide-group / .guide-group-header / .guide-group ul rules, so this layout change is purely additive at the card level - the cards themselves render identically.\n\nVerified: rendered build/temp/download.html contains exactly two .guide-group cards inside one .release-grid (one per active minor line - 7.0.10 and 7.1.0), 24 download/signature/release-notes references for 7.0.10 and 14 for 7.1.0 (the latter has more companion plugins on the same major), and zero pre-release card references because 7.1.0-RC1 has been superseded by 7.1.0 stable.\n\nAssisted-by: claude-code:claude-opus-4-7\n"
    },
    {
      "commit": "794b1605123e7573e8b05c4e457899a88c23614c",
      "tree": "ceeeec9f48c661fedd00be6e7a22729722000d21",
      "parents": [
        "953aed525b9cc8ffc71ecb1840bf6ee99ff4f712"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 22:13:29 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 07:33:33 2026 -0400"
      },
      "message": "Add SiteMap multi-major helpers for the multi-version page redesign\n\nAdds four query methods that drive the upcoming multi-major rendering changes on the home, downloads, and documentation pages:\n\n  highestStablePerMajor(File): the highest stable release per major - the cross-major reference point used to decide which pre-releases are still relevant.\n\n  latestStablePerMinorLine(File): one entry per X.Y minor line, pinned to the highest patch on that line. Drives one card per minor line on the redesigned downloads page.\n\n  latestPreReleasePerMajor(File): the latest M/RC per major, suppressed once a stable at or above it has shipped. Apache release policy still requires the milestone/RC artefacts to be linked, so this map drives that section. New-major M/RCs surface even when their major has no stable yet.\n\n  activeMinorLines(File): which X.Y lines should each get a card. Always includes every minor line of the highest stable major; if that major has fewer than two minor lines, also includes the previous major\u0027s lines so the page never shrinks to a single card right after a new major ships its first minor. Once the new major grows a second minor line, the previous major\u0027s lines roll off automatically.\n\n13 SiteMapSpec scenarios cover the today / 8.0.0 ship-day / 8.1.0 ship-day transitions, pre-release suppression, brand-new-major pre-releases, companion artefact lookup, and legacy \u0027releases:\u0027 key compatibility.\n\nNo callers consume these helpers yet - the next commit wires them into the downloads page redesign.\n\nAssisted-by: claude-code:claude-opus-4-7\n"
    },
    {
      "commit": "953aed525b9cc8ffc71ecb1840bf6ee99ff4f712",
      "tree": "acded9217b528323fb2ede5debbd9e7c50633f9f",
      "parents": [
        "180ca9fcc35fb98763d101afdb00739fe511214f"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 22:10:03 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 07:33:33 2026 -0400"
      },
      "message": "Move hard-coded companion plugin versions out of DownloadPage into releases.yml\n\nReplaces five method-local version constants in DownloadPage.renderDownload (redisVersion, quartzVersion, springSecurityVersion, grailsGithubActionsVersion, grailsGradlePublishVersion) with a data-driven loop over CompanionArtifact entries loaded from the new companionArtifacts:\u00277\u0027 block in releases.yml. Each entry carries its artifactId, version, mirror sub-directory, GitHub release-notes repo, and display name.\n\nAdds CompanionArtifact (an @Immutable value type) and SiteMap.companionArtifactsFor(File, int) which reads the companionArtifacts: section keyed by Grails major. Returns an empty list for majors with no entry, which lets the renderer transparently handle Grails 6 and earlier (GitHub-released, no companion plugins) and future majors before their plugins ship.\n\nDownloadPage.isApacheDistributed(String) now derives the Apache-mirrors-vs-GitHub-releases switch from ReleaseVersion.major \u003e\u003d 7 instead of the literal version.startsWith(\u00277\u0027) check. Grails 8 will pick up the Apache distribution path automatically once its core release is recorded; its companions ride the same companionArtifacts data path under the \u00278\u0027 key.\n\nVerified: rendered download.html contains identical Apache mirror URLs and versions to the legacy hard-coded output - spring-security 7.0.1, redis 5.0.1, quartz 4.0.1, github-actions 1.0.1, grails-publish 0.0.4.\n\nAssisted-by: claude-code:claude-opus-4-7\n"
    },
    {
      "commit": "180ca9fcc35fb98763d101afdb00739fe511214f",
      "tree": "2e8a7c2a9bbe423302e3678126b8c01496db353b",
      "parents": [
        "94f7fa0a776a3628241fb953a7fcb5dbeae57f1e"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 22:07:12 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 07:33:33 2026 -0400"
      },
      "message": "Migrate releases.yml to structured schema with companionArtifacts section\n\nRenames the top-level YAML key from \u0027releases:\u0027 to \u0027coreReleases:\u0027 and reserves a \u0027companionArtifacts:\u0027 section above it for per-major Apache-released plugins (Spring Security, Redis, Quartz, etc.). The companionArtifacts block is intentionally empty in this commit - the next commit relocates DownloadPage\u0027s hard-coded plugin version constants into it.\n\nSiteMap.versions() accepts either \u0027coreReleases:\u0027 (canonical) or the legacy \u0027releases:\u0027 key for one release cycle so external tooling that still writes the old schema keeps working during the migration window.\n\nSection ordering is intentional: companionArtifacts: is placed first so coreReleases: remains the last top-level block in the file. This keeps RecordReleaseTask\u0027s plain append-to-end-of-file working without structural YAML parsing.\n\nVerified: genDownloads and genDocsPage both run cleanly against the new schema.\n\nAssisted-by: claude-code:claude-opus-4-7\n"
    },
    {
      "commit": "94f7fa0a776a3628241fb953a7fcb5dbeae57f1e",
      "tree": "0b859329be869ee1d8920aed526467db72caa4af",
      "parents": [
        "673ea8413e737179b67c20495609c3cff92bc6ca"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 22:05:27 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 07:33:33 2026 -0400"
      },
      "message": "Replace local SoftwareVersion/Snapshot with ReleaseVersion facade over grails-core GrailsVersion\n\nReleaseVersion is a thin facade that delegates all parsing and comparison to org.grails.datastore.mapping.core.grailsversion.GrailsVersion (from grails-datastore-core). The facade exists for two reasons: (1) GrailsVersion\u0027s strict constructor only accepts 3+ part versions, so legacy entries in releases.yml such as \u00270.1\u0027, \u00271.0\u0027, \u00271.0.RC1\u0027, and \u00273.0.0.M1\u0027 are normalized into a form GrailsVersion accepts before construction, while the original text is preserved for display fidelity in dropdowns and URL building; (2) it keeps version parsing rules in grails-core, so this codebase no longer maintains its own regex-based parser.\n\nAlso fixes resolveDocumentationName, which previously labelled \u00277.0.0-M1\u0027 as \u0027Latest\u0027 because it matched on \u0027.M\u0027 (dot-style) and missed dash-style qualifiers. The new implementation routes through ReleaseVersion so both legacy dot-qualifiers and modern dash-qualifiers resolve correctly.\n\nThe vendored grails/doc/dropdown/SoftwareVersion.groovy and Snapshot.groovy under buildSrc/src/main/groovy/grails/doc/ are intentionally left untouched: per buildSrc/VENDOR.md they are byte-identical copies from apache/grails-core 7.2.x and any local modification would break the re-vendor procedure.\n\nReleaseVersionSpec ports the historical SoftwareVersionSpec test cases plus new coverage for 2-part and pre-release ordering. 38 of 38 tests pass. The single remaining failure in :buildSrc:test (ValidateGuidesTaskSpec \u003e shape mode rejects a non-40-char SHA) is pre-existing and unrelated; verified by running the same test against master.\n\nAssisted-by: claude-code:claude-opus-4-7\n"
    },
    {
      "commit": "673ea8413e737179b67c20495609c3cff92bc6ca",
      "tree": "1b074153c90e2281f0b1bac2d0898978e5375953",
      "parents": [
        "8ce3054190fac127d354be8f3fa88b21c2a47217"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 21:58:37 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sun May 03 07:33:33 2026 -0400"
      },
      "message": "Add grails-datastore-core dependency for canonical GrailsVersion model\n\nPulls in org.grails.datastore.mapping.core.grailsversion.GrailsVersion plus its companion Snapshot from grails-core via the grails-bom platform. The next commit replaces this repo\u0027s hand-rolled SoftwareVersion/Snapshot parser with a thin facade that delegates parsing and comparison to GrailsVersion, so the website no longer maintains its own regex-based version model.\n\nThe BOM (org.apache.grails:grails-bom) is required because grails-datastore-core\u0027s POM uses unversioned constraints on grails-common and grails-gradle-common, expecting downstream consumers to provide the BOM for resolution.\n\nAssisted-by: claude-code:claude-opus-4-7\n"
    },
    {
      "commit": "8ce3054190fac127d354be8f3fa88b21c2a47217",
      "tree": "3658155086b4fe3cc76c825e67f48b12ec753b51",
      "parents": [
        "687e16f3f11a35650280ffa8a9a22e5fcdeb36ec",
        "fd20076cb7dd143a3e15a628082de147bc7ce823"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 21:04:53 2026 -0500"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat May 02 21:04:53 2026 -0500"
      },
      "message": "Merge pull request #487 from apache/fix-ogurl-metadata-in-plugin-blog-archive-pages\n\nSet [%ogurl] metadata on plugins, blog-archive, blog-tag, and minutes pages"
    },
    {
      "commit": "687e16f3f11a35650280ffa8a9a22e5fcdeb36ec",
      "tree": "a189e3ae27121d96df73c7c0dcc047372d16d246",
      "parents": [
        "e5bd13bd8fdd1103a4b4009fcb58f759d821feaa",
        "a266abfcaa0a0b7c985819f90a7e79a0a200e2e0"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 21:04:14 2026 -0500"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat May 02 21:04:14 2026 -0500"
      },
      "message": "Merge pull request #486 from apache/fix-partials-not-expanded-in-plugin-blog-minutes-pages\n\nExpand [%PARTIAL:...] tokens in plugins, blog, and minutes pages"
    },
    {
      "commit": "a266abfcaa0a0b7c985819f90a7e79a0a200e2e0",
      "tree": "a189e3ae27121d96df73c7c0dcc047372d16d246",
      "parents": [
        "e5bd13bd8fdd1103a4b4009fcb58f759d821feaa"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 21:51:06 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 22:03:01 2026 -0400"
      },
      "message": "Expand [%PARTIAL:...] tokens in plugins, blog, and minutes pages\n\nThe site-wide template at templates/document.html embeds the shared\nchrome via three placeholders that the build is supposed to substitute\nat render time:\n\n  [%PARTIAL:site-head]\n  [%PARTIAL:site-header]\n  [%PARTIAL:site-footer]\n\nSubstitution is centralised in\nRenderSiteTask.renderHtmlWithTemplateContent, which delegates to\nexpandPartials(templateText, partialsRoot). The latter is a no-op when\npartialsRoot is null. RenderSiteTask itself wires the partials directory\nthrough to its task input and passes it on every call, so pages under\npages/*.html (index, community, download, faq, support, ...) render\ncorrectly.\n\nPluginsTask, BlogTask, and MinutesTask call\nrenderHtmlWithTemplateContent directly with only three arguments, so\npartialsRoot defaults to null and the [%PARTIAL:...] tokens ship to\nproduction verbatim. The user-visible result on\nhttps://grails.apache.org/plugins.html (and on every blog post, blog\ntag page, blog index, plugins-tags page, plugins-owners page, and\nminutes page) is missing site chrome - the page body has no top\nnavigation, no header bar, no footer.\n\nThis commit fixes the bug at the source rather than at every call site:\n\n  - Adds a `partialsDir` directory property to GrailsWebsiteExtension,\n    conventioned to `templates/partials` so all task wirings agree on\n    one location instead of hardcoding the path per task.\n  - Updates RenderSiteTask to source its existing `partialsDir` task\n    input from the extension rather than the hardcoded path.\n  - Adds the same `@InputDirectory partialsDir` task input to\n    PluginsTask, BlogTask, and MinutesTask, wires it from\n    `siteExt.partialsDir` in each `register()` method, and expands\n    the partials once at the top of each `@TaskAction` before the\n    template text reaches the static rendering helpers. Downstream\n    helpers keep their existing String templateText signatures and\n    forward the pre-expanded text into renderHtmlWithTemplateContent\n    without a partialsRoot - the second expandPartials pass inside\n    that helper is a no-op when partialsRoot is null, so behaviour\n    is unchanged for any code path that already worked.\n  - Imports `org.gradle.api.tasks.InputDirectory` into PluginsTask\n    (the only one of the three that didn\u0027t already have the import).\n\nVerification (`./gradlew clean build`):\n\n  - 502 .html files in build/dist/ scanned: 0 unresolved\n    [%PARTIAL:...] tokens (was 364+ across plugins, blog, blog tags,\n    plugins-tags, plugins-owners on the live site).\n  - Spot checks of plugins.html, blog/index.html, blog/tag/groovy.html,\n    plugins/tags/rest.html, plugins/owners/virtualdogbert.html,\n    pages/{index,community,download,faq,support}.html all show the\n    main-header / secondary-menu chrome rendering and the page body\n    no longer has the literal `[%PARTIAL:...]` text.\n  - Visual verification via Playwright on plugins.html and\n    blog/index.html: full Grails chrome, working top nav, \"Blog\" /\n    \"Plugins\" tab highlighted in the secondary nav, page body renders\n    correctly.\n\nOut of scope:\n\n  - 364 unresolved `[%ogurl]` tokens remain on plugins/blog/tag/owner\n    pages; that is a pre-existing distinct bug (PluginsTask and the\n    BlogTask archive/tag paths never set the `ogurl` metadata key) and\n    is already shipping on https://grails.apache.org/plugins.html and\n    https://grails.apache.org/blog/index.html today. Will ship in a\n    follow-up.\n  - MinutesTask currently emits to build/foundation/minutes/ rather\n    than build/dist/foundation/minutes/, so the rendered minutes pages\n    don\u0027t ship to production at all. That is also a separate\n    pre-existing bug and is left untouched here.\n\nAssisted-by: claude-code:claude-opus-4-7\n"
    },
    {
      "commit": "e5bd13bd8fdd1103a4b4009fcb58f759d821feaa",
      "tree": "fe3f85e58def14167b62759bdb42fb97550809b0",
      "parents": [
        "6b7477f379be9624000c247af308d128179d2223",
        "a2ac395abbdd8b392da9463de2be3b26ea116f6f"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 21:01:57 2026 -0500"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat May 02 21:01:57 2026 -0500"
      },
      "message": "Merge pull request #488 from apache/fix-minutes-task-output-path-not-under-dist\n\nMove MinutesTask output under build/dist/ so minutes pages reach production"
    },
    {
      "commit": "a2ac395abbdd8b392da9463de2be3b26ea116f6f",
      "tree": "fe3f85e58def14167b62759bdb42fb97550809b0",
      "parents": [
        "6b7477f379be9624000c247af308d128179d2223"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 21:59:42 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 21:59:42 2026 -0400"
      },
      "message": "Move MinutesTask output under build/dist/ so minutes pages reach production\n\nMinutesTask.renderMinutes wrote rendered foundation/minutes pages to\n`outputDir.dir(\u0027foundation/minutes\u0027)`, which resolves to\n`build/foundation/minutes/` because `siteExt.outputDir` is conventioned\nto the project\u0027s build directory. Every other rendering task in this\nproject writes underneath `build/dist/`:\n\n  RenderSiteTask  -\u003e outputDir.dir(\u0027dist\u0027)\n  BlogTask        -\u003e outputDir.dir(\u0027dist/blog\u0027)\n  PluginsTask     -\u003e outputDir.dir(\u0027dist\u0027),\n                     outputDir.dir(\u0027dist/plugins/tags\u0027),\n                     outputDir.dir(\u0027dist/plugins/owners\u0027)\n  PluginsTask     -\u003e outputDir.dir(\u0027dist/images\u0027)          (asset copy)\n  MinutesTask     -\u003e outputDir.dir(\u0027dist/images\u0027)          (asset copy)\n\nThe publish workflow ships only `build/dist/` to the\n`asf-site-production` branch on apache/grails-website, so anything the\nbuild emits outside `build/dist/` is invisible to production. Result:\nfoundation/minutes pages have not received build updates from\n`./gradlew build` since this divergence was introduced; the live\nfoundation/minutes/* pages on grails.apache.org are stale, rendered by\nsome earlier version of the build that put them in the right place.\n\nFix is one character: change the destination directory from\n`\u0027foundation/minutes\u0027` to `\u0027dist/foundation/minutes\u0027`. The downstream\n`renderRss` call uses `outputDir.parentFile` resolved against the same\ntarget, so the RSS feed correspondingly moves from\n`build/foundation/minutes.xml` to `build/dist/foundation/minutes.xml`,\nwhich matches how BlogTask emits `build/dist/rss.xml` from\n`build/dist/blog/`.\n\nVerification (./gradlew clean build):\n\n  - 6 .html minutes pages now appear under\n    build/dist/foundation/minutes/ (5 individual + 1 index).\n  - The minutes RSS feed appears at build/dist/foundation/minutes.xml.\n  - The previous build/foundation/minutes/ tree is gone.\n  - Other rendering tasks unaffected: 15 root pages, 125 blog posts,\n    214 plugins tag pages, 109 plugins owner pages all still render.\n\nOut of scope:\n\n  - The minutes pages still need PR #486 merged (to expand the\n    [%PARTIAL:...] tokens) and PR #487 merged (to populate [%ogurl])\n    before they render with full site chrome and correct social cards.\n    That ordering is fine; this PR is the prerequisite that makes the\n    pages reach production at all.\n\nAssisted-by: claude-code:claude-opus-4-7\n"
    },
    {
      "commit": "fd20076cb7dd143a3e15a628082de147bc7ce823",
      "tree": "ad4742d85715dfc513c9761278f72773ff069ced",
      "parents": [
        "6b7477f379be9624000c247af308d128179d2223"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 21:57:10 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 21:57:10 2026 -0400"
      },
      "message": "Set [%ogurl] metadata on plugins, blog-archive, blog-tag, and minutes pages\n\nThe site-wide template at templates/document.html embeds a per-page\nOpen Graph URL via \u003cmeta property\u003d\"og:url\" content\u003d\"[%ogurl]\"/\u003e.\nSubstitution happens in RenderSiteTask.replaceLineWithMetadata only\nwhen the metadata map handed to renderHtmlWithTemplateContent contains\na non-null `ogurl` key.\n\nThree rendering paths never set ogurl, so every page they emit ships\nthe literal \"[%ogurl]\" token in the og:url meta tag. The defect is\nvisible right now on https://grails.apache.org/plugins.html and on\nhttps://grails.apache.org/blog/index.html, and it propagates to every\nplugins/tags/\u003ctag\u003e.html, plugins/owners/\u003cowner\u003e.html,\nblog/tag/\u003ctag\u003e.html, and minutes page.\n\nSibling rendering paths already set ogurl correctly (RenderSiteTask\nsets it from siteMeta[\u0027url\u0027] + page.path; BlogTask.renderPostHtml sets\nit via postLink), so this change is just bringing the broken paths in\nline with the working ones.\n\nChanges:\n\nPluginsTask.renderHtml:\n  The base metadata map was reused across the main page and every\n  tag/owner page, which meant we couldn\u0027t simply set ogurl on it - we\n  need a per-page value. Reshape the wrap closure to accept the\n  per-page ogurl, copy the metadata into a fresh LinkedHashMap, set\n  ogurl on the copy, then render. Three call sites updated:\n    - plugins.html               -\u003e \u003csiteUrl\u003e/plugins.html\n    - plugins/tags/\u003ctag\u003e.html    -\u003e \u003csiteUrl\u003e/plugins/tags/\u003ctag\u003e.html\n    - plugins/owners/\u003cslug\u003e.html -\u003e \u003csiteUrl\u003e/plugins/owners/\u003cslug\u003e.html\n  Owner slug computation hoisted into a local so the file path and the\n  ogurl URL agree.\n\nBlogTask.renderArchive:\n  Set ogurl on the resolved metadata (mutating the .tap{} block) to\n  \u003csiteUrl\u003e/blog/index.html.\n\nBlogTask.renderTags:\n  Per-tag ogurl already lives in the LinkedHashMap.tap{} block that\n  customises the title for each tag page; add ogurl \u003d\n  \u003csiteUrl\u003e/blog/tag/\u003ctag\u003e.html alongside the title.\n\nMinutesTask.renderArchive:\n  Set ogurl on the resolved metadata to\n  \u003csiteUrl\u003e/foundation/minutes/index.html.\n\nMinutesTask.renderMinutesHtml:\n  Set ogurl from minutesLink(htmlMinutes) before\n  renderHtmlWithTemplateContent. minutesLink returns\n  \u003csiteUrl\u003e/foundation/minutes/\u003chtmlMinutes.path\u003e.\n\nVerification (./gradlew clean build):\n  - 502 .html files in build/dist/: 0 unresolved [%ogurl] tokens\n    (was 364 across plugins/blog/tag/owner pages on the live site).\n  - Spot checks confirmed each page emits its own correct og:url:\n      plugins.html                       -\u003e /plugins.html\n      blog/index.html                    -\u003e /blog/index.html\n      blog/tag/groovy.html               -\u003e /blog/tag/groovy.html\n      plugins/tags/rest.html             -\u003e /plugins/tags/rest.html\n      plugins/owners/virtualdogbert.html -\u003e /plugins/owners/virtualdogbert.html\n      foundation/minutes/20210321-tab.html\n                                         -\u003e /foundation/minutes/20210321-tab.html\n  - Regression: pages from RenderSiteTask (index, community, download,\n    faq, support) and individual blog posts still emit their pre-existing\n    correct og:url meta tags.\n\nAssisted-by: claude-code:claude-opus-4-7\n"
    },
    {
      "commit": "6b7477f379be9624000c247af308d128179d2223",
      "tree": "0e504ce13bad5727a103372a2a8883ae944fb2ed",
      "parents": [
        "29bcf9be35dcade32b688f4db15d3c382f94f94b",
        "bb0d09ff17115cbc26ce841072e3630b1c46da0b"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 20:38:14 2026 -0500"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat May 02 20:38:14 2026 -0500"
      },
      "message": "Merge pull request #485 from apache/modernize-guide-typography\n\nModernise guide page typography, TOC spacing, and inline-code styling"
    },
    {
      "commit": "bb0d09ff17115cbc26ce841072e3630b1c46da0b",
      "tree": "0e504ce13bad5727a103372a2a8883ae944fb2ed",
      "parents": [
        "29bcf9be35dcade32b688f4db15d3c382f94f94b"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 21:36:06 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 21:36:06 2026 -0400"
      },
      "message": "Modernise guide page typography, TOC spacing, and inline-code styling\n\nSelf-host Inter (body / UI) and JetBrains Mono (code) under\nassets/fonts/, replace the Bootstrap-3 raspberry inline-code styling\nwith a calm GitHub-style chip, fix the TOC where chapter numbers were\nrendered flush against their titles (\"1Getting Started\"), demote the\norange \"Improve this doc\" CTA to a subtle right-aligned link, flatten\n\"Get the Code\" / \"Download ZIP\" / SSH / HTTPS / clipboard buttons,\nand tighten heading hierarchy, link colour, and code-block chrome to\nmatch docs.spring.io / kotlinlang.org / docs.gradle.org as the quality\nbar.\n\nThe previous PR #484 added inline-code rules to guides/resources/css/\nguide.css with a #c7254e colour and Droid Sans Mono fallback that the\nuser reported as \"worse than before\". Those rules are removed; the\nbetter GitHub-style equivalents in assets/stylesheets/screen.css are\nre-scoped to body.guide / article.guide so they actually match the\nrendered DOM (the previous .content article p \u003e code selector pointed\nat a wrapper that doesn\u0027t exist on guide pages).\n\nInter and JetBrains Mono are SIL OFL 1.1 - attribution added to the\nroot NOTICE file alongside Roboto, with the licence text bundled at\nassets/fonts/OFL.txt. All fonts are same-origin so no CSP allowlist\nchanges are required.\n\nAssisted-by: claude-code:claude-opus-4-7\n"
    },
    {
      "commit": "29bcf9be35dcade32b688f4db15d3c382f94f94b",
      "tree": "8eb1b4961fefd198605645a7093ff0e59faa22ba",
      "parents": [
        "fab33d4a2192105ee838f3928535ea5becbdefb7",
        "b0f1a75d7a55fbdadfcd34fd3e5c83c3abb77577"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 19:19:57 2026 -0500"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat May 02 19:19:57 2026 -0500"
      },
      "message": "Merge pull request #484 from apache/fix-code-block-css-targets-guide-css\n\nRe-apply code-block CSS to guides/resources/css/guide.css (PR #482 follow-up)"
    },
    {
      "commit": "b0f1a75d7a55fbdadfcd34fd3e5c83c3abb77577",
      "tree": "8eb1b4961fefd198605645a7093ff0e59faa22ba",
      "parents": [
        "fab33d4a2192105ee838f3928535ea5becbdefb7"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 20:07:29 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 20:07:29 2026 -0400"
      },
      "message": "Re-apply code-block CSS to guides/resources/css/guide.css (where guides actually load)\n\nPR #482 added the code-block + inline-code refinements to buildSrc/src/main/template/css/main.css. That file is the source for guides/resources/css/main.css, but rendered guide pages NEVER load main.css - they load guides/resources/css/guide.css (relative \u0027../css/guide.css\u0027 on each rendered page) plus the absolute main-site assets/stylesheets/screen.css. The legacy buildSrc/src/main/template/style/{layout,guideItem,referenceItem}.html templates that DO link main.css are not used by the modern RenderGuidesPlugin pipeline.\n\nConfirmed by inspecting the live HTML at https://grails.apache.org/guides/grails-fields-custom-widgets-and-wrappers/8/guide/index.html: only screen.css + plugin.css + guide.css are linked; main.css is not.\n\nRe-applies the same five rule blocks from PR #482 (inline \u003ccode\u003e pill, always-visible language chip, .listingblock \u003e .title gray header bar, square top corners on titled listings, horizontal overflow on long lines) to guides/resources/css/guide.css. PR #482\u0027s main.css additions are left in place - harmless if unused, and they keep the legacy templates consistent if they ever get re-enabled.\n\nVerified: ./gradlew renderGuide_grails_fields_custom_widgets_and_wrappers_8 --rerun-tasks rebuilds the guide; \u0027p \u003e code\u0027 rule and three \u0027.listingblock \u003e .title\u0027 selectors land in build/dist/guides/grails-fields-custom-widgets-and-wrappers/8/css/guide.css. File size 5867 -\u003e 8432 bytes.\n\nAssisted-by: claude-code:claude-opus-4-7\n"
    },
    {
      "commit": "fab33d4a2192105ee838f3928535ea5becbdefb7",
      "tree": "3d4821cab4289ebf8941e4b8feeccf0078d97205",
      "parents": [
        "e1760b0d322ac7cc7e57e75cc1baba020fb71b76",
        "494bc4d47c75126ae16be0f888abbdafea728466"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 16:08:00 2026 -0500"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat May 02 16:08:00 2026 -0500"
      },
      "message": "Merge pull request #483 from apache/fix-grails-fields-v8-jdk-and-table-override\n\nFix grails-fields v8: JDK 21 minimum, start.grails.org, correct table-template paths"
    },
    {
      "commit": "e1760b0d322ac7cc7e57e75cc1baba020fb71b76",
      "tree": "8703c9313a31a7975edfecf7a46ac06e6cc147ac",
      "parents": [
        "fecf6e80c61f1b03942d6eefc704b51308ef4e4e",
        "57748ad2c8201ea73d4d3a7ef69cc23075796746"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 16:07:21 2026 -0500"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat May 02 16:07:21 2026 -0500"
      },
      "message": "Merge pull request #482 from apache/improve-code-block-styling\n\nPolish code-block rendering and tighten README contributor flow"
    },
    {
      "commit": "57748ad2c8201ea73d4d3a7ef69cc23075796746",
      "tree": "8703c9313a31a7975edfecf7a46ac06e6cc147ac",
      "parents": [
        "91d928d2a5c6ce64e3ec6d2de59b7d17cc2e8bd7"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 16:49:15 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 16:49:15 2026 -0400"
      },
      "message": "Add grails-fields v8 as a worked-example reference in the README\n\nAdds a \u0027Worked example\u0027 subsection right after the Step-by-step list, pointing at the v8 grails-fields-custom-widgets-and-wrappers guide as a concrete reference for new authors. The example cites the guide\u0027s source tree, its conf/guides.yml registry block, the upstream grails-guides repo, and a representative chapter that uses include::../snippets/...[] directives - so a new contributor can read the v8 guide\u0027s source alongside its rendered output to see all the pieces in context.\n\nGeneric step-by-step instructions are kept; the worked-example link is additive, not a replacement.\n\nAssisted-by: claude-code:claude-opus-4-7\n"
    },
    {
      "commit": "494bc4d47c75126ae16be0f888abbdafea728466",
      "tree": "0c3134a3567c7902e1e2f417ab561f3158ed8256",
      "parents": [
        "fecf6e80c61f1b03942d6eefc704b51308ef4e4e"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 16:47:52 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 16:47:52 2026 -0400"
      },
      "message": "Fix grails-fields v8: JDK 21, start.grails.org, correct table-template paths\n\nJDK requirement (requirements.adoc):\n\n- The guide stated JDK 17 minimum. Apache Grails 8 actually requires JDK 21 - confirmed via gradle.properties on the 8.0.x branch (javaVersion\u003d21) and .sdkmanrc (java\u003d21.0.7-librca). Updated to \u0027JDK 21 or later. JDK 21 LTS is the minimum required by Apache Grails 8; JDK 25 LTS also works.\u0027\n\nStarter source (createApp.adoc + download.adoc):\n\n- Replaced prev-snapshot.grails.org references with start.grails.org. start.grails.org is where contributors normally land; pointing readers at prev-snapshot is unnecessary friction.\n\n- Removed the curl/unzip block, the four-bullet query-string-feature-toggle explanation, and the NOTE callout about prev-snapshot.grails.org being unreachable. Readers can use the start.grails.org UI to configure the project; explaining the API query string is extra ceremony not needed in this guide.\n\nTable-template chapter rewrite (customTable.adoc + appWideTable.adoc + perDomainTable.adoc + perColumnCells.adoc):\n\n- THE GUIDE WAS DOCUMENTING AN UNREACHABLE PATH. The old bookTableTemplate.adoc said to put the override at grails-app/views/_fields/book/_table.gsp. That path is never picked up by the plugin. Verified via apache/grails-core/blob/8.0.x/grails-fields/grails-app/taglib/grails/plugin/formfields/FormFieldsTagLib.groovy: the table tag uses a hardcoded render(template: \u0027/templates/_fields/\\\u0027) call, NOT the standard FormFieldsTemplateService.findTemplate() chain. There is no per-class lookup for tables.\n\n- Rewrote customTable.adoc to describe three actual layers: app-wide _table.gsp, per-page template\u003d attribute, and per-column _displayWidget.gsp.\n\n- Renamed bookTableTemplate.adoc -\u003e appWideTable.adoc and rewrote it to put the override at the path the plugin actually reads: grails-app/views/templates/_fields/_table.gsp. Cited the FormFieldsTagLib source.\n\n- Added new perDomainTable.adoc covering the \u003cf:table template\u003d\u0027bookCards\u0027/\u003e attribute pattern + the matching template at grails-app/views/templates/_fields/_bookCards.gsp - this is the right way to opt one specific page into a different layout.\n\n- Renamed bookTableColumns.adoc -\u003e perColumnCells.adoc (content unchanged - per-property _displayWidget.gsp DOES use the standard lookup chain via \u003cf:displayWidget\u003e).\n\n- Moved the vendored snippet from snippets/grails-app/views/_fields/book/_table.gsp to snippets/grails-app/views/templates/_fields/_table.gsp to match the corrected path the chapter teaches.\n\n- Updated the v8 toc block in conf/guides.yml: bookTableTemplate -\u003e appWideTable, added perDomainTable, bookTableColumns -\u003e perColumnCells.\n\nVerification:\n\n- ./gradlew validateGuides -PvalidationMode\u003dboth: passes (84 guides, 0 errors)\n\n- ./gradlew renderGuide_grails_fields_custom_widgets_and_wrappers_8 --rerun-tasks: 39 HTML pages built, 0 Unresolved directive errors. appWideTable.html, perDomainTable.html, perColumnCells.html all generated and reference the correct templates/_fields/_table.gsp path.\n\nCoordination with #482 (the global code-block + README PR): no conflicts; the two PRs touch different files.\n\nAssisted-by: claude-code:claude-opus-4-7\n"
    },
    {
      "commit": "91d928d2a5c6ce64e3ec6d2de59b7d17cc2e8bd7",
      "tree": "b50362e2edd922205c94b2d8a7730c0cedf871b6",
      "parents": [
        "fecf6e80c61f1b03942d6eefc704b51308ef4e4e"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 16:42:15 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 16:42:15 2026 -0400"
      },
      "message": "Polish code-block rendering and tighten the README contributor flow\n\nCode-block CSS (buildSrc/src/main/template/css/main.css):\n\n- Inline \u003ccode\u003e in narrative paragraphs (p, li, td, dd, dt, h1-h6) now gets a subtle gray pill background + small color shift so it stands out from regular prose. Block \u003ccode\u003e inside \u003cpre\u003e is unchanged.\n\n- The \u003ccode data-lang\u003d\u0027...\u0027\u003e label on every listing block is now always visible (was hover-only on desktop) with a small inset chip so it doesn\u0027t look like it\u0027s escaping the box.\n\n- The \u0027filename\u0027 caption above each listing block (the .title block, e.g. \u0027grails-app/views/_fields/manyToOne/_widget.gsp\u0027) now sits flush against the code below as a gray header bar with monospace text, instead of floating italic text disconnected from the block.\n\n- Listings with a title get square top corners on the \u003cpre\u003e so the title bar and the code meet seamlessly.\n\n- Listings now overflow horizontally instead of word-wrapping mid-token, so long lines stay readable.\n\nREADME.md - Requesting a sample-app repository section:\n\n- Removed the dev-mailing-list and Slack bullet points - we already point readers at the community page just one sentence above; the bullets duplicated that and are out of step with how a contributor actually requests a repo today (most ask via Slack DM or a GitHub issue, not a mailing list email with a guide-name request).\n\n- Updated the post-provisioning sentence: A PMC member creates the repo and sets the default branch, then the contributor opens a PR from their personal GitHub fork (not push directly to grails-guides/\u003cname\u003e, which would require write access on grails-guides org).\n\nVerified by re-rendering grails-fields-custom-widgets-and-wrappers v8: the new selectors land in build/dist/guides/.../css/main.css at lines 1221-1271.\n\nAssisted-by: claude-code:claude-opus-4-7\n"
    },
    {
      "commit": "fecf6e80c61f1b03942d6eefc704b51308ef4e4e",
      "tree": "e08604b47490165f19ededc7839525ceea9021a0",
      "parents": [
        "94895cafe6041f24abc1482f897d2c2dd85cd41d",
        "0b5c4ce6c2e0d4327bb7fb067597f1a057e28908"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 13:00:17 2026 -0500"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat May 02 13:00:17 2026 -0500"
      },
      "message": "Merge pull request #479 from apache/add-grails-fields-custom-widgets-and-wrappers-v8\n\nAdd grails-fields-custom-widgets-and-wrappers v8 guide (first Grails 8 guide)"
    },
    {
      "commit": "94895cafe6041f24abc1482f897d2c2dd85cd41d",
      "tree": "255e18376775275875b1255f1aac23ae9df8f7a2",
      "parents": [
        "4611be0b6d1432da58d7362e37c317b2842e28f6",
        "5a5d6d72d057bc17d03bddfd823727e498553c27"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 12:58:45 2026 -0500"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat May 02 12:58:45 2026 -0500"
      },
      "message": "Merge pull request #481 from apache/fold-manifest-and-toc-into-conf-guides\n\nFold per-guide manifest.yml + toc.yml into conf/guides.yml (single source of truth)"
    },
    {
      "commit": "0b5c4ce6c2e0d4327bb7fb067597f1a057e28908",
      "tree": "e08604b47490165f19ededc7839525ceea9021a0",
      "parents": [
        "7a678836420a7c942c01def3773042ac83575199"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 13:53:33 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 13:53:33 2026 -0400"
      },
      "message": "Reshape v8 guide for #481: inline toc into conf/guides.yml, drop manifest.yml + toc.yml\n\nInlines the v8 guide\u0027s table of contents as the new versions[\u00278\u0027].toc block in conf/guides.yml and deletes the now-redundant per-guide manifest.yml + toc.yml files. Drive-by: dedupes 3 \u0027title: Getting Started\u0027 duplicate keys that came along with #481\u0027s bulk migration of upstream toc.yml files (harmless to SnakeYAML\u0027s parser, but flagged by stricter LSP YAML validators).\n\nRe-render verified: ./gradlew renderGuide_grails_fields_custom_widgets_and_wrappers_8 --rerun-tasks builds 39 HTML pages with 0 Unresolved directive errors. validateGuides -PvalidationMode\u003dboth passes.\n\nThis commit assumes #481 has merged (or will merge before this PR lands). The merge commit one back in this branch\u0027s history is the temporary local merge of #481\u0027s branch needed to make this commit\u0027s deletions buildable; once #481 lands on master, the merge collapses naturally.\n\nAssisted-by: claude-code:claude-opus-4-7\n"
    },
    {
      "commit": "7a678836420a7c942c01def3773042ac83575199",
      "tree": "dcb203af0384bb468a6d456899656049ec542db8",
      "parents": [
        "a309e3cf015aed04536e19212a10c58375e12d59",
        "5a5d6d72d057bc17d03bddfd823727e498553c27"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 13:49:13 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 13:49:13 2026 -0400"
      },
      "message": "Merge branch \u0027fold-manifest-and-toc-into-conf-guides\u0027 into add-grails-fields-custom-widgets-and-wrappers-v8\n"
    },
    {
      "commit": "5a5d6d72d057bc17d03bddfd823727e498553c27",
      "tree": "255e18376775275875b1255f1aac23ae9df8f7a2",
      "parents": [
        "4611be0b6d1432da58d7362e37c317b2842e28f6"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 13:45:39 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 13:45:39 2026 -0400"
      },
      "message": "Fold per-guide manifest.yml + toc.yml into conf/guides.yml\n\nEvery manifest.yml in the corpus had the same 10 keys, all of which were already redundantly present in conf/guides.yml (or trivially derivable from it). Every toc.yml was a small per-version yaml that lived next to the chapters but had no contributor-facing reason to exist as a separate file.\n\nAfter this PR, contributing a new guide is one entry in conf/guides.yml + chapter .adoc files (+ optional snippets/ for vendored sample-app source). No per-guide YAML files at all.\n\nRefactor:\n\n- RenderGuidesPlugin.manifestToAttributes -\u003e buildAttributes(guide, version, versionKey): builds the AsciiDoc attribute map directly from the conf/guides.yml entry, no manifest.yml file read. sampleRef.repo/branch surface as githubSlug/githubBranch (drives the rendered Get the Code sidebar).\n\n- RenderGuidesPlugin staging: per-version \toc: block in conf/guides.yml is materialized at staged build path as \u003cstaged\u003e/guide/toc.yml so the vendored DocPublisher (which still reads toc.yml) needs no changes.\n\n- ValidateGuidesTask: drops manifest.yml + toc.yml existence checks; adds a check that each version has a non-empty \toc block.\n\n- README Authoring a Guide section rewritten to reflect the simpler shape.\n\nData migration:\n\n- 126 toc.yml files folded into the matching conf/guides.yml versions[\u003cN\u003e].toc blocks (textual injection preserves existing formatting)\n\n- 126 toc.yml files deleted\n\n- 126 manifest.yml files deleted (every key was redundant)\n\nVerification:\n\n- ./gradlew validateGuides -PvalidationMode\u003dboth: passes (84 guides, 0 errors)\n\n- ./gradlew renderGuide_creating_your_first_grails_app_6 renderGuide_grails_database_migration_6 renderGuide_adding_commit_info_4 --rerun-tasks: builds clean, 0 Unresolved directive errors across all 3 guides\n\nNet diff: 4570 deletions, 2673 insertions across 256 files.\n\nAssisted-by: claude-code:claude-opus-4-7\n"
    },
    {
      "commit": "4611be0b6d1432da58d7362e37c317b2842e28f6",
      "tree": "70e1ca76f53802a751a0f5bd0ef034b9c2bdc3a4",
      "parents": [
        "42a28bdf76fe50d93bb33a6c34e973ea63f8fdfc",
        "35d2834c8d69ba990401b69c81f9b3c768d2b9ad"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 12:27:50 2026 -0500"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat May 02 12:27:50 2026 -0500"
      },
      "message": "Merge pull request #480 from apache/retire-vendorguide-and-sha-fields\n\nRetire VendorGuideTask + the dead SHA fields, simplify Authoring a Guide"
    },
    {
      "commit": "a309e3cf015aed04536e19212a10c58375e12d59",
      "tree": "f1a3d44e3f58c8472edac533cdc290ee8bb5f6d7",
      "parents": [
        "5e3eeaf0d74d16eaa56f8796e77483e6fede53f0"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 13:24:34 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 13:24:34 2026 -0400"
      },
      "message": "Drop SHA fields and snippets/MANIFEST.yml in anticipation of #480 cleanup\n\nAligns this guide\u0027s shape with the simpler schema landing in #480: removes githubSha and sourceCommitDate from manifest.yml, sampleRef.sha from conf/guides.yml, the entire snippets/MANIFEST.yml file, and the stale \u0027Per-guide-version metadata\u0027 / \u0027Source repo + branch + pinned SHA\u0027 header comments.\n\nRe-render verified: 0 Unresolved directive errors across 39 HTML pages.\n\nAssisted-by: claude-code:claude-opus-4-7\n"
    },
    {
      "commit": "35d2834c8d69ba990401b69c81f9b3c768d2b9ad",
      "tree": "70e1ca76f53802a751a0f5bd0ef034b9c2bdc3a4",
      "parents": [
        "42a28bdf76fe50d93bb33a6c34e973ea63f8fdfc"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 13:22:16 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 13:22:16 2026 -0400"
      },
      "message": "Retire VendorGuideTask + the dead SHA fields, simplify Authoring a Guide\n\nVendorGuideTask was the bulk-import helper for Phase 10 of the apache/grails-static-website#354 migration. Migration is at Phase 15, no re-vendoring has happened in any subsequent phase, and new guides (e.g. the upcoming v8) are authored directly with hand-vendored snippets. The task and its only collaborator (SnippetVendoringIncludeProcessor) have no remaining consumers.\n\nThe SHA fields the task wrote (conf/guides.yml sampleRef.sha, manifest.yml githubSha + sourceCommitDate, snippets/MANIFEST.yml as a whole file) are also write-only at site-render time - none of them flow into rendered HTML or AsciiDoc attributes. Removing them eliminates the maintenance burden (3x duplication per guide, drift risk, abbreviated-SHA validation failures on 6 pre-existing entries) without any functional loss.\n\nWhat this PR removes:\n\n- buildSrc/.../VendorGuideTask.groovy and its only caller (the per-version vendor task wiring in RenderGuidesPlugin.groovy)\n\n- buildSrc/.../SnippetVendoringIncludeProcessor.groovy (only invoked from VendorGuideTask)\n\n- The MIGRATION_GROUP, VENDOR_AGGREGATE_TASK, DEFAULT_GUIDES_WORKSPACE_REL, and Wiring.vendorTaskNames fields\n\n- ValidateGuidesTask\u0027s SHA_40_HEX regex + the entire sampleRef.sha required-and-must-be-40-hex validation block\n\n- 126 sha: lines from conf/guides.yml (every sampleRef block)\n\n- 252 githubSha + sourceCommitDate lines across 126 manifest.yml files\n\n- 126 snippets/MANIFEST.yml files (entire files - dead, write-only)\n\n- 123 stale \u0027# Generated by :vendorGuide\u0027 header comments\n\n- The fixture-era multi-line comment block in creating-your-first-grails-app/v6/manifest.yml\n\n- All Tier 2/3 SHA-pinning machinery from the README\u0027s Authoring a Guide section, replaced with a simpler \u0027Documentation\u0027 vs \u0027Documentation + sample app\u0027 two-flavour split\n\nWhat stays:\n\n- conf/guides.yml sampleRef.repo + sampleRef.branch (drives the rendered \u0027Get the Code\u0027 sidebar via githubSlug/githubBranch)\n\n- manifest.yml githubSlug + githubBranch (same)\n\n- All other manifest.yml fields (title, subtitle, authors, category, publicationDate, tags) which are exposed as AsciiDoc attributes\n\nVerification:\n\n- ./gradlew validateGuides -PvalidationMode\u003dboth: passes (84 guides, 0 errors). Pre-existing failure on master fixed as a side effect.\n\n- ./gradlew renderGuide_creating_your_first_grails_app_6 renderGuide_grails_database_migration_6 --rerun-tasks: builds clean, 0 Unresolved directive errors across both guides.\n\nNet diff: 4675 deletions, 31 insertions across 258 files.\n\nAssisted-by: claude-code:claude-opus-4-7\n"
    },
    {
      "commit": "5e3eeaf0d74d16eaa56f8796e77483e6fede53f0",
      "tree": "2de6c91323aa10303466fc890da4beefb2c72c6c",
      "parents": [
        "7291420c6db9b1eecfe634c6231f91b26089f745"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 12:48:57 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 12:48:57 2026 -0400"
      },
      "message": "Pin sample-app SHA and align package to example after creating upstream repo\n\nCreated grails-guides/grails-fields-custom-widgets-and-wrappers (default branch grails8) with initial/ (vanilla Grails 8 starter from prev-snapshot.grails.org with web/Hibernate/Tomcat/DevTools/JDK25) and complete/ (initial + every customisation from this guide). HEAD pinned in three places: conf/guides.yml sampleRef.sha, manifest.yml githubSha, snippets/MANIFEST.yml source.sha.\n\nThe starter unconditionally generates package\u003dexample for the URL path /create/web/example.library, so the vendored snippets and the chapter file paths/CLI commands have been refactored from example.library down to example to match what the starter actually produces. Re-render verified: 0 Unresolved directive errors across 39 HTML pages.\n\nAssisted-by: claude-code:claude-opus-4-7\n"
    },
    {
      "commit": "7291420c6db9b1eecfe634c6231f91b26089f745",
      "tree": "470c392bcaaf754d417298fade86dd47eae993e5",
      "parents": [
        "42a28bdf76fe50d93bb33a6c34e973ea63f8fdfc"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 12:35:13 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 12:35:13 2026 -0400"
      },
      "message": "Add grails-fields-custom-widgets-and-wrappers v8 guide\n\nFirst Grails 8 guide. Walks readers through replacing scaffolded CRUD GSPs with f:all, f:display, and f:table from the Fields plugin (bundled with grails-core in Grails 7+), then progressively customises wrappers, widgets, and table templates so per-property formatting concerns live once under grails-app/views/_fields/ instead of scattered across every domain class\u0027s GSPs.\n\nCovers the full Fields plugin customisation surface including the thinly-documented association widget intersection: dedicated chapters for many-to-one, one-to-one (owned vs independent dispatch), one-to-many (read-only with quick-add), and many-to-many (checkbox group) widget and display templates. Domain model spans all four association types via Author/Book/ContactInfo/Tag.\n\nTier 3 registry entry uses a placeholder SHA (40 zeros) and branch\u003dgrails8. Update conf/guides.yml sampleRef.sha, snippets/MANIFEST.yml source.sha, and manifest.yml githubSha after pushing the matching initial/ + complete/ trees to grails-guides/grails-fields-custom-widgets-and-wrappers#grails8. The vendored snippets/ directory IS the complete/ content - publish it as-is alongside an initial/ (a vanilla Grails 8 starter from prev-snapshot.grails.org).\n\nVerified locally: ./gradlew renderGuide_grails_fields_custom_widgets_and_wrappers_8 --rerun-tasks builds 39 HTML pages with no Unresolved directive errors and clean prev/next navigation. The 6 pre-existing validation errors in conf/guides.yml about short SHAs on other guides (visible on master) are unrelated to this change.\n\nAssisted-by: claude-code:claude-opus-4-7\n"
    },
    {
      "commit": "42a28bdf76fe50d93bb33a6c34e973ea63f8fdfc",
      "tree": "3b1e65cbc40dce0af6b901bd17bf76e622069772",
      "parents": [
        "ea5f36b357a1bc8be743d767e2b848b830a5134c",
        "e2eaef8d8f00c3c0e54b582af02261153af858f8"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 11:04:05 2026 -0500"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat May 02 11:04:05 2026 -0500"
      },
      "message": "Merge pull request #478 from apache/fix-mailing-list-url-encoded-at-sign\n\nRender mailing list URLs with literal @ instead of %40"
    },
    {
      "commit": "e2eaef8d8f00c3c0e54b582af02261153af858f8",
      "tree": "3b1e65cbc40dce0af6b901bd17bf76e622069772",
      "parents": [
        "ea5f36b357a1bc8be743d767e2b848b830a5134c"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 12:03:02 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 12:03:02 2026 -0400"
      },
      "message": "Render mailing list URLs with literal @ instead of %40\n\nCommit 81f78d1c3f URL-encoded the @ in lists.apache.org links to %40 to work around Asciidoctor\u0027s email autolink processor mangling the outer URL macro into nested anchor tags. The trade-off was that every guide page\u0027s helpWithGrails section displayed and linked to the ugly URL-encoded form.\n\nSwitch to AsciiDoc inline passthrough syntax (link:++URL++[text]) so the email autolink does not run over the URL target, while the rendered href keeps the literal @. README.md links go back to plain @ since markdown has no equivalent autolink concern.\n\nVerified by re-rendering creating-your-first-grails-app/v6: output HTML now contains \u003ca href\u003d\"https://lists.apache.org/list.html?dev@grails.apache.org\"\u003e with no nested anchors.\n\nAssisted-by: claude-code:claude-opus-4-7\n"
    },
    {
      "commit": "ea5f36b357a1bc8be743d767e2b848b830a5134c",
      "tree": "03a49a0e8333cb9aa0614e79b6961f5f13892133",
      "parents": [
        "e00890dd397fd6c0c409cc960dadf4fbb676a41f",
        "54ebeb9fd7cb62b54efe7e9a7ce8f0ea9df1e72e"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 10:25:25 2026 -0500"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat May 02 10:25:25 2026 -0500"
      },
      "message": "Merge pull request #477 from apache/stronger-guide-polish\n\nStrengthen guide page polish: visible borders, contrast, dark title chips"
    },
    {
      "commit": "54ebeb9fd7cb62b54efe7e9a7ce8f0ea9df1e72e",
      "tree": "03a49a0e8333cb9aa0614e79b6961f5f13892133",
      "parents": [
        "e00890dd397fd6c0c409cc960dadf4fbb676a41f"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 11:07:44 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 11:07:44 2026 -0400"
      },
      "message": "Strengthen guide page polish: visible borders, contrast, dark title chips\n\nEarlier polish in #475 was technically applied but visually subtle to the\npoint of being imperceptible (sub-pixel border, 4%-darker background).\nThis pass makes the changes obvious.\n\nImage blocks:\n- Added 1px solid border + 6px rounded corners + subtle box-shadow so\n  screenshots read as figures rather than floating bitmaps.\n- Increased margin around imageblocks to breathe better.\n- !important on max-width/height to defeat any inherited img rules from\n  legacy site CSS.\n\nCode blocks:\n- Stronger background (#f6f8fa GitHub-light) with 1px solid border.\n- Larger padding (18x20).\n- Subtle box-shadow for depth.\n- Title chip (the .title that names the file above the code) is now a\n  dark monospace pill (#1f2328 bg, #f6f8fa text) that visually connects\n  to the code body with a square top-left corner. Reads as a code panel.\n- !important on every property to defeat the inherited .listing-block typo\n  rule and any centering inherited from .content.\n\nInline code:\n- More contrast: #eaeef2 bg with #d0d7de border.\n- 4-corner border-radius, 0.875em monospace.\n- Now also styles \u003ccode\u003e inside \u003ctd\u003e and \u003ca\u003e.\n\n(refs apache/grails-static-website#354)\n"
    },
    {
      "commit": "e00890dd397fd6c0c409cc960dadf4fbb676a41f",
      "tree": "e9ba5488af35c93d7098084cd8f5ad02f817425a",
      "parents": [
        "80c56868539ffa9211e87c0e6e6f53ed88741600",
        "5bdf74f170d0f47a4d08364400ac5416f8110f00"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 08:42:21 2026 -0500"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat May 02 08:42:21 2026 -0500"
      },
      "message": "Merge pull request #476 from apache/fix-snippet-include-depth-regex\n\nFix unresolved snippet includes leaking [role\u003dinclude] into rendered guides"
    },
    {
      "commit": "5bdf74f170d0f47a4d08364400ac5416f8110f00",
      "tree": "e9ba5488af35c93d7098084cd8f5ad02f817425a",
      "parents": [
        "80c56868539ffa9211e87c0e6e6f53ed88741600"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 09:37:31 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 09:37:31 2026 -0400"
      },
      "message": "Fix unresolved snippet includes leaking [role\u003dinclude] into rendered guides\n\nThree related bugs caused dozens of guide chapter pages to ship visible\nliteral link:.../snippets/\u003cfile\u003e[role\u003dinclude] placeholders instead of\nthe inlined snippet content. All three are now fixed:\n\nA. inlineSnippetIncludes regex only matched include::../snippets/...\n   (one level up). Chapters in subdirectories of guide/ (e.g.\n   guide/writingTheApp/configurationProperties.adoc) need ../../snippets/\n   or ../../../snippets/, which the regex did not match. The unprocessed\n   include directive then fell through to AsciidoctorJ which emitted\n   the link:[role\u003dinclude] placeholder text.\n   Affected: 47 .adoc files across 6 guides. Fix: regex relaxed to\n   accept any positive number of leading ../ segments via (?:\\.\\./)+.\n\nB. Legacy {sourcedir}/../\u003cpath\u003e include form unhandled. 21 occurrences\n   in building-an-android-client-powered-by-a-grails-backend/v4 fell\n   through identically. Fix: added a sibling regex pattern handled by\n   the same loop body.\n\nC. Vendored common include filename had a typo: commmon- (3 m\u0027s)\n   instead of common-. The file resolved fine via the typo\u0027d path\n   but inlineCommonIncludes regex requires the common- prefix and\n   missed it. Fix: renamed file in guides/common/ and updated the 2\n   .adoc files referencing the typo\u0027d name.\n\nLocal verification: ./gradlew renderGuide_grails_configuration_properties_micronaut_6\nproduces HTML whose listingblock contents now show actual snippet bodies\ninstead of the [role\u003dinclude] placeholders.\n\n(refs apache/grails-static-website#354)\n"
    },
    {
      "commit": "80c56868539ffa9211e87c0e6e6f53ed88741600",
      "tree": "e07573f5064d2e6f7ddcef4c37fb9d4c14e7aa6c",
      "parents": [
        "8d2850c9a7b0d0c110b7999dbe16cae56f6b8eab",
        "98c044be7f282b025dd28a5eefbd6a8895eae078"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 08:03:31 2026 -0500"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat May 02 08:03:31 2026 -0500"
      },
      "message": "Merge pull request #475 from apache/polish-guide-page-ux\n\nPolish guide page UI: image overflow + code block alignment + visual treatment"
    },
    {
      "commit": "8d2850c9a7b0d0c110b7999dbe16cae56f6b8eab",
      "tree": "5370fc16bc0f228568727289e5adb5fbaeb7c2cb",
      "parents": [
        "d1183ef504617d04071b857f7b7286da39715ab5",
        "07884d3e80a1a0be216582b7cf787eab73b3dcdd"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 08:03:24 2026 -0500"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat May 02 08:03:24 2026 -0500"
      },
      "message": "Merge pull request #474 from apache/polish-readme-and-author-guide-flow\n\nPolish README + add Authoring a Guide section"
    },
    {
      "commit": "98c044be7f282b025dd28a5eefbd6a8895eae078",
      "tree": "a1cc93acc0da8f178baa757e05dc347a02f9a566",
      "parents": [
        "d1183ef504617d04071b857f7b7286da39715ab5"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 09:01:06 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 09:01:06 2026 -0400"
      },
      "message": "Polish guide page UI: image overflow + code block alignment + visual treatment\n\nAdds a scoped block at the end of assets/stylesheets/screen.css fixing three\npre-existing UX bugs on every rendered guide page:\n\n1. IMAGE OVERFLOW (.imageblock img):\n   Wide screenshots (often ~1200px) had no max-width rule and overflowed\n   the ~900px content column horizontally. Fixed with\n     .imageblock img, .imageblock svg {\n       max-width: 100%; height: auto;\n       display: block; margin: 0 auto;\n     }\n\n2. CODE-BLOCK ALIGNMENT (.listingblock):\n   The existing rule \u0027.listing-block .content\u0027 (note the hyphen) at line\n   310 was a typo - AsciiDoc emits \u0027.listingblock\u0027 (no hyphen), so the\n   rule has been inactive all along. Code blocks inherited the parent\n   .content\u0027s margin: 0 auto centering and looked misaligned next to\n   the surrounding left-aligned prose. Fixed with the correctly-spelled\n   selector + explicit text-align: left + width override.\n\n3. CODE-BLOCK VISUAL TREATMENT:\n   Previous styling was minimal (.content article pre had only background\n   + font + padding). Added: subtle border, rounded corners, horizontal\n   scroll for very long lines (instead of breaking the page), monospace\n   stack with system-default fonts as preferred over Courier New.\n\nAlso added consistent treatment for inline \u003ccode\u003e chips, .tableblock\n(left-aligned + horizontal scroll), and .admonitionblock (NOTE/TIP/\nWARNING) call-outs - all AsciiDoc-emitted containers that only appear\non guide pages so the rules don\u0027t affect any non-guide page.\n\nLocal verification: ./gradlew renderGuide_grails-tvmlapp_3 builds clean,\nthe deployed screen.css contains the new section, and the rendered HTML\nuses the .imageblock and .listingblock classes the new rules target.\n\n(refs apache/grails-static-website#354)\n"
    },
    {
      "commit": "07884d3e80a1a0be216582b7cf787eab73b3dcdd",
      "tree": "5370fc16bc0f228568727289e5adb5fbaeb7c2cb",
      "parents": [
        "d1183ef504617d04071b857f7b7286da39715ab5"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 08:47:05 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 08:47:05 2026 -0400"
      },
      "message": "Polish README: fix stale guides content + add Authoring a Guide section\n\nFull pass on the README:\n\nFixes:\n- Stale `./gradlew tasks --group\u003dgrails` (no group named \u0027grails\u0027 exists\n  anymore; tasks moved to the \u0027grails website\u0027 group).\n- `./gradlew buildGuide` -\u003e `./gradlew buildGuides` (former is a backward-\n  compat alias only).\n- Removed broken `docs/blogimages.png` reference (file does not exist).\n- Removed the stale embedded `tasks --group\u003dgrails` output snippet (gets\n  out of date the moment a new task is added).\n- Typos: `simpel` -\u003e `simple`, `preffix` -\u003e `prefix`, `live-serve` -\u003e\n  `live-server`, plus several broken sentences in the blog metadata\n  section.\n- Replaced the ad-hoc `What to change when a new release is published` line\n  with a real `Recording a release` section pointing at `recordRelease`.\n\nNew major section: Authoring a Guide\n- The three tiers (doc-only, initial code, initial+complete code).\n- Per-tier step-by-step.\n- Source layout (manifest.yml, toc.yml, snippets/) with an example.\n- Two `conf/guides.yml` registry shapes (with and without sampleRef).\n- \u0027Requesting a sample-app repository\u0027 subsection that points at the\n  community page (dev@grails.apache.org via lists.apache.org + Slack)\n  for Tier 2/3 guide authors who need a grails-guides/\u003cname\u003e repo created\n  by the PMC.\n- Local validation checklist (validateGuides, renderGuide_*,\n  buildAllGuides, verifyAllGuides).\n\nAlso added a Repository layout table at the top so newcomers can orient\nthemselves quickly.\n\n(refs apache/grails-static-website#354)\n"
    },
    {
      "commit": "d1183ef504617d04071b857f7b7286da39715ab5",
      "tree": "8bd082c496721bd6585e26a87dd168d3de130f43",
      "parents": [
        "540655d8689a913ed53658e3c6d5dbf216c06afd",
        "c8a7aa8f04ba49126893df15edb086fc1f3881a6"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 07:25:07 2026 -0500"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat May 02 07:25:07 2026 -0500"
      },
      "message": "Merge pull request #473 from apache/convert-release-script-to-gradle\n\nReplace release.sh with :recordRelease Gradle task"
    },
    {
      "commit": "c8a7aa8f04ba49126893df15edb086fc1f3881a6",
      "tree": "8bd082c496721bd6585e26a87dd168d3de130f43",
      "parents": [
        "540655d8689a913ed53658e3c6d5dbf216c06afd"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 08:22:26 2026 -0400"
      },
      "committer": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 08:22:45 2026 -0400"
      },
      "message": "Replace release.sh with :recordRelease Gradle task\n\nPer Appendix G of the v7 migration plan (owner directive 2026-04-30):\nevery operational script in this repo MUST be a Gradle task. release.sh\nwas the last remaining bash script on master after publish.sh was\nconverted in PR #445.\n\nNew RecordReleaseTask appends a release entry to conf/releases.yml\nidentically to the script: unquoted version, English-locale date in\n\u0027MMM dd, yyyy\u0027 format, trailing blank line.\n\nThe release.yml workflow now invokes\n  ./gradlew recordRelease -PreleaseVersion\u003d\u003cX.Y.Z\u003e\ninstead of\n  ./release.sh \u003cX.Y.Z\u003e\n\nLocal verification confirmed the appended block is byte-identical to\nwhat release.sh produced.\n\n(refs apache/grails-static-website#354)\n"
    },
    {
      "commit": "540655d8689a913ed53658e3c6d5dbf216c06afd",
      "tree": "f12dc5c1b9d37c141b0e8b9974e355bc279362ed",
      "parents": [
        "112e64846c2b745e8eefc11c73a163ee0da4a2c8",
        "81f78d1c3fbd1238a9a6afa06a7e97c414487a11"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 07:12:05 2026 -0500"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat May 02 07:12:05 2026 -0500"
      },
      "message": "Merge pull request #471 from apache/fix-mailing-list-link-autoescape\n\nFix broken mailing list links in helpWithGrails common include"
    },
    {
      "commit": "112e64846c2b745e8eefc11c73a163ee0da4a2c8",
      "tree": "12af0737625e29bfd3308d6c5d8a9f1bb823dea4",
      "parents": [
        "d6d24bfdfa6d814c4705c04af2303a76af07942e",
        "431f9524e1c041c4665edeefb2aaca1bf8f2154f"
      ],
      "author": {
        "name": "James Fredley",
        "email": "jamesfredley@users.noreply.github.com",
        "time": "Sat May 02 07:11:39 2026 -0500"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat May 02 07:11:39 2026 -0500"
      },
      "message": "Merge pull request #472 from apache/add-redirects-json-manifest\n\nAdd :generateRedirectsManifest task emitting _redirects.json"
    }
  ],
  "next": "431f9524e1c041c4665edeefb2aaca1bf8f2154f"
}
