From 3018247ec391b3d3ecf1aac3ad1bb8f982fd71cd Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Wed, 7 May 2025 16:17:09 +0200 Subject: [PATCH] Migrate to a different Maven publishing plugin Nessie and CEL-java are using the [Gradle Nexus publish plugin](https://github.com/gradle-nexus/publish-plugin), which works great for publishing to Nexus and Sonatype OSSRH. Sonatype OSSRH reaches EOL on June 30, 2025 - a migration to "Maven Central Repository" is necessary. [Sonatype mentions](https://central.sonatype.org/publish/publish-portal-ossrh-staging-api/#configuration) that is's enough to update the configuration of the `gradle-nexus/publish-plugin`. A "canary" release of CEL-Java worked fine, but a Nessie release ran into quite some errors (empty deployment, missing mandatory files). This might be because of the _Portal OSSRH Staging API_, which _is a partial reimplementation of the OSSRH / Nexus Repository Manager 2 Staging APIs_. The noteable differences between CEL-Java and Nessie is that Nessie has way more artifacts and the publishing/deployment takes much longer (< 5 minutes vs 25-30 minutes). In the mid/long term it is likely anyway the better alternative to use the [Portal Publisher API](https://central.sonatype.org/publish/publish-portal-api/). Althought there are many Gradle plugins around that support that API, none seems to fully fit the needs of multi-project deployments - to publish an aggregated file containing the contents for all modules to be released. Two Gradle plugins looked promising though: * [GradleUp/nmcp](https://github.com/GradleUp/nmcp) (author also took over the shadow plugin) has support for "aggregated deployments", but in the current form the plugin fails for projects that change the build-directory, which is what the Nessie Spark extensions do. * [zenhelix/maven-central-publish](https://github.com/zenhelix/maven-central-publish/tree/main) however was chosen as it was easier to integrate, but required some work in our root `build.gradle.kts` integrating the support for aggregated multi-project deployments. --- .github/workflows/release.yml | 2 +- build.gradle.kts | 163 +++++++++++++++--- .../src/main/kotlin/PublishingHelperPlugin.kt | 9 +- gradle/libs.versions.toml | 2 +- 4 files changed, 152 insertions(+), 24 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f87c2fc3..ad91cdc6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -101,7 +101,7 @@ jobs: ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.MAVEN_GPG_PASSPHRASE }} ORG_GRADLE_PROJECT_sonatypeUsername: ${{ secrets.OSSRH_ACCESS_ID }} ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.OSSRH_TOKEN }} - run: ./gradlew --rerun-tasks --no-watch-fs -x jmh publishToSonatype closeAndReleaseSonatypeStagingRepository -Prelease + run: ./gradlew publishAggregateMavenCentralDeployment -Prelease --no-scan --stacktrace - name: Bump to next development version run: echo "${NEXT_VERSION}-SNAPSHOT" > version.txt diff --git a/build.gradle.kts b/build.gradle.kts index d0004a32..cf5eebb7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,14 +14,21 @@ * limitations under the License. */ -import org.jetbrains.gradle.ext.* +import io.github.zenhelix.gradle.plugin.MavenCentralUploaderPlugin.Companion.MAVEN_CENTRAL_PORTAL_NAME +import io.github.zenhelix.gradle.plugin.extension.MavenCentralUploaderExtension +import io.github.zenhelix.gradle.plugin.extension.PublishingType +import io.github.zenhelix.gradle.plugin.task.PublishBundleMavenCentralTask +import io.github.zenhelix.gradle.plugin.task.ZipDeploymentTask +import java.time.Duration +import org.gradle.api.publish.plugins.PublishingPlugin.PUBLISH_TASK_GROUP +import org.jetbrains.gradle.ext.settings +import org.jetbrains.gradle.ext.taskTriggers plugins { signing - `maven-publish` alias(libs.plugins.testsummary) alias(libs.plugins.testrerun) - alias(libs.plugins.nexus.publish) + alias(libs.plugins.maven.central.publish) `cel-conventions` } @@ -33,24 +40,140 @@ tasks.named("wrapper") { distributionType = Wrapper.DistributionType.AL // Pass environment variables: // ORG_GRADLE_PROJECT_sonatypeUsername // ORG_GRADLE_PROJECT_sonatypePassword -// OR in ~/.gradle/gradle.properties set -// sonatypeUsername -// sonatypePassword -// Call targets: -// publishToSonatype -// closeAndReleaseSonatypeStagingRepository -nexusPublishing { - transitionCheckOptions { - // default==60 (10 minutes), wait up to 60 minutes - maxRetries.set(360) - // default 10s - delayBetween.set(java.time.Duration.ofSeconds(10)) +// Gradle targets: +// publishAggregateMavenCentralDeployment +// (zipAggregateMavenCentralDeployment to just generate the single, aggregated deployment zip) +// Ref: Maven Central Publisher API: +// https://central.sonatype.org/publish/publish-portal-api/#uploading-a-deployment-bundle +mavenCentralPortal { + credentials { + username.value(provider { System.getenv("ORG_GRADLE_PROJECT_sonatypeUsername") }) + password.value(provider { System.getenv("ORG_GRADLE_PROJECT_sonatypePassword") }) } - repositories { - // see https://central.sonatype.org/publish/publish-portal-ossrh-staging-api/#configuration - sonatype { - nexusUrl.set(uri("https://ossrh-staging-api.central.sonatype.com/service/local/")) - snapshotRepositoryUrl.set(uri("https://central.sonatype.com/repository/maven-snapshots/")) + + deploymentName = "${project.name}-$version" + + // publishingType + // AUTOMATIC = fully automatic release + // USER_MANAGED = user has to manually publish/drop + publishingType = + if (System.getenv("CI") != null) PublishingType.AUTOMATIC else PublishingType.USER_MANAGED + // baseUrl = "https://central.sonatype.com" + uploader { + // 2 seconds * 3600 = 7200 seconds = 2hrs + delayRetriesStatusCheck = Duration.ofSeconds(2) + maxRetriesStatusCheck = 3600 + + aggregate { + // Aggregate submodules into a single archive + modules = true + // Aggregate publications into a single archive for each module + modulePublications = true + } + } +} + +val mavenCentralDeploymentZipAggregation by configurations.creating + +mavenCentralDeploymentZipAggregation.isTransitive = true + +val zipAggregateMavenCentralDeployment by + tasks.registering(Zip::class) { + group = PUBLISH_TASK_GROUP + description = "Generates the aggregated Maven publication zip file." + + inputs.files(mavenCentralDeploymentZipAggregation) + from(mavenCentralDeploymentZipAggregation.map { zipTree(it) }) + // archiveFileName = mavenCentralPortal.deploymentName.orElse(project.name) + destinationDirectory.set(layout.buildDirectory.dir("aggregatedDistribution")) + doLast { logger.lifecycle("Built aggregated distribution ${archiveFile.get()}") } + } + +val publishAggregateMavenCentralDeployment by + tasks.registering(PublishBundleMavenCentralTask::class) { + group = PUBLISH_TASK_GROUP + description = + "Publishes the aggregated Maven publications $MAVEN_CENTRAL_PORTAL_NAME repository." + + dependsOn(zipAggregateMavenCentralDeployment) + inputs.file(zipAggregateMavenCentralDeployment.flatMap { it.archiveFile }) + + val task = this + + project.extensions.configure { + val ext = this + task.baseUrl.set(ext.baseUrl) + task.credentials.set( + ext.credentials.username.flatMap { username -> + ext.credentials.password.map { password -> + io.github.zenhelix.gradle.plugin.client.model.Credentials.UsernamePasswordCredentials( + username, + password, + ) + } + } + ) + + task.publishingType.set( + ext.publishingType.map { + when (it) { + PublishingType.AUTOMATIC -> + io.github.zenhelix.gradle.plugin.client.model.PublishingType.AUTOMATIC + PublishingType.USER_MANAGED -> + io.github.zenhelix.gradle.plugin.client.model.PublishingType.USER_MANAGED + } + } + ) + task.deploymentName.set(ext.deploymentName) + + task.maxRetriesStatusCheck.set(ext.uploader.maxRetriesStatusCheck) + task.delayRetriesStatusCheck.set(ext.uploader.delayRetriesStatusCheck) + + task.zipFile.set(zipAggregateMavenCentralDeployment.flatMap { it.archiveFile }) + } + } + +// Configure the 'io.github.zenhelix.maven-central-publish' plugin to all projects +allprojects.forEach { p -> + p.pluginManager.withPlugin("maven-publish") { + p.pluginManager.apply("io.github.zenhelix.maven-central-publish") + p.extensions.configure { + val aggregatedMavenCentralDeploymentZipPart by p.configurations.creating + aggregatedMavenCentralDeploymentZipPart.description = "Maven central publication zip" + val aggregatedMavenCentralDeploymentZipPartElements by p.configurations.creating + aggregatedMavenCentralDeploymentZipPartElements.description = + "Elements for the Maven central publication zip" + aggregatedMavenCentralDeploymentZipPartElements.isCanBeResolved = false + aggregatedMavenCentralDeploymentZipPartElements.extendsFrom( + aggregatedMavenCentralDeploymentZipPart + ) + aggregatedMavenCentralDeploymentZipPartElements.attributes { + attribute( + Usage.USAGE_ATTRIBUTE, + project.getObjects().named(Usage::class.java, "publication"), + ) + } + + val aggregatemavenCentralDeployment by + p.tasks.registering { + val zip = p.tasks.findByName("zipDeploymentMavenPublication") as ZipDeploymentTask + dependsOn(zip) + outputs.file(zip.archiveFile.get().asFile) + } + + val artifact = + p.artifacts.add( + aggregatedMavenCentralDeploymentZipPart.name, + aggregatemavenCentralDeployment, + ) { + builtBy(aggregatemavenCentralDeployment) + } + aggregatedMavenCentralDeploymentZipPart.outgoing.artifact(artifact) + + rootProject.dependencies.add( + mavenCentralDeploymentZipAggregation.name, + rootProject.dependencies.project(p.path, aggregatedMavenCentralDeploymentZipPart.name), + ) } } } diff --git a/buildSrc/src/main/kotlin/PublishingHelperPlugin.kt b/buildSrc/src/main/kotlin/PublishingHelperPlugin.kt index e71368f0..51167511 100644 --- a/buildSrc/src/main/kotlin/PublishingHelperPlugin.kt +++ b/buildSrc/src/main/kotlin/PublishingHelperPlugin.kt @@ -25,7 +25,6 @@ import org.gradle.api.artifacts.component.ModuleComponentSelector import org.gradle.api.artifacts.result.DependencyResult import org.gradle.api.publish.PublishingExtension import org.gradle.api.publish.maven.MavenPublication -import org.gradle.api.publish.maven.plugins.MavenPublishPlugin import org.gradle.api.publish.tasks.GenerateModuleMetadata import org.gradle.api.tasks.PathSensitivity import org.gradle.kotlin.dsl.configure @@ -43,7 +42,7 @@ class PublishingHelperPlugin : Plugin { project.run { extensions.create("publishingHelper", PublishingHelperExtension::class.java, this) - plugins.withType().configureEach { + plugins.withId("io.github.zenhelix.maven-central-publish") { configure { publications { register("maven") { @@ -63,6 +62,8 @@ class PublishingHelperPlugin : Plugin { suppressPomMetadataWarningsFor("testJavadocElements") suppressPomMetadataWarningsFor("testRuntimeElements") suppressPomMetadataWarningsFor("testSourcesElements") + suppressPomMetadataWarningsFor("testFixturesApiElements") + suppressPomMetadataWarningsFor("testFixturesRuntimeElements") groupId = "$group" version = project.version.toString() @@ -196,6 +197,10 @@ class PublishingHelperPlugin : Plugin { useInMemoryPgpKeys(signingKey, signingPassword) val publishing = project.extensions.getByType(PublishingExtension::class.java) afterEvaluate { sign(publishing.publications.getByName("maven")) } + + if (project.hasProperty("useGpgAgent")) { + useGpgCmd() + } } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 23765acd..9a8d0027 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -67,7 +67,7 @@ tomcat-annotations-api = { module = "org.apache.tomcat:annotations-api", version idea-ext = { id = "org.jetbrains.gradle.plugin.idea-ext", version = "1.1.10" } jandex = { id = "com.github.vlsi.jandex", version.ref = "jandexPlugin" } jmh = { id = "me.champeau.jmh", version = "0.7.3" } -nexus-publish = { id = "io.github.gradle-nexus.publish-plugin", version = "2.0.0" } +maven-central-publish = { id = "io.github.zenhelix.maven-central-publish", version = "0.8.0" } protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" } shadow = { id = "com.github.johnrengelman.shadow", version.ref = "shadowPlugin" } spotless = { id = "com.diffplug.spotless", version.ref = "spotlessPlugin" }