diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ef149e8c..d08f38e9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -315,3 +315,41 @@ jobs:
- name: Build Kotlin sample app
working-directory: kotlin/Examples/IDKitSampleApp
run: ./gradlew :app:assembleDebug
+
+ - name: Validate Kotlin Maven publication
+ run: |
+ set -euo pipefail
+
+ ./kotlin/Examples/IDKitSampleApp/gradlew -p kotlin :bindings:publishToMavenLocal
+ ./kotlin/Examples/IDKitSampleApp/gradlew -p kotlin \
+ -Pidkit.publish.mavenCentral=true \
+ :bindings:publishToMavenCentral --dry-run
+
+ VERSION="$(grep '^version=' kotlin/gradle.properties | cut -d= -f2- | tr -d '[:space:]')"
+ ARTIFACT_DIR="$HOME/.m2/repository/com/worldcoin/idkit/$VERSION"
+ ARTIFACT_BASE="$ARTIFACT_DIR/idkit-$VERSION"
+
+ for artifact in \
+ "$ARTIFACT_BASE.aar" \
+ "$ARTIFACT_BASE.pom" \
+ "$ARTIFACT_BASE.module" \
+ "$ARTIFACT_BASE-sources.jar" \
+ "$ARTIFACT_BASE-javadoc.jar"; do
+ if [ ! -s "$artifact" ]; then
+ echo "::error::Missing Maven publication artifact: $artifact"
+ exit 1
+ fi
+ done
+
+ grep -q 'com.worldcoin' "$ARTIFACT_BASE.pom"
+ grep -q 'idkit' "$ARTIFACT_BASE.pom"
+ grep -q 'aar' "$ARTIFACT_BASE.pom"
+
+ AAR_CONTENTS="$(mktemp)"
+ jar tf "$ARTIFACT_BASE.aar" > "$AAR_CONTENTS"
+ for abi in arm64-v8a armeabi-v7a x86 x86_64; do
+ if ! grep -q "^jni/$abi/libidkit.so$" "$AAR_CONTENTS"; then
+ echo "::error::Missing native library in AAR: jni/$abi/libidkit.so"
+ exit 1
+ fi
+ done
diff --git a/kotlin/Examples/IDKitSampleApp/app/build.gradle.kts b/kotlin/Examples/IDKitSampleApp/app/build.gradle.kts
index 3f32c1a5..0060a993 100644
--- a/kotlin/Examples/IDKitSampleApp/app/build.gradle.kts
+++ b/kotlin/Examples/IDKitSampleApp/app/build.gradle.kts
@@ -9,7 +9,7 @@ android {
defaultConfig {
applicationId = "com.worldcoin.idkit.sample"
- minSdk = 26
+ minSdk = 23
targetSdk = 35
versionCode = 1
versionName = "1.0"
diff --git a/kotlin/README.md b/kotlin/README.md
index a582ede0..36799fb5 100644
--- a/kotlin/README.md
+++ b/kotlin/README.md
@@ -5,6 +5,7 @@ Kotlin SDK for World ID verification, backed by the Rust core via UniFFI.
## Installation
The Kotlin package is published to GitHub Packages as `com.worldcoin:idkit`.
+The same publication is also prepared for `mavenLocal()` and Maven Central. Maven Central upload is available as an explicit local opt-in, but it is intentionally not wired into GitHub release workflows until the required secrets are available there and the workflow has been tested end to end.
GitHub Packages requires authentication for Maven downloads, even for public packages.
Create a token with `read:packages` and expose it through environment variables.
@@ -24,6 +25,25 @@ dependencyResolutionManagement {
}
```
+For local integration testing, build the Kotlin artifacts, publish them to `mavenLocal()`, and add `mavenLocal()` to the consuming app repositories:
+
+```bash
+bash scripts/build-kotlin.sh
+./kotlin/Examples/IDKitSampleApp/gradlew -p kotlin :bindings:publishToMavenLocal
+```
+
+Then add `mavenLocal()` to the consuming app repositories:
+
+```kotlin
+dependencyResolutionManagement {
+ repositories {
+ mavenLocal()
+ google()
+ mavenCentral()
+ }
+}
+```
+
Then add the dependency:
```kotlin
@@ -190,8 +210,53 @@ If Gradle is available locally:
```bash
gradle -p kotlin bindings:test
+./kotlin/Examples/IDKitSampleApp/gradlew -p kotlin :bindings:publishToMavenLocal
+```
+
+## Publishing
+
+The existing Kotlin release workflow publishes to GitHub Packages. That path is still active and uses GitHub's package credentials:
+
+```bash
+./kotlin/Examples/IDKitSampleApp/gradlew -p kotlin :bindings:publish
+```
+
+Without `-Pidkit.publish.mavenCentral=true`, this does not configure Maven Central upload or signing tasks.
+
+For local integration testing, publish the same artifact to the local Maven repository:
+
+```bash
+./kotlin/Examples/IDKitSampleApp/gradlew -p kotlin :bindings:publishToMavenLocal
+```
+
+To publish to Maven Central from a local machine that already has credentials, keep the secrets in `~/.gradle/gradle.properties`:
+
+```properties
+mavenCentralUsername=
+mavenCentralPassword=
+signing.keyId=
+signing.password=
+signing.secretKeyRingFile=/path/to/secring.gpg
```
+Then explicitly enable the Central publishing path for that Gradle invocation:
+
+```bash
+./kotlin/Examples/IDKitSampleApp/gradlew -p kotlin \
+ -Pidkit.publish.mavenCentral=true \
+ :bindings:publishToMavenCentral
+```
+
+To upload and release from the Central Portal deployment in one command, run:
+
+```bash
+./kotlin/Examples/IDKitSampleApp/gradlew -p kotlin \
+ -Pidkit.publish.mavenCentral=true \
+ :bindings:publishAndReleaseToMavenCentral
+```
+
+The automated release workflow continues publishing Kotlin artifacts to GitHub Packages, but it does not publish to Maven Central yet. Add Maven Central release-workflow steps only after the required credentials and end-to-end release path are ready.
+
## Troubleshooting
- `connection_failed`:
diff --git a/kotlin/bindings/build.gradle.kts b/kotlin/bindings/build.gradle.kts
index aecc07b9..5fc619d9 100644
--- a/kotlin/bindings/build.gradle.kts
+++ b/kotlin/bindings/build.gradle.kts
@@ -1,22 +1,64 @@
+import com.vanniktech.maven.publish.AndroidSingleVariantLibrary
+import org.gradle.api.publish.maven.MavenPublication
+import org.gradle.api.publish.maven.tasks.PublishToMavenLocal
+import org.gradle.api.publish.maven.tasks.PublishToMavenRepository
+import org.gradle.jvm.tasks.Jar
+
plugins {
id("com.android.library")
kotlin("android")
- `maven-publish`
+ id("com.vanniktech.maven.publish.base") version "0.34.0"
}
-group = "com.worldcoin"
+val libraryGroup = "com.worldcoin"
+val libraryArtifactId = "idkit"
-// Support version override from CI for dev releases
-version = System.getenv("PKG_VERSION")?.takeIf { it.isNotBlank() }
+// Allow callers to exercise the Maven publication with an explicit artifact version.
+val libraryVersion = System.getenv("PKG_VERSION")?.takeIf { it.isNotBlank() }
?: project.version.toString().takeIf { it.isNotBlank() && it != "unspecified" }
?: throw GradleException("Could not find version in kotlin/gradle.properties")
+val enableMavenCentralPublishing = providers.gradleProperty("idkit.publish.mavenCentral")
+ .map(String::toBoolean)
+ .orElse(false)
+
+val emptyJavadocJar by tasks.registering(Jar::class) {
+ archiveClassifier.set("javadoc")
+}
+
+val requiredNativeAbis = listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64")
+val verifyKotlinNativeLibraries by tasks.registering {
+ group = "verification"
+ description = "Verifies that Kotlin publishing includes native IDKit libraries for every Android ABI."
+
+ doLast {
+ val missingLibraries = requiredNativeAbis.map { abi ->
+ abi to layout.projectDirectory.file("src/main/jniLibs/$abi/libidkit.so").asFile
+ }.filter { (_, library) ->
+ !library.isFile || library.length() == 0L
+ }
+
+ if (missingLibraries.isNotEmpty()) {
+ val missing = missingLibraries.joinToString(separator = "\n") { (abi, library) ->
+ "- $abi: ${library.relativeTo(projectDir)}"
+ }
+ throw GradleException(
+ "Missing native libraries required for publishing:\n$missing\n" +
+ "Run `bash scripts/build-kotlin.sh` from the repository root before publishing.",
+ )
+ }
+ }
+}
+
+group = libraryGroup
+version = libraryVersion
+
android {
namespace = "com.worldcoin.idkit"
compileSdk = 35
defaultConfig {
- minSdk = 26
+ minSdk = 23
}
compileOptions {
@@ -28,12 +70,6 @@ android {
jvmTarget = "17"
}
- publishing {
- singleVariant("release") {
- withSourcesJar()
- }
- }
-
testOptions {
unitTests.all { test ->
val rustLibDir = project.projectDir.resolve("../../target/release").canonicalPath
@@ -53,47 +89,73 @@ dependencies {
testImplementation("net.java.dev.jna:jna:5.14.0")
}
-afterEvaluate {
- publishing {
- publications {
- create("maven") {
- from(components["release"])
- groupId = "com.worldcoin"
- artifactId = "idkit"
- version = project.version.toString()
- pom {
- name.set("IDKit Kotlin")
- description.set("Kotlin bindings for IDKit backed by the Rust core")
- url.set("https://github.com/worldcoin/idkit")
- licenses {
- license {
- name.set("MIT License")
- url.set("https://opensource.org/licenses/MIT")
- }
- }
- developers {
- developer {
- id.set("worldcoin")
- name.set("Worldcoin")
- }
- }
- scm {
- connection.set("scm:git:https://github.com/worldcoin/idkit.git")
- developerConnection.set("scm:git:ssh://git@github.com/worldcoin/idkit.git")
- url.set("https://github.com/worldcoin/idkit")
- }
- }
+mavenPublishing {
+ configure(
+ AndroidSingleVariantLibrary(
+ variant = "release",
+ sourcesJar = true,
+ publishJavadocJar = false,
+ ),
+ )
+
+ coordinates(libraryGroup, libraryArtifactId, libraryVersion)
+
+ pom {
+ name.set("IDKit Kotlin")
+ description.set("Kotlin bindings for IDKit backed by the Rust core")
+ url.set("https://github.com/worldcoin/idkit")
+ licenses {
+ license {
+ name.set("MIT License")
+ url.set("https://opensource.org/licenses/MIT")
+ }
+ }
+ developers {
+ developer {
+ id.set("worldcoin")
+ name.set("Worldcoin")
}
}
- repositories {
- maven {
- name = "GitHubPackages"
- url = uri("https://maven.pkg.github.com/worldcoin/idkit")
- credentials {
- username = System.getenv("GITHUB_ACTOR")
- password = System.getenv("GITHUB_TOKEN")
- }
+ scm {
+ connection.set("scm:git:https://github.com/worldcoin/idkit.git")
+ developerConnection.set("scm:git:ssh://git@github.com/worldcoin/idkit.git")
+ url.set("https://github.com/worldcoin/idkit")
+ }
+ }
+
+ if (enableMavenCentralPublishing.get()) {
+ publishToMavenCentral()
+ signAllPublications()
+ }
+}
+
+publishing {
+ repositories {
+ maven {
+ name = "GitHubPackages"
+ url = uri("https://maven.pkg.github.com/worldcoin/idkit")
+ credentials {
+ username = providers.environmentVariable("GITHUB_ACTOR")
+ .orElse(providers.environmentVariable("GITHUB_USER"))
+ .orNull
+ password = providers.environmentVariable("GITHUB_TOKEN").orNull
}
}
}
}
+
+tasks.withType().configureEach {
+ dependsOn(verifyKotlinNativeLibraries)
+}
+
+tasks.withType().configureEach {
+ dependsOn(verifyKotlinNativeLibraries)
+}
+
+afterEvaluate {
+ publishing {
+ publications.withType().configureEach {
+ artifact(emptyJavadocJar)
+ }
+ }
+}