diff --git a/build.sbt b/build.sbt index c90b04fe2..16409bcae 100644 --- a/build.sbt +++ b/build.sbt @@ -636,6 +636,7 @@ lazy val dependencyTreeProj = (project in file("dependency-tree")) name := "sbt-dependency-tree", pluginCrossBuild / sbtVersion := version.value, publishMavenStyle := true, + sbtPluginPublishLegacyMavenStyle := false, // mimaSettings, mimaPreviousArtifacts := Set.empty, ) @@ -1346,8 +1347,10 @@ def customCommands: Seq[Setting[?]] = Seq( ThisBuild / pomIncludeRepository := (_ => false) // drop repos other than Maven Central from POM ThisBuild / publishTo := { - val nexus = "https://oss.sonatype.org/" - Some("releases" at nexus + "service/local/staging/deploy/maven2") + val centralSnapshots = "https://central.sonatype.com/repository/maven-snapshots/" + val v = (ThisBuild / version).value + if (v.endsWith("SNAPSHOT")) Some("central-snapshots" at centralSnapshots) + else localStaging.value } ThisBuild / publishMavenStyle := true 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 8b069bba8..1ef4c57c2 100644 --- a/main-actions/src/main/scala/sbt/internal/sona/Sona.scala +++ b/main-actions/src/main/scala/sbt/internal/sona/Sona.scala @@ -10,18 +10,20 @@ package sbt 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 +import gigahorse.* +import gigahorse.support.apachehttp.Gigahorse import sbt.util.Logger import sjsonnew.JsonFormat -import sjsonnew.support.scalajson.unsafe.{ Converter, Parser } import sjsonnew.shaded.scalajson.ast.unsafe.JValue +import sjsonnew.support.scalajson.unsafe.{ Converter, Parser } +import java.net.URLEncoder +import java.nio.charset.StandardCharsets +import java.nio.file.Path +import java.util.Base64 import scala.annotation.nowarn -import scala.concurrent.*, duration.* +import scala.concurrent.* +import scala.concurrent.duration.* class Sona(client: SonaClient) extends AutoCloseable { def uploadBundle( @@ -36,20 +38,33 @@ class Sona(client: SonaClient) extends AutoCloseable { def close(): Unit = client.close() } -class SonaClient(reqTransform: Request => Request) extends AutoCloseable { +class SonaClient(reqTransform: Request => Request, uploadRequestTimeout: FiniteDuration) + extends AutoCloseable { import SonaClient.baseUrl - val gigahorseConfig = Gigahorse.config - .withRequestTimeout(2.minute) - .withReadTimeout(2.minute) - val http = Gigahorse.http(gigahorseConfig) + private val http = { + val defaultHttpRequestTimeout = 2.minutes + + val gigahorseConfig = Gigahorse.config + .withRequestTimeout(defaultHttpRequestTimeout) + .withReadTimeout(defaultHttpRequestTimeout) + + Gigahorse.http(gigahorseConfig) + } + def uploadBundle( bundleZipPath: Path, deploymentName: String, publishingType: PublishingType, log: Logger, ): String = { - val res = retryF(maxAttempt = 2) { (attempt: Int) => + val maxAttempt = 2 + val waitDurationBetweenAtttempt = 5.seconds + // Adding an extra 5.seconds as security margins + val totalAwaitDuration = + maxAttempt * uploadRequestTimeout + maxAttempt * waitDurationBetweenAtttempt + 5.seconds + + val res = retryF(maxAttempt, waitDurationBetweenAtttempt) { (attempt: Int) => log.info(s"uploading bundle to the Central Portal (attempt: $attempt)") // addQuery string doesn't work for post val q = queryString( @@ -66,13 +81,13 @@ class SonaClient(reqTransform: Request => Request) extends AutoCloseable { FormPart("bundle", bundleZipPath.toFile()) ) ) - .withRequestTimeout(600.second) + .withRequestTimeout(uploadRequestTimeout) http.run(reqTransform(req), Gigahorse.asString) } - awaitWithMessage(res, "uploading...", log) + awaitWithMessage(res, "uploading...", log, totalAwaitDuration) } - def queryString(kv: (String, String)*): String = + private def queryString(kv: (String, String)*): String = kv.map { case (k, v) => val encodedV = URLEncoder.encode(v, "UTF-8") s"$k=$encodedV" @@ -106,17 +121,17 @@ class SonaClient(reqTransform: Request => Request) extends AutoCloseable { } } - def deploymentStatus(deploymentId: String): PublisherStatus = { - val res = retryF(maxAttempt = 5) { (attempt: Int) => + private def deploymentStatus(deploymentId: String): PublisherStatus = { + val res = retryF(maxAttempt = 5, waitDurationBetweenAttempt = 5.seconds) { (attempt: Int) => deploymentStatusF(deploymentId) } - Await.result(res, 600.seconds) + Await.result(res, 10.minutes) } /** * https://central.sonatype.org/publish/publish-portal-api/#verify-status-of-the-deployment */ - def deploymentStatusF(deploymentId: String): Future[PublisherStatus] = { + private def deploymentStatusF(deploymentId: String): Future[PublisherStatus] = { val req = Gigahorse .url(s"${baseUrl}/publisher/status") .addQueryString("id" -> deploymentId) @@ -128,43 +143,52 @@ class SonaClient(reqTransform: Request => Request) extends AutoCloseable { * Retry future function on any error. */ @nowarn - def retryF[A1](maxAttempt: Int)(f: Int => Future[A1]): Future[A1] = { + private def retryF[A1](maxAttempt: Int, waitDurationBetweenAttempt: FiniteDuration)( + f: Int => Future[A1] + ): Future[A1] = { import scala.concurrent.ExecutionContext.Implicits.* def impl(retry: Int): Future[A1] = { val res = f(retry + 1) res.recoverWith { case _ if retry < maxAttempt => - Thread.sleep(5000) - impl(retry + 1) + sleep(waitDurationBetweenAttempt).flatMap(_ => impl(retry + 1)) } } impl(0) } - def awaitWithMessage[A1](f: Future[A1], msg: String, log: Logger): A1 = { + private def awaitWithMessage[A1]( + f: Future[A1], + msg: String, + log: Logger, + awaitDuration: FiniteDuration, + ): A1 = { import scala.concurrent.ExecutionContext.Implicits.* - def loop(attempt: Int): Unit = + def logLoop(attempt: Int): Unit = if (!f.isCompleted) { if (attempt > 0) { log.info(msg) } - Future { - blocking { - Thread.sleep(30.second.toMillis) - } - }.foreach(_ => loop(attempt + 1)) + sleep(30.second).foreach(_ => logLoop(attempt + 1)) } else () - loop(0) - Await.result(f, 600.seconds) + logLoop(0) + Await.result(f, awaitDuration) } def close(): Unit = http.close() + + private def sleep(duration: FiniteDuration)(implicit executor: ExecutionContext): Future[Unit] = + Future { + blocking { + Thread.sleep(duration.toMillis) + } + } } object Sona { def host: String = SonaClient.host - def oauthClient(userName: String, userToken: String): Sona = - new Sona(SonaClient.oauthClient(userName, userToken)) + def oauthClient(userName: String, userToken: String, uploadRequestTimeout: FiniteDuration): Sona = + new Sona(SonaClient.oauthClient(userName, userToken, uploadRequestTimeout)) } object SonaClient { @@ -175,8 +199,12 @@ object SonaClient { Parser.parseFromByteBuffer(r.bodyAsByteBuffer).get def as[A1: JsonFormat]: FullResponse => A1 = asJson.andThen(Converter.fromJsonUnsafe[A1]) val asPublisherStatus: FullResponse => PublisherStatus = as[PublisherStatus] - def oauthClient(userName: String, userToken: String): SonaClient = - new SonaClient(OAuthClient(userName, userToken)) + def oauthClient( + userName: String, + userToken: String, + uploadRequestTimeout: FiniteDuration + ): SonaClient = + new SonaClient(OAuthClient(userName, userToken), uploadRequestTimeout) } private case class OAuthClient(userName: String, userToken: String) diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 09943ddac..e385cf387 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -2755,7 +2755,7 @@ object Classpaths { private lazy val packagedDefaultArtifacts = packaged(defaultArtifactTasks) private lazy val sbt2Plus: Def.Initialize[Boolean] = Def.setting { val sbtV = (pluginCrossBuild / sbtBinaryVersion).value - sbtV != "1.0" && !sbtV.startsWith("0.") + !sbtV.startsWith("1.") && !sbtV.startsWith("0.") } val jvmPublishSettings: Seq[Setting[?]] = Seq( artifacts := artifactDefs(defaultArtifactTasks).value, @@ -2872,6 +2872,7 @@ object Classpaths { val uuid = UUID.randomUUID().toString().take(8) s"$o:$v:$uuid" }, + sonaUploadRequestTimeout := 10.minutes, ) def baseGlobalDefaults = diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index 950cc12ee..80ad5110e 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -629,6 +629,7 @@ object Keys { 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 sonaUploadRequestTimeout = settingKey[FiniteDuration]("Request timeout for Sonatype publishing").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/Opts.scala b/main/src/main/scala/sbt/Opts.scala index 07711a489..3746944e7 100644 --- a/main/src/main/scala/sbt/Opts.scala +++ b/main/src/main/scala/sbt/Opts.scala @@ -8,7 +8,7 @@ package sbt -import sbt.librarymanagement.{ MavenRepository, Resolver } +import sbt.librarymanagement.Resolver import sbt.librarymanagement.ivy.Credentials import java.io.File @@ -42,21 +42,7 @@ object Opts { } object resolver { import sbt.io.syntax.* - @deprecated("Use sonatypeOssReleases instead", "1.7.0") - val sonatypeReleases = Resolver.sonatypeRepo("releases") - // todo: fix - // val sonatypeOssReleases = Resolver.sonatypeOssRepos("releases") - @deprecated("Use sonatypeOssSnapshots instead", "1.7.0") - val sonatypeSnapshots = Resolver.sonatypeRepo("snapshots") - - // todo: fix - // val sonatypeOssSnapshots = Resolver.sonatypeOssRepos("snapshots") - - val sonatypeStaging = MavenRepository( - "sonatype-staging", - "https://oss.sonatype.org/service/local/staging/deploy/maven2" - ) val mavenLocalFile = Resolver.file("Local Repository", userHome / ".m2" / "repository")(using Resolver.defaultPatterns ) diff --git a/main/src/main/scala/sbt/internal/LintUnused.scala b/main/src/main/scala/sbt/internal/LintUnused.scala index 7f786e18d..69a797082 100644 --- a/main/src/main/scala/sbt/internal/LintUnused.scala +++ b/main/src/main/scala/sbt/internal/LintUnused.scala @@ -50,6 +50,7 @@ object LintUnused { shellPrompt, sLog, traceLevel, + sonaDeploymentName, ), includeLintKeys := Set( scalacOptions, diff --git a/main/src/main/scala/sbt/internal/librarymanagement/Publishing.scala b/main/src/main/scala/sbt/internal/librarymanagement/Publishing.scala index 94dfd37a7..2c3fb9ceb 100644 --- a/main/src/main/scala/sbt/internal/librarymanagement/Publishing.scala +++ b/main/src/main/scala/sbt/internal/librarymanagement/Publishing.scala @@ -15,7 +15,9 @@ import sbt.internal.util.MessageOnlyException import sbt.io.IO import sbt.io.Path.contentOf import sbt.librarymanagement.ivy.Credentials -import sona.{ Sona, PublishingType } +import sona.{ PublishingType, Sona } + +import scala.concurrent.duration.FiniteDuration object Publishing { val sonaRelease: Command = @@ -36,23 +38,24 @@ object Publishing { bundlePath } - private def sonatypeReleaseAction(pt: PublishingType)(s0: State): State = { + private def sonatypeReleaseAction(publishingType: PublishingType)(s0: State): State = { import sbt.ProjectExtra.extract val extracted = Project.extract(s0) val log = extracted.get(Keys.sLog) - val dn = extracted.get(Keys.sonaDeploymentName) - val v = extracted.get(Keys.version) - if (v.endsWith("-SNAPSHOT")) { + val version = extracted.get(Keys.version) + if (version.endsWith("-SNAPSHOT")) { log.error("""SNAPSHOTs are not supported on the Central Portal; configure ThisBuild / publishTo to publish directly to the central-snapshots. see https://www.scala-sbt.org/1.x/docs/Using-Sonatype.html for details.""") s0.fail } else { + val deploymentName = extracted.get(Keys.sonaDeploymentName) + val uploadRequestTimeout = extracted.get(Keys.sonaUploadRequestTimeout) val (s1, bundle) = extracted.runTask(Keys.sonaBundle, s0) val (s2, creds) = extracted.runTask(Keys.credentials, s1) - val client = fromCreds(creds) + val client = fromCreds(creds, uploadRequestTimeout) try { - client.uploadBundle(bundle.toPath(), dn, pt, log) + client.uploadBundle(bundle.toPath(), deploymentName, publishingType, log) s2 } finally { client.close() @@ -60,10 +63,10 @@ see https://www.scala-sbt.org/1.x/docs/Using-Sonatype.html for details.""") } } - private def fromCreds(creds: Seq[Credentials]): Sona = { + private def fromCreds(creds: Seq[Credentials], uploadRequestTimeout: FiniteDuration): Sona = { val cred = Credentials .forHost(creds, Sona.host) .getOrElse(throw new MessageOnlyException(s"no credentials are found for ${Sona.host}")) - Sona.oauthClient(cred.userName, cred.passwd) + Sona.oauthClient(cred.userName, cred.passwd, uploadRequestTimeout) } } diff --git a/sbt b/sbt index ae9f076ae..9000ad3ac 100755 --- a/sbt +++ b/sbt @@ -1,7 +1,7 @@ #!/usr/bin/env bash set +e -declare builtin_sbt_version="1.11.0" +declare builtin_sbt_version="1.11.3" declare -a residual_args declare -a java_args declare -a scalac_args diff --git a/sbt-app/src/sbt-test/dependency-management/sbt-plugin-publish/build.sbt b/sbt-app/src/sbt-test/dependency-management/sbt-plugin-publish/build.sbt index b49b1a248..bd97aa61d 100644 --- a/sbt-app/src/sbt-test/dependency-management/sbt-plugin-publish/build.sbt +++ b/sbt-app/src/sbt-test/dependency-management/sbt-plugin-publish/build.sbt @@ -10,8 +10,8 @@ lazy val sbtPlugin1 = project.in(file("sbt-plugin-1")) name := "sbt-plugin-1", addSbtPlugin("ch.epfl.scala" % "sbt-plugin-example-diamond" % "0.5.0"), publishTo := Some(resolver), - checkPackagedArtifacts := checkPackagedArtifactsDef("sbt-plugin-1", true).value, - checkPublish := checkPublishDef("sbt-plugin-1", true).value + checkPackagedArtifacts := checkPackagedArtifactsDef("sbt-plugin-1", false).value, + checkPublish := checkPublishDef("sbt-plugin-1", false).value ) lazy val testMaven1 = project.in(file("test-maven-1"))