diff --git a/.github/workflows/verify.yaml b/.github/workflows/verify.yaml index b0bdfa9..292bb7e 100644 --- a/.github/workflows/verify.yaml +++ b/.github/workflows/verify.yaml @@ -48,6 +48,15 @@ jobs: min-coverage-changed-files: 60 - name: Coverage verify run: ./gradlew koverVerify + - name: Publish Maven Central + if: success() + env: + GITHUB_HEAD_REF: ${{ github.head_ref }} + GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }} + GPG_SIGNING_PASSWORD: ${{ secrets.GPG_SIGNING_PASSWORD }} + SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + run: ./gradlew snapshotAll printDevSnapshotReleaseNote detekt: name: Detekt runs-on: ubuntu-latest diff --git a/build.gradle.kts b/build.gradle.kts index 6b84e27..b121d8c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,13 +2,17 @@ import io.gitlab.arturbosch.detekt.Detekt import io.gitlab.arturbosch.detekt.report.ReportMergeTask group = "io.github.turchenkoalex" -version = "1.0-SNAPSHOT" plugins { + `java-library` + `maven-publish` + signing alias(libs.plugins.kotlin.jvm) apply false alias(libs.plugins.kotlinx.serialization) apply false alias(libs.plugins.detekt) alias(libs.plugins.kover) + alias(libs.plugins.nebula.release) + alias(libs.plugins.nexus.publish) } allprojects { @@ -78,3 +82,239 @@ subprojects { } } } + +val publishProjects = setOf( + "core", + "cors", +) + +val settingsProvider = SettingsProvider() + +tasks.withType { + doFirst { + settingsProvider.validateGPGSecrets() + } + dependsOn(tasks.getByName("build")) +} + +tasks.withType { + doFirst { + settingsProvider.validateSonatypeCredentials() + } +} + +val snapshotAllTask = tasks.register("snapshotAll") { + group = "publishing" +} + +subprojects { + if (this.name in publishProjects) { + apply(plugin = "java-library") + apply(plugin = "maven-publish") + apply(plugin = "signing") + version = sanitizeVersion() + + java { + withJavadocJar() + withSourcesJar() + } + + publishing { + publications { + create("mavenJava") { + from(components["java"]) + + groupId = "io.github.turchenkoalex" + artifactId = "kotlet-${project.name}" + version = sanitizeVersion() + + versionMapping { + usage("java-api") { + fromResolutionOf("runtimeClasspath") + } + usage("java-runtime") { + fromResolutionResult() + } + } + + pom { + name.set("kotlet-${project.name}") + description.set("Kotlet ${project.name} library") + url.set("https://github.com/turchenkoalex/kotlet") + licenses { + license { + name.set("Apache License, Version 2.0") + url.set("https://opensource.org/licenses/Apache-2.0") + } + } + developers { + developer { + id.set("turchenkoalex") + name.set("Aleksandr Turchenko") + } + } + scm { + connection.set("scm:git:git://github.com/turchenkoalex/kotlet.git") + developerConnection.set("scm:git:ssh://github.com/turchenkoalex/kotlet.git") + url.set("https://github.com/turchenkoalex/kotlet") + } + } + } + } + + repositories { + maven { + setUrl(layout.buildDirectory.dir("staging-deploy")) + } + } + } + + signing { + useInMemoryPgpKeys(settingsProvider.gpgSigningKey, settingsProvider.gpgSigningPassword) + sign(publishing.publications["mavenJava"]) + } + + snapshotAllTask.configure { + dependsOn(tasks.getByName("publishToSonatype")) + } + } +} + +tasks.register("printFinalReleaseNote") { + doLast { + printFinalReleaseNote( + groupId = "io.github.turchenkoalex", + artifactId = "kotlet", + sanitizedVersion = project.sanitizeVersion() + ) + } + dependsOn(tasks.getByName("final")) +} + +tasks.register("printDevSnapshotReleaseNote") { + doLast { + printDevSnapshotReleaseNote( + groupId = "io.github.turchenkoalex", + artifactId = "kotlet", + sanitizedVersion = project.sanitizeVersion() + ) + } + dependsOn(snapshotAllTask) +} + + +nexusPublishing { + repositories { + sonatype { + useStaging.set(!project.isSnapshotVersion()) + packageGroup.set("io.github.turchenkoalex") + username.set(settingsProvider.sonatypeUsername) + password.set(settingsProvider.sonatypePassword) + nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) + snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) + } + } +} + +// We want to change SNAPSHOT versions format from: +// ..-dev.#+. (local branch) +// ..-dev.#+ (github pull request) +// to: +// ..-dev+-SNAPSHOT +fun Project.sanitizeVersion(): String { + val version = version.toString() + return if (project.isSnapshotVersion()) { + val githubHeadRef = settingsProvider.githubHeadRef + if (githubHeadRef != null) { + // github pull request + version + .replace(Regex("-dev\\.\\d+\\+[a-f0-9]+$"), "-dev+$githubHeadRef-SNAPSHOT") + } else { + // local branch + version + .replace(Regex("-dev\\.\\d+\\+"), "-dev+") + .replace(Regex("\\.[a-f0-9]+$"), "-SNAPSHOT") + } + } else { + version + } +} + +fun Project.isSnapshotVersion() = version.toString().contains("-dev.") + +fun printFinalReleaseNote(groupId: String, artifactId: String, sanitizedVersion: String) { + println() + println("========================================================") + println() + println("New RELEASE artifact version were published:") + println(" groupId: $groupId") + println(" artifactId: $artifactId") + println(" version: $sanitizedVersion") + println() + println("Discover on Maven Central:") + println(" https://repo1.maven.org/maven2/${groupId.replace('.', '/')}/$artifactId/") + println() + println("Edit or delete artifacts on OSS Nexus Repository Manager:") + println(" https://oss.sonatype.org/#nexus-search;gav~$groupId~~~~") + println() + println("Control staging repositories on OSS Nexus Repository Manager:") + println(" https://oss.sonatype.org/#stagingRepositories") + println() + println("========================================================") + println() +} + +fun printDevSnapshotReleaseNote(groupId: String, artifactId: String, sanitizedVersion: String) { + println() + println("========================================================") + println() + println("New developer SNAPSHOT artifact version were published:") + println(" groupId: $groupId") + println(" artifactId: $artifactId") + println(" version: $sanitizedVersion") + println() + println("Discover on Maven Central:") + println(" https://s01.oss.sonatype.org/content/repositories/snapshots/${groupId.replace('.', '/')}/$artifactId/") + println() + println("Edit or delete artifacts on OSS Nexus Repository Manager:") + println(" https://s01.oss.sonatype.org/#nexus-search;gav~$groupId~~~~") + println() + println("========================================================") + println() +} + +class SettingsProvider { + + val gpgSigningKey: String? + get() = System.getenv(GPG_SIGNING_KEY_PROPERTY) + + val gpgSigningPassword: String? + get() = System.getenv(GPG_SIGNING_PASSWORD_PROPERTY) + + val sonatypeUsername: String? + get() = System.getenv(SONATYPE_USERNAME_PROPERTY) + + val sonatypePassword: String? + get() = System.getenv(SONATYPE_PASSWORD_PROPERTY) + + val githubHeadRef: String? + get() = System.getenv(GITHUB_HEAD_REF_PROPERTY) + + fun validateGPGSecrets() = require( + value = !gpgSigningKey.isNullOrBlank() && !gpgSigningPassword.isNullOrBlank(), + lazyMessage = { "Both $GPG_SIGNING_KEY_PROPERTY and $GPG_SIGNING_PASSWORD_PROPERTY environment variables must not be empty" } + ) + + fun validateSonatypeCredentials() = require( + value = !sonatypeUsername.isNullOrBlank() && !sonatypePassword.isNullOrBlank(), + lazyMessage = { "Both $SONATYPE_USERNAME_PROPERTY and $SONATYPE_PASSWORD_PROPERTY environment variables must not be empty" } + ) + + private companion object { + private const val GPG_SIGNING_KEY_PROPERTY = "GPG_SIGNING_KEY" + private const val GPG_SIGNING_PASSWORD_PROPERTY = "GPG_SIGNING_PASSWORD" + private const val SONATYPE_USERNAME_PROPERTY = "SONATYPE_USERNAME" + private const val SONATYPE_PASSWORD_PROPERTY = "SONATYPE_PASSWORD" + private const val GITHUB_HEAD_REF_PROPERTY = "GITHUB_HEAD_REF" + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c3a2ee5..2e94e7e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,10 +3,13 @@ auth0-jwt = "4.4.0" detekt = "1.23.7" jakarta = "6.1.0" jetty = "12.0.16" +jreleaser = "1.15.0" kotlin = "2.1.0" kotlinx-serialization = "1.8.0" kover = "0.9.1" mockk = "1.13.16" +nebula = "19.0.10" +nexus = "1.3.0" opentelemetry = "1.46.0" opentelemetry-instrumentation-api = "2.11.0" opentelemetry-semconv = "1.29.0-alpha" @@ -19,6 +22,8 @@ jmh = { id = "me.champeau.jmh", version = "0.7.2" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } +nebula-release = { id = "com.netflix.nebula.release", version.ref = "nebula" } +nexus-publish = { id = "io.github.gradle-nexus.publish-plugin", version.ref = "nexus" } [libraries] diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 81aa1c0..d6e308a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists