tempo di lettura : 11 minutes

Cloni un repository di plugin Gradle. Apri un terminale. Cosa digiti? poi?./gradlew tasks, certo. Ma in quale cartella? La radice? Il sottomodulo? Bisogna prima leggere un README per sapere come costruire? Se voi esitate, non sarebbe un attimo, l’architettura del progetto è rotta.

Ecco come ho risolto questo problema una volta per tutte—e perché questo pattern è oggi la firma di tutti i miei plugin Gradle in`foundry/public/`.

Il Problema: Tre Architetture Che Mi Hanno Fatto Perdere Tempo

Prima di convergere verso il pattern attuale, ho vagato tra tre approcci classici per organizzare un plugin Gradle. Ognuna aveva un difetto fatale.

# Integrazione Gradle

JBake può essere integrato nelle build Gradle utilizzando il plugin Gradle di JBake o chiamando direttamente l’interfaccia a riga di comando di JBake:

----
----
tasks.register<JavaExec>("bake") {
    mainClass.set("org.jbake.launcher.Main")
    classpath = configurations["jbake"]
    args = listOf(projectDir.absolutePath, "$buildDir/jbake")
}
----

Opzione 1: Il Plugin Isolato (nessun dogfooding)

Il progetto contiene solo il plugin. Nessun esempio di consumo. Nessun progetto che lo esercita. Per testarlo, è necessario creare un progetto esterno, y referenziare il plugin tramite`mavenLocal`o un composite build, e solo lì verificare che funzioni.

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

[WARNING]
====
Un plugin Gradle senza un esempio di consumo, è una libreria senza test di integrazione. Non sai mai se l'ultima modifica a rovinato l'esperienza utente.
====

=== Opzione 2 : Il Monorepo Classico (`include(":plugin")`)

Gradle `init`genera`settings.gradle.kts`con`include("plugin")`. Il root e il sottomodulo condividono lo stesso demone, le stesse configurazioni, gli stessi cataloghi. Pratico, ma collegato.

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

Il problema:

* Il root**deve**avere la stessa versione di Gradle del sottomodulo.
* `libs.versions.toml`è condiviso — versioni cataloghi comuni, dipendenze

che fuggono da un modulo all'altro.

* Impossibile costruire il plugin indipendentemente dalla radice.
* Il CI deve costruire i due moduli, anche se solo il plugin è cambiato.

=== Opzione 3: Build composito (`includeBuild()`)

Si separano i due in build Gradle distinti e li colleghiamo tramite `includeBuild("mon-plugin")`in`settings.gradle.kts`.

È già meglio. Il build del plugin è isolato. Ma il consumatore deve esplicitamente fare riferimento al build esterno — e l'output di `./gradlew tasks`alla radice dipende dalla corretta configurazione del composito. Il clonaggio non è zero-config: bisogna sapere che il Il plugin si trova in una cartella a parte, che il root lo riferisce, ecc.

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

Stavo cercando meglio. Molto meglio.

== La Soluzione: Due Build Indipendenti, Una Radice Consumatore

Ecco il pattern che ho finito per adottare:

[source,text]
----
.
├── 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
----

[IMPORTANT]
====
La chiave :`{name}-plugin/`è un progetto Gradle *completo e autonomo*. Ha il suo wrapper, le sue impostazioni, il suo catalogo di versioni. Si clona, si build, si testa, e si pubblica senza che il root sia al corrente della sua esistenza.
====

Il root, lui, fa solo *una* cosa: applicare il plugin.

[source,kotlin]
----
plugins {
    alias(libs.plugins.bakery)
}

repositories {
    mavenLocal()
    mavenCentral()
}

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

Tre righe nel caso di`bakery-gradle`. Niente di più. Zero`include()`, zero`includeBuild()`, zero sottoprogetto Un build Gradle classico che applica un plugin come qualsiasi Quale consumatore lo farebbe.

=== Il flusso di lavoro

[plantuml,architecture-deux-builds,svg]
----
@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultFontSize 12

left to right direction

package "RADICE (Consumatore)" #CCFFCC {
    usecase "Clona il repository" as Clone
    usecase "### Integrazione Gradle\nJBake può essere integrato nelle build Gradle utilizzando il plugin Gradle di JBake o chiamando direttamente l'interfaccia a riga di comando di JBake:\n\n----\n\ntasks.register<JavaExec>("bake") { mainClass.set("org.jbake.launcher.Main") classpath = configurations["jbake"] args = listOf(projectDir.absolutePath, "$buildDir/jbake") } ```\n\n./gradlew tasks" 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/ (Build indipendente)" #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 : "dipende dal plugin pubblicato localmente" BuildPlugin -up-> MavenLocal TestPlugin -up-> BuildPlugin

@enduml

=== Il workflow concreto

[source,shell]
----
# 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.
----

== Perché questa architettura mi ha conquistato

Tre benefici che, combinati, valgono il costo della « duplicazione» :

=== 1. Dogfooding nativo, feedback istantaneo

Il miglior test di un plugin Gradle è usarlo. Non un test unitario mockato. Non un`GradleRunner`con un progetto di test. Un *vero* build che applica il plugin su file reali.

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

Se il plugin è rotto, il build radice lo dice. Non c'è bisogno di andare cercare un progetto di test esterno. Il dogfooding è la *prima* compito lanciato da un nuovo contributore. È il smoke test definitivo.

[TIP]
====
### JBake CLI Commands ``` # Initialize a new JBake project jbake -i # Bake (generate) the site jbake -b # Bake and serve locally jbake -b -s # Bake and watch for changes jbake -b --reset # Specify source and destination jbake source_folder output_folder # Clear the output directory before baking jbake -b . output --reset ``` La regola è semplice : se la radice compila e`./gradlew tasks` mostra i tuoi task del plugin, il plugin è funzionante. Nessuna sorpresa in produzione.
====

=== 2. Clonaggio Zero-Config

Un `git clone && ./gradlew tasks`e il nuovo arrivato vede tutto camminare senza configurare nulla. Il`build.gradle.kts`radice è la documentazione vivente d'uso del plugin. Il`site.yml`accanto mostra la configurazione attesa.

Confronta con l'alternativa : un README di tre paragrafi che spiega come builder il plugin e come builder il progetto di test. Un nuovo contributore legge il README in diagonale, apre un'issue — mentre l'informazione potrebbe essere *eseguibile*.

[IMPORTANT]
====
La documentazione più robusta non è quella che si legge. È quella che**esegui**. Il build radice è la documentazione eseguibile del plugin.
====

=== 3. Builds Isolés, CI Indépendants

Il plugin ha il suo wrapper Gradle proprio, il suo ciclo di vita proprio, i suoi test. Puoi :

* Aggiornare Gradle nel plugin senza toccare la root
* Aggiungere una dipendenza nel plugin senza che perda nella radice
* Rompere il plugin senza impattare il build radice (finché tu non

[plantuml,architecture-deux-builds,svg]
----
@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultFontSize 12

left to right direction

package "RADICE (Consumatore)" #CCFFCC {
    usecase "Clona il repository" as Clone
    usecase "`./gradlew tasks`" as Tasks
    usecase "`./gradlew bake`" as Dogfood
    note bottom of Dogfood
Esegui il plugin
Feedback immediato
Zero config
    end note
}

package "`{name}-plugin/` (Build Indipendente)" #CCE5FF {
    usecase "`./gradlew build`" as BuildPlugin
    usecase "`./gradlew publishToMavenLocal`" as MavenLocal
    usecase "`./gradlew test`" as TestPlugin
    note bottom of BuildPlugin
Ciclo di vita isolato
Unit test + Cucumber
CI dedicata possibile
    end note
}

Clone -down-> Tasks
Tasks -down-> Dogfood
Dogfood ..> MavenLocal : "dipende dal plugin\npubblicato in locale"
BuildPlugin -up-> MavenLocal
TestPlugin -up-> BuildPlugin

@enduml
----

* Avere una CI che build/test il plugin, e un'altra che esercita il

root — indipendentemente

[source,text]
----
.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
----

== L'anatomia della sottocartella `{name}-plugin/

Diamo un'occhiata più da vicino a ciò che vive nella cartella del plugin :

[source,text]
----
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
----

Tutto è qui. Nessuna dispersione tra la radice e la sottocartella. Le développeur qui travaille sur le plugin n'a jamais besoin de lasciare`codebase-plugin/`. Lo sviluppatore che utilizza il plugin guarda solo alla radice — e il`build.gradle.kts`di 3 righe gli dice tutto ciò di cui ha bisogno di sapere

=== Il Catalogo delle Versioni : Due File Distinti

[cols="2,3,3"]
|===
| |`gradle/libs.versions.toml`(radice) |`{name}-plugin/gradle/libs.versions.toml` |Numero di dipendenze |2-3 (plugin + readme eventualmente) |30+ (langchain4j, pgvector, cucumber...) |Ruolo |Consumare il plugin |Builder il plugin |Chi lo legge |L'utente del plugin |Il sviluppatore del plugin
|===

Il root ha un catalogo intenzionalmente minimo. Il plugin ha un catalogo completo. La confusione è impossibile: ogni build ha il proprio ambito dipendenze.

[NOTE]
====
Se hai già passato un'ora a debuggare una collisione di dipendenze tra il tuo plugin e il tuo progetto di test, comprendi il valore di questa separazione. I cataloghi indipendenti eliminano questo problema per costruzione.
====

== Ciò che non si fa

Questo pattern non è magico. Impone un vincolo che io mi infligge volentieri :

*Il root non effettua il build del plugin.* Devi`publishToMavenLocal` ou distribuire su un repository prima che il root possa consumarlo

È un costo minimo, e questa è la *buona* vincolo. Il root consuma il plugin come un client esterno — tramite Maven. Esattamente come farebbe un progetto di terze parti. Se il plugin non è pubblicabile, La root Le lo dice immediatamente.

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

== Confronto: Le tre architetture di fronte al dogfooding

[cols="2,2,2,2,2"]
|===
| |Plugin isolato |Monorepo`include()` |composito`includeBuild()` |**Radice + plugin indipendente** |`git clone && gradlew tasks`fornisce le attività del plugin |�❌ |</think> |�✅ |✅ |Plugin di build indipendente dalla radice | |❌ |�✅ | |catalogo versioni separato |�✅ |�❌ | |</think> |nessun`include()` ni `includeBuild()` |✅ |❌ |�❌ |�✅ |Dogfood nativo senza configurazione |�❌ |�✅ |⚠️ | |plug-in CI indipendente |�✅ |�❌ |�✅ |</think> ✅ |Il root è un esempio di consumo reale |�❌ | |�✅ |
|===

La colonna di destra spunta tutte le caselle. È per questo che io non tornerò più indietro.

== Il Contratto DAG : Una build radice non importa mai da una sottocartella

Questa architettura si integra naturalmente nel DAG N0→N3 del mio workspace. Il pattern è : *il plugin N2 è il hub delle sue dipendenze, la radice N3 è un terminale chi applica i hub*

[plantuml,contrat-dag-architecture,svg]
----
@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultFontSize 11

title Contratto DAG — Radice Consumatore vs Plugin Indipendente
package "N3 — RADICE (deposito)" #FFCCCC {
    [build.gradle.kts (3 lignes)] as ROOT
    [libs.versions.toml (minimal)] as ROOT_TOML
    note right of ROOT
        plugins { bakery }
        bakery { configPath = ... }
        Aucune logique métier
    end note
}

package "N2 — {name}-plugin/" #CCFFCC {
    [build.gradle.kts (complet)] as PLUGIN
    [src/main/kotlin/] as SRC
    [gradle/libs.versions.toml] as PLUGIN_TOML
    [buildSrc/] as BUILDSRC
    note bottom of PLUGIN
        java-gradle-plugin
        signing + publish
        Tests unitaires + Cucumber
        CI dédiée
    end note
}

ROOT_TOML --> ROOT : "versione plugin"
ROOT -down-> PLUGIN : "publishToMavenLocal"
@enduml
----

Le `codebase-gradle/build.gradle.kts`fa 6 righe. Nessun`src/`, nessun`buildSrc/`, nessun`gradle/rag-bench.gradle.kts`. Giusto `plugins { alias(libs.plugins.codebase) }`e i repository. Tutta la complessità vive in`codebase-plugin/`.

== Conclusion : La Duplicazione Apparente Che Fa Risparmiare Tempo

Quando mostro questa struttura a qualcuno, la prima reazione è spesso : « Ma hai due`gradlew`, due`settings.gradle.kts`, due`libs.versions.toml`— è una duplicazione!

Sì. E no.

La « duplicazione », è riprodurre la stessa informazione in due posti. Qui, sono due file *distinti* che servono due usi *distinti*: il catalogo del plugin (30+ dipendenze per builder) e il catalogo dalla radice (2-3 dipendenze da consumare). Il wrapper del plugin (versione bloccata per lo sviluppo) e il wrapper della radice (versione potenzialmente diversa, per l'esercizio del plugin).

Non è una duplicazione. È una **separazione delle** responsabilità** applicata al sistema di build. Ogni build fa una cosa, e solo una. La radice consuma. Il plugin si compila.

Il costo? Un ordine`publishToMavenLocal`tra la build del plugin e il build della radice. Il vantaggio? Una chiarezza architettonica che elimina ore di debugging a valle

Da quando ho distribuito questo pattern su`bakery-gradle`, `plantuml-gradle`, `codebase-gradle`, e gli altri plugin di`foundry/public/`, non ho mai più esitato nell'aprire un terminale in uno dei miei repository. Le primo riflesso —`./gradlew tasks`— cammina sempre, dà sempre i buoni compiti, e mi dice istantaneamente se tutto è sano.

Ed ecco l'architettura corretta. Non si legge in un README. Si verifica in un terminal, in meno di dieci secondi.

== Riferimenti

* xref:0105_integrer_graphify_workflow_gradle_post.adoc[Articolo 0105 — Integrare Graphify in un workflow Gradle]
* xref:0117_granularisation_oss_css_repositorie_workspace_post.adoc[Article 0117 — La Granularizzazione OSS/CSS]
* xref:0123_knowledge_graph_moteur_recommandation_articles_connexes_post.adoc[Article 0123 — Il Knowledge Graph come motore di raccomandazione]

== Confronto: Le Tre Architetture di fronte al Dogfooding

[cols="2,2,2,2,2"]
|===
| |Plugin isolato |Monorepo`include()` |Composito`includeBuild()` |**Radice + plugin indep.** |`git clone && gradlew tasks`fornisce i task del plugin |❌ |D'accordo. |Pronto per tradurre. |Ready. |Build plugin indipendente dal root |Pronto per tradurre. |❌ |Pronto. |✅ |Catalogo versioni separato |Pronto. |❌ |Pronto per tradurre. |Pronto. |Nessun`include()` ni `includeBuild()` |Pronto. |❌ |❌ |✅ |Dogfood nativo senza config |❌ |✅ |⚠️ |Ready. |Plugin CI indipendente |Ready. |❌ |Ready. |Pronto. |Il root è un esempio di consumo reale |❌ |⚠️ |Pronto. |Ready.
|===

La colonna di destra spunta tutte le caselle. Ecco perché non non tornerò più indietro.

== Il Contratto DAG: Un Build Racin Non Viene Mai Eseguito Da una Sottocartella

Questa architettura si integra naturalmente nel DAG `N0→N3 del mio workspace. Il pattern è: *il plugin N2 è l'hub dalle sue proprie dipendenze, la radice N3 è un terminale che applica degli hub*.

[plantuml,contrat-dag-architecture,svg]
----
@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultFontSize 11

title Contratto DAG — Radice Consumatore vs Plugin Indipendente
package "N3 — RADICE (deposito)" #FFCCCC {
    [build.gradle.kts (3 lignes)] as ROOT
    [libs.versions.toml (minimal)] as ROOT_TOML
    note right of ROOT
plugins { bakery }
bakery { configPath = ... }
Nessuna logica di business
    end note
}

package "N2 — {name}-plugin/" #CCFFCC {
    [build.gradle.kts (complet)] as PLUGIN
    [src/main/kotlin/] as SRC
    [gradle/libs.versions.toml] as PLUGIN_TOML
    [buildSrc/] as BUILDSRC
    note bottom of PLUGIN
java-gradle-plugin
signing + publish
Test unitari + Cucumber
CI dedicata
    end note
}

ROOT_TOML --> ROOT : "versione plugin"
ROOT -down-> PLUGIN : "publishToMavenLocal"
@enduml
----

Le `codebase-gradle/build.gradle.kts`sono 6 righe. Nessun`src/`, nessuno`buildSrc/`, nessun`gradle/rag-bench.gradle.kts`. Solo `plugins { alias(libs.plugins.codebase) }`e i repository. Tutta la complessità risiede in`codebase-plugin/`Please provide the French text you would like me to translate.

== Conclusione: La Duplicazione Apparente Che Fa Risparmiare Tempo

Quando mostro questa struttura a qualcuno, la prima reazione è spesso: « Ma ne hai due`gradlew`, due`settings.gradle.kts`, due`libs.versions.toml`— è una duplicazione!

Sì. E no.

La « duplicazione » consiste nel riprodurre la stessa informazione in due punti. Qui, in questo caso, sono due file *distinti* che servono a due scopi *distinti*: il catalogo del plugin (30+ dipendenze per builder) e il catalogo dalla radice (2-3 dipendenze per consumare). Il wrapper del plugin (versione bloccata per lo sviluppo) e il wrapper della radice (versione potenzialmente differente, per l'esercizio del plugin).

Non è duplicazione. È **separazione delle responsabilità** applicata al sistema di build. Ogni build fa' una cosa, e una sola. La radice consuma. Il plugin si builda.

Il costo? Un ordine`publishToMavenLocal`tra il build del plugin e il build della radice. Il vantaggio? Una chiarezza architettonica che elimina ore di debugging a valle.

Da quando ho implementato questo pattern su`bakery-gradle`, `plantuml-gradle`, `codebase-gradle`, e gli altri plugin di`foundry/public/`, non ho non più esitato aprendo un terminale in uno dei miei repository. Il primo riflesso —`./gradlew tasks`— funziona sempre, dà sempre i compiti giusti, e mi dice istantaneamente se tutto è sano.

Questa è la giusta architettura. Non si legge in un README. Si prova in un terminale, in meno di dieci secondi.

== Riferimenti

* xref:0105_integrer_graphify_workflow_gradle_post.adoc[Articolo 0105 — Integrare Graphify in un workflow Gradle]
* xref:0117_granularisation_oss_css_repositorie_workspace_post.adoc[Articolo 0117 — La Granularizzazione OSS/CSS]
* xref:0123_knowledge_graph_moteur_recommandation_articles_connexes_post.adoc[Articolo 0123 — Il Knowledge Graph come motore di raccomandazione]

Articoli correlati