mono-repo-integration
SkillDev toolsGuides your agent through merging a standalone Grails plugin repo into the grails-core monorepo build step by step.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the mono-repo-integration skill
About this capability
Grails - the Web Application Framework
What this skill tells your AI
The instructions your AI receives, as published by apache/grails-core in .agents/skills/mono-repo-integration/SKILL.md and read by ahel’s review.
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-<name>/) 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:
git log --oneline --grep "Spring Security Merge"
git show <sha> # inspect any individual step
Guiding Principles (NON-NEGOTIABLE)
- 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 mustapply from:the monorepo's existing rootgradle/*.gradlefiles instead. - No hard-coded dependency versions. The monorepo uses the Grails BOM (
grails-bom). Drop version numbers from the importedbuild.gradlefiles and fromgradle.properties; rely onplatform("org.apache.grails:grails-bom:$grailsVersion"). Only add a version todependencies.gradle(and reference it) if the dependency is genuinely not already managed by a BOM. Check first:grep -i '<artifact>' dependencies.gradle grails-bom/*/build/*-constraints.adoc. - Publish the same way. Add each published module to
publishedProjectsingradle/publish-root-config.gradle, and have each moduleapply from: '<root>/gradle/publish-config.gradle'(the monorepo's, not the imported one). Do not keep a per-repo publish-config. - 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(...), andemeritus(...)— and match on the person, not just the handle (e.g.christianoestreichmay already be present asctoestreich;ldaleyasalkemist;graemerocheris afounder;sbglasius/matreiare activedevelopers;burtbeckwith/puneetbehl/pledbrook/marcpalmer/jeffscottbrownareemeritus). 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="<name>" --format=%ad --date=short -1). If they have not contributed recently, add them asemeritus(...), notcontributor(...). Most authors from a long-dormant imported plugin will be emeritus. Only usecontributor(...)for genuinely active contributors.
- Dedupe against ALL author lists in
- Functional/example apps live under
grails-test-examples/. Move the imported repo'sexamples/*and*-test-appprojects out of the plugin folder intograils-test-examples/<name>/...and wire them throughgradle/functional-test-config.gradle. - 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. - 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. - 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.gradleattribute map). - Follow
CLAUDE.mdrules throughout:jakarta.*notjavax.*, Apache license header on every new file, 4-space indent, no wildcard imports, tests via public APIs. - Inter-module dependency syntax differs by project kind. The plugin/library modules themselves depend on sibling monorepo modules via
project(':grails-...'). Thegrails-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 importedproject(...)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 <Name> Merge - <step>.
Phase 0 — Import the repository (preserving history)
Bring the standalone repo in under a top-level grails-<name>/ 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-<name> <url> && git fetch grails-<name>), then:
# <ref> is the imported repo's release branch, e.g. grails-redis/5.0.x
git merge -s ours --no-commit --allow-unrelated-histories grails-<name>/<ref>
git read-tree --prefix=grails-<name>/ -u grails-<name>/<ref>
git commit -m "Initial import of Grails <Name> Repository"
This produces the single Initial import of Grails <Name> Repository commit (the starting point the rest of this skill restructures). Example actually used for redis:
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-<name> -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/,publishedProjectsingradle/publish-root-config.gradle, thecontributor(...)block inPublishPlugin.groovy, the guide layout undergrails-doc/src/en/guide/, and the CI test-filter flags inDEVELOPMENT.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 genericLICENSE/NOTICEautomatically, 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-levelNOTICE/LICENSEandlicenses/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.gitignorebefore deleting.- RAT config (the repo's
gradle/rat-*.gradle) — almost always lists files that must be excluded forratlicense validation to pass (templates, generated files, third-party-licensed assets shipped with the plugin). Port every still-relevant exclude into the rootgradle/rat-root-config.gradle(paths rewritten to the newgrails-<name>/...location). Missing these causes./gradlew ratto 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') != '<config>' }), 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 (aredis/postgresthe 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 intobuild-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, rootbuild.gradle - the repo's own
gradle/*.gradleconvention 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<Name>/-Pskip<Name>Tests) 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-<name>/plugin/{grails-app,src,build.gradle}tograils-<name>/so the plugin project's dir is simplygrails-<name>/(noprojectDirmapping needed, since the dir name matches the project name). Multi-module repos (like spring-security) keep nestedplugin/docsfolders. - Relocates example/functional apps to
grails-test-examples/<name>/...(e.g.grails-<name>/examples/<app>→grails-test-examples/<name>/<app>), moved verbatim. If the repo has only a single functional app, flatten it directly intograils-test-examples/<name>/(drop the redundant per-app subfolder) and name the projectgrails-test-examples-<name>. Each deployable module gets a clean folder; nested project dirs are mapped explicitly viaprojectDirinsettings.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 theinclude(...)list with agrails-<name>-...project name, then setproject(':grails-<name>-...').projectDir = new File(settingsDir, 'grails-<name>/<path>'). Add functional/example apps asgrails-test-examples-<name>-...mapped intograils-test-examples/<name>/....- Each module
build.gradle: keep theplugins { ... }block and dependencies, but (a) strip versions in favor of the BOM, (b) replace theapply { from ... }block to point at the rootgradle/*.gradlefiles. Declare all Gradle plugins in theplugins { }block (the composite build resolves the project'sorg.apache.grails.*convention plugins there) rather than the legacyapply plugin: '...'form — match the modern test projects (e.g.grails-test-examples/scaffolding). Note:apply from: '<script>.gradle'for applying gradle script snippets is a separate, still-standard mechanism — only plugin application moves intoplugins { }. (test-config.gradle,publish-config.gradle,docs-config.gradle,java-config.gradle,reproducible-config.gradle, etc.). Delete the module's reference to any deleted per-repo gradle file. For dependencies on other monorepo modules, useproject(':grails-...')syntax (not Maven coordinates) — these are the published Gradle projects building alongside this one (see Principle 10). - Coordinates change to the Apache namespace. The imported plugin publishes under its old group (e.g.
org.grails.plugins:grails-<name>); under the ASF it becomesorg.apache.grails:grails-<name>. In the pluginbuild.gradle, setgroup = 'org.apache.grails'(and drop any standalonegrailsPublish { ... }block — the monorepo'sbuildsrc.publishconvention plugin supplies the POM metadata). Register the rename in BOTH places that track it: add a row toRENAME.md(the documented old→new mapping table) and a per-repo<name>_mappingsblock toetc/bin/rename_gradle_artifacts.sh(mirroring theredis_mappingsblock), so the coordinate migration is recorded and the sed-based rewrite script stays complete. Update install/usage snippets in the migrated docs to the new coordinates too. dependencies.gradle: add only the genuinely-unmanaged dependency coordinates + versions (alphabetical, in bothbomDependencyVersionsandbomDependencies).gradle.properties: add only non-BOM-managed version props; match the existing<name>Versionnaming style.gradle/publish-root-config.gradle: add every published module topublishedProjects(alphabetical).build-logic/plugins/.../PublishPlugin.groovy: merge in the imported developers (alphabetical, deduped against both lists). Per Principle 4, check each author's commit recency and add inactive ones asemeritus('<id>', '<name>', project)rather thancontributor(...).- Test filter flags — add
only<Name>Tests/skip<Name>Testsconsistently across:gradle/test-config.gradle,gradle/functional-test-config.gradle,gradle/grails-data-tck-config.gradle,build-logic/docs-core/build.gradle, and document both inDEVELOPMENT.md. (Spring Security addedonlySpringSecurityTests/skipSpringSecurityTests.) Only introduce a dedicated<name>-test-config.gradleif the plugin truly needs bespoke test wiring (Spring Security did for Geb/integration); prefer reusing the shared one. gradle/rat-root-config.gradle: add RAT excludes for template files, generated files, and third-party-licensed assets the module ships..github/workflows/gradle.yml: add a CI job (or extend an existing matrix) that runs the newonly<Name>Testsslice. Reproduce every test matrix / config variant the imported CI ran (the axes you recorded in Phase 1) — do not collapse a multi-config matrix into one run. If a matrix variant only affects a single example app (e.g. the Spring SecurityTESTCONFIGvariants only change the core functional-test-app), give it its own dedicated matrix job scoped to that project (./gradlew :grails-test-examples-...-functional-test-app:check -DTESTCONFIG=${{ matrix.test-config }} -PgebAtCheckWaiting) rather than re-running the wholeonly<Name>Testsslice per variant — this matches how the standalone repo split itscoreTestsandfunctionalTestsjobs, and avoids re-running unrelated apps N times. For tests that need an external service (DB, cache, broker), prefer Testcontainers driven by a SpockIGlobalExtensionover a GitHub Actionsservices:container — this is the repo's established convention (see the mongodb example apps'SpringBootStart*Extension/*ContainerVersionpattern; the monorepo has noservices:blocks). Check the BOM first — the container module is often already managed (e.g.com.redis:testcontainers-redis,org.testcontainers:*). The extension starts the container, reads the image version from a-P<name>ContainerVersionsystem property (wired through the test-config), and injects host/port into the spec (the imported tests usually already read host/port from env/config). Run the CI job with-Ponly<Name>Tests -P<name>ContainerVersion=<v>across a version matrix. Also pass-Pskip<Name>Testson the plain./gradlew build(-with-tests) jobs so the service-dependent tests don't run where no container is provisioned.- Gate publishing on the new test jobs. Every new test job you add MUST be added to the
publishjob'sneeds:list and itsif:result guard ((needs.<job>.result == 'success' || needs.<job>.result == 'skipped')) ingradle.yml. Otherwise the snapshot publish runs even when the plugin's tests fail — the tests exist but don't protect anything. After adding a job, grep thepublish(and any other publishing) job'sneeds/ifand confirm your new job id is present in both.
Phase 3 — Migrate documentation into the guide
- The guide is driven by
grails-doc/src/en/guide/toc.yml, NOT byindex.adoc'sinclude::directives.DocPublisherbuilds the guide fromtoc.yml(it throws "Legacy TOC is no longer supported" if absent). You MUST add your chapter there or it will not render — editingindex.adocalone does nothing. Format: a top-levelkey:is a chapter mapped to<key>.adoc;title:sets the heading; childkey: Titleentries map to<chapter>/<key>.adoc. Section keys must be globally UNIQUE across the entire guide (the publisher errors with "Duplicate section name" otherwise) and the key doubles as the filename — follow thespringSecurityCoreconvention (fully-qualified unique keys likeredisInstallation, not genericinstallationwhich collides with other chapters). Internal headings inside a section file start at====(level 4), matching existing files likeservices.adoc. (Keepindex.adocin sync too if the repo maintains it, buttoc.ymlis what the build reads.) - A guide section is mandatory — always add one. If the imported repo had no dedicated documentation (no
docs/, no.adocguide), the docs are typically living in the project'sREADME.md. In that case convert theREADME.mdinto an appropriate guide section rather than skipping documentation. Strip README-only boilerplate (build/CI badges, "Building"/"Publishing to mavenLocal" dev instructions, release history) and keep the user-facing usage content. - Create
grails-doc/src/en/guide/<area>/<name>/...adoc. For plugins with a security flavor this isgrails-doc/src/en/guide/security/securityPlugins/...; otherwise pick the matching guide area (or a new top-level area for the plugin). Convert Markdown READMEs to AsciiDoc. - Wire the new pages into the guide TOC (
grails-doc/src/en/guide/index.adocand the relevant section.adoc). - If the docs pull in source snippets or example output, register the source dirs in
grails-doc/build.gradle(sourcedir/functionalSourceDir-style attribute map) and point them at the relocatedgrails-test-examples/<name>/...apps. - Drop release history / authors (Principle 7) and remove hard-coded legacy-docs URLs (Principle 8). Where a link to the old independently-published docs must remain (historical versions), route it through a single attribute in
grails-doc/build.gradlerather than hard-coding it per page. - Preserve "helpful" links — READMEs commonly end with a list of useful external references (upstream project docs, command references, related libraries, blog posts/presentations). Don't discard these: add a dedicated section to the plugin's guide documentation (e.g. a "Reference" / "Useful Links" / "Further Reading" page or trailing section) to hold them, rather than dropping them with the README.
- Use BOM syntax in install/usage snippets (no hard-coded plugin/dependency versions) (Principle 2).
Phase 4 — Wire up the relocated functional/example apps
The apps were physically moved to grails-test-examples/<name>/... back in Initial Moves; this phase is the content edits on them:
- Update each app's
build.gradleto depend on the plugin by Maven coordinates (e.g.implementation 'org.apache.grails:grails-<name>'), notproject(':grails-<name>')—grails-test-examplesapps consume modules as published artifacts, the opposite convention from the plugin modules themselves (see Principle 10). Use the BOM, andapply from:the rootgradle/functional-test-config.gradle/gradle/test-config.gradle(or the dedicated<name>-test-config.gradle). - Confirm their
settings.gradlemappings (Phase 2) and thatfunctional-test-config.gradlerecognizes thegrails-test-examples-<name>-*prefix for the test-filter flags.
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 3k
- Forks
- 975
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
mono-repo-integration- Source
- github.com/apache/grails-core