From 929b0bf525338c43050343b60eb55edd8d58965f Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Sun, 18 May 2025 15:17:14 -0400 Subject: [PATCH] fix: Fix Sonatype publishing **Problem** 1. query string wasn't passed in, so sonaRelease wasn't working 2. deployment name should be human readable **Solution** This fixes the query string passing by hand-crafting the URL. This also generates human readable deployment name. --- .../main/scala/sbt/internal/sona/Sona.scala | 48 +++++++++++++------ main/src/main/scala/sbt/Defaults.scala | 8 +++- main/src/main/scala/sbt/Keys.scala | 1 + .../librarymanagement/Publishing.scala | 5 +- 4 files changed, 43 insertions(+), 19 deletions(-) diff --git a/main-actions/src/main/scala/sbt/internal/sona/Sona.scala b/main-actions/src/main/scala/sbt/internal/sona/Sona.scala index f3f95ff1e..2b3e1d88a 100644 --- a/main-actions/src/main/scala/sbt/internal/sona/Sona.scala +++ b/main-actions/src/main/scala/sbt/internal/sona/Sona.scala @@ -11,6 +11,7 @@ package internal package sona import gigahorse.*, support.apachehttp.Gigahorse +import java.net.URLEncoder import java.util.Base64 import java.nio.charset.StandardCharsets import java.nio.file.Path @@ -30,7 +31,7 @@ class Sona(client: SonaClient) extends AutoCloseable { log: Logger, ): Unit = { val deploymentId = client.uploadBundle(bundleZipPath, deploymentName, pt, log) - client.waitForDeploy(deploymentId, pt, log) + client.waitForDeploy(deploymentId, deploymentName, pt, 1, log) } def close(): Unit = client.close() } @@ -49,16 +50,17 @@ class SonaClient(reqTransform: Request => Request) extends AutoCloseable { log: Logger, ): String = { val res = retryF(maxAttempt = 2) { (attempt: Int) => - log.info(s"uploading bundle to Sonatype Central (attempt: $attempt)") + log.info(s"uploading bundle to the Central Portal (attempt: $attempt)") + // addQuery string doesn't work for post + val q = queryString( + "name" -> deploymentName, + "publishingType" -> (publishingType match { + case PublishingType.Automatic => "AUTOMATIC" + case PublishingType.UserManaged => "USER_MANAGED" + }) + ) val req = Gigahorse - .url(s"${baseUrl}/publisher/upload") - .addQueryString( - "name" -> deploymentName, - "publishingType" -> (publishingType match { - case PublishingType.Automatic => "AUTOMATIC" - case PublishingType.UserManaged => "USER_MANAGED" - }) - ) + .url(s"${baseUrl}/publisher/upload?$q") .post( MultipartFormBody( FormPart("bundle", bundleZipPath.toFile()) @@ -70,23 +72,39 @@ class SonaClient(reqTransform: Request => Request) extends AutoCloseable { awaitWithMessage(res, "uploading...", log) } + def queryString(kv: (String, String)*): String = + kv.map { + case (k, v) => + val encodedV = URLEncoder.encode(v, "UTF-8") + s"$k=$encodedV" + } + .mkString("&") + def waitForDeploy( deploymentId: String, + deploymentName: String, publishingType: PublishingType, + attempt: Int, log: Logger, ): Unit = { val status = deploymentStatus(deploymentId) - log.info(s"deployment $deploymentId ${status.deploymentState}") + log.info(s"deployment $deploymentName ${status.deploymentState} ${attempt}/n") + val sleepSec = + if (attempt <= 3) List(5, 5, 10, 15)(attempt) + else 30 status.deploymentState match { case DeploymentState.FAILED => sys.error(s"deployment $deploymentId failed") case DeploymentState.PENDING | DeploymentState.PUBLISHING | DeploymentState.VALIDATING => - Thread.sleep(5000) - waitForDeploy(deploymentId, publishingType, log) + Thread.sleep(sleepSec * 1000L) + waitForDeploy(deploymentId, deploymentName, publishingType, attempt + 1, log) case DeploymentState.PUBLISHED if publishingType == PublishingType.Automatic => () case DeploymentState.VALIDATED if publishingType == PublishingType.UserManaged => () + case DeploymentState.VALIDATED => + Thread.sleep(sleepSec * 1000L) + waitForDeploy(deploymentId, deploymentName, publishingType, attempt + 1, log) case _ => - Thread.sleep(5000) - waitForDeploy(deploymentId, publishingType, log) + Thread.sleep(sleepSec * 1000L) + waitForDeploy(deploymentId, deploymentName, publishingType, attempt + 1, log) } } diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 4eca2e3b9..e6a57844d 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -10,7 +10,7 @@ package sbt import java.io.{ File, PrintWriter } import java.nio.file.{ Path => NioPath } -import java.util.Optional +import java.util.{ Optional, UUID } import java.util.concurrent.TimeUnit import lmcoursier.CoursierDependencyResolution import lmcoursier.definitions.{ Configuration => CConfiguration } @@ -3102,6 +3102,12 @@ object Classpaths { if (!alreadyContainsCentralCredentials) SysProp.sonatypeCredentalsEnv.toSeq else Nil }, + sonaDeploymentName := { + val o = organization.value + val v = version.value + val uuid = UUID.randomUUID().toString().take(8) + s"$o:$v:$uuid" + }, ) @nowarn("cat=deprecation") diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index 003af173f..661199c84 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -570,6 +570,7 @@ object Keys { val stagingDirectory = settingKey[File]("Local staging directory for Sonatype publishing").withRank(CSetting) val sonaBundle = taskKey[File]("Local bundle for Sonatype publishing").withRank(DTask) val localStaging = settingKey[Option[Resolver]]("Local staging resolver for Sonatype publishing").withRank(CSetting) + val sonaDeploymentName = settingKey[String]("The name used for deployment").withRank(DSetting) val classifiersModule = taskKey[GetClassifiersModule]("classifiers-module").withRank(CTask) val compatibilityWarningOptions = settingKey[CompatibilityWarningOptions]("Configures warnings around Maven incompatibility.").withRank(CSetting) diff --git a/main/src/main/scala/sbt/internal/librarymanagement/Publishing.scala b/main/src/main/scala/sbt/internal/librarymanagement/Publishing.scala index 343f80593..103934c63 100644 --- a/main/src/main/scala/sbt/internal/librarymanagement/Publishing.scala +++ b/main/src/main/scala/sbt/internal/librarymanagement/Publishing.scala @@ -11,7 +11,6 @@ package internal package librarymanagement import java.nio.file.Path -import java.util.UUID import sbt.internal.util.MessageOnlyException import sbt.io.IO import sbt.io.Path.contentOf @@ -40,12 +39,12 @@ object Publishing { private def sonatypeReleaseAction(pt: PublishingType)(s0: State): State = { val extracted = Project.extract(s0) val log = extracted.get(Keys.sLog) + val dn = extracted.get(Keys.sonaDeploymentName) val (s1, bundle) = extracted.runTask(Keys.sonaBundle, s0) val (s2, creds) = extracted.runTask(Keys.credentials, s1) val client = fromCreds(creds) try { - val uuid = UUID.randomUUID().toString().take(8) - client.uploadBundle(bundle.toPath(), uuid, pt, log) + client.uploadBundle(bundle.toPath(), dn, pt, log) s2 } finally { client.close()