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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ build/

# Properties
local.properties
settings.xml

# Idea
.idea/
Expand Down
141 changes: 141 additions & 0 deletions PUBLISHING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# 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.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Project> {
override fun apply(target: Project) {
with(target) {
Expand Down Expand Up @@ -68,16 +77,8 @@ private fun Project.configurePublishingToSunatype(
}

configure<PublishingExtension> {
// 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<MavenPublication>("release") {
groupId = publicationGroupId
Expand All @@ -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()
Expand Down Expand Up @@ -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
private fun Project.getExtraString(name: String): String? = if (rootProject.extra.has(name)) rootProject.extra[name]?.toString() else null
33 changes: 32 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,42 @@ 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>("wrapper") {
distributionType = Wrapper.DistributionType.ALL
gradleVersion = "9.1.0"
}

// PUBLISHING './gradlew clean modo-compose:bundleReleaseAar modo-compose:publishAllPublicationsToSonatypeRepository'
// 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
7 changes: 5 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
[versions]
composeWheelPicker = "1.0.0-beta05"
leakcanaryAndroid = "2.14"
modo = "0.11.0"
androidGradlePlugin = "8.13.0"
modo = "0.11.0-rc2"
#noinspection AndroidGradlePluginVersion
androidGradlePlugin = "8.13.2"
Comment thread Fixed
Comment thread Fixed
Comment thread Fixed
nexusPublish = "2.0.0"
detektComposeVersion = "0.3.20"
detektVersion = "1.23.6"
junit = "4.13.2"
Expand Down Expand Up @@ -71,6 +73,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" }
Expand Down
Loading