Skip to content

Commit a9b6b5d

Browse files
authored
build: extract publishing config into a build-logic convention plugin (#105)
PR: #105
1 parent b46dfa1 commit a9b6b5d

16 files changed

Lines changed: 200 additions & 471 deletions

File tree

CLAUDE.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,12 @@ Layered, from the bottom up:
108108
public entry point (e.g. only `OkioIoProvider` is public in `sdk-io-okio3`).
109109
- **`sdk-core` has zero non-SLF4J runtime deps** — I/O, Jackson, and concurrency libraries live only in
110110
adapter modules. SLF4J is `compileOnly` (added by the root build to every Kotlin module).
111+
- **Published modules apply `id("dexpace.published-module")`** — the convention plugin in the `build-logic`
112+
included build (`build-logic/src/main/kotlin/dexpace.published-module.gradle.kts`) carries the
113+
`maven-publish` + `signing` setup, shared POM, staging repo, and CI-gated signing. Do not re-inline a
114+
`publishing {}`/`signing {}` block in a module; a new publishable module just applies the plugin, and a
115+
module that must not be published simply omits it. Coordinates (`group`/`version`) come from
116+
`gradle.properties` and apply to every project.
111117
- **Commit style:** `feat:` / `test:` / `docs:` / `chore:` prefixes; `merge:` for work-unit merge commits.
112118

113119
## Things That Will Bite You

build-logic/build.gradle.kts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright (c) 2026 dexpace and Omar Aljarrah
3+
*
4+
* Licensed under the MIT License. See LICENSE in the project root.
5+
* SPDX-License-Identifier: MIT
6+
*/
7+
8+
// The `kotlin-dsl` plugin lets this build compile precompiled script plugins — every
9+
// `src/main/kotlin/*.gradle.kts` file becomes a plugin whose id is its file name minus the
10+
// `.gradle.kts` suffix (e.g. `dexpace.published-module`). Consumers in the main build apply
11+
// it by that id once `settings.gradle.kts` has `includeBuild("build-logic")` on the plugin
12+
// classpath.
13+
plugins {
14+
`kotlin-dsl`
15+
// Style-gate this included build's own scripts. `build-logic` is a separate build with its
16+
// own settings, so the root build's `subprojects { ktlint }` block does not reach it; without
17+
// this the convention-plugin `.kts` files would escape the repository's Kotlin style checks.
18+
// The version is pinned literally (not via the version catalog) to keep this build catalog-free
19+
// — see the rationale in `settings.gradle.kts`; it must match `ktlint-plugin` in
20+
// `gradle/libs.versions.toml`.
21+
id("org.jlleitschuh.gradle.ktlint") version "12.1.1"
22+
}
23+
24+
repositories {
25+
mavenCentral()
26+
gradlePluginPortal()
27+
}
28+
29+
// Pin the toolchain so this included build compiles reproducibly regardless of the JDK running
30+
// the Gradle daemon. This is plugin code for the build JVM, not shipped bytecode, so the version
31+
// only needs to be recent enough for `kotlin-dsl`.
32+
kotlin {
33+
jvmToolchain(21)
34+
}
35+
36+
ktlint {
37+
ignoreFailures.set(false)
38+
}
39+
40+
// `kotlin-dsl` adds its plugin wrappers and DSL accessors (under build/generated-sources) to the
41+
// `main` Kotlin source set, so the source-set ktlint tasks would otherwise lint tool-generated
42+
// code. Drop the generated tree from those tasks' inputs; the hand-written convention scripts under
43+
// src/ — and the build script via `runKtlintCheckOverKotlinScripts` — are still checked.
44+
val generatedSourcesDir = layout.buildDirectory.dir("generated-sources").get().asFile
45+
tasks.withType<org.jlleitschuh.gradle.ktlint.tasks.BaseKtLintCheckTask>().configureEach {
46+
val handWritten = source.filter { file -> !file.startsWith(generatedSourcesDir) }.files
47+
setSource(handWritten)
48+
}

build-logic/settings.gradle.kts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
* Copyright (c) 2026 dexpace and Omar Aljarrah
3+
*
4+
* Licensed under the MIT License. See LICENSE in the project root.
5+
* SPDX-License-Identifier: MIT
6+
*/
7+
8+
// Standalone settings for the `build-logic` included build. This build compiles the
9+
// repository's convention plugins (precompiled `*.gradle.kts` script plugins) so that the
10+
// production modules can apply them by id instead of duplicating configuration.
11+
//
12+
// `build-logic` deliberately depends on nothing from the version catalog: its sole convention
13+
// plugin wires the core `maven-publish` and `signing` plugins, which ship with Gradle itself
14+
// and therefore need no version. Keeping the included build catalog-free avoids extra
15+
// `dependencyResolutionManagement { versionCatalogs { ... } }` plumbing and the associated
16+
// classpath coupling between the main build and its own build logic.
17+
18+
rootProject.name = "build-logic"
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Copyright (c) 2026 dexpace and Omar Aljarrah
3+
*
4+
* Licensed under the MIT License. See LICENSE in the project root.
5+
* SPDX-License-Identifier: MIT
6+
*/
7+
8+
// Convention plugin for every module that is published to Maven Central. It carries the
9+
// `maven-publish` + `signing` setup, the shared POM metadata, the staging repository, and the
10+
// CI-gated signing configuration that was previously copied verbatim into all nine module
11+
// build scripts.
12+
//
13+
// A module opts in with `plugins { id("dexpace.published-module") }`. The publication name,
14+
// coordinates, POM, repository, and signing behaviour are then identical across modules; the
15+
// `name`/`description` fields derive from `project.name`, so a module needs no further
16+
// publishing configuration. A module that must NOT be published simply does not apply this
17+
// plugin.
18+
19+
plugins {
20+
`maven-publish`
21+
signing
22+
}
23+
24+
// Coordinates (`group`/`version`) are not set here. Gradle applies them from the repository-root
25+
// `gradle.properties` to the root project and every subproject, so each consuming module already
26+
// carries the shared `org.dexpace` coordinates and current version by the time this plugin runs —
27+
// a coordinate bump is a one-line edit in that file.
28+
29+
// The `library` publication is built from the `java` software component, which only exists once a
30+
// `java`/`java-library`/`kotlin("jvm")` plugin is applied. Every current consumer applies
31+
// `kotlin("jvm")`, but this plugin neither applies nor requires one, so the whole publication +
32+
// signing setup is guarded on the Kotlin JVM plugin. Without the guard, a module that opted in
33+
// without a Java/Kotlin plugin would fail with an opaque "SoftwareComponent with name java not
34+
// found".
35+
pluginManager.withPlugin("org.jetbrains.kotlin.jvm") {
36+
publishing {
37+
publications {
38+
create<MavenPublication>("library") {
39+
from(components["java"])
40+
pom {
41+
name.set(project.name)
42+
description.set("Dexpace Java SDK — ${project.name}")
43+
url.set("https://github.com/dexpace/java-sdk")
44+
licenses {
45+
license {
46+
name.set("MIT License")
47+
url.set("https://github.com/dexpace/java-sdk/blob/main/LICENSE")
48+
distribution.set("repo")
49+
}
50+
}
51+
developers {
52+
developer {
53+
id.set("dexpace")
54+
name.set("Dexpace SDK Team")
55+
}
56+
}
57+
scm {
58+
connection.set("scm:git:https://github.com/dexpace/java-sdk.git")
59+
developerConnection.set("scm:git:ssh://github.com/dexpace/java-sdk.git")
60+
url.set("https://github.com/dexpace/java-sdk")
61+
}
62+
}
63+
}
64+
}
65+
repositories {
66+
// Local staging repository. CI must override this to publish to a real remote.
67+
maven {
68+
name = "local"
69+
url = uri(rootProject.layout.buildDirectory.dir("staging-repo"))
70+
}
71+
}
72+
}
73+
74+
signing {
75+
isRequired = (System.getenv("CI") == "true")
76+
val signingKey = project.findProperty("signing.key") as String? ?: System.getenv("SIGNING_KEY")
77+
val signingPassword =
78+
project.findProperty("signing.password") as String? ?: System.getenv("SIGNING_PASSWORD")
79+
if (!signingKey.isNullOrBlank() && !signingPassword.isNullOrBlank()) {
80+
useInMemoryPgpKeys(signingKey, signingPassword)
81+
}
82+
sign(publishing.publications["library"])
83+
}
84+
}

build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ plugins {
3838
alias(libs.plugins.detekt)
3939
}
4040

41-
group = "org.dexpace"
42-
version = "0.0.1-alpha.1"
41+
// `group` and `version` are set once in `gradle.properties` and applied by Gradle to the root
42+
// project and every subproject — see that file.
4343

4444
// Coverage: aggregate every Kover-enabled subproject through this root project's reports.
4545
dependencies {

gradle.properties

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,11 @@ kotlin.code.style=official
44
# (org.gradle.configuration-cache is intentionally left out and tracked separately.)
55
org.gradle.caching=true
66
org.gradle.parallel=true
7+
8+
# Project coordinates. Gradle applies `group` and `version` from gradle.properties to every
9+
# project in this build, so the root and all published modules share one source of truth — no
10+
# per-module or per-script literal. A coordinate bump is a one-line edit here. (The `build-logic`
11+
# included build is a separate build with its own scope and does not read these; it needs no
12+
# coordinates of its own.)
13+
group=org.dexpace
14+
version=0.0.1-alpha.1

sdk-async-coroutines/build.gradle.kts

Lines changed: 3 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,11 @@
88
plugins {
99
kotlin("jvm")
1010
id("org.jetbrains.kotlinx.kover")
11-
`maven-publish`
12-
signing
11+
// Publishing, signing, POM metadata, and coordinates come from this convention plugin
12+
// (build-logic/src/main/kotlin/dexpace.published-module.gradle.kts).
13+
id("dexpace.published-module")
1314
}
1415

15-
group = "org.dexpace"
16-
version = "0.0.1-alpha.1"
17-
1816
// Java 8 bytecode is inherited from the root build script — the module ships to JDK 8 consumers
1917
// just like `sdk-core` does.
2018

@@ -40,50 +38,3 @@ dependencies {
4038
tasks.test {
4139
useJUnitPlatform()
4240
}
43-
44-
publishing {
45-
publications {
46-
create<MavenPublication>("library") {
47-
from(components["java"])
48-
pom {
49-
name.set(project.name)
50-
description.set("Dexpace Java SDK — ${project.name}")
51-
url.set("https://github.com/dexpace/java-sdk")
52-
licenses {
53-
license {
54-
name.set("MIT License")
55-
url.set("https://github.com/dexpace/java-sdk/blob/main/LICENSE")
56-
distribution.set("repo")
57-
}
58-
}
59-
developers {
60-
developer {
61-
id.set("dexpace")
62-
name.set("Dexpace SDK Team")
63-
}
64-
}
65-
scm {
66-
connection.set("scm:git:https://github.com/dexpace/java-sdk.git")
67-
developerConnection.set("scm:git:ssh://github.com/dexpace/java-sdk.git")
68-
url.set("https://github.com/dexpace/java-sdk")
69-
}
70-
}
71-
}
72-
}
73-
repositories {
74-
maven {
75-
name = "local"
76-
url = uri(rootProject.layout.buildDirectory.dir("staging-repo"))
77-
}
78-
}
79-
}
80-
81-
signing {
82-
isRequired = (System.getenv("CI") == "true")
83-
val signingKey = project.findProperty("signing.key") as String? ?: System.getenv("SIGNING_KEY")
84-
val signingPassword = project.findProperty("signing.password") as String? ?: System.getenv("SIGNING_PASSWORD")
85-
if (!signingKey.isNullOrBlank() && !signingPassword.isNullOrBlank()) {
86-
useInMemoryPgpKeys(signingKey, signingPassword)
87-
}
88-
sign(publishing.publications["library"])
89-
}

sdk-async-netty/build.gradle.kts

Lines changed: 3 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,11 @@
88
plugins {
99
kotlin("jvm")
1010
id("org.jetbrains.kotlinx.kover")
11-
`maven-publish`
12-
signing
11+
// Publishing, signing, POM metadata, and coordinates come from this convention plugin
12+
// (build-logic/src/main/kotlin/dexpace.published-module.gradle.kts).
13+
id("dexpace.published-module")
1314
}
1415

15-
group = "org.dexpace"
16-
version = "0.0.1-alpha.1"
17-
1816
dependencies {
1917
implementation(project(":sdk-core"))
2018
// `netty-common` carries `io.netty.util.concurrent.Future`/`Promise`/`EventExecutor` —
@@ -30,50 +28,3 @@ dependencies {
3028
tasks.test {
3129
useJUnitPlatform()
3230
}
33-
34-
publishing {
35-
publications {
36-
create<MavenPublication>("library") {
37-
from(components["java"])
38-
pom {
39-
name.set(project.name)
40-
description.set("Dexpace Java SDK — ${project.name}")
41-
url.set("https://github.com/dexpace/java-sdk")
42-
licenses {
43-
license {
44-
name.set("MIT License")
45-
url.set("https://github.com/dexpace/java-sdk/blob/main/LICENSE")
46-
distribution.set("repo")
47-
}
48-
}
49-
developers {
50-
developer {
51-
id.set("dexpace")
52-
name.set("Dexpace SDK Team")
53-
}
54-
}
55-
scm {
56-
connection.set("scm:git:https://github.com/dexpace/java-sdk.git")
57-
developerConnection.set("scm:git:ssh://github.com/dexpace/java-sdk.git")
58-
url.set("https://github.com/dexpace/java-sdk")
59-
}
60-
}
61-
}
62-
}
63-
repositories {
64-
maven {
65-
name = "local"
66-
url = uri(rootProject.layout.buildDirectory.dir("staging-repo"))
67-
}
68-
}
69-
}
70-
71-
signing {
72-
isRequired = (System.getenv("CI") == "true")
73-
val signingKey = project.findProperty("signing.key") as String? ?: System.getenv("SIGNING_KEY")
74-
val signingPassword = project.findProperty("signing.password") as String? ?: System.getenv("SIGNING_PASSWORD")
75-
if (!signingKey.isNullOrBlank() && !signingPassword.isNullOrBlank()) {
76-
useInMemoryPgpKeys(signingKey, signingPassword)
77-
}
78-
sign(publishing.publications["library"])
79-
}

sdk-async-reactor/build.gradle.kts

Lines changed: 3 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,11 @@
88
plugins {
99
kotlin("jvm")
1010
id("org.jetbrains.kotlinx.kover")
11-
`maven-publish`
12-
signing
11+
// Publishing, signing, POM metadata, and coordinates come from this convention plugin
12+
// (build-logic/src/main/kotlin/dexpace.published-module.gradle.kts).
13+
id("dexpace.published-module")
1314
}
1415

15-
group = "org.dexpace"
16-
version = "0.0.1-alpha.1"
17-
1816
dependencies {
1917
implementation(project(":sdk-core"))
2018
// Reactor itself is Java 8 compatible and ships with `Mono.fromFuture(...)` / `Mono.toFuture()`
@@ -33,50 +31,3 @@ dependencies {
3331
tasks.test {
3432
useJUnitPlatform()
3533
}
36-
37-
publishing {
38-
publications {
39-
create<MavenPublication>("library") {
40-
from(components["java"])
41-
pom {
42-
name.set(project.name)
43-
description.set("Dexpace Java SDK — ${project.name}")
44-
url.set("https://github.com/dexpace/java-sdk")
45-
licenses {
46-
license {
47-
name.set("MIT License")
48-
url.set("https://github.com/dexpace/java-sdk/blob/main/LICENSE")
49-
distribution.set("repo")
50-
}
51-
}
52-
developers {
53-
developer {
54-
id.set("dexpace")
55-
name.set("Dexpace SDK Team")
56-
}
57-
}
58-
scm {
59-
connection.set("scm:git:https://github.com/dexpace/java-sdk.git")
60-
developerConnection.set("scm:git:ssh://github.com/dexpace/java-sdk.git")
61-
url.set("https://github.com/dexpace/java-sdk")
62-
}
63-
}
64-
}
65-
}
66-
repositories {
67-
maven {
68-
name = "local"
69-
url = uri(rootProject.layout.buildDirectory.dir("staging-repo"))
70-
}
71-
}
72-
}
73-
74-
signing {
75-
isRequired = (System.getenv("CI") == "true")
76-
val signingKey = project.findProperty("signing.key") as String? ?: System.getenv("SIGNING_KEY")
77-
val signingPassword = project.findProperty("signing.password") as String? ?: System.getenv("SIGNING_PASSWORD")
78-
if (!signingKey.isNullOrBlank() && !signingPassword.isNullOrBlank()) {
79-
useInMemoryPgpKeys(signingKey, signingPassword)
80-
}
81-
sign(publishing.publications["library"])
82-
}

0 commit comments

Comments
 (0)