--- name: mono-repo-integration description: Step-by-step process for merging a previously-standalone Grails plugin repository (e.g. grails-spring-security, grails-redis) into the grails-core monorepo as one or more Gradle subprojects, wiring it into the shared build, publishing, docs, and CI the same way the existing modules are. license: Apache-2.0 compatibility: opencode, claude, grok, gemini, copilot, cursor, windsurf metadata: audience: maintainers frameworks: grails versions: 7 --- ## What I Do - Merge a standalone Grails plugin repo (its source was copied into a top-level folder such as `grails-/`) into the grails-core monorepo build. - Strip the imported repo's standalone build/release/CI infrastructure and rewire its modules onto the monorepo's **shared** Gradle config, publishing, docs guide, and CI. This skill is the generalization of the **Spring Security merge** (git commits prefixed `Spring Security Merge - ...`, starting at `6d06f6c84f` / `fd1939a7e2`). When in doubt, read those commits — they are the canonical worked example: ```bash git log --oneline --grep "Spring Security Merge" git show # inspect any individual step ``` ## Guiding Principles (NON-NEGOTIABLE) 1. **No custom/duplicated gradle files.** The imported repo ships its own `gradle/*.gradle` (test-config, publish-config, java-config, docs-config, reproducible-config, rat-root-config, examples-config, etc.). **Delete them all.** Every module must `apply from:` the monorepo's existing root `gradle/*.gradle` files instead. 2. **No hard-coded dependency versions.** The monorepo uses the Grails BOM (`grails-bom`). Drop version numbers from the imported `build.gradle` files and from `gradle.properties`; rely on `platform("org.apache.grails:grails-bom:$grailsVersion")`. Only add a version to `dependencies.gradle` (and reference it) if the dependency is genuinely not already managed by a BOM. Check first: `grep -i '' dependencies.gradle grails-bom/*/build/*-constraints.adoc`. 3. **Publish the same way.** Add each published module to `publishedProjects` in `gradle/publish-root-config.gradle`, and have each module `apply from: '/gradle/publish-config.gradle'` (the monorepo's, not the imported one). Do not keep a per-repo publish-config. 4. **Integrate authors into the publish plugin.** Merge the imported repo's developer list into `build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/PublishPlugin.groovy`, keeping each list alphabetized by handle. Two rules: - **Dedupe against ALL author lists** in `PublishPlugin.groovy` — `founder(...)`, `developer(...)`, `contributor(...)`, and `emeritus(...)` — and match on the person, not just the handle (e.g. `christianoestreich` may already be present as `ctoestreich`; `ldaley` as `alkemist`; `graemerocher` is a `founder`; `sbglasius`/`matrei` are active `developer`s; `burtbeckwith`/`puneetbehl`/`pledbrook`/`marcpalmer`/`jeffscottbrown` are `emeritus`). Never add someone already in any list under any handle. - **Classify by recency, defaulting to `emeritus`.** Check each author's most recent commit (`git log --all --author="" --format=%ad --date=short -1`). If they have not contributed recently, add them as `emeritus(...)`, not `contributor(...)`. Most authors from a long-dormant imported plugin will be emeritus. Only use `contributor(...)` for genuinely active contributors. 5. **Functional/example apps live under `grails-test-examples/`.** Move the imported repo's `examples/*` and `*-test-app` projects out of the plugin folder into `grails-test-examples//...` and wire them through `gradle/functional-test-config.gradle`. 6. **Docs go into the guide.** Migrate documentation into `grails-doc/src/en/guide/...` as AsciiDoc. There is no standalone docs subproject. Markdown READMEs must be converted to `.adoc`. 7. **Drop historical release history and author/changelog sections from docs.** The monorepo guide does not store per-plugin release history, "previous work", or author lists. Remove `history.adoc`, `authors.adoc`, `previouswork.adoc`, changelog tables, and README "release history" sections. 8. **Remove hard-coded URLs to the old project docs.** Replace absolute links to the legacy standalone documentation site with guide-relative cross-references or BOM/attribute-driven links (see `grails-doc/build.gradle` attribute map). 9. Follow `CLAUDE.md` rules throughout: `jakarta.*` not `javax.*`, Apache license header on every new file, 4-space indent, no wildcard imports, tests via public APIs. 10. **Inter-module dependency syntax differs by project kind.** The plugin/library modules themselves depend on sibling monorepo modules via **`project(':grails-...')`**. The `grails-test-examples/` apps depend on those same modules via **Maven coordinates** (`'org.apache.grails:grails-...'`), consuming them as published artifacts. Do not mix these up — convert the imported `project(...)` references in example/test apps to coordinates during Phase 4. ## Phased Process Mirror the Spring Security commit sequence. Make one focused commit per phase, messaged ` Merge - `. ### Phase 0 — Import the repository (preserving history) Bring the standalone repo in under a top-level `grails-/` prefix using a subtree-style merge, so the imported commit history is preserved (joined via `-s ours`) while the working tree is populated from `read-tree`. Add the source repo as a remote first (`git remote add grails- && git fetch grails-`), then: ```bash # is the imported repo's release branch, e.g. grails-redis/5.0.x git merge -s ours --no-commit --allow-unrelated-histories grails-/ git read-tree --prefix=grails-/ -u grails-/ git commit -m "Initial import of Grails Repository" ``` This produces the single `Initial import of Grails Repository` commit (the starting point the rest of this skill restructures). Example actually used for redis: ```bash git merge -s ours --no-commit --allow-unrelated-histories grails-redis/5.0.x git read-tree --prefix=grails-redis/ -u grails-redis/5.0.x git commit -m "Initial import of Grails Redis Repository" ``` ### Phase 0.5 — Survey - `git log --oneline | grep -i "Initial import"` to find the import commit. - Map the imported tree: `find grails- -type d`. Identify: plugin module(s), example/test apps, docs (adoc or README), the developer list (`gradle/publish-config.gradle` → `it.developers`), and all standalone infra. - List what the monorepo already provides so you reuse it: `ls gradle/`, `publishedProjects` in `gradle/publish-root-config.gradle`, the `contributor(...)` block in `PublishPlugin.groovy`, the guide layout under `grails-doc/src/en/guide/`, and the CI test-filter flags in `DEVELOPMENT.md`. ### Phase 1 — Initial Moves (examine infra, then port-or-delete + restructure) **Do not blindly delete.** Most of the imported repo's standalone infrastructure is removed because the monorepo already provides it — but several files carry repo-specific customizations that must be *carried over* into the monorepo's equivalents first. Examine each, decide port-or-delete, then delete the standalone copy. `git diff` the imported file against the monorepo's equivalent to see exactly what is custom. **Examine and port (customizations must survive):** - **`NOTICE` / `LICENSE`** — first determine whether they are *standard* (boilerplate Apache header/notice) or *customized*. If standard, just delete them: the monorepo's shared gradle plugins (applied to every module) generate/import the generic `LICENSE`/`NOTICE` automatically, so no carry-over is needed. Only when they are customized (bundled third-party components, extra attribution clauses) do you diff against the monorepo's top-level `NOTICE`/`LICENSE` and `licenses/` and merge the repo-specific additions in before deleting the imported copies. - **`.gitignore`** — may contain custom excludes (generated dirs, plugin-specific artifacts). Fold any non-duplicate entries into the monorepo's root `.gitignore` before deleting. - **RAT config** (the repo's `gradle/rat-*.gradle`) — almost always lists files that must be excluded for `rat` license validation to pass (templates, generated files, third-party-licensed assets shipped with the plugin). **Port every still-relevant exclude** into the root `gradle/rat-root-config.gradle` (paths rewritten to the new `grails-/...` location). Missing these causes `./gradlew rat` to fail later. (Cross-reference Phase 2.) - **`gradle.properties`** — versions should generally match the monorepo, but watch for third-party libraries pinned here that *should* be BOM-managed: those must be imported into the BOM (`dependencies.gradle` / `grails-bom`) rather than carried as loose properties. Carry over only genuinely non-BOM-managed props (Phase 2). - **`.github/workflows/`** — **never blindly delete these.** The monorepo has its own CI, but the imported workflows almost always encode test coverage that must be reproduced exactly. Before deleting, enumerate everything each job does and confirm the monorepo job you add in Phase 2 covers the **same level of testing** — not a single reduced run. In particular capture: (a) **test matrices / config variants** — e.g. grails-spring-security ran its functional tests across a 9-value `-DTESTCONFIG=` matrix (`static`, `annotation`, `requestmap`, `basic`, `basicCacheUsers`, `misc`, `putWithParams`, `bcrypt`, `issue503`); the specs are gated with `@IgnoreIf({ System.getProperty('TESTCONFIG') != '' })`, so a single run silently skips 8/9 configs and looks green while covering almost nothing. Every matrix axis (config, JVM version, container version, DB flavor) must be reproduced. (b) required **service containers** (a `redis`/`postgres` the functional tests need — prefer Testcontainers per Phase 2). (c) extra validations / dependency-setup steps. Write down each axis and value here, then reproduce them in the Phase 2 job. When in doubt, diff the imported job step-by-step against the monorepo job and account for every flag. - **`buildSrc/`** — usually removable, but inspect for custom tasks/plugins/conventions the build actually depends on; integrate any such behavior into `build-logic/` or the root gradle config before deleting. **Examine briefly, then typically delete:** - **`etc/`** — typically build-verification/release scripts (reproducible-build checks, artifact verification). Usually safe to drop, but do a short scan to confirm nothing the monorepo lacks is referenced by the build. - **`.asf.yaml`, `.sdkmanrc`, `CODE_OF_CONDUCT.md`, `HEADER`, `ISSUE_TEMPLATE.md`** — standalone-repo metadata superseded by the monorepo's; delete. **Delete outright (always superseded by the monorepo):** - `gradlew`, `gradlew.bat`, `gradle/wrapper/`, `gradle-bootstrap/` - repo-root `settings.gradle`, root `build.gradle` - the repo's own `gradle/*.gradle` convention files (test-config, publish-config, java-config, docs-config, reproducible-config, examples-config, and the now-ported rat config) - `README.md` — its content is migrated to the guide in Phase 3, then deleted. **Test-skip property note:** the monorepo's `grails-core` CI workflows pass a skip flag (e.g. `-Pskip`/`-PskipTests`) to exclude this plugin's functional tests from the default runs, and a **separate dedicated workflow** runs them (with any required service containers). Note here what the imported CI needed; wire the flag + dedicated job in Phase 2. **Restructure** directories to the monorepo convention. **Initial Moves is pure deletion + relocation — do NOT rewrite file contents here.** Keeping moves and content edits in separate commits means git records relocations as renames, so every later phase's content change diffs cleanly against the moved file instead of appearing as a delete+add. Concretely, the Initial Moves commit: - **Collapses a single-plugin repo's source up to the repo root** — move `grails-/plugin/{grails-app,src,build.gradle}` to `grails-/` so the plugin project's dir is simply `grails-/` (no `projectDir` mapping needed, since the dir name matches the project name). Multi-module repos (like spring-security) keep nested `plugin`/`docs` folders. - **Relocates example/functional apps to `grails-test-examples//...`** (e.g. `grails-/examples/` → `grails-test-examples//`), moved verbatim. If the repo has only a **single** functional app, flatten it directly into `grails-test-examples//` (drop the redundant per-app subfolder) and name the project `grails-test-examples-`. Each deployable module gets a clean folder; nested project dirs are mapped explicitly via `projectDir` in `settings.gradle`, so directory names can differ from project names. The content edits to these moved files (build-script rewrites, dependency-by-coordinate conversion, applying root gradle config) happen in the *later* phases and will show as clean diffs. ### Phase 2 — Integrate the build Edit, in this order: - **`settings.gradle`** (root): add each module to the `include(...)` list with a `grails--...` project name, then set `project(':grails--...').projectDir = new File(settingsDir, 'grails-/')`. Add functional/example apps as `grails-test-examples--...` mapped into `grails-test-examples//...`. - **Each module `build.gradle`**: keep the `plugins { ... }` block and dependencies, but (a) strip versions in favor of the BOM, (b) replace the `apply { from ... }` block to point at the **root** `gradle/*.gradle` files. **Declare all Gradle plugins in the `plugins { }` block** (the composite build resolves the project's `org.apache.grails.*` convention plugins there) rather than the legacy `apply plugin: '...'` form — match the modern test projects (e.g. `grails-test-examples/scaffolding`). Note: `apply from: '