읽기 시간 : 11 minutes

Gradle 플러그인 저장소를 복제합니다. 터미널을 엽니다. 무엇을 입력하시나요? 그리고?./gradlew tasks, 물론이죠. 하지만 어느 폴더인가요? 루트 폴더인가요? 서브모듈 폴더인가요? 먼저 README를 읽어야 builder 방법을 알 수 있나요? 주저한다면 그저 한 초일 뿐인데, 프로젝트 아키텍처가 깨졌습니다.

다음은 제가 이 문제를 완전히 해결한 방법 — 그리고 왜 이 패턴은 오늘날 내 모든 Gradle 플러그인의 서명인`foundry/public/`.

문제: 내 시간을 잡아먹은 세 가지 아키텍처

현재 패턴에 수렴하기 전에, 나는 세 가지 접근 방식 사이에서 헤맸다. Gradle 플러그인을 구성하는 전통적인 방법들마다 치명적인 결함이 있었다.

Option 1 : 분리된 플러그인 (Dogfooding 안 함)

프로젝트에는 플러그인만 포함되어 있습니다. 소비 예시가 없습니다. 없음 프로젝트가 그것을 수행한다. 테스트하려면 외부 프로젝트를 만들어야 합니다, y 를 통해 플러그인을 참조하다`mavenLocal`또는 복합 빌드, 그리고 단지 거기서 그것이 작동하는지 확인하다

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

소비 예시가 없는 Gradle 플러그인은 소비 예시가 없는 라이브러리다 통합 테스트. 마지막 수정이 무엇인지 결코 알 수 없습니다. 사용자 경험을 깨뜨렸습니다.

옵션 2 : 클래식 모노레포 (include(":plugin"))

Gradle`init`생성합니다`settings.gradle.kts`와 함께`include("plugin")`. 루트와 서브모듈은 같은 데몬과 동일한 구성을 공유합니다, 같은 카탈로그들. 편리하지만 결합된.

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

문제점:

  • 루트해야 한다서브 모듈과 동일한 Gradle 버전을 가지고 있다.

  • `libs.versions.toml`공유됨—공통 버전 카탈로그, 의존성

하나의 모듈에서 다른 모듈로 도망치는.

  • 플러그인을 루트와 독립적으로 빌드할 수 없습니다.

  • CI는 두 모듈을 모두 빌드해야 하며, 플러그인만 변경된 경우에도 마찬가지입니다.

옵션 3 : 컴포지트 빌드 (includeBuild())

우리는 두 개를 별도의 Gradle 빌드로 분리하고 이를 통해 연결합니다. includeBuild("mon-plugin")`안에`settings.gradle.kts.

이미 더 나아졌습니다. 플러그인 빌드가 격리되었습니다. 하지만 소비자 외부 빌드를 명시적으로 참조해야 한다 — 출력의 `./gradlew tasks`루트에서 의 올바른 구성에 따라 달라집니다 복합체. 복제는 zero-config이 아니다: 알아야 할 점은 플러그인은 별도의 폴더에 위치하고, 루트가 이를 참조합니다 등.

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

나는 더 나은 것을 찾고 있었어. 훨씬 더 나은 것을.

해결책: 독립된 두 개의 빌드, 소비자 루트

제가 결국 채택한 패턴은 다음과 같습니다:

.
├── 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

키 :`{name}-plugin/`는 완전하고 독립적인 Gradle 프로젝트입니다. 그는 자체 wrapper, 자체 settings, 자체 카탈로그 de를 가지고 있습니다. 버전. 그것은 클론되고, 빌드되고, 테스트되고, 게시되며, 루트 없이 존재를 알고 있어야 합니다.

루트는 오직 'une' 일만 한다: 플러그인을 적용한다.

plugins {
    alias(libs.plugins.bakery)
}

repositories {
    mavenLocal()
    mavenCentral()
}

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

경우에 따라 세 줄`bakery-gradle`. 그만입니다. 제로`include(), 영`includeBuild(), 제로 하위 프로젝트. 어떤 플러그인을 적용하는 것처럼 일반적인 Gradle 빌드 어떤 소비자가それを 할까요?

워크플로우

@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultFontSize 12

left to right direction

package "루트 (소비자)" #CCFFCC {
    usecase "저장소를 복제" as Clone
    usecase "./gradlew 작업" 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/ (독립 빌드)" #CCE5FF {
    usecase "./gradlew 빌드" 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 : "플러그인에 의존함\n로컬에서 게시됨"
BuildPlugin -up-> MavenLocal
TestPlugin -up-> BuildPlugin

@enduml

구체적인 워크플로

# 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.

왜 이 아키텍처가 나를 사로잡았는가?

세 가지 혜택이 결합되면 «복제» 비용에 상응하는 가치가 있습니다 :

1. 네이티브 도그푸딩, 즉각적인 피드백

Gradle 플러그인을 테스트하는 가장 좋은 방법은 사용하는 것이다. 모의 단위 테스트가 아닙니다. 아닌`GradleRunner`프로젝트와 함께 테스트. 실제 빌드가 실제 파일에 플러그인을 적용합니다.

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

플러그인이 깨진 경우, 루트 빌드가 이를 알려줍니다. 갈 필요가 없습니다. 외부 테스트 프로젝트를 찾는. 도그푸딩은 첫 번째 새로운 기여자가 시작하는 작업. 이것이 최종적인 smoke test입니다.

규칙은 간단합니다 : 루트가 컴파일되고`./gradlew tasks` 플러그인 작업을 표시하고, 플러그인이 정상적으로 작동합니다. 놀라운 점은 없습니다. 생산 중

2. 제로-콘피그 클로닝

Un `git clone && ./gradlew tasks`그리고 새로 온 사람은 모든 것을 본다 아무것도 설정하지 않고 걷다.`build.gradle.kts`뿌리는 플러그인의 살아있는 사용 설명서. Le`site.yml`옆에 예상된 구성을 보여줍니다.

대안과 비교해 보세요 : 세 개의 문단으로 구성된 README가 설명하십시오: 플러그인을 어떻게 빌드하고 프로젝트를 어떻게 빌드하는지 테스트의. 새로운 기여자가 README를 대각선으로 읽는다, 잘못하고, 이슈를 열어 — 그런데 정보가 실행 가능 해야 함

가장 견고한 문서는 읽는 문서가 아니다. 이것은 우리가실행하다. 루트 빌드는 문서입니다. 플러그인 실행 파일.

3. 격리된 빌드, 독립된 CI

플러그인에는 자체 Gradle 래퍼와 자체 수명 주기가 있습니다, 자신의 테스트. 할 수 있습니다 :

  • 플러그인에서 루트를 건드리지 않고 Gradle 업그레이드

  • 플러그인에 종속성을 추가하지만 루트로 유출되지 않도록

  • 플러그인을 깨트리되 루트 빌드에 영향을 주지 않고 (당신이

손상된 버전을 게시하지 마세요)

  • 플러그인을 빌드/테스트하는 CI를 하나 가지고, 또 다른 CI는 플러그인을 실행하도록 하는

root — 독립적으로

.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

서브 폴더의 해부학 `{name}-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

거기에 다 있습니다. 루트와 하위 폴더 사이에 분산이 없습니다. 플러그인에서 작업하는 개발자한테 절대 필요하지 않다 떠나다`codebase-plugin/`. 플러그인을 사용하는 개발자 뿌리만 바라보고 — 그리고`build.gradle.kts`3줄의 그는 그에게 필요한 모든 것을 말해준다.

버전 카탈로그 : 두 개의 별도 파일

gradle/libs.versions.toml(뿌리)

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

의존성 개수

2-3 (플러그인 + README 선택 사항)

30+ (langchain4j, pgvector, cucumber…​)

역할

플러그인 소비

플러그인 빌드

누가 읽나

플러그인 사용자

플러그인 개발자

루트는 의도적으로 최소한의 카탈로그를 가지고 있다. 플러그인은 카탈로그를 가지고 있다. 완전. 혼동은 불가능합니다 : 각 빌드는 자신만의 스코프를 가집니다. 의존성의

의존성 충돌을 디버깅하는 데 한 시간을 이미 보냈다면 당신의 플러그인과 테스트 프로젝트 사이에서, 그 가치를 이해합니다. 이 분리. 독립적인 카탈로그는 이 문제를 제거합니다. 구성으로.

우리가 하지 않는 것

이 패턴은 마법이 아니다. 그것은 내가 지켜야 할 제약을 부과한다. 기꺼이 내게 가한다 :

루트는 플러그인을 빌드하지 않습니다. 당신은 해야 합니다.publishToMavenLocal ou 루트가 이를 소비하기 전에 레포지터리에 배포하라.

이는 최소한의 비용이며, 이것이 좋은 제약입니다. 루트 플러그인을 외부 클라이언트처럼 소비합니다 — Maven을 통해. 정확히 서드파티 프로젝트가 하는 것처럼. 플러그인을 게시할 수 없는 경우, 루트가 즉시 알려줍니다.

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

비교 : 세 가지 아키텍처가 마주하는 도그푸딩

독립형 플러그인

모노레포`include()`

복합`includeBuild()`

루트 + 독립형 플러그인.

`git clone && gradlew tasks`플러그인의 작업을 제공한다

❌

(blank)

✅

✅

루트와 독립적인 플러그인 빌드

✅

❌

✅

✅

카탈로그 버전 분리

✅

❌

✅

✅

없다`include()` ni includeBuild()

✅

❌

❌

✅

네이티브 도그푸드 구성 없이

❌

✅

⚠️

✅

독립형 CI 플러그인

(Empty output)

❌

✅

✅

Root은 실제 소비의 예시입니다.

❌

⚠️

(No content to translate; output empty.)

✅

오른쪽 열은 모든 체크박스를 체크한다. 그러니깐 내가 더 뒤로 돌아가겠습니다.

DAG 계약: 루트 빌드는 하위 폴더에서 절대 가져오지 않는다.

이 아키텍처는 DAG N0→N3에 자연스럽게 통합됩니다. 내 워크스페이스에서. 패턴은 : *플러그인 N2는 허브입니다 그 자체의 종속성에서 N3 루트는 단말이다. 허브*를 적용하는

@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultFontSize 11

title DAG 계약 — 소비자 루트 vs 독립 플러그인
package "N3 — 뿌리 (저장소)" #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 : "버전 플러그인"
ROOT -down-> PLUGIN : "publishToMavenLocal"
@enduml

Le codebase-gradle/build.gradle.kts`6줄이다. 없다`src/, 없음`buildSrc/, 없어`gradle/rag-bench.gradle.kts. 그냥 plugins { alias(libs.plugins.codebase) }`그리고 저장소들. 모든 복잡함이 존재한다`codebase-plugin/.

결론 : 시간을 절약해 주는 겉보기상의 중복

내가 이 구조를 누군가에게 보여줄 때, 첫 번째 반응은 자주: “두 개 있어”gradlew, 두`settings.gradle.kts`, 두`libs.versions.toml`— 이건 중복입니다!

예. 그리고 아닙니다.

중복 »은 같은 정보를 두 곳에 복제하는 것이다. 여기서, 두 개의 별개의 파일은 두 가지 별개의 용도로 사용됩니다 : 플러그인 카탈로그 (30+ builder용 의존성)와 카탈로그 루트에서 (소비를 위해 2-3개의 의존성). 플러그인 래퍼 (개발을 위한 잠긴 버전)과 루트 래퍼 (잠시 다른 버전, 플러그인 연습을 위해).

이것은 분리의 책임이 빌드 시스템에 적용됩니다. 각 빌드 한 가지 일만 한다. 루트는 소비한다. 플러그인이 스스로 빌드된다.

비용? 한 주문`publishToMavenLocal`플러그인 빌드 사이 그리고 루트 빌드. 이점은? 아키텍처의 명확함을 제공하는 하류 디버깅에 소요되는 시간을 없앤다

이 패턴을 배포한 이후로`bakery-gradle`, plantuml-gradle, codebase-gradle, 그리고 다른 플러그인의`foundry/public/, 저는 더 이상 주저하지 않고 내 저장소 중 하나에서 터미널을 열 때. 그 첫 번째 반사 —./gradlew tasks`— 항상 걷고, 항상 준다 좋은 작업들, 그리고 모든 것이 정상인지 즉시 알려줍니다.

그게 바로 좋은 아키텍처다. README에서는 읽을 수 없다. 터미널에서 체험되며, 10초 미만에 이루어집니다.

참고문헌

관련 기사