diff --git a/README.md b/README.md index e412cd17..eadf7d06 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ systems like Buck and Bazel. 2. [Sample app gold standard](docs/SAMPLE-APP.md) — multi-feature layout to copy 3. [Dependency matrix](docs/DEPENDENCY-MATRIX.md) — what may depend on what 4. [External deps catalogs](docs/DEPS-CATALOG.md) · [Compose](docs/COMPOSE.md) · [Environment](docs/ENV.md) +5. [Plugin publish path](docs/PLUGIN-PUBLISH.md) — Portal metadata DSL + release notes (F-016) Configuration made easy: @@ -188,6 +189,8 @@ Sample multi-feature gold standard: **F-014** ([`docs/SAMPLE-APP.md`](docs/SAMPLE-APP.md)). Android getting-started tutorial: **F-015** ([`docs/GETTING-STARTED.md`](docs/GETTING-STARTED.md)). +Plugin Portal publish path: **F-016** +([`docs/PLUGIN-PUBLISH.md`](docs/PLUGIN-PUBLISH.md)). Icons made by Freepik from www.flaticon.com diff --git a/TICKETS.md b/TICKETS.md index c40574b5..0b8538e6 100644 --- a/TICKETS.md +++ b/TICKETS.md @@ -23,7 +23,7 @@ Update this file when picking or finishing work. Cron workers must pick the **hi | F-013 | done | Compose support for Android library/ui targets | GH #96; `compose` flags + `composeWidget`; `docs/COMPOSE.md` | | F-014 | done | Sample app: gold-standard multi-feature structure | packageName + source-root alignment; `docs/SAMPLE-APP.md`; home/characters pattern | | F-015 | done | Android project tutorial (getting started) | GH #53; `docs/GETTING-STARTED.md` + README entry | -| F-016 | todo | Plugin publish path (Portal user + target publish config) | GH #132, #133 | +| F-016 | done | Plugin publish path (Portal user + target publish config) | GH #132 done (`formaPluginConfiguration`/`formaPublishedPlugin`); GH #133 Portal org is human admin — see `docs/PLUGIN-PUBLISH.md` | | F-017 | todo | Configuration-time performance pass | GH #106, #42 | ## P2 — forma-core extraction diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index b93c331e..6292e251 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -105,6 +105,13 @@ compile dep and consumer `agpVersion` in lockstep. `publishPlugins` on `:android` depends on publishing all sibling plugins. +**Publish configuration (F-016 / GH #132):** shared metadata lives in +`plugins/build.gradle.kts` via `formaPluginConfiguration { … }` +(`plugins/buildSrc`). Each plugin module calls +`formaPublishedPlugin(name = …)` instead of copying `rootProject.ext` + +`gradlePlugin { }` blocks. Operator guide: [`docs/PLUGIN-PUBLISH.md`](PLUGIN-PUBLISH.md) +(Portal org/credentials for GH #133 are human-owned; not stored in-repo). + ### 2.1 Target templates (suffixes) Defined in `plugins/android/.../AndroidTargets.kt`: @@ -269,7 +276,8 @@ F-004 fixes applied (2026-07-11): 4. Gradle setup via `gradle/actions/setup-gradle@v4` (successor of `gradle-build-action`). 5. Dropped unconditional `--scan` (no build-scan account coupling in CI). -Still optional / later: plugin publish or Plugin Marker validation job; deeper Gradle remote cache. +Still optional / later: CI job for `publishPlugins --validate-only` (needs no +keys) or full Portal publish via secrets; deeper Gradle remote cache. --- @@ -326,7 +334,7 @@ Suggested extraction order (tickets F-020…F-024): | ~~Compose flag in settings, limited target support~~ → per-target flags + `composeWidget` | F-013 done | | ~~Missing Android getting-started tutorial~~ → [`docs/GETTING-STARTED.md`](GETTING-STARTED.md) | F-015 done | | Shared `library` suffix for JVM vs Android library | F-020 | -| Plugin publish / Portal path | F-016 | +| Plugin publish / Portal path | F-016 (`docs/PLUGIN-PUBLISH.md`; Portal org GH #133 is human) | | Configuration-time cost (includer walk, stores) | F-017 | --- diff --git a/docs/PLUGIN-PUBLISH.md b/docs/PLUGIN-PUBLISH.md new file mode 100644 index 00000000..cbb0cd5d --- /dev/null +++ b/docs/PLUGIN-PUBLISH.md @@ -0,0 +1,152 @@ +# Publishing Forma plugins (F-016) + +How Forma itself is published to the **Gradle Plugin Portal**, and how the +in-repo publish DSL is structured (GH #132). Creating the shared Portal +organization account (GH #133) is a human/admin step — this doc records the +expected ownership model and credential flow so workers do not invent secrets. + +## What gets published + +| Project | Plugin id | Notes | +|---------|-----------|--------| +| `plugins/:android` | `tools.forma.android` | Main product entrypoint | +| `plugins/:target` | `tools.forma.target` | Target identity | +| `plugins/:validation` | `tools.forma.validation` | Validators | +| `plugins/:owners` | `tools.forma.owners` | Ownership metadata | +| `plugins/:config` | `tools.forma.config` | Settings store | +| `plugins/:deps` | `tools.forma.deps` | Dep model + catalogs | +| `includer/` | `tools.forma.includer` | Separate build (own version) | +| `depgen/` | `tools.forma.depgen` | Separate build | + +Current shared version for the `plugins/` multi-project: **`0.1.3`** +(see `formaPluginConfiguration` in `plugins/build.gradle.kts`). + +`:android`’s `publishPlugins` task depends on sibling `publishPlugins` so a +single `:android:publishPlugins` publishes the whole set. + +## Forma-style publish configuration (GH #132) + +Duplicated `rootProject.ext[…]` + per-module `gradlePlugin { … }` blocks are +replaced by two helpers from `plugins/buildSrc`: + +### 1. Root: `formaPluginConfiguration { … }` + +```kotlin +// plugins/build.gradle.kts +formaPluginConfiguration { + group = "tools.forma" + version = "0.1.3" + website = "https://forma.tools/" + vcsUrl = "https://github.com/formatools/forma.git" + displayName = "Forma - Meta Build System with Gradle and Android support" + description = "Best way to structure your Gradle Project" + tags = listOf("kotlin", "android", "structure", "target", "rules", "project") +} +``` + +### 2. Subproject: `formaPublishedPlugin(…)` + +```kotlin +// plugins/android/build.gradle.kts (and target, validation, …) +plugins { + id("com.gradle.plugin-publish") + // + kotlin-dsl or kotlin("jvm") as needed +} + +formaPublishedPlugin( + name = "android", + // optional overrides: + // description = "…", + // extraTags = listOf("agp"), + // implementationClass = "tools.forma.android.plugin.FormaPlugin", +) +``` + +Defaults (matching historical Portal metadata): + +| Field | Default | +|-------|---------| +| `id` | `{group}.{name}` → e.g. `tools.forma.android` | +| `implementationClass` | `{id}.plugin.FormaPlugin` | +| `displayName` / `description` / `tags` | from root `formaPluginConfiguration` | +| `website` / `vcsUrl` | from root config | +| `group` / `version` | from root config | + +Helpers live in: + +- `plugins/buildSrc/src/main/kotlin/FormaPluginPublishExtension.kt` +- `plugins/buildSrc/src/main/kotlin/formaPluginPublish.kt` + +`includer` and `depgen` keep standalone `gradlePlugin { … }` blocks for now +(different versions / release cadence). They can adopt the same helpers later +if those builds are folded into a shared convention. + +## Portal user / credentials (GH #133) + +**Goal:** publish under a shared **Forma** Plugin Portal identity so the team +is not tied to a single personal Gradle account. + +| Item | Status / expectation | +|------|----------------------| +| Portal account / org for Forma | **Human action** — create/claim on [plugins.gradle.org](https://plugins.gradle.org/) (not automatable without owner login) | +| Plugin ownership transfer | Existing `tools.forma.*` plugins may need transfer from the historical personal publisher to the Forma org (Portal UI) | +| Local publish keys | `~/.gradle/gradle.properties` (never commit): `gradle.publish.key`, `gradle.publish.secret` | +| CI publish | Optional later: GitHub Actions secrets + a `workflow_dispatch` publish job — **not** enabled by default | + +Do **not** store Portal keys in the repo, in cron env files, or in +`docs/PROGRESS.md`. Workers may validate packaging without credentials +(see below). + +### Operator checklist (human) + +1. Create or claim the Forma Plugin Portal user/org (GH #133). +2. Generate a publish key pair for that identity. +3. Transfer or re-publish `tools.forma.android` (and siblings) under that + identity as needed. +4. Distribute keys to trusted maintainers via password manager / CI secrets — + not git. +5. Bump `formaPluginConfiguration.version` when cutting a release, then run + the publish command below. + +## Validate packaging (no credentials) + +From a machine with JDK 17+ (`source scripts/env-mac.sh` on the worker Mac): + +```bash +cd plugins +./gradlew build +# Server-side + local descriptor validation without uploading (plugin-publish 1.x): +./gradlew :android:publishPlugins --validate-only +``` + +`--validate-only` exercises plugin markers / metadata without requiring a +successful upload. A full release still needs Portal credentials: + +```bash +cd plugins +./gradlew :android:publishPlugins +``` + +## Consumer coordinates + +Unchanged for apps: + +```kotlin +plugins { + id("tools.forma.android") version "0.1.3" +} +``` + +Badge / maven-metadata still point at the Plugin Portal +(`tools.forma.android`). Composite include of `plugins/` remains the +recommended path for Forma development (see +[`docs/GETTING-STARTED.md`](GETTING-STARTED.md)). + +## Related tickets + +| ID | Scope | +|----|--------| +| F-016 | This doc + `formaPluginConfiguration` / `formaPublishedPlugin` | +| GH #132 | Target-shaped publish config (implemented) | +| GH #133 | Shared Portal user (admin; documented, not automated) | +| F-024 | Coordinate plan when `forma-core` is extracted (`tools.forma:core` vs android plugins) | diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 33e5711d..1665b824 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -2,6 +2,37 @@ Newest entries first. +## 2026-07-12 — F-016 Plugin publish path (Portal + target publish config) + +- **Ticket:** F-016 → `done` (GH #132 code path; GH #133 Portal org remains human admin) +- **Branch:** `forma/F-016-plugin-publish` (from `origin/v2`) +- **Code (`plugins/buildSrc` + plugin modules):** + - Added `FormaPluginPublishExtension` + top-level helpers: + - `formaPluginConfiguration { … }` on plugins root (group/version/website/vcs/tags/…) + - `formaPublishedPlugin(name = …)` on each publishable subproject + - Replaced duplicated `rootProject.ext` + `gradlePlugin { }` blocks in + `:android`, `:target`, `:validation`, `:owners`, `:config`, `:deps` + - Root `plugins/build.gradle.kts` now only applies plugin-publish version + + `formaPluginConfiguration` (no raw `ext { }` map) + - buildSrc depends on `gradleApi()` only (must not put plugin-publish on + buildSrc classpath — causes "already on the classpath with unknown version") +- **Docs:** + - `docs/PLUGIN-PUBLISH.md` — DSL usage, validate-only, credential model, + GH #133 operator checklist (no secrets in-repo) + - README Getting started + Progress pointers; ARCHITECTURE §2 publish note; + TICKETS status +- **Verify (real tool output, OpenJDK 17):** + - `plugins/`: `./gradlew build` → **BUILD SUCCESSFUL** (63 tasks) + - Plugin descriptors still `tools.forma.{android,target,validation,owners,config,deps}` + with `*.plugin.FormaPlugin`; group/version `tools.forma` / `0.1.3` + - `./gradlew :android:publishPlugins --validate-only` → fails only on + **Missing publishing keys** (expected without Portal credentials; packaging OK) + - `application/`: `./gradlew :binary:assembleDebug` → **BUILD SUCCESSFUL** +- **Commits/PRs:** this run — push + PR base `v2` +- **Blockers:** GH #133 (create/claim shared Forma Plugin Portal identity + + distribute keys) is outside automation +- **Next step:** F-017 Configuration-time performance pass (GH #106, #42) + ## 2026-07-12 — F-015 Android project tutorial (getting started) - **Ticket:** F-015 → `done` (GH #53) diff --git a/plugins/android/build.gradle.kts b/plugins/android/build.gradle.kts index 635201ab..537fc667 100644 --- a/plugins/android/build.gradle.kts +++ b/plugins/android/build.gradle.kts @@ -7,23 +7,7 @@ plugins { id("com.gradle.plugin-publish") } -group = rootProject.ext["group"] as String -version = rootProject.ext["version"] as String - -gradlePlugin { - website.set(rootProject.ext["website"] as String) - vcsUrl.set(rootProject.ext["vcsUrl"] as String) - plugins { - create(name) { - id = "$group.$name" - displayName = rootProject.ext["displayName"] as String - description = rootProject.ext["description"] as String - implementationClass = "$id.plugin.FormaPlugin" - @Suppress("UNCHECKED_CAST") - tags.set(rootProject.ext["tags"] as List) - } - } -} +formaPublishedPlugin(name = "android") tasks.named("compileKotlin", KotlinCompilationTask::class.java) { compilerOptions { diff --git a/plugins/build.gradle.kts b/plugins/build.gradle.kts index 62511857..875700ce 100644 --- a/plugins/build.gradle.kts +++ b/plugins/build.gradle.kts @@ -3,16 +3,14 @@ plugins { id("com.gradle.plugin-publish") version "1.2.1" apply false } -ext { - set("group", "tools.forma") - set("version", "0.1.3") - set("tags", listOf("kotlin", "android", "structure", "target", "rules", "project")) - set("website", "https://forma.tools/") - set("vcsUrl", "https://github.com/formatools/forma.git") - set("displayName", "Forma - Meta Build System with Gradle and Android support") - set("description", "Best way to structure your Gradle Project") +// Shared Plugin Portal metadata for all subprojects (F-016 / GH #132). +// Subprojects call formaPublishedPlugin(name = …) instead of copying gradlePlugin blocks. +formaPluginConfiguration { + group = "tools.forma" + version = "0.1.3" + website = "https://forma.tools/" + vcsUrl = "https://github.com/formatools/forma.git" + displayName = "Forma - Meta Build System with Gradle and Android support" + description = "Best way to structure your Gradle Project" + tags = listOf("kotlin", "android", "structure", "target", "rules", "project") } - -group = ext["group"] as String - -version = ext["version"] as String diff --git a/plugins/buildSrc/build.gradle.kts b/plugins/buildSrc/build.gradle.kts new file mode 100644 index 00000000..e23ef42d --- /dev/null +++ b/plugins/buildSrc/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + `kotlin-dsl` +} + +repositories { + gradlePluginPortal() + mavenCentral() +} + +dependencies { + // Only gradleApi — do NOT put com.gradle.plugin-publish on the buildSrc + // classpath or subprojects fail with "plugin is already on the classpath + // with an unknown version" when they apply the same plugin id. + implementation(gradleApi()) +} diff --git a/plugins/buildSrc/src/main/kotlin/FormaPluginPublishExtension.kt b/plugins/buildSrc/src/main/kotlin/FormaPluginPublishExtension.kt new file mode 100644 index 00000000..a0128d1a --- /dev/null +++ b/plugins/buildSrc/src/main/kotlin/FormaPluginPublishExtension.kt @@ -0,0 +1,29 @@ +/** + * Shared metadata for publishing Forma Gradle plugins to the Plugin Portal. + * + * Configured once on the plugins root via [formaPluginConfiguration], then + * consumed by each subproject through [formaPublishedPlugin]. + * + * See GH #132 / F-016 and docs/PLUGIN-PUBLISH.md. + */ +open class FormaPluginPublishExtension { + /** Maven group / plugin id prefix (e.g. `tools.forma`). */ + var group: String = "tools.forma" + + /** Shared artifact version for all Forma plugins in this build. */ + var version: String = "0.0.0" + + var website: String = "https://forma.tools/" + var vcsUrl: String = "https://github.com/formatools/forma.git" + + /** Default Portal display name (subprojects may override). */ + var displayName: String = + "Forma - Meta Build System with Gradle and Android support" + + /** Default Portal description (subprojects may override). */ + var description: String = "Best way to structure your Gradle Project" + + /** Default Portal tags; subprojects may replace or append via extraTags. */ + var tags: List = + listOf("kotlin", "android", "structure", "target", "rules", "project") +} diff --git a/plugins/buildSrc/src/main/kotlin/formaPluginPublish.kt b/plugins/buildSrc/src/main/kotlin/formaPluginPublish.kt new file mode 100644 index 00000000..b026a27f --- /dev/null +++ b/plugins/buildSrc/src/main/kotlin/formaPluginPublish.kt @@ -0,0 +1,104 @@ +import org.gradle.api.Project +import org.gradle.kotlin.dsl.create +import org.gradle.kotlin.dsl.extra +import org.gradle.kotlin.dsl.getByType +import org.gradle.plugin.devel.GradlePluginDevelopmentExtension + +private const val EXT_NAME = "formaPluginConfiguration" + +/** + * Root-level publish metadata for the Forma plugins multi-project. + * + * Example (`plugins/build.gradle.kts`): + * ``` + * formaPluginConfiguration { + * group = "tools.forma" + * version = "0.1.3" + * website = "https://forma.tools/" + * vcsUrl = "https://github.com/formatools/forma.git" + * displayName = "Forma - Meta Build System with Gradle and Android support" + * description = "Best way to structure your Gradle Project" + * tags = listOf("kotlin", "android", "structure", "target", "rules", "project") + * } + * ``` + */ +fun Project.formaPluginConfiguration(configure: FormaPluginPublishExtension.() -> Unit) { + require(this == rootProject) { + "formaPluginConfiguration must be called on the plugins root project" + } + val extension = + extensions.findByType(FormaPluginPublishExtension::class.java) + ?: extensions.create(EXT_NAME, FormaPluginPublishExtension::class.java) + extension.configure() + group = extension.group + version = extension.version + // Keep legacy ext keys for any residual consumers / debugging. + extra["group"] = extension.group + extra["version"] = extension.version + extra["website"] = extension.website + extra["vcsUrl"] = extension.vcsUrl + extra["displayName"] = extension.displayName + extra["description"] = extension.description + extra["tags"] = extension.tags +} + +/** + * Registers this project as a published Gradle plugin using root + * [formaPluginConfiguration] defaults. + * + * Replaces the copy-pasted `gradlePlugin { … rootProject.ext[…] }` blocks + * (GH #132). Defaults match historical Forma Portal metadata: + * - plugin id: `{group}.{name}` (e.g. `tools.forma.android`) + * - implementation class: `{id}.plugin.FormaPlugin` + * + * Example (`plugins/android/build.gradle.kts`): + * ``` + * formaPublishedPlugin( + * name = "android", + * // optional overrides: + * // description = "Android targets for Forma", + * // extraTags = listOf("agp"), + * ) + * ``` + * + * @param name short plugin name segment (usually the Gradle project name) + * @param id full plugin id; default `{group}.{name}` + * @param displayName Portal display name; default from root config + * @param description Portal description; default from root config + * @param implementationClass FQCN of the plugin implementation class + * @param tags full tag list replacement; default from root config + * @param extraTags tags appended after [tags] / root defaults + */ +fun Project.formaPublishedPlugin( + name: String = this.name, + id: String? = null, + displayName: String? = null, + description: String? = null, + implementationClass: String? = null, + tags: List? = null, + extraTags: List = emptyList(), +) { + val rootConfig = rootProject.extensions.getByType() + group = rootConfig.group + version = rootConfig.version + + val pluginId = id ?: "${rootConfig.group}.$name" + val pluginDisplayName = displayName ?: rootConfig.displayName + val pluginDescription = description ?: rootConfig.description + val pluginImpl = implementationClass ?: "$pluginId.plugin.FormaPlugin" + val pluginTags = (tags ?: rootConfig.tags) + extraTags + + extensions.configure(GradlePluginDevelopmentExtension::class.java) { + website.set(rootConfig.website) + vcsUrl.set(rootConfig.vcsUrl) + plugins { + create(name) { + this.id = pluginId + this.displayName = pluginDisplayName + this.description = pluginDescription + this.implementationClass = pluginImpl + this.tags.set(pluginTags) + } + } + } +} diff --git a/plugins/config/build.gradle.kts b/plugins/config/build.gradle.kts index 2bd8c1bc..2202662f 100644 --- a/plugins/config/build.gradle.kts +++ b/plugins/config/build.gradle.kts @@ -3,23 +3,7 @@ plugins { id("com.gradle.plugin-publish") } -group = rootProject.ext["group"] as String -version = rootProject.ext["version"] as String - -gradlePlugin { - website.set(rootProject.ext["website"] as String) - vcsUrl.set(rootProject.ext["vcsUrl"] as String) - plugins { - create(name) { - id = "$group.$name" - displayName = rootProject.ext["displayName"] as String - description = rootProject.ext["description"] as String - implementationClass = "$id.plugin.FormaPlugin" - @Suppress("UNCHECKED_CAST") - tags.set(rootProject.ext["tags"] as List) - } - } -} +formaPublishedPlugin(name = "config") dependencies { implementation(gradleApi()) diff --git a/plugins/deps/build.gradle.kts b/plugins/deps/build.gradle.kts index d75a2c2b..6ffc7c0a 100644 --- a/plugins/deps/build.gradle.kts +++ b/plugins/deps/build.gradle.kts @@ -3,23 +3,7 @@ plugins { id("com.gradle.plugin-publish") } -group = rootProject.ext["group"] as String -version = rootProject.ext["version"] as String - -gradlePlugin { - website.set(rootProject.ext["website"] as String) - vcsUrl.set(rootProject.ext["vcsUrl"] as String) - plugins { - create(name) { - id = "$group.$name" - displayName = rootProject.ext["displayName"] as String - description = rootProject.ext["description"] as String - implementationClass = "$id.plugin.FormaPlugin" - @Suppress("UNCHECKED_CAST") - tags.set(rootProject.ext["tags"] as List) - } - } -} +formaPublishedPlugin(name = "deps") dependencies { implementation(project(":validation")) diff --git a/plugins/owners/build.gradle.kts b/plugins/owners/build.gradle.kts index 2bd8c1bc..d4032895 100644 --- a/plugins/owners/build.gradle.kts +++ b/plugins/owners/build.gradle.kts @@ -3,23 +3,7 @@ plugins { id("com.gradle.plugin-publish") } -group = rootProject.ext["group"] as String -version = rootProject.ext["version"] as String - -gradlePlugin { - website.set(rootProject.ext["website"] as String) - vcsUrl.set(rootProject.ext["vcsUrl"] as String) - plugins { - create(name) { - id = "$group.$name" - displayName = rootProject.ext["displayName"] as String - description = rootProject.ext["description"] as String - implementationClass = "$id.plugin.FormaPlugin" - @Suppress("UNCHECKED_CAST") - tags.set(rootProject.ext["tags"] as List) - } - } -} +formaPublishedPlugin(name = "owners") dependencies { implementation(gradleApi()) diff --git a/plugins/target/build.gradle.kts b/plugins/target/build.gradle.kts index 2bd8c1bc..6b9912aa 100644 --- a/plugins/target/build.gradle.kts +++ b/plugins/target/build.gradle.kts @@ -3,23 +3,7 @@ plugins { id("com.gradle.plugin-publish") } -group = rootProject.ext["group"] as String -version = rootProject.ext["version"] as String - -gradlePlugin { - website.set(rootProject.ext["website"] as String) - vcsUrl.set(rootProject.ext["vcsUrl"] as String) - plugins { - create(name) { - id = "$group.$name" - displayName = rootProject.ext["displayName"] as String - description = rootProject.ext["description"] as String - implementationClass = "$id.plugin.FormaPlugin" - @Suppress("UNCHECKED_CAST") - tags.set(rootProject.ext["tags"] as List) - } - } -} +formaPublishedPlugin(name = "target") dependencies { implementation(gradleApi()) diff --git a/plugins/validation/build.gradle.kts b/plugins/validation/build.gradle.kts index 5b26ca53..eda85d77 100644 --- a/plugins/validation/build.gradle.kts +++ b/plugins/validation/build.gradle.kts @@ -3,23 +3,7 @@ plugins { id("com.gradle.plugin-publish") } -group = rootProject.ext["group"] as String -version = rootProject.ext["version"] as String - -gradlePlugin { - website.set(rootProject.ext["website"] as String) - vcsUrl.set(rootProject.ext["vcsUrl"] as String) - plugins { - create(name) { - id = "$group.$name" - displayName = rootProject.ext["displayName"] as String - description = rootProject.ext["description"] as String - implementationClass = "$id.plugin.FormaPlugin" - @Suppress("UNCHECKED_CAST") - tags.set(rootProject.ext["tags"] as List) - } - } -} +formaPublishedPlugin(name = "validation") dependencies { implementation(project(":target"))