jamesfredley commented on issue #354:
URL: 
https://github.com/apache/grails-static-website/issues/354#issuecomment-3904912096

   # Grails Guides: Publishing Architecture and Migration Plan
   
   This document describes how Grails guide publishing works today, what's 
wrong with
   it, and the plan to consolidate everything into 
`apache/grails-static-website`.
   
   ---
   
   ## Table of Contents
   
   ### Part 1 -- Current System
   
   - [Numbers at a Glance](#numbers-at-a-glance)
   - [Current Architecture Overview](#current-architecture-overview)
   - [Complete Repo Inventory](#complete-repo-inventory)
     - [Repos with both initial/ and complete/ 
(79)](#repos-with-both-initial-and-complete-directories-79)
     - [Repos with complete/ only 
(14)](#repos-with-complete-only----no-initial-14)
     - [Repos with neither (1)](#repos-with-neither-initial-nor-complete-1)
     - [Repos not published in guides.json 
(10)](#repos-in-org-but-not-published-in-guidesjson-10)
     - [Known data issues](#known-data-issues)
   - [Grails Version Analysis](#grails-version-analysis)
     - [Branch distribution across repos](#branch-distribution-across-repos)
     - [Published versions in guides.json](#published-versions-in-guidesjson)
     - [What "multi-version" really means 
today](#what-multi-version-really-means-today)
     - [Unpublished branch analysis](#unpublished-branch-analysis)
   - [How Publishing Works Today](#how-publishing-works-today)
     - [Step 1: Scaffold the guide](#step-1-scaffold-the-guide)
     - [Step 2: Create the GitHub repo](#step-2-create-the-github-repo)
     - [Step 3: Set up CI manually](#step-3-set-up-ci-manually)
     - [Step 4: CI runs and the guide gets 
published](#step-4-ci-runs-and-the-guide-gets-published)
     - [Step 5: The guide appears on 
guides.grails.org](#step-5-the-guide-appears-on-guidesgrailsorg)
   - [Current CI/CD Flow Diagram](#current-cicd-flow-diagram)
   - [Current Component Reference](#current-component-reference)
   - [Current Pain Points](#current-pain-points)
   
   ### Part 2 -- Migration Plan
   
   - [Consolidation Plan](#consolidation-plan)
     - [Phase 1: Move guide metadata here](#phase-1-move-guide-metadata-here)
     - [Phase 2: Move common doc snippets and theme resources 
here](#phase-2-move-common-doc-snippets-and-theme-resources-here)
     - [Phase 3: Move guide-build.gradle 
here](#phase-3-move-guide-buildgradle-here)
     - [Phase 4: Move ALL guide AsciiDoc source 
here](#phase-4-move-all-guide-asciidoc-source-here)
     - [Phase 5: Simplify deployment](#phase-5-simplify-deployment)
     - [Phase 6: Strip doc-publishing from guide 
repos](#phase-6-strip-doc-publishing-from-guide-repos)
     - [Phase 7: Wind down 
grails-guides-template](#phase-7-wind-down-grails-guides-template)
     - [URL Redirect Plan](#url-redirect-plan)
   - [Migration Checklist](#migration-checklist)
   
   ### Part 3 -- New System (After Migration)
   
   - [How Publishing Will Work After 
Migration](#how-publishing-will-work-after-migration)
     - [Adding a new guide 
(doc-only)](#adding-a-new-guide-doc-only-no-sample-project)
     - [Adding a new guide with sample 
projects](#adding-a-new-guide-with-sample-projects)
     - [Updating an existing guide](#updating-an-existing-guide)
     - [Adding a new Grails version to an existing 
guide](#adding-a-new-grails-version-to-an-existing-guide)
     - [How versioning simplifies with 
migration](#how-versioning-simplifies-with-migration)
   - [Comparison: Before vs After](#comparison-before-vs-after)
   - [Key Files Reference](#key-files-reference)
   
   ---
   
   # Part 1 -- Current System
   
   ## Numbers at a Glance
   
   | Metric | Count |
   |--------|-------|
   | Repos in `grails-guides` org (excluding template) | 94 |
   | Repos published in `guides.json` | 84 |
   | Total `guides.json` entries (including multi-version) | 125 |
   | Single-version guides in `guides.json` | 44 |
   | Multi-version guides in `guides.json` | 40 |
   | Repos in org but NOT in `guides.json` (unpublished) | 10 |
   | Repos with both `initial/` and `complete/` dirs | 79 |
   | Repos with `complete/` only (no `initial/`) | 14 |
   | Repos with neither `initial/` nor `complete/` | 1 |
   | Repos with a `grails6` branch | 3 |
   | Repos with a `grails5` branch | 37 |
   | Repos with a `grails-4` or `grails4` branch | 27 |
   | Repos with a `grails3` branch | 40 |
   
   ---
   
   ## Current Architecture Overview
   
   Guide publishing spans **three layers**:
   
   1. **94 individual guide repos** in the `grails-guides` GitHub org (e.g.
      `grails-guides/creating-your-first-grails-app`) -- each is a standalone 
Grails
      project containing AsciiDoc source, an `initial/` starter project, and a
      `complete/` solution.
   
   2. **`grails-guides/grails-guides-template`** (infrastructure + deployment 
repo) --
      provides the shared build plugin (`gradle/guide-build.gradle`), guide 
scaffolding
      script (`create-guide.sh`), CI/CD scripts (`githubactions/`), common 
AsciiDoc
      snippets (`src/main/docs/`), and theme resources (`src/main/resources/`).
      Its **`gh-pages` branch** stores the built HTML for every guide, the 
central
      `guides.json` registry, and the guides index site served at
      `https://guides.grails.org`.
   
   3. **`apache/grails-static-website`** (this repo) -- generates the guides 
*index site*
      (listing, search, tag/category pages) by fetching `guides.json` at build 
time.
      The output is pushed back to `grails-guides/grails-guides-template` 
`gh-pages`.
   
   > **Note**: Many CI scripts and individual guide `build.gradle` files still 
reference
   > the old URL `grails/grails-guides`. This was the former location of
   > `grails-guides/grails-guides-template` before it was moved to the 
`grails-guides`
   > GitHub org. GitHub redirects mask this, but the references are stale.
   
   ### The `grails-guides` GitHub Org
   
   The `grails-guides` GitHub org contains **95 repositories**:
   
   - **`grails-guides-template`** -- the infrastructure repo described above.
   - **94 individual guide repositories** -- each named after the guide topic.
   
   Every individual guide repo in the org:
   - Has its own GitHub Actions CI workflow (manually set up -- not generated 
by the
     scaffolding script)
   - May maintain version branches (`master`, `grails3`, `grails4`, `grails5`, 
`grails6`)
   - Applies `guide-build.gradle` remotely from `grails-guides-template` at 
Gradle build time
   - Downloads and executes CI scripts from `grails-guides-template` at CI 
runtime via `curl`
   - Pushes built HTML and metadata to `grails-guides-template` `gh-pages`
   
   ---
   
   ## Complete Repo Inventory
   
   ### Repos with both `initial/` and `complete/` directories (79)
   
   These repos contain runnable sample Grails applications. After migration, 
these
   repos will **remain in the `grails-guides` org** as sample project repos 
(with
   doc-publishing stripped out). AsciiDoc source moves to 
`grails-static-website`.
   
   | Repo | Published | Repo branches |
   |------|-----------|---------------|
   | `adding-commit-info` | Yes (master, grails3) | fix-commits-upgrade, 
gradle4, grails3, grails-3.2.8, grails-4, master |
   | `angular2-combined` | Yes | grail-4, master |
   | `building-a-react-app` | Yes (master) | master |
   | `building-a-vue-app` | Yes (master) | master, rewrite |
   | `building-an-android-client-powered-by-a-grails-backend` | Yes (master) | 
master |
   | `building-an-ios-objectc-client-powered-by-a-grails-backend` | Yes 
(master) | grails5, grails-4, master |
   | `building-an-ios-swift-client-powered-by-a-grails-backend` | Yes (master) 
| grails5, grails-4, master, upgrade |
   | `command-objects-and-forms` | Yes (master, grails3) | grails3, grails-4, 
master, tests, upgrade |
   | `creating-your-first-grails-app` | Yes (master, grails3) | grails3, 
grails4, master, upgrade |
   | `database-per-tenant` | Yes (master) | grails5, ilopmar-patch-1, master |
   | `discriminator-per-tenant` | Yes (master) | grails5, master |
   | `gorm-event-listeners` | Yes (master, grails3) | fix_typos, grails3, 
grails-4, many-to-many, master, without-tags |
   | `gorm-graphql-with-react-and-apollo` | Yes (master) | master, + many 
dependabot branches |
   | `gorm-ratpack` | Yes (master, grails3) | grails3, master |
   | `grails_api_ai` | **No** | master |
   | `grails_i18n` | Yes (master, grails3) | grails3, master |
   | `grails_url_mappings` | Yes (master) | grails5, master |
   | `grails-android-security` | **No** | master, upgrade |
   | `grails-as-docker-container` | Yes (master, grails3) | grails3, grails5, 
grails-4, guide, master |
   | `grails-async-promises` | Yes (master, grails3) | grails3, master |
   | `grails-basicauth` | Yes (master, grails3) | grails3, master |
   | `grails-code-coverage` | Yes (master, grails3) | grails3, master |
   | `grails-codenarc` | Yes (master) | master |
   | `grails-configuration-properties-micronaut` | Yes (grails4, grails6) | 
basics-g5-changes, grails4, grails5, grails6, master |
   | `grails-controller-testing` | Yes (master, grails3) | grails_three_three, 
grails3, master |
   | `grails-custom-security-tenant-resolver` | Yes (master, grails3) | 
current-user-with-spring-security-service, grails3, master |
   | `grails-database-migration` | Yes (grails4, grails3, grails6) | grails3, 
grails5, grails6, master |
   | `grails-deploy-aws-elastic-beanstalk` | **No** | master |
   | `grails-deploy-heroku` | **No** | master |
   | `grails-deploy-pws` | **No** | grails5, grails-4, master |
   | `grails-docker-external-services` | Yes (master) | grails5, master |
   | `grails-dynamic-multiple-datasources` | Yes (master, grails3) | grails3, 
grails5, master |
   | `grails-elasticsearch` | Yes | master |
   | `grails-email` | Yes (master, grails3) | grails3, grails5, master |
   | `grails-events` | Yes (master, grails3) | grails3, master |
   | `grails-geb-multiple-browsers` | Yes (master, grails3) | grails3, master |
   | `grails-google-cloud` | Yes (master) | grails5, grails-4, 
grails-gr8-demos, master |
   | `grails-google-home` | Yes (master) | alvarosanchez-patch-1, grails5, 
grails-4, master |
   | `grails-gorm-data-services` | Yes (master, grails3) | grails3, grails-4, 
master |
   | `grails-ios-objectivec-security` | **No** | master, upgrade |
   | `grails-javamelody` | Yes | grails5, master |
   | `grails-logicaldelete` | Yes (master, grails3) | grails3, grails5, master |
   | `grails-micronaut-http` | Yes (grails5) | bean-inject-error, grails5, 
grails6, master |
   | `grails-mock-basics` | Yes (master, grails3) | grails3, master |
   | `grails-mock-http-server` | Yes (master, grails3) | grails3, grails5, 
master |
   | `grails-mock-logging-slf4j-test` | Yes (master, grails3) | grails3, 
grails5, grails-4-datastore-rest-client, grails-4, master |
   | `grails-multi-datasource` | Yes (master, grails3) | error, grails3, 
grails5, master |
   | `grails-multi-project-build` | Yes (master) | grails5, grails-4, master, 
upgrade_guide |
   | `grails-oauth-facebook` | **No** | master |
   | `grails-oauth-google` | Yes (master) | grails5, master, suggestions |
   | `grails-oauth-twitter` | Yes (master) | grails5, master |
   | `grails-on-circleci-basics` | Yes (master) | grails5, grails-4, master |
   | `grails-on-github-actions` | Yes (master) | grails5, master |
   | `grails-on-travis-basics` | Yes (master, grails3) | grails3, grails5, 
master |
   | `grails-rabbitmq` | Yes | grails5, grails-4, master |
   | `grails-restapi-angularjs` | Yes (master) | grails5, grails-4, master |
   | `grails-scheduled` | Yes (grails3, master) | grails3, grails5, grails-4, 
master, suggestions |
   | `grails-schwartz` | Yes | grails5, master |
   | `grails-soap` | Yes (master, grails3) | grails3, master |
   | `grails-spring-security-core-plugin-custom-authentication` | Yes (master, 
grails3) | grails3, grails5, master |
   | `grails-taglib-wyswyg-trix` | Yes (master, grails3) | grails3, grails-4, 
master |
   | `grails-test-domain-class-constraints` | Yes (master, grails3) | 
errorCodes, grails_three_three, grails3, grails-4, master, unique-constraint |
   | `grails-test-security` | Yes (master, grails3) | grails3, master |
   | `grails-tvmlapp` | Yes (master, grails3) | grails3, grails5, master |
   | `grails-upload-file` | Yes (master, grails3) | grails3, grails5, grails-4, 
master |
   | `grails-vs-nodejs` | Yes (master) | master |
   | `grails-vue-combined` | Yes (master) | grails-4, master |
   | `grails-yourkit-profiling` | Yes (master) | grails5, master |
   | `neo4j-movies` | Yes | master, upgrade |
   | `querying-gorm-dynamic-finders` | Yes (master, grails3) | grails3, master |
   | `react-combined` | Yes | grails-4, master, review |
   | `react-router-spring-security` | **No** | master |
   | `react-spring-security` | Yes (master) | master, upgrade |
   | `rest-hibernate` | Yes (master, grails3) | grails3, grails-4, master |
   | `rest-mongodb` | Yes (master) | master, upgrade |
   | `server-sent-events` | Yes (master, grails3) | grails3, grails5, grails-4, 
master |
   | `using-hal-with-json-views` | Yes (master, grails3) | grails3, master, 
upgrade |
   | `vaadin-grails` | Yes | 3.3.0.RC1, grails-4, master, upgrade |
   | `web-app-step-by-step` | **No** | compilestatic, link-to-grails-download, 
master, sdkman-link, sett, sett-rev |
   
   ### Repos with `complete/` only -- no `initial/` (14)
   
   These repos have a completed sample project but no starter project. The 7
   quickcast repos are video-companion code (no interactive tutorial flow). 
After
   migration, repos with meaningful sample code remain in the org; doc-only 
repos
   (quickcasts) can be archived entirely.
   
   | Repo | Published | Repo branches | Notes |
   |------|-----------|---------------|-------|
   | `gorm-without-grails` | Yes (master) | grails3, master | Standalone GORM 
demo |
   | `grails-configuration-properties` | Yes (master, grails3) | grails3, 
master | |
   | `grails-elasticbeanstalk` | Yes (master) | grails5, grails-4, master | |
   | `grails-file-download-excel` | Yes (master, grails3) | grails3, grails5, 
master | |
   | `grails-micronaut-kafka` | Yes (master)* | master | *Typo in 
`guides.json`: listed as `grails-micronaut-kakfa` |
   | `grails-quickcast-logging` | Yes | master | Quickcast (video companion) |
   | `grails-quickcasts-angularjs-scaffolding-with-grails-3` | Yes | master | 
Quickcast |
   | `grails-quickcasts-developing-grails-3-applications-with-intellij-idea` | 
Yes | master | Quickcast |
   | `grails-quickcasts-interceptors` | Yes | master | Quickcast |
   | `grails-quickcasts-json-views` | Yes | master | Quickcast |
   | `grails-quickcasts-multi-project-builds` | Yes | master | Quickcast |
   | `grails-quickcasts-retrieving-config-values` | Yes | master | Quickcast |
   | `using-the-react-profile` | Yes (master) | master | |
   | `using-the-vue-profile` | Yes (master, grails3) | dependabot branch, 
grails3, master | |
   
   ### Repos with neither `initial/` nor `complete/` (1)
   
   | Repo | Published | Repo branches | Notes |
   |------|-----------|---------------|-------|
   | `grails-acl` | **No** | master | Doc-only, never published |
   
   ### Repos in org but NOT published in `guides.json` (10)
   
   These repos exist in the `grails-guides` org but have no corresponding entry 
in
   `guides.json`, meaning they do not appear on `https://guides.grails.org`. 
Their
   CI may never have successfully run `updateGuidesJson`, or they may be 
incomplete.
   
   | Repo | `initial/` | `complete/` | Branches |
   |------|------------|-------------|----------|
   | `grails_api_ai` | yes | yes | master |
   | `grails-acl` | no | no | master |
   | `grails-android-security` | yes | yes | master, upgrade |
   | `grails-deploy-aws-elastic-beanstalk` | yes | yes | master |
   | `grails-deploy-heroku` | yes | yes | master |
   | `grails-deploy-pws` | yes | yes | grails5, grails-4, master |
   | `grails-ios-objectivec-security` | yes | yes | master, upgrade |
   | `grails-oauth-facebook` | yes | yes | master |
   | `react-router-spring-security` | yes | yes | master |
   | `web-app-step-by-step` | yes | yes | compilestatic, 
link-to-grails-download, master, sdkman-link, sett, sett-rev |
   
   ### Known data issues
   
   - **Typo in `guides.json`**: `grails-micronaut-kakfa` should be 
`grails-micronaut-kafka`
   - **Category case inconsistency**: `guides.json` has both `"Grails + 
DevOps"` (6 guides)
     and `"Grails + Devops"` (1 guide) -- should be unified
   - **Branch value issue**: `grails-on-travis-basics` has a `guides.json` 
entry with
     `githubBranch` set to `"master,grails3"` (comma inside the value) instead 
of being
     two separate entries
   - **Underscored repo names**: `grails_api_ai`, `grails_i18n`, 
`grails_url_mappings`
     use underscores instead of hyphens (inconsistent with all other repo names)
   
   ---
   
   ## Grails Version Analysis
   
   ### Branch distribution across repos
   
   Repos use version branches to maintain guides for different Grails versions. 
The
   branch naming is inconsistent -- some use `grails4`, others use `grails-4`:
   
   | Branch | Repos | Notes |
   |--------|-------|-------|
   | `master` | 93 | Present in almost every repo; maps to Grails 4 in 
`guides.json` |
   | `grails3` | 40 | Grails 3.x version |
   | `grails-4` | 25 | Grails 4.x version (hyphenated variant) |
   | `grails4` | 2 | Grails 4.x version (non-hyphenated variant) |
   | `grails5` | 37 | Grails 5.x version |
   | `grails6` | 3 | Grails 6.x version |
   
   **Only 3 repos have been updated to Grails 6**: 
`grails-configuration-properties-micronaut`,
   `grails-database-migration`, and `grails-micronaut-http`.
   
   ### Published versions in `guides.json`
   
   The `guides.json` branch-to-major-version mapping (in 
`GuidesFetcher.groovy`):
   
   ```groovy
   BRANCH_TO_MAJOR = [grails3: 3, grails4: 4, master: 4, grails5: 5, grails6: 6]
   ```
   
   This means `master` = Grails 4 in the published guides. The mapping does NOT
   include `grails-4` (hyphenated), so repos with a `grails-4` branch but no
   `grails4` branch are effectively not publishing a separate Grails 4 version 
--
   their `master` branch serves as the Grails 4 version.
   
   Of the 40 multi-version guides in `guides.json`:
   - **37** have exactly `master` + `grails3` (Grails 4 + Grails 3)
   - **1** has `grails4` + `grails6` 
(`grails-configuration-properties-micronaut`)
   - **1** has `grails4` + `grails3` + `grails6` (`grails-database-migration`)
   - **1** has a data issue (`grails-on-travis-basics` -- comma-separated in 
one entry)
   
   ### What "multi-version" really means today
   
   In practice, almost all multi-version guides are just Grails 3 vs Grails 4 
(`master`).
   Despite 37 repos having a `grails5` branch and 25 having a `grails-4` 
branch, these
   branches have NOT been published to `guides.json` because:
   
   1. The `githubactions-build.sh` template curls `build-guide` from
      `grails-guides-template` **`master`** branch
   2. The `master` branch version of `build-guide` only handles `master` and 
`grails3`
      branches for publishing
   3. The `6.0.x` branch version handles `grails3`-`grails6` but is not used by 
any
      guide repo's CI
   
   This means the `grails5` branches in 37 repos and `grails-4` branches in 25 
repos
   exist in Git but their built docs were **never published**.
   
   ### Unpublished branch analysis
   
   The `grails5` and `grails-4` branches were never published to `guides.json` 
(see
   above), but many contain distinct content that should be considered for 
publishing
   during migration. The table below shows each branch's relationship to 
`master`.
   
   **`grails5` branches (37 repos)**:
   
   | Status | Count | Meaning |
   |--------|-------|---------|
   | Ahead of master | 35 | Branch has unique commits not in master -- 
**distinct content** |
   | Behind master | 2 | Branch is a subset of master -- no unique content |
   
   | Repo | Ahead | Behind | Status | Commit summary | File-level changes |
   |------|-------|--------|--------|----------------|--------------------|
   | `building-an-ios-objectc-client-powered-by-a-grails-backend` | 1 | 0 | 
ahead | grails5 upgrade | 19 files. Notable: `AnnouncementService.groovy` 
modified (5 lines), `AnnouncementControllerSpec.groovy` modified (85 lines) |
   | `building-an-ios-swift-client-powered-by-a-grails-backend` | 1 | 0 | ahead 
| grails5 upgrade | 20 files. Notable: `AnnouncementService.groovy` modified (4 
lines), `AnnouncementControllerSpec.groovy` modified (85 lines) |
   | `database-per-tenant` | 1 | 0 | ahead | grails5 upgrade | 20 files. 
Notable: codenarc `rules.groovy`, `testrules.groovy`, `codenarc.gradle` removed 
|
   | `discriminator-per-tenant` | 1 | 0 | ahead | grails5 upgrade | 19 files. 
Notable: `tenantSelection.adoc` modified (2 lines) |
   | `grails_url_mappings` | 0 | 1 | behind | *(no unique content)* | -- |
   | `grails-as-docker-container` | 1 | 0 | ahead | "grails4 upgrade" *(may be 
mislabeled)* | 18 files: build infra only |
   | `grails-configuration-properties-micronaut` | 4 | 0 | ahead | grails 5 
upgrade + CI updates | 14 files. Notable: `gradle.yml` workflow modified, 
`githubactions-build.sh` modified |
   | `grails-database-migration` | 1 | 0 | ahead | grails5 upgrade | 18 files: 
build infra only |
   | `grails-deploy-pws` | 1 | 0 | ahead | grails5 upgrade | 20 files. Notable: 
`deployWithGradle.adoc` modified (2 lines) |
   | `grails-docker-external-services` | 1 | 0 | ahead | grails 5 upgrade | 18 
files: build infra only |
   | `grails-dynamic-multiple-datasources` | 1 | 0 | ahead | grails5 upgrade | 
2 files: 2 `build.gradle` only |
   | `grails-elasticbeanstalk` | 1 | 0 | ahead | "grails5 - problem with 
management endpoint" *(may be incomplete)* | 11 files: build infra only |
   | `grails-email` | 1 | 0 | ahead | grails 5 upgrade | 18 files: build infra 
only |
   | `grails-file-download-excel` | 1 | 0 | ahead | "grails5 update - geb 
issue" *(Geb problem noted)* | 11 files: build infra only |
   | `grails-google-cloud` | 1 | 0 | ahead | grails5 upgrade | 19 files. 
Notable: `UploadBookFeaturedImageService.groovy` modified (5 lines) |
   | `grails-google-home` | 1 | 0 | ahead | grails 5 upgrade | 16 files: build 
infra only |
   | `grails-javamelody` | 1 | 0 | ahead | grails5 upgrade | 22 files. Notable: 
2 `GebConfig.groovy` removed, `gettingStarted.adoc` modified (2 lines) |
   | `grails-logicaldelete` | 1 | 0 | ahead | "grails5 upgrade - geb issue" 
*(Geb problem noted)* | 18 files: build infra only |
   | `grails-micronaut-http` | 4 | 0 | ahead | CI/config updates | 3 files: 
`gradle.yml`, `githubactions-build.sh`, `gradle.properties` only |
   | `grails-mock-http-server` | 1 | 0 | ahead | grails5 | 19 files. Notable: 
`OpenweathermapServiceSpec.groovy` modified (2 lines) |
   | `grails-mock-logging-slf4j-test` | 1 | 0 | ahead | grails5 changes | 18 
files: build infra only |
   | `grails-multi-datasource` | 1 | 0 | ahead | grails 5 upgrade | 19 files. 
Notable: `MultipleDataSourceSpec.groovy` modified (16 lines) |
   | `grails-multi-project-build` | 1 | 0 | ahead | grails5 upgrade | 11 files: 
build infra only |
   | `grails-oauth-google` | 1 | 0 | ahead | grails5 upgrade | 20 files. 
Notable: `howto.adoc` modified (4 lines) |
   | `grails-oauth-twitter` | 2 | 0 | ahead | grails 5 upgrade (2 commits) | 22 
files. Notable: `GebConfig.groovy` removed, `howto.adoc` modified (4 lines) |
   | `grails-on-circleci-basics` | 1 | 0 | ahead | grails5 upgrade | 18 files: 
build infra only |
   | `grails-on-github-actions` | 1 | 0 | ahead | grails5 upgrade | 17 files: 
build infra only |
   | `grails-on-travis-basics` | 1 | 0 | ahead | grails5 upgrade | 20 files. 
Notable: `DefaultHomePageSpec.groovy` modified (3 lines), 
`integrationTest.adoc` modified (2 lines) |
   | `grails-rabbitmq` | 2 | 0 | ahead | grails5 upgrade (2 commits) | 35 
files. Notable: `BookPageViewDataServiceSpec.groovy` modified (2 lines), 
`buildingAnalytics.adoc` modified (4 lines) |
   | `grails-restapi-angularjs` | 1 | 0 | ahead | grails5 upgrade | 19 files: 
build infra only |
   | `grails-scheduled` | 1 | 0 | ahead | grails 5 upgrade | 18 files: build 
infra only |
   | `grails-schwartz` | 1 | 0 | ahead | grails5 upgrade | 21 files. Notable: 2 
`GebConfig.groovy` removed |
   | `grails-spring-security-core-plugin-custom-authentication` | 1 | 0 | ahead 
| grails5 upgrade | 18 files: build infra only |
   | `grails-tvmlapp` | 1 | 0 | ahead | grails5 upgrade | 18 files. Notable: 
`QuickcastSpec.groovy` modified (3 lines) |
   | `grails-upload-file` | 1 | 0 | ahead | grails5 upgrade | 19 files. 
Notable: codenarc `rules.groovy` removed |
   | `grails-yourkit-profiling` | 2 | 0 | ahead | grails5 upgrade (2 commits) | 
18 files: build infra only |
   | `server-sent-events` | 0 | 1 | behind | *(no unique content)* | -- |
   
   > **Summary**: 616 files across 35 repos. "Build infra" = gradle wrapper, 
build.gradle, gradle.properties, application.yml, settings.gradle. Only 15 of 
35 repos have any changes beyond build infra. The Grails 5 migration is almost 
entirely a build/infrastructure upgrade -- only 10 source/test files were 
touched across all repos.
   
   **`grails-4` branches (25 repos)**:
   
   | Status | Count | Meaning |
   |--------|-------|---------|
   | Diverged from master | 15 | Both branch and master have unique commits -- 
**distinct content** |
   | Behind master | 10 | Branch is a subset of master -- no unique content |
   
   | Repo | Ahead | Behind | Status | Commit summary | File-level changes |
   |------|-------|--------|--------|----------------|--------------------|
   | `adding-commit-info` | 0 | 16 | behind | *(no unique content)* | -- |
   | `building-an-ios-objectc-client-powered-by-a-grails-backend` | 2 | 1 | 
diverged | Gradle 5 upgrade, Grails 4 upgrade with client HTTP change | 12 
files. Notable: `AnnouncementService.groovy` modified (2 lines), 
`AnnouncementControllerSpec.groovy` modified (82 lines) |
   | `building-an-ios-swift-client-powered-by-a-grails-backend` | 3 | 1 | 
diverged | Gradle 5 upgrade, Grails 4 upgrade with HTTP client change, add view 
plugin | 10 files. Notable: `AnnouncementService.groovy` modified (2 lines), 
`AnnouncementControllerSpec.groovy` modified (82 lines) |
   | `command-objects-and-forms` | 0 | 32 | behind | *(no unique content)* | -- 
|
   | `gorm-event-listeners` | 0 | 23 | behind | *(no unique content)* | -- |
   | `grails-as-docker-container` | 0 | 15 | behind | *(no unique content)* | 
-- |
   | `grails-deploy-pws` | 2 | 1 | diverged | Gradle wrapper upgrade (2 
commits) | 9 files: build infra only |
   | `grails-elasticbeanstalk` | 3 | 1 | diverged | Gradle 5, Grails 4 (Spring 
Boot 2 health actuator change), actuator path fix | 10 files. Notable: 
`HealthSpec.groovy` modified (2 lines), `deploy.adoc` modified (2 lines), 
`writingTheApp.adoc` modified (88 lines) |
   | `grails-google-cloud` | 3 | 1 | diverged | Gradle upgrade, Grails 4 
(pathingJar needed), test correction | 11 files. Notable: 
`UploadBookFeaturedImageService.groovy` modified (5 lines), `BookSpec.groovy` 
modified (8 lines) |
   | `grails-google-home` | 3 | 2 | diverged | Gradle 4 upgrade, Grails 4 
upgrade, remove codenarc | 10 files. Notable: codenarc `rules.groovy` removed, 
`codenarc.gradle` removed |
   | `grails-gorm-data-services` | 2 | 22 | diverged | Upgrade to Grails 4 (2 
commits) | 11 files. Notable: `BootStrap.groovy` modified (1 line), 2 `.gradle` 
cache files removed |
   | `grails-mock-logging-slf4j-test` | 0 | 14 | behind | *(no unique content)* 
| -- |
   | `grails-multi-project-build` | 0 | 28 | behind | *(no unique content)* | 
-- |
   | `grails-on-circleci-basics` | 0 | 23 | behind | *(no unique content)* | -- 
|
   | `grails-rabbitmq` | 2 | 1 | diverged | Gradle 5 upgrade, Grails 4 upgrade 
| 16 files: build infra only (4 subprojects) |
   | `grails-restapi-angularjs` | 2 | 1 | diverged | Gradle 5 upgrade, Grails 4 
upgrade | 9 files: build infra only |
   | `grails-scheduled` | 0 | 4 | behind | *(no unique content)* | -- |
   | `grails-taglib-wyswyg-trix` | 0 | 27 | behind | *(no unique content)* | -- 
|
   | `grails-test-domain-class-constraints` | 26 | 42 | diverged | Grails 3.3.8 
-> 4.0 SNAPSHOT: Gradle 5, GORM 7, Hibernate 5.3.7, asset-pipeline, codenarc, 
remove Geb/mavenLocal, add openjdk11, update views | 83 files. Notable: 
`.travis.yml` modified, codenarc `rules.groovy` modified, `HotelSpec.groovy` 
modified, `GebConfig.groovy` added, 44 scaffold assets refreshed (images, JS, 
CSS, GSP views for Grails 4 templates) |
   | `grails-upload-file` | 9 | 24 | diverged | Gradle wrapper, asset-pipeline, 
Hibernate core, Grails/GORM versions, replace addResources, codenarc | 20 
files. Notable: `HotelController.groovy`, `PointOfInterestController.groovy`, 
`RestaurantController.groovy` modified (2 lines each) |
   | `grails-vue-combined` | 9 | 1 | diverged | Grails 4 server upgrade, views 
compile fix (#208), Grails 4 RC1 + Vue profile 2 | 121 files. Full webpack to 
Vue CLI migration: ~25 webpack/eslint/nightwatch/jest configs removed, Vue CLI 
configs added, `yarn.lock` added, client assets updated (bootstrap.css 13K 
lines, grails.css, Welcome.vue), server `Application.groovy` and 
`logback.groovy` modified, `config.adoc` modified |
   | `react-combined` | 7 | 3 | diverged | Gradle 5, Grails 4 upgrade, views 
compile fix, fix issue #3 | 13 files. Notable: project restructured 
(`gradle.properties` moved to `server/`), `writingTheApp.adoc` modified (3 
lines) |
   | `rest-hibernate` | 20 | 37 | diverged | Gradle 5, Grails 4.0 SNAPSHOT, 
GORM 7, remove Geb/mavenLocal, GORM Data Service, @CompileStatic, codenarc, add 
func tests | 27 files. Notable: `ProductController.groovy` modified (8 lines), 
`BootStrap.groovy` modified (3 lines), `ProductService.groovy` added (9 lines), 
`HomeSpec.groovy` added (37 lines), `ProductFunctionalSpec.groovy` added (157 
lines), `ProductControllerSpec.groovy` modified (15 lines), codenarc configs 
modified |
   | `server-sent-events` | 0 | 18 | behind | *(no unique content)* | -- |
   | `vaadin-grails` | 2 | 1 | diverged | Gradle 5 + Grails upgrade, guide text 
changes | 16 files. Notable: `UrlMappings.groovy` modified, 
`DemoGrailsUI.groovy` modified, `README.md` modified, `gettingStarted.adoc`, 
`profile.adoc`, `toc.yml` modified |
   
   > **Summary**: 377 files across 15 diverged repos. More diverse than Grails 
5 due to the Grails 3-to-4 scope. Outliers: `grails-vue-combined` (121 files, 
webpack to Vue CLI migration), `grails-test-domain-class-constraints` (83 
files, scaffold assets), `rest-hibernate` (27 files, new service + functional 
tests). The 10 "behind" branches are subsets of master with no unique work.
   
   **`grails4` branches (2 repos)**:
   
   | Repo | Ahead | Behind | Status |
   |------|-------|--------|--------|
   | `creating-your-first-grails-app` | 0 | 30 | behind (no unique content) |
   | `grails-configuration-properties-micronaut` | 0 | 0 | identical to master |
   
   Neither `grails4` branch has unique content.
   
   ---
   
   ## How Publishing Works Today
   
   This is the end-to-end lifecycle of a guide in the current system, from 
creation
   to appearing on `https://guides.grails.org`:
   
   ### Step 1: Scaffold the guide
   
   An author clones `grails-guides-template` and runs `create-guide.sh`:
   
   ```bash
   ./create-guide.sh my-new-guide
   ```
   
   This script:
   1. Runs `grails create-app my-new-guide` to generate a Grails app
   2. Renames the app directory to `initial/`
   3. Copies `initial/` to `complete/`
   4. Writes `settings.gradle` with `include 'complete', 'initial'`
   5. Copies template files from `src/main/project/` into the guide root
   
   The template files copied are:
   - `build.gradle` -- applies `guide-build.gradle` remotely
   - `gradle.properties` -- metadata stub with `TODO` placeholders
   - `githubactions-build.sh` -- CI entrypoint (curls scripts at runtime)
   - `src/main/docs/guide/` -- AsciiDoc skeleton (`toc.yml`, 
`gettingStarted.adoc`, etc.)
   
   **What is NOT created by the script:**
   - `.github/workflows/` -- the GitHub Actions workflow YAML must be created 
manually
   - No CI configuration is auto-generated
   
   ### Step 2: Create the GitHub repo
   
   The author manually:
   1. Creates a new repository in the `grails-guides` org on GitHub
   2. The repo name must match the guide directory name
   3. Pushes the scaffolded project
   
   ### Step 3: Set up CI manually
   
   The author must manually:
   1. Create `.github/workflows/gradle.yml` (or similar) -- there is no 
template for this
   2. Configure repository secrets: `GH_TOKEN`, `GIT_NAME`, `GIT_EMAIL`
   
   A typical workflow looks like this (from `creating-your-first-grails-app`):
   
   ```yaml
   name: Java CI with Gradle
   on:
     push:
       branches: [ master ]
     pull_request:
       branches: [ master ]
   jobs:
     build:
       runs-on: ubuntu-latest
       steps:
       - uses: actions/checkout@v2
       - name: Set up JDK 11
         uses: actions/setup-java@v2
         with:
           java-version: '11'
           distribution: 'adopt'
           cache: gradle
       - name: Run script
         run: ./githubactions-build.sh
         env:
           GH_TOKEN: ${{ secrets.GH_TOKEN }}
           GIT_NAME: ${{ secrets.GIT_NAME }}
           GIT_EMAIL: ${{ secrets.GIT_EMAIL }}
   ```
   
   Note: many of these workflows are outdated (actions/checkout@v2, JDK 11, 
adopt
   distribution). The `grails-guides-template` README still references Travis 
CI as
   the CI system, with instructions for `travis encrypt` -- GitHub Actions was
   retrofitted without updating the docs.
   
   ### Step 4: CI runs and the guide gets published
   
   On push, the workflow runs `./githubactions-build.sh`. This is where it gets 
wild:
   
   #### 4a. `githubactions-build.sh` (in the guide repo)
   
   This script does **not** contain any build logic. It downloads and executes 
scripts
   from `grails-guides-template` at runtime via `curl`:
   
   ```bash
   #!/bin/bash
   set -e
   export EXIT_STATUS=0
   
   # Download the build script from grails-guides-template master branch at 
runtime
   curl -O 
https://raw.githubusercontent.com/grails/grails-guides/master/githubactions/build-guide
   chmod 777 build-guide
   ./build-guide || EXIT_STATUS=$?
   
   if [[ $EXIT_STATUS -ne 0 ]]; then
       echo "build-guide failed"
       exit $EXIT_STATUS
   fi
   
   # Download the republish script from grails-guides-template master branch at 
runtime
   curl -O 
https://raw.githubusercontent.com/grails/grails-guides/master/githubactions/republish-guides-website.sh
   chmod 777 republish-guides-website.sh
   ./republish-guides-website.sh || EXIT_STATUS=$?
   
   exit $EXIT_STATUS
   ```
   
   Every single guide repo has an identical copy of this file. All 94 of them 
`curl`
   the same two scripts from `grails-guides-template` `master` branch (using 
the old
   `grails/grails-guides` URL).
   
   #### 4b. `build-guide` (curled from `grails-guides-template`)
   
   This is the actual build logic. It:
   
   1. Runs `./gradlew -Dgeb.env=chromeHeadless check` (tests)
   2. Runs `./gradlew publishGuide` (AsciiDoc -> HTML)
   3. **Only for specific branches** (`master`, `grails3`, `grails4`, 
`grails5`, `grails6`):
      - Clones `grails-guides-template` `gh-pages` branch
      - Runs `./gradlew updateGuidesJson` -- **this is how a guide gets added to
        `guides.json`** (the central registry). The task reads 
`gradle.properties` and
        upserts the guide's metadata into the JSON file.
      - Copies built HTML to the appropriate directory on `gh-pages`:
        - `master` branch -> `<guide-name>/` (root, no version prefix)
        - `grails3` branch -> `grails3/<guide-name>/`
        - `grails4` branch -> `grails4/<guide-name>/`
        - etc.
      - Commits and pushes to `gh-pages`
   
   **Branch discrepancy**: The `master` branch of `grails-guides-template` has a
   `build-guide` that only handles `master` and `grails3`. The `6.0.x` branch 
version
   handles `grails3`, `grails4`, `grails5`, `grails6` (not `master`). Since the
   `githubactions-build.sh` template curls from `master`, guides using that 
template
   only get the older behavior.
   
   #### 4c. `republish-guides-website.sh` (curled from `grails-guides-template`)
   
   After the guide is built, this script **regenerates the entire guides index 
site**:
   
   1. Clones `grails/grails-static-website` (this repo, using old URL) `master` 
branch
   2. Runs `./gradlew buildGuide` -- which fetches `guides.json` from 
`gh-pages` and
      generates index/tag/category HTML pages
   3. Clones `grails-guides-template` `gh-pages` again
   4. Copies `build/dist/*` (the generated index pages) into `gh-pages`
   5. Commits and pushes
   
   This means **every single guide CI build triggers a full rebuild of the 
entire guides
   index site**. And the same rebuild also runs every 2 hours via the cron job 
in
   `grails-static-website`'s `publish.yml`.
   
   ### Step 5: The guide appears on `guides.grails.org`
   
   Once the `gh-pages` push completes, the guide is live:
   - Guide HTML: `https://guides.grails.org/<guide-name>/guide/index.html`
   - Versioned: 
`https://guides.grails.org/grails{N}/<guide-name>/guide/index.html`
   - Listed on the index: `https://guides.grails.org/index.html`
   - Searchable by tags and categories
   
   The guide was added to `guides.json` by `updateGuidesJson` (step 4b) and the 
index
   was regenerated by `republish-guides-website.sh` (step 4c), so it shows up 
in the
   listing immediately.
   
   ---
   
   ## Current CI/CD Flow Diagram
   
   ```
   Guide Author pushes to grails-guides/<guide-name>
           |
           v
   .github/workflows/gradle.yml  (manually created, per-repo)
           |
           v
   githubactions-build.sh  (identical in every repo)
           |
           |-- curl build-guide from grails-guides-template master (old URL)
           |-- curl republish-guides-website.sh from grails-guides-template 
master (old URL)
           |
           v
   build-guide  (downloaded at runtime)
           |
           |-- ./gradlew check                    (run tests)
           |-- ./gradlew publishGuide             (AsciiDoc -> HTML via 
grails-docs)
           |       |
           |       |-- build.gradle: apply from   (curls guide-build.gradle from
           |       |   grails-guides-template master or 6.0.x, old URL)
           |       |
           |       |-- guide-build.gradle:
           |       |     prepareResources          (downloads 
grails-guides-template
           |       |                                as ZIP to get theme 
resources)
           |       |     publishGuide              (uses 
org.grails:grails-docs:6.0.0-RC1)
           |       |
           |       v
           |   build/docs/guide/index.html
           |
           |-- git clone grails-guides-template gh-pages (old URL)
           |-- ./gradlew updateGuidesJson          (upserts into guides.json)
           |-- cp build/docs -> gh-pages/grails{N}/<guide-name>/
           |-- git push gh-pages
           |
           v
   republish-guides-website.sh  (downloaded at runtime)
           |
           |-- git clone grails-static-website (this repo, old URL)
           |-- ./gradlew buildGuide
           |       |
           |       |-- GuidesFetcher: HTTP GET guides.json from gh-pages
           |       |-- GuidesTask: generates index.html, categories/*.html, 
tags/*.html
           |       v
           |   build/dist/
           |
           |-- git clone grails-guides-template gh-pages (old URL)
           |-- cp build/dist/* -> gh-pages/
           |-- git push gh-pages
           |
           v
   https://guides.grails.org  (GitHub Pages from grails-guides-template 
gh-pages)
   ```
   
   Additionally, `grails-static-website`'s own `publish.yml` runs the same
   `./gradlew buildGuide` + push to `grails-guides-template gh-pages` 
independently:
   - On every push to `master`
   - On a cron schedule every 2 hours
   - On manual `workflow_dispatch`
   
   ---
   
   ## Current Component Reference
   
   ### `grails-guides-template` structure (source branches: `master`, `6.0.x`)
   
   ```
   grails-guides-template/
   ├── create-guide.sh                  # Scaffolds new guide projects
   ├── build.gradle                     # Downloads navbar assets from 
grails-navbar repo
   ├── gradle/
   │   └── guide-build.gradle           # Shared build plugin applied by all 
guides
   ├── githubactions/
   │   ├── build-guide                  # CI: test + build + publish guide HTML
   │   └── republish-guides-website.sh  # CI: regenerate guides index site
   ├── src/main/
   │   ├── docs/
   │   │   ├── common-completesolution.adoc
   │   │   ├── common-gradle.adoc
   │   │   ├── common-howto.adoc
   │   │   ├── common-installingGrails.adoc
   │   │   ├── common-requirements.adoc
   │   │   └── ... (~45 common-*.adoc snippets)
   │   ├── project/                     # Template copied into new guides
   │   │   ├── build.gradle
   │   │   ├── gradle.properties
   │   │   ├── githubactions-build.sh
   │   │   └── src/main/docs/guide/
   │   │       ├── toc.yml
   │   │       ├── gettingStarted.adoc
   │   │       ├── howto.adoc
   │   │       ├── requirements.adoc
   │   │       ├── runningTheApp.adoc
   │   │       └── writingTheApp.adoc
   │   └── resources/                   # Theme assets
   │       ├── css/                     # guide.css, screen.css, navbar.css, 
etc.
   │       ├── fonts/                   # Archia family + FontAwesome
   │       └── img/                     # Grails logo, category icons
   └── README.md                        # Still references Travis CI
   ```
   
   ### `grails-guides-template` `gh-pages` branch (deployment)
   
   ```
   gh-pages/
   ├── guides.json                      # Central metadata registry (125 
entries, 84 unique guides)
   ├── index.html                       # Guides listing (generated by 
static-website)
   ├── categories/*.html                # Per-category pages
   ├── tags/*.html                      # Per-tag pages
   ├── sitemap.xml
   ├── stylesheets/                     # Site CSS
   ├── javascripts/                     # Site JS
   ├── images/                          # Site images
   ├── creating-your-first-grails-app/  # Guide HTML (master branch builds)
   │   └── guide/index.html
   ├── grails3/                         # Grails 3 versioned guides
   │   └── <guide-name>/guide/index.html
   ├── grails4/                         # Grails 4 versioned guides
   ├── grails5/                         # Grails 5 versioned guides
   └── grails6/                         # Grails 6 versioned guides
   ```
   
   ### Individual guide repo structure (e.g. `creating-your-first-grails-app`)
   
   ```
   creating-your-first-grails-app/
   ├── .github/workflows/
   │   └── gradle.yml                   # Manually created, NOT from template
   ├── githubactions-build.sh           # Curls build-guide + republish at 
runtime
   ├── build.gradle                     # apply from: guide-build.gradle 
(remote, old URL)
   ├── gradle.properties                # Guide metadata
   ├── settings.gradle                  # include 'complete', 'initial'
   ├── initial/                         # Starter Grails app
   ├── complete/                        # Completed solution
   ├── src/main/
   │   ├── docs/guide/
   │   │   ├── toc.yml
   │   │   └── *.adoc                   # Guide content
   │   └── resources/img/               # Guide-specific images
   ├── gradle/wrapper/                  # Gradle wrapper
   ├── gradlew / gradlew.bat
   └── README.md
   ```
   
   ### Guide `build.gradle`
   
   Every guide's `build.gradle` remotely applies the shared build script. Some 
reference
   `master`, some `6.0.x`:
   
   ```groovy
   plugins {
       id 'org.asciidoctor.convert' version '1.5.3'
   }
   asciidoctorj {
       version = '1.5.4'
   }
   // NOTE: old URL; actual repo is grails-guides/grails-guides-template
   apply 
from:"https://raw.githubusercontent.com/grails/grails-guides/master/gradle/guide-build.gradle";
   ```
   
   ### Guide `gradle.properties`
   
   ```properties
   title=Creating your first Grails Application
   subtitle=Learn how to create your first Grails app
   authors=Zachary Klein
   copyright=Copies of this document may be made for your own use and for...
   githubSlug=grails-guides/creating-your-first-grails-app
   githubBranch=master
   category=Grails Apprentice
   tags=mysql,gsp,grails4
   publicationDate=23 Jan 2017
   ```
   
   | Property          | Description                          | Required |
   |-------------------|--------------------------------------|----------|
   | `title`           | Display title                        | Yes      |
   | `subtitle`        | Short description                    | Yes      |
   | `authors`         | Comma-separated authors              | Yes      |
   | `githubSlug`      | Repo identifier (`grails-guides/...`)| Yes      |
   | `githubBranch`    | Git branch                           | Yes      |
   | `tags`            | Comma-separated tags                 | Yes      |
   | `category`        | Must match a predefined category     | Yes      |
   | `publicationDate` | Format: `dd MMM yyyy`                | Yes      |
   
   ### Shared build plugin: `guide-build.gradle`
   
   Applied remotely by every guide. Key tasks:
   
   - **`prepareResources`** -- downloads `grails-guides-template` (branch 
`6.0.x`) as
     a ZIP from GitHub and extracts theme resources (CSS, fonts, images) and 
common
     AsciiDoc snippets. This is a network fetch on every build.
   - **`copyLocalGuideImgResources`** -- copies guide-specific images from
     `src/main/resources/img/` into the extracted build tree.
   - **`publishGuide`** -- runs `grails.doc.gradle.PublishGuide` (from
     `org.grails:grails-docs:6.0.0-RC1`) to convert AsciiDoc to HTML.
     Output: `build/docs/guide/single.html` renamed to 
`build/docs/guide/index.html`.
     Also creates a redirect `build/docs/index.html`.
   - **`updateGuidesJson`** -- reads `gh-pages/guides.json`, upserts the 
current guide's
     metadata from `gradle.properties`, writes the updated JSON back. This is 
how a
     guide registers itself in the central metadata file.
   
   Validation at task graph resolution time enforces that all required 
properties are
   set and not `TODO`.
   
   ### Guides index generation (this repo: `grails-static-website`)
   
   The `buildGuides` Gradle task (registered in `GrailsWebsitePlugin`) 
orchestrates:
   
   ```
   buildGuides
   ├── genGuides  (GuidesTask)    -- fetches guides.json, generates HTML pages
   ├── copyAssets (AssetsTask)     -- copies CSS/JS/images
   └── genSitemap (SitemapTask)   -- generates sitemap.xml
   ```
   
   **`GuidesFetcher`** (`buildSrc/.../model/guides/GuidesFetcher.groovy`):
   - Fetches `guides.json` from
     
`https://raw.githubusercontent.com/grails-guides/grails-guides-template/gh-pages/guides.json`
   - Parses each entry into a `GuideDto`
   - Groups by `githubSlug`; creates `SingleGuide` (one branch) or
     `GrailsVersionedGuide` (multiple branches like grails3/4/5/6)
   - Branch-to-version mapping: `grails3`->3, `grails4`->4, `master`->4, 
`grails5`->5, `grails6`->6
   - Filters out future-dated guides, sorts newest-first
   
   **`GuidesTask`** (`buildSrc/.../tasks/GuidesTask.groovy`):
   - Calls `GuidesFetcher.fetchGuides()` and `TagUtils.populateTags(guides)`
   - Generates `build/temp/guides.html` (main listing)
   - Generates `build/temp/tags/<tag>.html` for each tag
   - Generates `build/temp/categories/<category>.html` for each category
   - Renders all through the site template into `build/dist/`
   
   **`GuidesPage`** (`buildSrc/.../model/guides/GuidesPage.groovy`):
   - Renders HTML using Groovy `MarkupBuilder`
   - 14 predefined categories with SVG icons
   - Links to `https://guides.grails.org/<name>/guide/index.html` (single)
   - Links to `https://guides.grails.org/grails{N}/<slug>/guide/index.html` 
(versioned)
   - Features: latest guides list (8 most recent), tag cloud, search box, 
category grouping
   
   ### Main site publishing (`publish.yml` workflow)
   
   The GitHub Actions workflow runs on push to master, every 2 hours via cron, 
and on
   manual dispatch:
   
   ```yaml
   # Step 1: Build and publish the main grails.apache.org site
   GRADLE_TASK: build
   GITHUB_SLUG: apache/grails-website
   GH_BRANCH: asf-site-production
   
   # Step 2: Build and publish the guides.grails.org index site
   GRADLE_TASK: buildGuide
   GITHUB_SLUG: grails-guides/grails-guides-template
   GH_BRANCH: gh-pages
   ```
   
   Both steps use `publish.sh`, which runs the Gradle task, clones the target 
repo's
   deploy branch, copies `build/dist/` into it, and pushes.
   
   ---
   
   ## Current Pain Points
   
   1. **Runtime `curl` of CI scripts** -- every guide CI run downloads 
`build-guide`
      and `republish-guides-website.sh` from GitHub at runtime. The CI logic is 
not
      committed to the guide repos; it's fetched live. This is fragile (GitHub 
outage
      = broken CI) and opaque (the actual CI behavior is invisible in the guide 
repo).
   
   2. **Remote `apply from` for Gradle builds** -- every guide's `build.gradle` 
fetches
      `guide-build.gradle` from GitHub at Gradle configuration time. Combined 
with
      `prepareResources` downloading a ZIP of `grails-guides-template`, a 
single guide
      build makes 3+ HTTP requests to GitHub before any real work begins.
   
   3. **Cascading index rebuilds** -- every single guide CI build runs
      `republish-guides-website.sh`, which clones this entire repo, runs
      `./gradlew buildGuide`, and pushes the result. If 5 guides push in the 
same hour,
      the index gets rebuilt 5 times -- plus once more from the cron job. These 
can also
      race and cause push conflicts on `gh-pages`.
   
   4. **Stale old-URL references everywhere** -- the `githubactions-build.sh` 
in all 94
      guide repos curls from `grails/grails-guides` (old URL). The 
`build-guide` and
      `republish-guides-website.sh` scripts clone `grails/grails-guides` (old 
URL) and
      `grails/grails-static-website` (old URL). Guide `build.gradle` files 
apply from
      `grails/grails-guides` (old URL). GitHub redirects mask this but add 
fragility.
   
   5. **`master` vs `6.0.x` branch discrepancy in `build-guide`** -- the 
`master`
      branch version of `build-guide` only handles `master` and `grails3` 
branches.
      The `6.0.x` branch version handles `grails3`-`grails6` (not `master`). 
Since
      the curled template references `master`, guides using the default 
template only
      publish for `master` and `grails3`. This is why only 3 guides have 
published
      Grails 6 content despite 3 repos having a `grails6` branch, and why 37 
repos
      with `grails5` branches have never published Grails 5 content.
   
   6. **No workflow template in scaffolding** -- `create-guide.sh` copies
      `build.gradle`, `gradle.properties`, `githubactions-build.sh`, and docs, 
but does
      NOT create `.github/workflows/`. The GitHub Actions workflow must be 
created and
      configured manually for each guide, leading to inconsistency across 94 
repos.
   
   7. **Outdated README** -- the `grails-guides-template` README documents 
Travis CI
      as the CI system (with `travis encrypt` instructions). GitHub Actions was
      retrofitted without updating the documentation.
   
   8. **Outdated guide workflows** -- many guide repos use 
`actions/checkout@v2`,
      `actions/setup-java@v2`, JDK 11, and `adopt` distribution. These are 
years out
      of date.
   
   9. **Overloaded `gh-pages` branch** -- `grails-guides-template` `gh-pages` 
stores
      built guide HTML (pushed by 94 individual guide CIs), `guides.json` 
(mutated by
      `updateGuidesJson` from each guide CI), AND the index site (pushed by both
      `republish-guides-website.sh` and `grails-static-website`'s cron). 
Multiple
      independent CI pipelines write to the same branch concurrently.
   
   10. **`guides.json` as the integration point** -- metadata is serialized to 
JSON in
       one repo, fetched via HTTP in another. Changes to the schema require 
coordinating
       across repos.
   
   11. **Stale `grails-docs` dependency** -- the `PublishGuide` task comes from
       `org.grails:grails-docs:6.0.0-RC1`, a Grails-specific doc toolchain. 
Modern
       AsciiDoctor Gradle plugins are more capable and actively maintained.
   
   12. **94 repos to maintain** -- the `grails-guides` org has 94 individual 
guide repos
       plus the template. Most target older Grails versions (only 3 have Grails 
6
       branches). Maintaining permissions, branch policies, and secrets across 
all of
       them is significant overhead.
   
   13. **10 unpublished repos** -- 10 repos exist in the org but never made it 
into
       `guides.json`, so they are invisible on the guides site. Their status is 
unclear.
   
   14. **Inconsistent branch naming** -- 25 repos use `grails-4` (hyphenated) 
while 2
       use `grails4` (unhyphenated). The `GuidesFetcher` branch mapping only 
recognizes
       `grails4`, meaning the 25 repos with `grails-4` branches are not 
properly mapped.
   
   ---
   
   # Part 2 -- Migration Plan
   
   ## Consolidation Plan
   
   ### Goal
   
   Move all guide AsciiDoc source and publishing infrastructure into
   `grails-static-website` so that:
   - All guide AsciiDoc source lives in this repo
   - Guide metadata is defined in a single YAML file (`conf/guides.yml`)
   - Built guide HTML and the index site are deployed together
   - One CI pipeline handles everything
   - No runtime `curl` of scripts, no remote `apply from`, no cascading rebuilds
   - The `grails-guides` GitHub org retains only repos with 
`initial/`+`complete/`
     sample applications (79 repos), serving purely as runnable code companions
   
   ### Phase 1: Move guide metadata here
   
   **What**: Create `conf/guides.yml` in this repo containing all guide metadata
   currently in `guides.json`. Stop fetching from GitHub at build time.
   
   **Changes**:
   - Add `conf/guides.yml` with all 84 published guide entries (title, authors, 
tags,
     category, publication date, Grails versions)
   - Fix the `grails-micronaut-kakfa` typo to `grails-micronaut-kafka`
   - Unify `"Grails + DevOps"` / `"Grails + Devops"` category case
   - Add entries for the 10 currently unpublished repos (or explicitly exclude 
them)
   - Add a `guides` property to `GrailsWebsiteExtension` pointing to it
   - Modify `GuidesFetcher` to read from a local YAML file instead of a remote 
JSON URL
   - Update `GuidesTask` to accept the local file as an `@InputFile`
   
   **Benefits**: No more network dependency during builds. Metadata changes are
   version-controlled with the site. Adding/updating a guide is a simple YAML 
edit + PR.
   
   **Impact on `grails-guides` org**: None yet. Individual guide repos continue 
to
   function as before; their CI just becomes the non-authoritative source for 
metadata.
   
   ### Phase 2: Move common doc snippets and theme resources here
   
   **What**: Bring `src/main/docs/common-*.adoc` (~45 files) and 
`src/main/resources/`
   (CSS, fonts, images) from `grails-guides-template` into this repo.
   
   **Changes**:
   - Copy `grails-guides-template/src/main/docs/common-*.adoc` to 
`guides/common/`
   - Copy `grails-guides-template/src/main/resources/` to `guides/resources/`
   - Update `guide-build.gradle` (or its replacement) to reference local paths
   - Individual guide repos can still exist but would reference resources from 
this repo
   
   **Benefits**: Theme and common content are versioned with the site. No ZIP 
download
   at build time (`prepareResources` task eliminated).
   
   **Impact on `grails-guides-template`**: Its source branches (`master`, 
`6.0.x`) become
   redundant for `src/main/docs/` and `src/main/resources/`. Only `gh-pages` is 
still
   needed until Phase 5.
   
   ### Phase 3: Move `guide-build.gradle` here
   
   **What**: Bring the shared Gradle build plugin into `buildSrc/` alongside 
the existing
   site build logic.
   
   **Changes**:
   - Port `guide-build.gradle` tasks (`publishGuide`, `updateGuidesJson`) into
     `buildSrc/` as proper Gradle task classes (like `GuidesTask` already is)
   - Replace the `org.grails:grails-docs` `PublishGuide` dependency with the 
standard
     `org.asciidoctor.jvm.convert` Gradle plugin
   - Individual guide repos would `apply from` this repo (or use a published 
plugin)
   
   **Benefits**: Build logic is testable, type-safe Groovy/Java classes. Modern
   AsciiDoctor tooling. Single place to update build conventions.
   
   **Impact on `grails-guides-template`**: `gradle/guide-build.gradle`,
   `create-guide.sh`, and `githubactions/` become dead code. The repo's source 
branches
   are now fully superseded.
   
   **Impact on individual guide repos**: Their `build.gradle` must be updated 
to point
   the `apply from` URL at this repo (or use a published Gradle plugin). This 
can be
   done incrementally per guide.
   
   ### Phase 4: Move ALL guide AsciiDoc source here
   
   **What**: Move the AsciiDoc source from every guide repo into this repo. 
This is
   **mandatory** for all 84 published guides and optionally for the 10 
unpublished ones.
   
   **Changes**:
   - Create `guides/<guide-name>/` directories containing `guide/toc.yml` and 
`.adoc` files
   - Add a Gradle task to build all local guides in one pass
   - The `buildGuides` task builds all guide HTML from local AsciiDoc source
   - `conf/guides.yml` references the local path for each guide
   
   **Per-guide migration procedure**:
   
   1. **Copy AsciiDoc**: Copy `src/main/docs/guide/` from the guide repo into
      `guides/<guide-name>/` in this repo
   2. **Copy guide-specific images**: Copy `src/main/resources/img/` into
      `guides/<guide-name>/images/`
   3. **Update `conf/guides.yml`**: Ensure the guide entry has `source: local`
   4. **Remove doc-publishing from the guide repo**: Delete `src/main/docs/`,
      `githubactions-build.sh`, the doc-related parts of `build.gradle`, and
      `gradle.properties` metadata fields used only for doc publishing
   5. **Update the guide repo CI**: If the repo has `initial/`+`complete/` 
sample
      projects, keep a simplified workflow that only builds/tests the sample 
apps
      (no more `publishGuide`, `updateGuidesJson`, or 
`republish-guides-website.sh`).
      If the repo has no sample projects, archive it entirely.
   
   **What happens to each repo category**:
   
   | Category | Count | Action |
   |----------|-------|--------|
   | Has both `initial/` and `complete/` | 79 | AsciiDoc moves here. Repo keeps 
sample projects with simplified CI. |
   | Has `complete/` only (non-quickcast) | 7 | AsciiDoc moves here. Strip 
doc-publishing, archive the repo. |
   | Has `complete/` only (quickcast) | 7 | AsciiDoc moves here. Strip 
doc-publishing, archive the repo. |
   | Has neither dir | 1 | `grails-acl` -- doc-only, never published. Archive 
the repo. |
   | Unpublished (in org, not in `guides.json`) | 10 | Evaluate each: migrate 
AsciiDoc if the guide has value; archive if abandoned. |
   
   **Proposed structure**:
   ```
   grails-static-website/
   ├── conf/
   │   └── guides.yml                 <-- all guide metadata (84+ entries)
   ├── guides/
   │   ├── common/                    <-- shared AsciiDoc snippets (from 
grails-guides-template)
   │   ├── resources/                 <-- theme CSS, fonts, images (from 
grails-guides-template)
   │   ├── creating-your-first-grails-app/
   │   │   ├── toc.yml
   │   │   ├── images/                <-- guide-specific images
   │   │   └── guide/*.adoc
   │   ├── grails-mock-basics/
   │   │   ├── toc.yml
   │   │   └── guide/*.adoc
   │   └── ... (84+ guide directories)
   ├── buildSrc/
   │   └── .../guides/
   │       ├── GuidesFetcher.groovy      <-- reads conf/guides.yml
   │       ├── GuidesTask.groovy         <-- generates index site
   │       ├── GuideBuildTask.groovy     <-- NEW: builds individual guide HTML
   │       └── ...
   └── .github/workflows/
       └── publish.yml                <-- single workflow for site + guides
   ```
   
   ### Phase 5: Simplify deployment
   
   **What**: Deploy everything (main site + guide HTML + guide index) as part of
   `grails.apache.org`. All guides will be served at 
`https://grails.apache.org/guides/`.
   
   **Changes**:
   - Build output produces one unified `build/dist/` tree:
     ```
     build/dist/
     ├── index.html                  <-- grails.apache.org homepage
     ├── guides/
     │   ├── index.html              <-- grails.apache.org/guides/ listing
     │   ├── categories/*.html
     │   ├── tags/*.html
     │   └── <guide-name>/
     │       └── guide/index.html    <-- individual guide HTML
     └── ...
     ```
   - A single `publish.yml` step pushes to `apache/grails-website` 
`asf-site-production`
   - Remove the second `publish.sh` step that pushes to 
`grails-guides-template` `gh-pages`
   - Eliminate `republish-guides-website.sh` -- no more cascading rebuilds
   - Update `GUIDES_URL` in `GuidesPage.groovy` from 
`https://guides.grails.org` to
     `https://grails.apache.org/guides`
   
   **New URL structure**:
   - `https://grails.apache.org/guides/` -- guides listing (index)
   - `https://grails.apache.org/guides/<guide-name>/guide/index.html` -- 
individual guide
   - `https://grails.apache.org/guides/tags/<tag>.html` -- guides by tag
   - `https://grails.apache.org/guides/categories/<category>.html` -- guides by 
category
   
   **Benefits**: One deploy, one repo, one domain. No cross-repo push
   choreography. No concurrent writes to `gh-pages`. Drastically simpler CI/CD.
   
   ### Phase 6: Strip doc-publishing from guide repos
   
   **What**: After AsciiDoc source has been migrated (Phase 4), strip the 
doc-publishing
   machinery from every guide repo in the `grails-guides` org.
   
   **Per-repo procedure for repos keeping `initial/`+`complete/` (79 repos)**:
   
   1. Delete `src/main/docs/` (AsciiDoc source -- now in 
`grails-static-website`)
   2. Delete `githubactions-build.sh` (no longer needed)
   3. Simplify `build.gradle`:
      - Remove the `apply from:` that fetches `guide-build.gradle`
      - Remove the `asciidoctor` plugin
      - Keep only what's needed to build/test `initial/` and `complete/` 
subprojects
   4. Remove doc-publishing metadata from `gradle.properties` (title, subtitle, 
authors,
      githubSlug, githubBranch, category, tags, publicationDate)
   5. Update `.github/workflows/gradle.yml`:
      - Remove the `./githubactions-build.sh` step
      - Replace with `./gradlew check` to just run tests on the sample apps
      - Update to modern actions versions (checkout@v4, setup-java@v4, JDK 17+)
      - Remove `GH_TOKEN` / `GIT_NAME` / `GIT_EMAIL` secrets (no longer pushing 
to gh-pages)
   6. Add a `README.md` note: "Guide content is published from
      
[apache/grails-static-website](https://github.com/apache/grails-static-website).
      This repo contains only the sample applications."
   
   **For repos with `complete/` only (14 repos)**:
   
   Same stripping procedure as above, then archive the repo. These repos have no
   `initial/` project, so they do not serve as interactive tutorial companions.
   The `complete/` code is preserved in the archived repo for reference.
   
   **For repos with neither dir (1 repo: `grails-acl`)**:
   
   Archive the repo. It was never published and has no sample code.
   
   ### Phase 7: Wind down `grails-guides-template`
   
   **What**: After consolidation is complete, clean up `grails-guides-template`.
   
   **Impact on `grails-guides/grails-guides-template`**:
   - Source branches (`master`, `6.0.x`) are fully superseded after Phases 2-3 
--
     all build logic, common docs, and theme resources now live in 
`grails-static-website`
   - `gh-pages` branch continues to serve `guides.grails.org` via GitHub Pages, 
but
     all HTML files are replaced with meta-refresh redirects to 
`grails.apache.org/guides/`
     (see URL Redirect Plan below)
   - **Action**: Replace all HTML on `gh-pages` with redirect stubs. Archive 
the repo's
     source branches. The `gh-pages` branch stays live to serve redirects 
indefinitely.
   
   **Impact on the `grails-guides` org after all phases**:
   
   | What remains | Count | Purpose |
   |-------------|-------|---------|
   | Repos with `initial/`+`complete/` sample apps | 79 | Runnable code 
companions for guides (no doc-publishing) |
   | Repos archived (`complete/`-only, doc-only, obsolete) | 15 | Read-only 
reference (14 complete-only + 1 no-code) |
   | `grails-guides-template` | 1 | Source branches archived; `gh-pages` stays 
live for redirects |
   
   ### URL Redirect Plan
   
   Current guide URLs follow the pattern:
   ```
   https://guides.grails.org/<guide-name>/guide/index.html          (single 
version)
   https://guides.grails.org/grails{N}/<guide-name>/guide/index.html (versioned)
   ```
   
   After consolidation, guides will be served from `grails.apache.org/guides/`. 
To
   preserve existing links, the `grails-guides-template` `gh-pages` branch 
(which
   serves `guides.grails.org` via GitHub Pages) will be updated with 
meta-refresh
   redirects pointing to the new `grails.apache.org` URLs.
   
   **Redirect implementation**: Replace all HTML files on `gh-pages` with 
redirect
   stubs using `<meta http-equiv="refresh">`:
   
   ```html
   <!DOCTYPE html>
   <html>
   <head>
     <meta http-equiv="refresh" content="0; 
url=https://grails.apache.org/guides/<guide-name>/guide/index.html">
     <link rel="canonical" 
href="https://grails.apache.org/guides/<guide-name>/guide/index.html">
   </head>
   <body>
     <p>This page has moved to
       <a href="https://grails.apache.org/guides/<guide-name>/guide/index.html">
         grails.apache.org/guides/<guide-name>/guide/index.html
       </a>.
     </p>
   </body>
   </html>
   ```
   
   This covers:
   - `https://guides.grails.org/<guide-name>/guide/index.html` ->
     `https://grails.apache.org/guides/<guide-name>/guide/index.html`
   - `https://guides.grails.org/grails{N}/<guide-name>/guide/index.html` ->
     `https://grails.apache.org/guides/grails{N}/<guide-name>/guide/index.html`
   - `https://guides.grails.org/index.html` ->
     `https://grails.apache.org/guides/index.html`
   - `https://guides.grails.org/tags/*.html` ->
     `https://grails.apache.org/guides/tags/*.html`
   - `https://guides.grails.org/categories/*.html` ->
     `https://grails.apache.org/guides/categories/*.html`
   
   The `gh-pages` branch stays live (not archived) to serve redirects 
indefinitely.
   The `grails-guides-template` repo itself can still be archived since the 
`gh-pages`
   branch is independent of the source branches.
   
   ---
   
   ## Migration Checklist
   
   **Phase 1 -- Metadata**
   - [ ] Create `conf/guides.yml` from current `guides.json` (84 published + 10 
unpublished to evaluate)
   - [ ] Fix typo: `grails-micronaut-kakfa` -> `grails-micronaut-kafka`
   - [ ] Unify category: `"Grails + DevOps"` / `"Grails + Devops"`
   - [ ] Fix data issue: `grails-on-travis-basics` branch value 
`"master,grails3"`
   - [ ] Update `GuidesFetcher` to read local YAML
   - [ ] Verify index site renders identically
   
   **Phase 2 -- Common docs and resources**
   - [ ] Copy ~45 `common-*.adoc` snippets to `guides/common/`
   - [ ] Copy theme resources (CSS, fonts, images) to `guides/resources/`
   
   **Phase 3 -- Build plugin**
   - [ ] Port `guide-build.gradle` tasks into `buildSrc/`
   - [ ] Replace `grails-docs` with standard `asciidoctor-gradle-plugin`
   - [ ] Add `GuideBuildTask` for building individual guide HTML
   
   **Phase 4 -- Guide AsciiDoc source (all 84 published guides)**
   - [ ] Migrate AsciiDoc from all 79 repos with `initial/`+`complete/`
   - [ ] Migrate AsciiDoc from all 14 repos with `complete/`-only
   - [ ] Migrate AsciiDoc from `grails-acl` (if worth preserving)
   - [ ] Evaluate and migrate the 10 unpublished repos
   - [ ] Verify all guides build and render correctly from local source
   
   **Phase 5 -- Deployment**
   - [ ] Update `publish.yml` to produce unified output under 
`build/dist/guides/`
   - [ ] Remove second `publish.sh` step for `grails-guides-template`
   - [ ] Update `GUIDES_URL` in `GuidesPage.groovy` to 
`https://grails.apache.org/guides`
   - [ ] Verify all guide URLs work at `grails.apache.org/guides/`
   
   **Phase 6 -- Strip doc-publishing from guide repos**
   - [ ] Strip doc-publishing from 79 repos with `initial/`+`complete/`
   - [ ] Simplify CI workflows in all retained repos
   - [ ] Archive repos with no meaningful sample code
   - [ ] Remove `GH_TOKEN` / `GIT_NAME` / `GIT_EMAIL` secrets from all repos
   
   **Phase 7 -- Wind down template**
   - [ ] Replace all HTML files on `grails-guides-template` `gh-pages` with 
meta-refresh redirects to `grails.apache.org/guides/`
   - [ ] Verify all `guides.grails.org` URLs redirect correctly
   - [ ] Archive `grails-guides-template` source branches (`master`, `6.0.x`)
   - [ ] Keep `gh-pages` branch live for redirects
   - [ ] Add deprecation notice to `grails-guides` org description
   
   ---
   
   # Part 3 -- New System (After Migration)
   
   ## How Publishing Will Work After Migration
   
   After consolidation, adding or updating a guide is straightforward. There 
are no
   repos to create, no CI to configure, no secrets to set up, no scripts to 
curl.
   
   ### Adding a new guide (doc-only, no sample project)
   
   This is the simplest case -- a guide that is pure documentation with no 
runnable
   `initial/`+`complete/` sample apps.
   
   1. **Create the guide directory**:
      ```
      guides/my-new-grails8-feature/
      ├── toc.yml
      └── guide/
          ├── intro.adoc
          ├── writingTheApp.adoc
          └── summary.adoc
      ```
   
   2. **Write `toc.yml`** (table of contents):
      ```yaml
      title: My New Grails 8 Feature
      authors:
        - Your Name
      ---
      introduction: intro.adoc
      writingTheApp:
        title: Writing the Application
        guide: writingTheApp.adoc
      summary: summary.adoc
      ```
   
   3. **Write `.adoc` files** using AsciiDoc syntax. Use `include::` to pull in
      shared snippets from `guides/common/` (e.g. `common-requirements.adoc`).
   
   4. **Add an entry in `conf/guides.yml`**:
      ```yaml
      - name: my-new-grails8-feature
        title: "My New Grails 8 Feature"
        subtitle: "Learn how to use the new feature in Grails 8"
        authors: "Your Name"
        category: "Grails Apprentice"
        tags: [grails8, new-feature]
        publicationDate: "15 Feb 2026"
        grailsVersions: [8]
      ```
   
   5. **Build and preview locally**:
      ```bash
      ./gradlew buildGuides
      # Open build/dist/guides/my-new-grails8-feature/guide/index.html
      ```
   
   6. **Submit a PR** to `apache/grails-static-website`. Once merged, the guide 
is
      automatically built and deployed by `publish.yml`.
   
   That's it. One PR, one repo, automatic deployment.
   
   ### Adding a new guide with sample projects
   
   For guides that include runnable `initial/`+`complete/` Grails applications:
   
   1. **Create the guide content in this repo** (same as above -- steps 1-4).
   
   2. **Add `sampleProjectRepo`** to the guide's `conf/guides.yml` entry:
      ```yaml
      - name: my-new-grails8-guide
        title: "My New Grails 8 Guide"
        subtitle: "A hands-on guide to building with Grails 8"
        authors: "Your Name"
        category: "Grails Apprentice"
        tags: [grails8]
        publicationDate: "15 Feb 2026"
        grailsVersions: [8]
        sampleProjectRepo: "grails-guides/my-new-grails8-guide"
      ```
   
   3. **Create the sample project repo** in the `grails-guides` org:
      ```
      grails-guides/my-new-grails8-guide/
      ├── initial/          <-- starter Grails app
      ├── complete/         <-- finished solution
      ├── settings.gradle   <-- include 'complete', 'initial'
      ├── build.gradle      <-- just builds/tests the sample apps (no doc 
publishing)
      └── .github/workflows/
          └── build.yml     <-- runs ./gradlew check only
      ```
   
      The sample project repo is simple -- no `githubactions-build.sh`, no
      `apply from:` remote scripts, no doc-publishing metadata, no secrets 
needed
      for pushing to `gh-pages`. It just builds and tests the sample apps.
   
   4. **Submit two PRs**:
      - PR to `apache/grails-static-website` with the guide content + YAML entry
      - Create the sample project repo with `initial/`+`complete/`
   
   ### Updating an existing guide
   
   1. **Edit the AsciiDoc** in `guides/<guide-name>/guide/*.adoc`.
   2. **Update `conf/guides.yml`** if metadata changed (e.g. adding a new 
Grails version).
   3. **Submit a PR**. That's it.
   
   If the guide has a sample project repo in `grails-guides`, update the sample 
code
   there in a separate PR.
   
   ### Adding a new Grails version to an existing guide
   
   1. **Update `conf/guides.yml`** to add the version:
      ```yaml
      grailsVersions: [6, 7, 8]  # was [6, 7]
      ```
   
   2. **Update AsciiDoc** if there are version-specific instructions (use 
AsciiDoc
      conditionals or separate sections as needed).
   
   3. **Update the sample project repo** (if one exists) -- either update the 
existing
      `initial/`+`complete/` to the new Grails version, or add versioned 
directories
      (e.g. `complete-grails8/`) if multiple versions need distinct sample code.
   
   4. **Submit a PR**.
   
   ### How versioning simplifies with migration
   
   In the current system, multi-version guides require maintaining separate Git 
branches
   per Grails version, each with its own CI pipeline writing to `gh-pages`. 
After
   consolidation:
   
   1. **No more multi-branch complexity**: Guide version metadata lives in
      `conf/guides.yml` with a simple `grailsVersions: [4, 5, 6]` list. The 
AsciiDoc
      source is a single copy with version-conditional content if needed.
   
   2. **Publish previously unpublished versions**: The 35 `grails5` branches 
with
      distinct content and the 15 `grails-4` branches with distinct content can 
now
      be published by adding their AsciiDoc and sample code during migration. 
These
      were never published only because the CI scripts didn't handle them.
   
   3. **Explicit version support**: Instead of inferring version from branch 
names
      with inconsistent patterns (`grails4` vs `grails-4`), each guide declares
      supported versions explicitly in `conf/guides.yml`.
   
   4. **Single-pass build**: One Gradle build generates all guide HTML for all 
versions,
      instead of each version branch triggering separate CI pipelines that race 
to
      write to `gh-pages`.
   
   5. **Easy to add Grails 7/8 guides**: Adding a new guide version is a YAML 
edit,
      not a branch + CI + secrets setup.
   
   ---
   
   ## Comparison: Before vs After
   
   | | Current system | After migration |
   |---|---|---|
   | **Create a new guide** | Run `create-guide.sh`, create GitHub repo, 
manually create workflow YAML, configure 3 secrets, push | Create 
`guides/<name>/` dir + `conf/guides.yml` entry, submit PR |
   | **Where guide content lives** | Scattered across 94 repos | Single 
`guides/` directory in this repo |
   | **How it gets published** | Push to guide repo -> CI curls scripts -> 
builds AsciiDoc -> pushes to gh-pages -> curls another script -> rebuilds index 
-> pushes again | Push to this repo -> CI builds everything -> deploys |
   | **CI configuration** | Per-repo workflow YAML + 3 secrets + runtime curl 
of scripts | Single `publish.yml` in this repo |
   | **Preview changes** | Must push to trigger CI; no local preview | 
`./gradlew buildGuides` locally |
   | **Add a Grails version** | Create a branch, hope the CI handles it (it 
probably won't due to `build-guide` branch discrepancy) | Edit one line in 
`conf/guides.yml` |
   | **Time to publish** | Minutes (CI cascade across repos) | Seconds (single 
build + deploy) |
   | **Guide URL** | `guides.grails.org/<name>/guide/index.html` | 
`grails.apache.org/guides/<name>/guide/index.html` |
   | **Repos to maintain** | 94 guide repos + 1 template repo | 1 repo (this 
one) + sample project repos |
   
   ---
   
   ## Key Files Reference
   
   ### grails-static-website (this repo)
   
   | File | Purpose |
   |------|---------|
   | `buildSrc/.../GrailsWebsitePlugin.groovy` | Registers all tasks including 
`genGuides` and `buildGuides` |
   | `buildSrc/.../tasks/GuidesTask.groovy` | Generates guide index, tag, and 
category HTML pages |
   | `buildSrc/.../model/guides/GuidesFetcher.groovy` | Fetches and parses 
`guides.json` from GitHub |
   | `buildSrc/.../model/guides/GuidesPage.groovy` | Renders guide listing HTML 
(categories, tags, search) |
   | `buildSrc/.../model/guides/Guide.groovy` | Guide interface (category, 
name, title, tags, etc.) |
   | `buildSrc/.../model/guides/SingleGuide.groovy` | Guide with one Grails 
version |
   | `buildSrc/.../model/guides/GrailsVersionedGuide.groovy` | Guide spanning 
multiple Grails versions |
   | `.github/workflows/publish.yml` | CI: builds main site + guides index, 
pushes to deploy branches |
   | `publish.sh` | Runs Gradle build, clones deploy repo, copies output, 
pushes |
   
   ### grails-guides/grails-guides-template (infrastructure + deployment repo)
   
   | File | Branch | Purpose |
   |------|--------|---------|
   | `gradle/guide-build.gradle` | `master`, `6.0.x` | Shared build script 
applied by all guide repos |
   | `create-guide.sh` | `master`, `6.0.x` | Scaffolds a new guide project |
   | `src/main/docs/common-*.adoc` | `master`, `6.0.x` | ~45 reusable AsciiDoc 
snippets included by guides |
   | `src/main/resources/` | `master`, `6.0.x` | CSS, fonts, images for guide 
HTML theme |
   | `src/main/project/` | `master`, `6.0.x` | Template files copied into new 
guides (build.gradle, gradle.properties, githubactions-build.sh, docs skeleton) 
|
   | `githubactions/build-guide` | `master` | CI script: test + build + publish 
a single guide + update guides.json. **`master` version handles 
`master`+`grails3` only** |
   | `githubactions/build-guide` | `6.0.x` | CI script: same but handles 
`grails3`-`grails6` (not `master`) |
   | `githubactions/republish-guides-website.sh` | `master`, `6.0.x` | CI 
script: clone static-website, rebuild index, push to gh-pages |
   | `gh-pages` branch | -- | Deployment: built guide HTML, `guides.json` (125 
entries), and index site |
   | `README.md` | `master`, `6.0.x` | Still references Travis CI as the CI 
system |
   
   ### Individual guide repos (grails-guides org, 94 repos)
   
   | File | Purpose |
   |------|---------|
   | `.github/workflows/gradle.yml` | GitHub Actions workflow -- manually 
created, NOT from template. Often outdated (actions@v2, JDK 11). |
   | `githubactions-build.sh` | CI entrypoint. Identical in every repo. Curls 
`build-guide` and `republish-guides-website.sh` from `grails-guides-template` 
`master` at runtime. |
   | `build.gradle` | Applies `guide-build.gradle` remotely via `apply from:` 
URL (old `grails/grails-guides` URL). Some point to `master`, some to `6.0.x`. |
   | `gradle.properties` | Guide metadata (title, authors, tags, category, 
publicationDate, githubSlug, githubBranch) |
   | `settings.gradle` | Includes `complete` and `initial` subprojects |
   | `src/main/docs/guide/toc.yml` | Table of contents for the guide |
   | `src/main/docs/guide/*.adoc` | AsciiDoc source files |
   | `src/main/resources/img/` | Guide-specific images |
   | `initial/` | Starter Grails app for the reader to begin with (79 repos) |
   | `complete/` | Finished Grails app -- the solution (93 repos) |
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to