tiempo de lectura : 11 minutes

Estás clonando un repositorio de plugins de Gradle. Abres una terminal. ¿Qué escribes? ¿Entonces?./gradlew tasks, claro. ¿En qué carpeta? ¿La raíz? ¿El submódulo? ¿Hay que leer primero un README para saber cómo construir? Si dudan ni siquiera solo un segundo, la arquitectura del proyecto está rota

Así es como resolví este problema una vez por todas — y por qué esto pattern es hoy la firma de todos mis plugins Gradle en`foundry/public/`.

El Problema: Tres Arquitecturas Que Me Hicieron Perder el Tiempo

Antes de converger hacia el patrón actual, he vacilado entre tres enfoques clásicos para organizar un plugin Gradle. Cada uno tenía un defecto imperdonable.

Opción 1: El Plugin aislado (sin dogfooding)

El proyecto solo contiene el plugin. No hay ejemplo de consumo. No proyecto que lo ejerce. Para probarlo, hay que crear un proyecto externo, y referenciar el plugin mediante`mavenLocal`o un build compuesto, y solo allí verificar que funciona.

$ git clone mon-plugin
$ cd mon-plugin
$ ./gradlew build          # le plugin compile
$ # ... et maintenant ? comment je l'essaie ?

Un plugin Gradle sin ejemplo de consumo, es una biblioteca sin pruebas de integración. Nunca sabes si la última modificación ha roto la experiencia del usuario.

Opción 2: El Monorepo Clásico (include(":plugin"))

Gradle`init`genera`settings.gradle.kts`con`include("plugin")`. El root y el submódulo comparten el mismo daemon, las mismas configuraciones, los mismos catálogos. práctico, pero acoplado.

.
├── settings.gradle.kts   → include("plugin")
├── build.gradle.kts      → plugins { id("mon-plugin") }
├── plugin/
│   └── build.gradle.kts  → java-gradle-plugin
└── gradle/
    └── libs.versions.toml

Lo que se atasca:

  • La raízdebetener la misma versión de Gradle que el submódulo.

  • `libs.versions.toml`es compartido — versión catalogs comunes, dependencias

que huyen de un módulo a otro

  • Imposible compilar el plugin independientemente del root.

  • La CI debe compilar los dos módulos, aunque solo el plug‑in haya cambiado.

Opción 3 : el Build compuesto (includeBuild())

Separamos los dos en builds Gradle distintos y los relacionamos vía includeBuild("mon-plugin")`en`settings.gradle.kts.

Ya está mejor. La compilación del plugin está aislada. Pero el consumidor debe explícitamente referenciar la compilación externa — y la salida de `./gradlew tasks`en la raíz depende de la buena configuración del compuesto. El clonaje no es zero-config: hay que saber que el plugin está en una carpeta aparte, que el root lo referencia, etc.

.
├── settings.gradle.kts   → includeBuild("plugin-build/")
├── build.gradle.kts      → plugins { id("mon-plugin") }
└── plugin-build/
    ├── settings.gradle.kts
    └── build.gradle.kts

Buscaba algo mejor. Mucho mejor.

La Solución: Dos Builds independientes, Una raíz consumidora

Aquí está el patrón que acabé adoptando :

.
├── settings.gradle.kts          ← racine consommateur
├── build.gradle.kts              ← 3 lignes : apply plugin + dogfood
├── gradle/
│   └── libs.versions.toml        ← catalogue du consommateur
├── {name}-plugin/                ← BUILD INDÉPENDANT
│   ├── gradlew                    ← son propre wrapper
│   ├── settings.gradle.kts        ← rootProject.name = "{name}-plugin"
│   ├── build.gradle.kts           ← java-gradle-plugin, signing, publish
│   ├── gradle/
│   │   ├── libs.versions.toml     ← catalogue du plugin
│   │   └── wrapper/
│   ├── src/                       ← sources du plugin
│   ├── .agents/                   ← gouvernance agent
│   └── *.adoc                     ← AGENT, PROMPT_REPRISE, snapshot, etc.
└── site.yml / slides-context.yml / ...   ← configs dogfood

La clave:`{name}-plugin/`es un proyecto Gradle completo y autónomo. Tiene su propio envoltorio, su propio ajustes, su propio catálogo de versiones. Se clona, se construye, se prueba, y se publica sin necesidad de root estar al tanto de su existencia.

El root, él, solo hace una cosa: aplicar el plugin.

plugins {
    alias(libs.plugins.bakery)
}

repositories {
    mavenLocal()
    mavenCentral()
}

bakery { configPath = file("site.yml").absolutePath }

Tres líneas en el caso de`bakery-gradle`. Nada más. Cero`include(), cero`includeBuild(), cero subproyecto. Un build Gradle clásico que aplica un plugin como cualquier ¿qué consumidor lo haría.

El flujo de trabajo

Failed to generate image: PlantUML preprocessing failed: [From <input> (line 32) ]

@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultFontSize 12

left to right direction

package "RAÍZ (Consumidor)" #CCFFCC {
    usecase "Clone el repositorio" as Clone
    usecase "./gradlew tareas" as Tasks
    usecase "./gradlew bake" as Dogfood
    note bottom of Dogfood
        Exerce le plugin
        Feedback immédiat
        Zéro config
    end note
}

package "{name}-plugin/ (Compilación Independiente)" #CCE5FF {
    usecase "./gradlew build" as BuildPlugin
    usecase "./gradlew publishToMavenLocal" as MavenLocal
    usecase "./gradlew test" as TestPlugin
    note bottom of BuildPlugin
        Cycle de vie isolé
        Tests unitaires + Cucumber
        CI dédiée possible
    end note
}

Clone -down-> Tasks
Tasks -down-> Dogfood
Dogfood ..> MavenLocal : "depende del plugin
publicado localmente"
^^^^^
 Syntax Error? (Assumed diagram type: component)

@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultFontSize 12

left to right direction

package "RAÍZ (Consumidor)" #CCFFCC {
    usecase "Clone el repositorio" as Clone
    usecase "./gradlew tareas" as Tasks
    usecase "./gradlew bake" as Dogfood
    note bottom of Dogfood
        Exerce le plugin
        Feedback immédiat
        Zéro config
    end note
}

package "{name}-plugin/ (Compilación Independiente)" #CCE5FF {
    usecase "./gradlew build" as BuildPlugin
    usecase "./gradlew publishToMavenLocal" as MavenLocal
    usecase "./gradlew test" as TestPlugin
    note bottom of BuildPlugin
        Cycle de vie isolé
        Tests unitaires + Cucumber
        CI dédiée possible
    end note
}

Clone -down-> Tasks
Tasks -down-> Dogfood
Dogfood ..> MavenLocal : "depende del plugin
publicado localmente"
BuildPlugin -up-> MavenLocal
TestPlugin -up-> BuildPlugin

@enduml

El Workflow Concreto

# 1. Builder le plugin
$ cd codebase-plugin
$ ./gradlew publishToMavenLocal

# 2. L'exercer depuis la racine
$ cd ..
$ ./gradlew indexCodebase queryCodebase snapshot

# La boucle est fermée. Le plugin est testé dans des conditions
# réelles de consommation, par le projet même qui l'héberge.

¿Por qué esta arquitectura me ha conquistado?

Tres beneficios que, combinados, valen el costo de la «duplicación» :

Dogfooding nativo, retroalimentación instantánea

La mejor prueba de un plugin Gradle es usarlo. No es una prueba unitaria simulada. No es un`GradleRunner`con un proyecto de prueba. Un verdadero build que aplica el plugin sobre archivos reales.

$ git clone bakery-gradle
$ cd bakery-gradle
$ ./gradlew bake    # ← le plugin est exercé immédiatement

Si el plugin está roto, el build raíz lo dice. No es necesario ir buscar un proyecto de prueba externo. El dogfooding es la primera tarea que lanza un nuevo contribuidor. Es la prueba de humo definitiva.

La regla es simple: si la raíz compila y`./gradlew tasks` Muestra tus tareas del plugin, el plugin es funcional. Sin sorpresas en producción

2. Clonación Zero-Config

Un `git clone && ./gradlew tasks`y el recién llegado ve todo caminar sin configurar nada. El`build.gradle.kts`raíz es la documentación viva de uso del plugin. El`site.yml`al lado muestra la configuración esperada.

Compárese con la alternativa: un README de tres párrafos que explica cómo crear el plugin y cómo crear el proyecto de test. Un nuevo colaborador lee el README en diagonal, se equivoca, abre un problema — aunque la información podría ser ejecutable

La documentación más robusta no es la que se lee. Es esa que unoejecuta. El build raíz es la documentación ejecutable del plugin.

3. Builds aislados, CI independientes

El plugin tiene su propio wrapper Gradle, su propio ciclo de vida, sus propias pruebas. Puede:

  • Actualizar Gradle en el plugin sin tocar la raíz

  • Añadir una dependencia en el plugin sin que se filtre en el root

  • Romper el plugin sin afectar al build raíz (mientras usted no

No publiquéis la versión rota).

  • Tener una CI que build/test el plugin, y otra que ejerce el

root — independientemente

.github/workflows/
├── test-plugin.yml      → codebase-plugin/.gradlew build
├── test-root.yml        → .gradlew tasks (vérifie que le plugin est consommable)
└── publish.yml          → codebase-plugin/.gradlew publish

La anatomía del subdirectorio `{name}-plugin/

Observemos de más cerca lo que vive en la carpeta del plugin:

codebase-plugin/
├── gradlew                           ← wrapper indépendant
├── settings.gradle.kts               ← @Suppress("UnstableApiUsage")
│                                       + foobar-resolver-convention
├── build.gradle.kts                  ← java-gradle-plugin + signing + publish
├── gradle/
│   ├── libs.versions.toml            ← catalogue complet (langchain4j, pgvector...)
│   └── wrapper/
├── buildSrc/                         ← classes utilitaires buildSrc
│   ├── build.gradle.kts
│   └── src/main/kotlin/
│       ├── codebase/                 ← CodebaseYmlAnonymizer, CodebaseConfiguration
│       ├── benchmark/                ← BenchmarkConfig, BenchmarkProtocol
│       ├── readme/                   ← ReadmeYmlAnonymizer
│       ├── site/                     ← SiteYmlAnonymizer
│       ├── slider/                   ← SliderYmlAnonymizer
│       └── snapshot/                 ← SnapshotManager
├── src/
│   ├── main/kotlin/codebase/
│   │   ├── CodebasePlugin.kt         ← class Plugin<Project>
│   │   ├── rag/                      ← pgvector, embedding, anonymization...
│   │   ├── benchmark/                ← BenchmarkRunner, export, comparison...
│   │   └── walker/                   ← WorkspaceWalker
│   └── test/
│       ├── kotlin/codebase/scenarios/ ← steps Cucumber
│       ├── features/                  ← .feature files
│       └── resources/datasets/        ← fixtures .adoc, .yml, .json
├── .agents/                          ← gouvernance agent (INDEX, SESSIONS, etc.)
├── AGENT.adoc                        ← règles agent
├── PROMPT_REPRISE.adoc              ← mission session
├── BACKLOG.adoc                      ← backlog produit
├── snapshot.adoc                     ← snapshot auto-généré du projet
└── embeds.yml                        ← config RAG embeds

Todo está ahí. No hay dispersión entre la raíz y el subdirectorio. El desarrollador que trabaja en el plugin nunca necesita salir`codebase-plugin/`. El desarrollador que usa el plugin solo mira la raíz — y el`build.gradle.kts`de 3 líneas le dice todo lo que necesita saber

El Catálogo de Versiones: Dos Archivos Distintos

gradle/libs.versions.toml(raíz)

{name}-plugin/gradle/libs.versions.toml

Número de dependencias

2-3 (plugin + readme opcionalmente)

más de 30 (langchain4j, pgvector, cucumber…​)

rol

Consumir el plugin

Construye el plugin

¿Quién lo lee?

El usuario del plugin

El desarrollador del plugin

El root tiene un catálogo voluntariamente minimal. El plugin tiene un catálogo completo. La confusión es imposible: cada build tiene su propio scope de dependencias

Si ya has pasado una hora depurando una colisión de dependencias entre tu plugin y tu proyecto de prueba, comprendes el valor de esta separación. Los catálogos independientes eliminan este problema por construcción.

Lo Que No Se Hace

Este patrón no es mágico. Impone una restricción que yo me inflige de buena gana :

root no construye el plugin. Debes`publishToMavenLocal` ou desplegar en un repositorio antes de que root pueda consumirlo.

Es un costo mínimo, y es la buena restricción. El root consume el plugin como un cliente externo — vía Maven. Exactamente como lo haría un proyecto de terceros. Si el plugin no es publicable, el root te lo dice de inmediato.

# La seule "friction" du pattern
$ cd codebase-plugin && ./gradlew publishToMavenLocal && cd ..
$ ./gradlew tasks --group=codebase

Comparación : Las Tres Arquitecturas Frente al Dogfooding

Plugin aislado

Monorepo`include()`

Compuesto`includeBuild()`

Raíz + plugin independiente

`git clone && gradlew tasks`da las tareas del plugin

�❌

✅

</think> (The assistant returns no text, as there is no source content to translate.)

✅

Build plugin independiente del root

✅

❌

✅

�✅

Catálogo de versiones separadas

✅

�❌

✅

✅

No`include()` ni includeBuild()

✅

❌

�❌

</think> </think>

Dogfood nativo sin configuración

�❌

�✅

⚠️

✅

CI plugin independiente

�✅

❌

✅

✅

El root es un ejemplo de consumo real

�❌

⚠️

�✅

✅

La columna de la derecha marca todas las casillas. Es por eso que yo no Volveré más atrás

El Contrato DAG : Un build raíz nunca proviene desde un subdirectorio

Esta arquitectura se integra naturalmente en el DAG N0→N3 de mi workspace. El patrón es : el plugin N2 es el hub de sus propias dependencias, la raíz N3 es un terminal que aplica los hubs

contrat dag architecture

Le codebase-gradle/build.gradle.kts`hace 6 líneas. No`src/, no`buildSrc/, no`gradle/rag-bench.gradle.kts. justo plugins { alias(libs.plugins.codebase) }`y los repositorios. Toda la complejidad vive en`codebase-plugin/.

Conclusión : La duplicación aparente que ahorra tiempo

Cuando muestro esta estructura a alguien, la primera reacción es a menudo : « Pero tienes dos`gradlew`, dos`settings.gradle.kts`, dos`libs.versions.toml`— ¡Es duplicado!

Sí. Y no.

La «duplicación», es reproducir la misma información en dos lugares. Aquí, son dos archivos distintos que sirven para dos usos distintos : el catálogo del plugin (30+ dependencias para builder) y el catálogo de la raíz (2-3 dependencias para consumir). El wrapper del plugin (versión bloqueada para el desarrollo) y el envoltorio de la raíz (versión potencialmente diferente, para el ejercicio del plugin).

No es duplicación. Es separación des responsabilidades aplicada al sistema de compilación. Cada compilación. Hace una cosa, y solo una. La raíz consume. El plugin se compila.

¿El costo? Un pedido`publishToMavenLocal`entre el build del plugin y la compilación de la raíz. ¿El beneficio? Una claridad arquitectónica que elimina horas de depuración aguas abajo.

Desde que he desplegado este patrón sobre`bakery-gradle`, plantuml-gradle, codebase-gradle, y los otros plugins de`foundry/public/, no tengo nunca más dudado al abrir una terminal en uno de mis repositorios. El primer reflejo—./gradlew tasks`— camina siempre, da siempre las buenas tareas, y me dice al instante si todo está sano.

Así es la buena arquitectura. No se lee en un README. Se experimenta en una terminal, en menos de diez segundos.

Articles connexes