From cea6162f0d73c0217fbd78fec5f44cff14c5cd16 Mon Sep 17 00:00:00 2001 From: Karenkov Igor Date: Thu, 8 Jan 2026 16:52:56 +0700 Subject: [PATCH 1/8] Migrated publishing to suppoer central sonatype repo --- README.md | 4 ++ .../com/github/terrakok/PublishingPlugin.kt | 64 +++++++++++-------- build.gradle.kts | 27 +++++++- gradle/libs.versions.toml | 5 +- 4 files changed, 69 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index a9262ae7..bdc98f95 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,10 @@ Each integration of Modo is a * There are some built-in implementations of `ContainerScreen` like `StackScreen` and `MultiScreen`. * You can easily create custom `Action` by extending `Action` or `ReducerAction`. +# For Maintainers + +See [PUBLISHING.md](PUBLISHING.md) for instructions on publishing new versions to Maven Central. + # License ``` diff --git a/build-logic/convention/src/main/kotlin/com/github/terrakok/PublishingPlugin.kt b/build-logic/convention/src/main/kotlin/com/github/terrakok/PublishingPlugin.kt index 618a9f33..a0e01ff1 100644 --- a/build-logic/convention/src/main/kotlin/com/github/terrakok/PublishingPlugin.kt +++ b/build-logic/convention/src/main/kotlin/com/github/terrakok/PublishingPlugin.kt @@ -13,14 +13,23 @@ import org.gradle.kotlin.dsl.assign import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.extra import org.gradle.kotlin.dsl.get -import org.gradle.kotlin.dsl.maven import org.gradle.kotlin.dsl.register import org.gradle.kotlin.dsl.withType import org.gradle.plugins.signing.Sign import org.gradle.plugins.signing.SigningExtension import java.util.Properties -// PUBLISHING './gradlew clean bundleReleaseAar publishAllPublicationsToSonatypeRepository' +/** + * Configures Maven publishing for library modules. + * + * Sets up: + * - AAR, sources, and javadoc artifacts + * - POM metadata (name, description, developers, licenses) + * - GPG artifact signing + * + * Repository configuration is in root build.gradle.kts (nexus-publish plugin). + * See PUBLISHING.md for publishing instructions. + */ class PublishingPlugin : Plugin { override fun apply(target: Project) { with(target) { @@ -68,16 +77,8 @@ private fun Project.configurePublishingToSunatype( } configure { - // Configure maven central repository - repositories { - maven("https://oss.sonatype.org/service/local/staging/deploy/maven2/") { - name = "sonatype" - credentials { - username = getExtraString("ossrhUsername") - password = getExtraString("ossrhPassword") - } - } - } + // Repository configuration is handled by nexus-publish plugin in root build.gradle.kts + // This plugin only configures the publication artifacts and metadata // Configure all publications publications.register("release") { groupId = publicationGroupId @@ -87,9 +88,9 @@ private fun Project.configurePublishingToSunatype( pom { name = "Modo" description = "Navigation library for Jetpack Compose based on UDF principles" - url = "https://github.com/terrakok/Modo" + url = "https://github.com/ikarenkov/Modo" scm { - url = "https://github.com/terrakok/Modo" + url = "https://github.com/ikarenkov/Modo" } setupLicense() setupDevelopers() @@ -133,28 +134,35 @@ private fun MavenPom.setupLicense() { } private fun Project.readEnvironmentVariables() { - extra["signing.keyId"] = null - extra["signing.password"] = null - extra["signing.secretKeyRingFile"] = null - extra["sonatypeUsername"] = null - extra["sonatypePassword"] = null + // Read credentials from local.properties or environment variables + // Set them on rootProject.extra so they're accessible from root build.gradle.kts + rootProject.extra["signing.keyId"] = null + rootProject.extra["signing.password"] = null + rootProject.extra["signing.secretKeyRingFile"] = null + rootProject.extra["sonatypeUsername"] = null + rootProject.extra["sonatypePassword"] = null -// Grabbing secrets from local.properties file or from environment variables, which could be used on CI val secretPropsFile = project.rootProject.file("local.properties") if (secretPropsFile.exists()) { + // Read all properties from local.properties secretPropsFile.reader().use { Properties().apply { load(it) } }.onEach { (name, value) -> - extra[name.toString()] = value + rootProject.extra[name.toString()] = value + } + // Convert relative path to absolute for signing key file + if (rootProject.extra.has("signing.secretKeyRingFile")) { + rootProject.extra["signing.secretKeyRingFile"] = project.rootProject.layout.projectDirectory + .file(rootProject.extra["signing.secretKeyRingFile"].toString()) } - extra["signing.secretKeyRingFile"] = project.rootProject.layout.projectDirectory.file(extra["signing.secretKeyRingFile"].toString()) } else { - extra["signing.keyId"] = System.getenv("SIGNING_KEY_ID") - extra["signing.password"] = System.getenv("SIGNING_PASSWORD") - extra["signing.secretKeyRingFile"] = System.getenv("SIGNING_SECRET_KEY_RING_FILE") - extra["sonatypeUsername"] = System.getenv("SONATYPE_USERNAME") - extra["sonatypePassword"] = System.getenv("SONATYPE_PASSWORD") + // Read from environment variables (for CI) + rootProject.extra["signing.keyId"] = System.getenv("SIGNING_KEY_ID") + rootProject.extra["signing.password"] = System.getenv("SIGNING_PASSWORD") + rootProject.extra["signing.secretKeyRingFile"] = System.getenv("SIGNING_SECRET_KEY_RING_FILE") + rootProject.extra["sonatypeUsername"] = System.getenv("SONATYPE_USERNAME") + rootProject.extra["sonatypePassword"] = System.getenv("SONATYPE_PASSWORD") } } -private fun Project.getExtraString(name: String): String? = if (extra.has(name)) extra[name]?.toString() else null \ No newline at end of file +private fun Project.getExtraString(name: String): String? = if (rootProject.extra.has(name)) rootProject.extra[name]?.toString() else null \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index a57c5591..1e9e4b03 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -11,6 +11,7 @@ plugins { alias(libs.plugins.modo.publishing) apply false alias(libs.plugins.modo.detekt) alias(libs.plugins.modo.collectSarif) + alias(libs.plugins.nexus.publish) } tasks.named("wrapper") { @@ -18,4 +19,28 @@ tasks.named("wrapper") { gradleVersion = "9.1.0" } -// PUBLISHING './gradlew clean modo-compose:bundleReleaseAar modo-compose:publishAllPublicationsToSonatypeRepository' \ No newline at end of file +// Read credentials from local.properties for nexus-publish plugin +val localProperties = file("local.properties").takeIf { it.exists() }?.let { + java.util.Properties().apply { load(it.inputStream()) } +} + +nexusPublishing { + repositories { + sonatype { + // OSSRH Staging API compatibility endpoint for Central Portal + nexusUrl.set(uri("https://ossrh-staging-api.central.sonatype.com/service/local/")) + snapshotRepositoryUrl.set(uri("https://central.sonatype.com/repository/maven-snapshots/")) + + // Use Central Portal credentials from local.properties or environment + username.set(providers.environmentVariable("SONATYPE_USERNAME") + .orElse(provider { localProperties?.getProperty("sonatypeUsername") ?: "" })) + password.set(providers.environmentVariable("SONATYPE_PASSWORD") + .orElse(provider { localProperties?.getProperty("sonatypePassword") ?: "" })) + } + } + + this.packageGroup.set("com.github.terrakok") +} + +// Publishing configuration for Central Portal (via OSSRH Staging API compatibility endpoint) +// See PUBLISHING.md for detailed publishing instructions \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dc0c9d10..98a27118 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,8 +1,9 @@ [versions] composeWheelPicker = "1.0.0-beta05" leakcanaryAndroid = "2.14" -modo = "0.11.0" -androidGradlePlugin = "8.13.0" +modo = "0.11.0-rc1" +androidGradlePlugin = "8.13.2" +nexusPublish = "2.0.0" detektComposeVersion = "0.3.20" detektVersion = "1.23.6" junit = "4.13.2" From 3a1261efff6d736d27ce675e36c30ed2086478c7 Mon Sep 17 00:00:00 2001 From: Karenkov Igor Date: Mon, 12 Jan 2026 11:45:50 +0700 Subject: [PATCH 2/8] Add publishing instructions into PUBLISHING.md --- PUBLISHING.md | 143 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 PUBLISHING.md diff --git a/PUBLISHING.md b/PUBLISHING.md new file mode 100644 index 00000000..7b5cad28 --- /dev/null +++ b/PUBLISHING.md @@ -0,0 +1,143 @@ +# Publishing to Maven Central + +This guide is for library maintainers who need to publish new versions of Modo to Maven Central. + +## Prerequisites + +### 1. Credentials Setup + +Create `local.properties` file in the project root (gitignored): + +```properties +sonatypeUsername=your-central-portal-username +sonatypePassword=your-central-portal-password +signing.keyId=your-gpg-key-id +signing.password=your-gpg-key-password +signing.secretKeyRingFile=path/to/secring.gpg +``` + +**Where to get credentials:** +- **Sonatype credentials**: Register at [Maven Central Portal](https://central.sonatype.com) and generate a user token +- **GPG signing key**: Generate using `gpg --gen-key` and export with `gpg --export-secret-keys` + +### 2. Update Version + +Update the version in `gradle/libs.versions.toml`: +```toml +[versions] +modo = "x.y.z" # Update this +``` + +## Publishing Workflows + +### Option 1: Manual Release (Recommended) + +This workflow publishes to a staging repository and validates artifacts, but requires manual approval before releasing to Maven Central. + +```bash +./gradlew clean modo-compose:bundleReleaseAar \ + publishAllPublicationsToSonatypeRepository \ + closeSonatypeStagingRepository +``` + +**What happens:** +1. ✅ Artifacts are built (AAR, sources, javadoc) +2. ✅ Artifacts are signed with GPG +3. ✅ Uploaded to staging repository +4. ✅ Repository is closed and validated +5. ⏸️ **Waits for manual approval** + +**Next steps:** +1. Go to https://central.sonatype.com/publishing +2. Find your deployment (should show as "VALIDATED") +3. Review the artifacts +4. Click **"Publish"** to release to Maven Central +5. Or click **"Drop"** to discard if something is wrong + +**Why this is recommended:** +- ✅ Review artifacts before public release +- ✅ Can drop/fix if errors are found +- ✅ Safer for production releases +- ⚠️ Remember: Once published, versions are **immutable** + +### Option 2: Automatic Release + +This workflow automatically publishes to Maven Central after validation, with no manual review step. + +```bash +./gradlew clean modo-compose:bundleReleaseAar \ + publishAllPublicationsToSonatypeRepository \ + closeAndReleaseSonatypeStagingRepository +``` + +**What happens:** +1. ✅ Artifacts are built, signed, and uploaded +2. ✅ Repository is closed and validated +3. ✅ **Automatically released to Maven Central** +4. ⏳ Artifacts appear on Maven Central within 10-30 minutes + +**Use with caution:** +- ⚠️ No manual review - artifacts become public immediately +- ⚠️ Better for hotfixes or when you're very confident +- ⚠️ Can't undo once released + +## Important Notes + +### Gradle Session State + +⚠️ **All publishing tasks must run in a single command.** Running them separately will fail: + +```bash +# ❌ This will fail +./gradlew publishAllPublicationsToSonatypeRepository +./gradlew closeSonatypeStagingRepository # Error: No staging repository found + +# ✅ This works +./gradlew publishAllPublicationsToSonatypeRepository closeSonatypeStagingRepository +``` + +**Why?** The staging repository ID is stored in Gradle's task state, which only exists during one session. See the note in the project explaining this behavior. + +### Version Immutability + +Once a version is published to Maven Central: +- ❌ **Cannot be changed** +- ❌ **Cannot be deleted** +- ❌ **Cannot be republished** + +If you publish a broken version, you must release a new version with a fix. + +### Timing + +- **Validation**: Immediate (during close task) +- **Publication to Maven Central**: 10-30 minutes after release +- **Maven Central search**: May take up to 2 hours to index + +## Troubleshooting + +### "Component already exists" Error + +You're trying to republish an existing version. Solution: +1. Bump the version in `gradle/libs.versions.toml` +2. Or drop the deployment from https://central.sonatype.com/publishing (if not yet published) + +### "401 Unauthorized" Error + +Your credentials are invalid or from the old OSSRH system. Solution: +1. Verify you're using credentials from https://central.sonatype.com (not oss.sonatype.org) +2. Regenerate user token if needed +3. Check `local.properties` has correct `sonatypeUsername` and `sonatypePassword` + +### "No staging repository found" Error + +You ran tasks in separate Gradle invocations. Solution: +- Run all tasks in a single command (see "Gradle Session State" above) + +## Architecture Notes + +This project uses: +- **`maven-publish` plugin**: Creates and signs artifacts (configured in `PublishingPlugin.kt`) +- **`gradle-nexus/publish-plugin`**: Manages Nexus staging workflow (configured in root `build.gradle.kts`) +- **OSSRH Staging API compatibility endpoint**: Bridges old Gradle plugins with new Central Portal + +The migration from OSSRH to Central Portal is complete, using the compatibility endpoint to maintain existing workflow. From 49d60e50777e67bf811706871ee8325e47f605a8 Mon Sep 17 00:00:00 2001 From: Karenkov Igor Date: Mon, 12 Jan 2026 11:47:16 +0700 Subject: [PATCH 3/8] Bumped version to 0.11.0-rc2 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 98a27118..cc3a8c44 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] composeWheelPicker = "1.0.0-beta05" leakcanaryAndroid = "2.14" -modo = "0.11.0-rc1" +modo = "0.11.0-rc2" androidGradlePlugin = "8.13.2" nexusPublish = "2.0.0" detektComposeVersion = "0.3.20" From 8f7011dad9d6d585950f8dc3b86833212931a087 Mon Sep 17 00:00:00 2001 From: Karenkov Igor Date: Mon, 12 Jan 2026 11:50:03 +0700 Subject: [PATCH 4/8] Formated Gradle command as single line --- PUBLISHING.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/PUBLISHING.md b/PUBLISHING.md index 7b5cad28..d9dadeea 100644 --- a/PUBLISHING.md +++ b/PUBLISHING.md @@ -35,9 +35,7 @@ modo = "x.y.z" # Update this This workflow publishes to a staging repository and validates artifacts, but requires manual approval before releasing to Maven Central. ```bash -./gradlew clean modo-compose:bundleReleaseAar \ - publishAllPublicationsToSonatypeRepository \ - closeSonatypeStagingRepository +./gradlew clean modo-compose:bundleReleaseAar publishAllPublicationsToSonatypeRepository closeSonatypeStagingRepository ``` **What happens:** From 57a8d35b1730806c443c3174beb713d5b80af8a3 Mon Sep 17 00:00:00 2001 From: Karenkov Igor Date: Fri, 23 Jan 2026 00:28:21 +0700 Subject: [PATCH 5/8] Fixed missing nexus publish plugin declaration --- gradle/libs.versions.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cc3a8c44..f901c293 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -72,6 +72,7 @@ modo-compose-library = { id = "modo-jetpack-compose-library" } modo-collectSarif = { id = "modo-collect-sarif" } detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detektVersion" } +nexus-publish = { id = "io.github.gradle-nexus.publish-plugin", version.ref = "nexusPublish" } android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" } android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } From db9dddef443a17c642a7e979bee768a515d292d6 Mon Sep 17 00:00:00 2001 From: Karenkov Igor Date: Mon, 26 Jan 2026 19:50:48 +0700 Subject: [PATCH 6/8] Fix detekt --- .gitignore | 1 + build.gradle.kts | 14 ++++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 158f3b0f..cb518114 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ build/ # Properties local.properties +settings.xml # Idea .idea/ diff --git a/build.gradle.kts b/build.gradle.kts index 1e9e4b03..aac12011 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -32,10 +32,16 @@ nexusPublishing { snapshotRepositoryUrl.set(uri("https://central.sonatype.com/repository/maven-snapshots/")) // Use Central Portal credentials from local.properties or environment - username.set(providers.environmentVariable("SONATYPE_USERNAME") - .orElse(provider { localProperties?.getProperty("sonatypeUsername") ?: "" })) - password.set(providers.environmentVariable("SONATYPE_PASSWORD") - .orElse(provider { localProperties?.getProperty("sonatypePassword") ?: "" })) + username.set( + providers + .environmentVariable("SONATYPE_USERNAME") + .orElse(provider { localProperties?.getProperty("sonatypeUsername") ?: "" }) + ) + password.set( + providers + .environmentVariable("SONATYPE_PASSWORD") + .orElse(provider { localProperties?.getProperty("sonatypePassword") ?: "" }) + ) } } From 4f939de9c152e62cf62d1f871269c9eba6521f83 Mon Sep 17 00:00:00 2001 From: Karenkov Igor Date: Tue, 27 Jan 2026 00:13:45 +0700 Subject: [PATCH 7/8] Suppressed agp update issue --- gradle/libs.versions.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f901c293..4f5b1c20 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,6 +2,7 @@ composeWheelPicker = "1.0.0-beta05" leakcanaryAndroid = "2.14" modo = "0.11.0-rc2" +#noinspection AndroidGradlePluginVersion androidGradlePlugin = "8.13.2" nexusPublish = "2.0.0" detektComposeVersion = "0.3.20" @@ -16,7 +17,7 @@ androidxFragment = "1.5.7" androidxAppCompat = "1.6.1" androidxJunit = "1.1.5" androidTools = "31.4.0" -kotlin = "2.0.21" +kotlin = "2.2.10" kotlinCompilerExtension = "1.5.12" minSdk = "21" compileSdk = "36" From 33b5e1fcec88e33462c47b47b71073ce455a80bb Mon Sep 17 00:00:00 2001 From: Karenkov Igor Date: Tue, 27 Jan 2026 12:35:29 +0700 Subject: [PATCH 8/8] Roll back kotlin version --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4f5b1c20..621c1275 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ androidxFragment = "1.5.7" androidxAppCompat = "1.6.1" androidxJunit = "1.1.5" androidTools = "31.4.0" -kotlin = "2.2.10" +kotlin = "2.0.21" kotlinCompilerExtension = "1.5.12" minSdk = "21" compileSdk = "36"