Mike Croteau
Mike Croteau

Reputation: 1132

How do you publish a Kotlin artifact to Maven Central?

I am struggling here to publish a Kotlin artifact to Maven Central. How do I go about doing this. Here is my build.gradle.kts. It seems as if I have to run the publish command twice. Once for the release and the other for the snapshot repository. Is this correct or am I doing something wrong?

Also modules... I was able to see something publish to Nexus however it was of type module not type jar. Any ideas why? I defined packaging below as jar, the build directory has a jar and when publishing a jar is uploaded. Im not sure what could be causing this.

build.gradle.kts

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    id("org.jetbrains.kotlin.jvm") version "1.6.10"
    id("org.jetbrains.dokka") version "1.6.10"
    id("maven-publish")
    id("signing")
    id("jacoco")
    application
}

repositories {
    mavenCentral()
}

group = "foo.bar"
version = "0.001"

val dokkaOutputDir = "$buildDir/dokka"

application{
    mainClass.set("example.MainKt")
}

tasks.dokkaHtml {
    outputDirectory.set(file(dokkaOutputDir))
}

val deleteDokkaOutputDir by tasks.register<Delete>("deleteDokkaOutputDirectory") {
    delete(dokkaOutputDir)
}

val javadocJar = tasks.register<Jar>("javadocJar") {
    dependsOn(deleteDokkaOutputDir, tasks.dokkaHtml)
    archiveClassifier.set("javadoc")
    from(dokkaOutputDir)
}

tasks.test {
    useJUnitPlatform()
    testLogging.showStandardStreams = true
}

tasks.test {
    extensions.configure(JacocoTaskExtension::class) {
        destinationFile = file("$buildDir/jacoco/jacoco.exec")
    }
    finalizedBy("jacocoTestReport")
}

jacoco {
    toolVersion = "0.8.7"
}

tasks.jacocoTestReport {
    reports {
        xml.isEnabled = false
        csv.isEnabled = false
        html.isEnabled = true
        html.destination = file("$buildDir/reports/coverage")
    }
}

val coverage by tasks.registering {
    group = ""
    description = "Runs the unit tests with coverage"
    dependsOn(":test", ":jacocoTestReport")
    tasks["jacocoTestReport"].mustRunAfter(tasks["test"])
    tasks["jacocoTestCoverageVerification"].mustRunAfter(tasks["jacocoTestReport"])
}

dependencies {
    implementation("com.h2database:h2:2.1.210")
    implementation("org.jetbrains.kotlin:kotlin-stdlib")
    implementation("org.jetbrains.kotlin:kotlin-reflect:1.6.10")
    implementation("org.jacoco:org.jacoco.core:0.8.7")
    testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.2")
    testImplementation("org.junit.jupiter:junit-jupiter-engine:5.8.2")
}

publishing {
    repositories {
        maven {
            name = "Snapshot"
            val releasesRepoUrl = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/"
            val snapshotRepoUrl = "https://s01.oss.sonatype.org/content/repositories/snapshots/"
            setUrl { snapshotRepoUrl }
            credentials {
                username = ""
                password = ""
            }
        }
    }

    publications {
        create<MavenPublication>("Maven") {
            groupId = "foo.bar"
            artifactId = "foo"
            version = "0.001-SNAPSHOT"
            from(components["kotlin"])
        }
        withType<MavenPublication> {
            pom {
                packaging = "jar"
                name.set("foo")
                description.set("Project Foo")
                url.set("fooyee.com")
                licenses {
                    license {
                        name.set("MIT license")
                        url.set("https://opensource.org/licenses/MIT")
                    }
                }
                issueManagement {
                    system.set("Github")
                    url.set("https://github.com/mcroteau/foo/issues")
                }
                scm {
                    connection.set("scm:git:git://github.com/mcroteau/foo.git")
                    developerConnection.set("scm:git:[email protected]:mcroteau/foo.git")
                    url.set("https://github.com/mcroteau/foo")
                }
                developers {
                    developer {
                        name.set("Mike Croteau")
                        email.set("[email protected]")
                    }
                }
            }
        }
    }
}

signing {
    useInMemoryPgpKeys(
        "",
        ""
    )
    sign(publishing.publications)
}

val compileKotlin: KotlinCompile by tasks
compileKotlin.kotlinOptions {
    jvmTarget = "1.8"
}

Thank you for any help!

Upvotes: 6

Views: 8843

Answers (1)

aSemy
aSemy

Reputation: 7110

tl;dr:

  • see a complete example, and select the snapshot/release repo based on the version tag
      repositories {
          maven {
              // change URLs to point to your repos, e.g. http://my.org/repo
              val releasesRepoUrl = uri(layout.buildDirectory.dir("repos/releases"))
              val snapshotsRepoUrl = uri(layout.buildDirectory.dir("repos/snapshots"))
              url = if (version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl
          }
      }
    
  • Change from(components["kotlin"]) to from(components["java"])
  • remove the Dokka config, use the defaults

Publishing to snapshot/release

It seems as if I have to run the publish command twice. Once for the release and the other for the snapshot repository

Normally only artifacts with a version that ends in -SNAPSHOT would be published to the snapshot-repo, and only non-snapshot versions would be published to the release-repo, so it makes sense you'd have to run the command twice.

You can configure Gradle so it will publish to the release or snapshot repo depending on the tag. There's a pretty good example on the Gradle website here: https://docs.gradle.org/current/userguide/publishing_maven.html#publishing_maven:complete_example

Here's what it would look like, adapted to your situation

// select the repo based on the version
publishing {
    repositories {
        name = "sonatypeRepository"
        maven {
            val releasesRepoUrl = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/"
            val snapshotsRepoUrl = "https://s01.oss.sonatype.org/content/repositories/snapshots/"
            url = if (version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl
        }
    }
}

However your username and password will probably be different for each repo. Gradle has a nice way of picking up credentials from environment variables / $GRADLE_HOME/gradle.properties based on the repository name https://docs.gradle.org/current/samples/sample_publishing_credentials.html

Here's what that would look like for you:

// select the repo based on the version, and get the credentials from Gradle properties
publishing {
  repositories {

    if (version.toString().endsWith("SNAPSHOT")) {
      maven("https://s01.oss.sonatype.org/content/repositories/snapshots/") {
        name = "sonatypeReleaseRepository"
        credentials(PasswordCredentials::class)
      }
    } else {
      maven("https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/") {
        name = "sonatypeSnapshotRepository"
        credentials(PasswordCredentials::class)
      }
    }

  }
}

And finally set the credentials either as environment variables

ORG_GRADLE_PROJECT_sonatypeSnapshotRepositoryUsername=my-sonaytype-SNAPSHOT-username
ORG_GRADLE_PROJECT_sonatypeSnapshotRepositoryPassowrd=Tr0ub4dor

Or in $GRADLE_HOME/gradle.properties

sonatypeReleaseRepositoryUsername=my-sonaytype-RELEASE-username
sonatypeReleaseRepositoryPassowrd=CorrectHorseBatteryStaple

To release,

  1. update the version in build.gradle.kts to a -SNAPSHOT version
  2. git commit, and git tag (with the same version)
  3. ./gradlew publish
  4. repeat with a non -SNAPSHOT version...

This can also be improved by writing some more logic in Gradle (or see the end of this answer for info on JitPack and the 'Gradle Git Versioning Plugin'.

Artifact a module, not a JAR

I was able to see something publish to Nexus however it was of type module not type jar.

I'm not certain, but I suspect this is due to this config:

        create<MavenPublication>("Maven") {
            groupId = "foo.bar"
            artifactId = "foo"
            version = "0.001-SNAPSHOT"
            from(components["kotlin"])
        }

I think setting the version to be different from the project version could be prone to errors. I'd leave it out, as it will default to the Gradle project version. The same goes for the group and artifact IDs.

Writing from(components["kotlin"]) is tempting, but, to cut a long answer short, it should be from(components["java"]) (because it's producing a JAR).

So I think this is closer to a working solution:

publishing {
  publications {
    create<MavenPublication>("Maven") {
      from(components["java"])
    }
 }
}

Dokka

The Dokka tasks you've created don't look correct to me (though they're probably not causing you problems with releasing).

I suspect the defaults will work fine, so you can remove all of the Dokka tasks & config, and you can request a Javadoc JAR as follows:

java {
  withJavadocJar()
  // withSourcesJar() // if you want sources?
}

These should automatically be included in any publication you make.


Recommended additional tools

Here's a couple of tools that I like using, to make releasing easier.

These won't necessarily help directly with releasing to Maven, but they can be a nice alternative, or support. I'm including them in case anyone else has a problem publishing, and would like a workaround.

Git tag versioning

The release procedure can involve editing build.gradle.kts (or some other file), and personally I'm not a fan of this. I'd recommend looking at the Gradle Git Versioning Plugin. Once this is set up...

plugins {
  id("me.qoomon.git-versioning")
}

version = "0.0.0-SNAPSHOT"
gitVersioning.apply {
  refs {
    branch(".+") { version = "\${ref}-SNAPSHOT" }
    tag("v(?<version>.*)") { version = "\${ref.version}" }
  }

Now the version number is entirely controlled by git tags. Releasing becomes

  1. Merge to the main branch
  2. Create a tag in the 'release' format, e.g. v1.2.3
  3. Run ./gradlew publish

Jitpack

If you have an open source project, something that's easier to set up than Sonatype and Maven central is JitPack. This project will automatically fetch your code and release a new artifact. Consumers only have to add an additional Maven repository as a dependency provider:

// build.gradle.kts
repositories {
  mavenCentral()
  maven("https://jitpack.io")
}

Upvotes: 7

Related Questions