Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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 <a href="https://www.flaticon.com/authors/freepik" title="Freepik">Freepik</a>
from <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a>
2 changes: 1 addition & 1 deletion TICKETS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 10 additions & 2 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`:
Expand Down Expand Up @@ -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.

---

Expand Down Expand Up @@ -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 |

---
Expand Down
152 changes: 152 additions & 0 deletions docs/PLUGIN-PUBLISH.md
Original file line number Diff line number Diff line change
@@ -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) |
31 changes: 31 additions & 0 deletions docs/PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
18 changes: 1 addition & 17 deletions plugins/android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>)
}
}
}
formaPublishedPlugin(name = "android")

tasks.named("compileKotlin", KotlinCompilationTask::class.java) {
compilerOptions {
Expand Down
22 changes: 10 additions & 12 deletions plugins/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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
15 changes: 15 additions & 0 deletions plugins/buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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())
}
29 changes: 29 additions & 0 deletions plugins/buildSrc/src/main/kotlin/FormaPluginPublishExtension.kt
Original file line number Diff line number Diff line change
@@ -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<String> =
listOf("kotlin", "android", "structure", "target", "rules", "project")
}
Loading
Loading