From d97373454fbb3dedb4f94982525e7e108d28308a Mon Sep 17 00:00:00 2001 From: Roberto Tyley <52038+rtyley@users.noreply.github.com> Date: Thu, 29 May 2025 16:18:28 +0100 Subject: [PATCH 01/13] Add `sonaDeploymentName` to `excludeLintKeys` sbt added the `sonaDeploymentName` key with https://github.com/sbt/sbt/pull/8126, released with https://github.com/sbt/sbt/releases/tag/v1.11.0 - in use, this seems to be getting lint warnings (as introduced by https://github.com/sbt/sbt/pull/5153): ``` [warn] there's a key that's not used by any other settings/tasks: [warn] [warn] * scala-collection-plus / sonaDeploymentName [warn] +- /home/runner/work/scala-collection-plus/scala-collection-plus/build.sbt:3 [warn] [warn] note: a setting might still be used by a command; to exclude a key from this `lintUnused` check [warn] either append it to `Global / excludeLintKeys` or call .withRank(KeyRanks.Invisible) on the key ``` https://github.com/rtyley/scala-collection-plus/actions/runs/15326291872/job/43121748535#step:6:19 ...https://github.com/sbt/sbt/pull/5153 does say that "notable exceptions are settings used exclusively by a command" - I _think_ that maybe `sonaDeploymentName` is only used by the commands `sonaRelease` & `sonaUpload`, so it's necessary to add it to `excludeLintKeys`? --- main/src/main/scala/sbt/internal/LintUnused.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/main/src/main/scala/sbt/internal/LintUnused.scala b/main/src/main/scala/sbt/internal/LintUnused.scala index 8df675c15..d41169be7 100644 --- a/main/src/main/scala/sbt/internal/LintUnused.scala +++ b/main/src/main/scala/sbt/internal/LintUnused.scala @@ -44,6 +44,7 @@ object LintUnused { serverConnectionType, serverIdleTimeout, shellPrompt, + sonaDeploymentName, ), includeLintKeys := Set( scalacOptions, From d93e1226e3e11982c749a75f73b5a5bf15c35b57 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Sun, 1 Jun 2025 17:53:33 -0400 Subject: [PATCH 02/13] ci: Use Central Portal for publishing --- build.sbt | 5 +++-- project/build.properties | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/build.sbt b/build.sbt index ea8b89e18..acdea896c 100644 --- a/build.sbt +++ b/build.sbt @@ -1494,7 +1494,8 @@ ThisBuild / pomIncludeRepository := { _ => false } 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/" + if (isSnapshot.value) Some("central-snapshots" at centralSnapshots) + else localStaging.value } ThisBuild / publishMavenStyle := true diff --git a/project/build.properties b/project/build.properties index 73df629ac..6520f6981 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.10.7 +sbt.version=1.11.0 From 2036ce6836b6666da5d8e4031d8c0bf9736f7b46 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Sun, 1 Jun 2025 22:27:07 -0400 Subject: [PATCH 03/13] fix: Default sbtPluginPublishLegacyMavenStyle to false **Problem** Central Portal no longer supports the legacy sbt plugin layout. **Solution** Make the default more friendly by setting sbtPluginPublishLegacyMavenStyle to false. --- build.sbt | 6 ++++-- main/src/main/scala/sbt/Defaults.scala | 2 +- .../dependency-management/sbt-plugin-publish/build.sbt | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/build.sbt b/build.sbt index acdea896c..47c94aebd 100644 --- a/build.sbt +++ b/build.sbt @@ -11,7 +11,7 @@ import scala.util.Try // ThisBuild settings take lower precedence, // but can be shared across the multi projects. ThisBuild / version := { - val v = "1.11.0-SNAPSHOT" + val v = "1.11.1-SNAPSHOT" nightlyVersion.getOrElse(v) } ThisBuild / version2_13 := "2.0.0-SNAPSHOT" @@ -671,6 +671,7 @@ lazy val dependencyTreeProj = (project in file("dependency-tree")) crossScalaVersions := Seq(baseScalaVersion), name := "sbt-dependency-tree", publishMavenStyle := true, + sbtPluginPublishLegacyMavenStyle := false, // mimaSettings, mimaPreviousArtifacts := Set.empty, ) @@ -1495,7 +1496,8 @@ ThisBuild / pomIncludeRepository := { _ => } ThisBuild / publishTo := { val centralSnapshots = "https://central.sonatype.com/repository/maven-snapshots/" - if (isSnapshot.value) Some("central-snapshots" at centralSnapshots) + val v = (ThisBuild / version).value + if (v.endsWith("SNAPSHOT")) Some("central-snapshots" at centralSnapshots) else localStaging.value } ThisBuild / publishMavenStyle := true diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index e6a57844d..04f555768 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -2986,7 +2986,7 @@ object Classpaths { Defaults.globalDefaults( Seq( publishMavenStyle :== true, - sbtPluginPublishLegacyMavenStyle :== true, + sbtPluginPublishLegacyMavenStyle :== false, publishArtifact :== true, (Test / publishArtifact) :== false ) 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")) From 3e25bd3e4696528c5eee9730af404f6db24d6101 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Sun, 1 Jun 2025 23:14:58 -0400 Subject: [PATCH 04/13] deps: Update lm-coursier-shaded 2.1.9 --- project/Dependencies.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index ce443cb03..2d69ed3be 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -77,7 +77,7 @@ object Dependencies { def addSbtZincCompile = addSbtModule(sbtZincPath, "zincCompile", zincCompile) def addSbtZincCompileCore = addSbtModule(sbtZincPath, "zincCompileCore", zincCompileCore) - val lmCoursierShaded = "io.get-coursier" %% "lm-coursier-shaded" % "2.1.8" + val lmCoursierShaded = "io.get-coursier" %% "lm-coursier-shaded" % "2.1.9" def sjsonNew(n: String) = Def.setting("com.eed3si9n" %% n % "0.10.1") // contrabandSjsonNewVersion.value From d00431745828b6b67b1564226096aa1252df8255 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Mon, 2 Jun 2025 00:20:11 -0400 Subject: [PATCH 05/13] sbt 1.11.1 --- sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sbt b/sbt index 36f85dbf5..78aa4a2ca 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.1" declare -a residual_args declare -a java_args declare -a scalac_args From 575228a6394e148fac016f7ae0d49b1d08b9dc13 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Tue, 3 Jun 2025 06:41:12 -0400 Subject: [PATCH 06/13] lm-coursier 2.1.10 --- project/Dependencies.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 2d69ed3be..1f2529776 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -77,7 +77,7 @@ object Dependencies { def addSbtZincCompile = addSbtModule(sbtZincPath, "zincCompile", zincCompile) def addSbtZincCompileCore = addSbtModule(sbtZincPath, "zincCompileCore", zincCompileCore) - val lmCoursierShaded = "io.get-coursier" %% "lm-coursier-shaded" % "2.1.9" + val lmCoursierShaded = "io.get-coursier" %% "lm-coursier-shaded" % "2.1.10" def sjsonNew(n: String) = Def.setting("com.eed3si9n" %% n % "0.10.1") // contrabandSjsonNewVersion.value From 009efd64223658f830e6b4c0842d9f2bbae5d89f Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Sat, 7 Jun 2025 03:00:43 -0400 Subject: [PATCH 07/13] update to lm 1.11.2 and deprecate sonatypeOssSnapshots --- main/src/main/scala/sbt/Opts.scala | 43 +++++++++++++++++++++++++++--- project/Dependencies.scala | 2 +- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/main/src/main/scala/sbt/Opts.scala b/main/src/main/scala/sbt/Opts.scala index ca271448d..40281553b 100644 --- a/main/src/main/scala/sbt/Opts.scala +++ b/main/src/main/scala/sbt/Opts.scala @@ -16,6 +16,7 @@ import java.net.URL import sbt.io.Path import Path._ +import scala.annotation.nowarn /** Options for well-known tasks. */ object Opts { @@ -42,22 +43,52 @@ object Opts { } object resolver { import sbt.io.syntax._ - @deprecated("Use sonatypeOssReleases instead", "1.7.0") + @deprecated("Sonatype OSS Repository Hosting (OSSRH) will be sunset on 2025-06-30", "1.7.0") val sonatypeReleases = Resolver.sonatypeRepo("releases") + + @deprecated("Sonatype OSS Repository Hosting (OSSRH) will be sunset on 2025-06-30", "1.11.2") val sonatypeOssReleases = Resolver.sonatypeOssRepos("releases") - @deprecated("Use sonatypeOssSnapshots instead", "1.7.0") + @deprecated( + """Sonatype OSS Repository Hosting (OSSRH) will be sunset on 2025-06-30; use the following instead: + resolvers += Resolver.sonatypeCentralSnapshots""", + "1.7.0" + ) val sonatypeSnapshots = Resolver.sonatypeRepo("snapshots") + + @deprecated( + """Sonatype OSS Repository Hosting (OSSRH) will be sunset on 2025-06-30; use the following instead: + resolvers += Resolver.sonatypeCentralSnapshots""", + "1.11.2" + ) val sonatypeOssSnapshots = Resolver.sonatypeOssRepos("snapshots") + @deprecated( + """Sonatype OSS Repository Hosting (OSSRH) will be sunset on 2025-06-30; use the following instead: + publishTo := { + if (isSnapshot.value) Some(Resolver.sonatypeCentralSnapshots) + else localStaging.value + }""", + "1.11.2" + ) val sonatypeStaging = MavenRepository( "sonatype-staging", "https://oss.sonatype.org/service/local/staging/deploy/maven2" ) + val mavenLocalFile = Resolver.file("Local Repository", userHome / ".m2" / "repository")( Resolver.defaultPatterns ) + @deprecated( + """Bintray was shut down""", + "1.11.2" + ) val sbtSnapshots = Resolver.bintrayRepo("sbt", "maven-snapshots") + + @deprecated( + """Bintray was shut down""", + "1.11.2" + ) val sbtIvySnapshots = Resolver.bintrayIvyRepo("sbt", "ivy-snapshots") } } @@ -77,10 +108,14 @@ object DefaultOptions { doc.title(name) ++ doc.version(version) def resolvers(snapshot: Boolean): Vector[Resolver] = { - if (snapshot) Vector(resolver.sbtSnapshots) else Vector.empty + if (snapshot) Vector(resolver.sbtSnapshots: @nowarn("cat=deprecation")) else Vector.empty } def pluginResolvers(plugin: Boolean, snapshot: Boolean): Vector[Resolver] = { - if (plugin && snapshot) Vector(resolver.sbtSnapshots, resolver.sbtIvySnapshots) + if (plugin && snapshot) + Vector( + resolver.sbtSnapshots: @nowarn("cat=deprecation"), + resolver.sbtIvySnapshots: @nowarn("cat=deprecation"), + ) else Vector.empty } def addResolvers: Setting[_] = Keys.resolvers ++= { resolvers(Keys.isSnapshot.value) } diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 1f2529776..6564f942b 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -14,7 +14,7 @@ object Dependencies { // sbt modules private val ioVersion = nightlyVersion.getOrElse("1.10.5") private val lmVersion = - sys.props.get("sbt.build.lm.version").orElse(nightlyVersion).getOrElse("1.11.0") + sys.props.get("sbt.build.lm.version").orElse(nightlyVersion).getOrElse("1.11.2") val zincVersion = nightlyVersion.getOrElse("1.10.8") private val sbtIO = "org.scala-sbt" %% "io" % ioVersion From 0045c34d28fbf483b679b79138bacf3dd09ba1e4 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Sat, 7 Jun 2025 16:19:15 -0400 Subject: [PATCH 08/13] sbt 1.11.2 --- sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sbt b/sbt index 78aa4a2ca..46894c7b6 100755 --- a/sbt +++ b/sbt @@ -1,7 +1,7 @@ #!/usr/bin/env bash set +e -declare builtin_sbt_version="1.11.1" +declare builtin_sbt_version="1.11.2" declare -a residual_args declare -a java_args declare -a scalac_args From 1cd9c1ec64f22dc81f25bd491e0d5c265f20eb97 Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Wed, 4 Jun 2025 17:16:02 +0100 Subject: [PATCH 09/13] fix: semanticdb expects Wildcard imports to be in the last position Signed-off-by: Leonidas Spyropoulos --- main-actions/src/main/scala/sbt/internal/sona/Sona.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2b3e1d88a..63d6e6591 100644 --- a/main-actions/src/main/scala/sbt/internal/sona/Sona.scala +++ b/main-actions/src/main/scala/sbt/internal/sona/Sona.scala @@ -168,7 +168,7 @@ object Sona { } object SonaClient { - import sbt.internal.sona.codec.JsonProtocol.{ *, given } + import sbt.internal.sona.codec.JsonProtocol.{ given, * } val host: String = "central.sonatype.com" val baseUrl: String = s"https://$host/api/v1" val asJson: FullResponse => JValue = (r: FullResponse) => From c3925c85ef4fd96be7f521de737773717e04241b Mon Sep 17 00:00:00 2001 From: Dmitrii Naumenko Date: Thu, 19 Jun 2025 14:18:41 +0200 Subject: [PATCH 10/13] fix: do not treat values "1.x" of "pluginCrossBuild/sbtBinaryVersion" as sbt 2 (fixes #8166) As a consequence, this fixes artifact name contraction for maven style and thus fixes publishing to Sonatype when a cross-built sbt version is different from 1.0 --- main/src/main/scala/sbt/Defaults.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 04f555768..afe752b21 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -2996,7 +2996,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, From 084ca08f34f64b1467fac45cb35f5491c95e48ef Mon Sep 17 00:00:00 2001 From: Jules Ivanic Date: Wed, 2 Jul 2025 13:53:50 +0400 Subject: [PATCH 11/13] Allow users to configure the timeout when publishing to the Maven Central repo --- .../main/scala/sbt/internal/sona/Sona.scala | 24 ++++++++++--------- main/src/main/scala/sbt/Defaults.scala | 1 + main/src/main/scala/sbt/Keys.scala | 1 + .../librarymanagement/Publishing.scala | 21 +++++++++------- 4 files changed, 27 insertions(+), 20 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 63d6e6591..65369ce87 100644 --- a/main-actions/src/main/scala/sbt/internal/sona/Sona.scala +++ b/main-actions/src/main/scala/sbt/internal/sona/Sona.scala @@ -36,13 +36,16 @@ class Sona(client: SonaClient) extends AutoCloseable { def close(): Unit = client.close() } -class SonaClient(reqTransform: Request => Request) extends AutoCloseable { +class SonaClient(reqTransform: Request => Request, requestTimeout: FiniteDuration) + extends AutoCloseable { import SonaClient.baseUrl - val gigahorseConfig = Gigahorse.config - .withRequestTimeout(2.minute) - .withReadTimeout(2.minute) - val http = Gigahorse.http(gigahorseConfig) + private val gigahorseConfig = Gigahorse.config + .withRequestTimeout(requestTimeout) + .withReadTimeout(requestTimeout) + + private val http = Gigahorse.http(gigahorseConfig) + def uploadBundle( bundleZipPath: Path, deploymentName: String, @@ -66,7 +69,6 @@ class SonaClient(reqTransform: Request => Request) extends AutoCloseable { FormPart("bundle", bundleZipPath.toFile()) ) ) - .withRequestTimeout(600.second) http.run(reqTransform(req), Gigahorse.asString) } awaitWithMessage(res, "uploading...", log) @@ -155,7 +157,7 @@ class SonaClient(reqTransform: Request => Request) extends AutoCloseable { }.foreach(_ => loop(attempt + 1)) } else () loop(0) - Await.result(f, 600.seconds) + Await.result(f, requestTimeout + 5.seconds) } def close(): Unit = http.close() @@ -163,8 +165,8 @@ class SonaClient(reqTransform: Request => Request) extends AutoCloseable { 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, requestTimeout: FiniteDuration): Sona = + new Sona(SonaClient.oauthClient(userName, userToken, requestTimeout)) } object SonaClient { @@ -175,8 +177,8 @@ 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, requestTimeout: FiniteDuration): SonaClient = + new SonaClient(OAuthClient(userName, userToken), requestTimeout) } 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 afe752b21..79001b1b3 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -3108,6 +3108,7 @@ object Classpaths { val uuid = UUID.randomUUID().toString().take(8) s"$o:$v:$uuid" }, + sonaRequestTimeout := 2.minutes, ) @nowarn("cat=deprecation") diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index 661199c84..a509d856d 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -571,6 +571,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 sonaRequestTimeout = 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/internal/librarymanagement/Publishing.scala b/main/src/main/scala/sbt/internal/librarymanagement/Publishing.scala index eca6e2219..99221e98a 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,22 +38,23 @@ object Publishing { bundlePath } - private def sonatypeReleaseAction(pt: PublishingType)(s0: State): State = { + private def sonatypeReleaseAction(publishingType: PublishingType)(s0: State): State = { 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 requestTimeout = extracted.get(Keys.sonaRequestTimeout) val (s1, bundle) = extracted.runTask(Keys.sonaBundle, s0) val (s2, creds) = extracted.runTask(Keys.credentials, s1) - val client = fromCreds(creds) + val client = fromCreds(creds, requestTimeout) try { - client.uploadBundle(bundle.toPath(), dn, pt, log) + client.uploadBundle(bundle.toPath(), deploymentName, publishingType, log) s2 } finally { client.close() @@ -59,10 +62,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], requestTimeout: 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, requestTimeout) } } From 8c0010a086a438c9d0d715b4ae48fc9547a011e2 Mon Sep 17 00:00:00 2001 From: Jules Ivanic Date: Fri, 4 Jul 2025 16:31:31 +0400 Subject: [PATCH 12/13] Review: the new configurable request timeout value should be `10.minutes` and should only be used for upload --- .../main/scala/sbt/internal/sona/Sona.scala | 95 ++++++++++++------- main/src/main/scala/sbt/Defaults.scala | 2 +- main/src/main/scala/sbt/Keys.scala | 2 +- .../librarymanagement/Publishing.scala | 8 +- 4 files changed, 66 insertions(+), 41 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 65369ce87..2536176c1 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,15 +38,19 @@ class Sona(client: SonaClient) extends AutoCloseable { def close(): Unit = client.close() } -class SonaClient(reqTransform: Request => Request, requestTimeout: FiniteDuration) +class SonaClient(reqTransform: Request => Request, uploadRequestTimeout: FiniteDuration) extends AutoCloseable { import SonaClient.baseUrl - private val gigahorseConfig = Gigahorse.config - .withRequestTimeout(requestTimeout) - .withReadTimeout(requestTimeout) + private val http = { + val defaultHttpRequestTimeout = 2.minutes - private val http = Gigahorse.http(gigahorseConfig) + val gigahorseConfig = Gigahorse.config + .withRequestTimeout(defaultHttpRequestTimeout) + .withReadTimeout(defaultHttpRequestTimeout) + + Gigahorse.http(gigahorseConfig) + } def uploadBundle( bundleZipPath: Path, @@ -52,7 +58,12 @@ class SonaClient(reqTransform: Request => Request, requestTimeout: FiniteDuratio 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( @@ -69,12 +80,13 @@ class SonaClient(reqTransform: Request => Request, requestTimeout: FiniteDuratio FormPart("bundle", bundleZipPath.toFile()) ) ) + .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") @@ -110,16 +122,16 @@ class SonaClient(reqTransform: Request => Request, requestTimeout: FiniteDuratio } } - 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) @@ -130,43 +142,52 @@ class SonaClient(reqTransform: Request => Request, requestTimeout: FiniteDuratio /** 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, requestTimeout + 5.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, requestTimeout: FiniteDuration): Sona = - new Sona(SonaClient.oauthClient(userName, userToken, requestTimeout)) + def oauthClient(userName: String, userToken: String, uploadRequestTimeout: FiniteDuration): Sona = + new Sona(SonaClient.oauthClient(userName, userToken, uploadRequestTimeout)) } object SonaClient { @@ -177,8 +198,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, requestTimeout: FiniteDuration): SonaClient = - new SonaClient(OAuthClient(userName, userToken), requestTimeout) + 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 79001b1b3..5fbd07191 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -3108,7 +3108,7 @@ object Classpaths { val uuid = UUID.randomUUID().toString().take(8) s"$o:$v:$uuid" }, - sonaRequestTimeout := 2.minutes, + sonaUploadRequestTimeout := 10.minutes, ) @nowarn("cat=deprecation") diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index a509d856d..5c78976ee 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -571,7 +571,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 sonaRequestTimeout = settingKey[FiniteDuration]("Request timeout for Sonatype publishing").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/internal/librarymanagement/Publishing.scala b/main/src/main/scala/sbt/internal/librarymanagement/Publishing.scala index 99221e98a..6ee7071f6 100644 --- a/main/src/main/scala/sbt/internal/librarymanagement/Publishing.scala +++ b/main/src/main/scala/sbt/internal/librarymanagement/Publishing.scala @@ -49,10 +49,10 @@ see https://www.scala-sbt.org/1.x/docs/Using-Sonatype.html for details.""") s0.fail } else { val deploymentName = extracted.get(Keys.sonaDeploymentName) - val requestTimeout = extracted.get(Keys.sonaRequestTimeout) + 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, requestTimeout) + val client = fromCreds(creds, uploadRequestTimeout) try { client.uploadBundle(bundle.toPath(), deploymentName, publishingType, log) s2 @@ -62,10 +62,10 @@ see https://www.scala-sbt.org/1.x/docs/Using-Sonatype.html for details.""") } } - private def fromCreds(creds: Seq[Credentials], requestTimeout: FiniteDuration): 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, requestTimeout) + Sona.oauthClient(cred.userName, cred.passwd, uploadRequestTimeout) } } From 2293bddfefe382a48c1672df4e009bd7c5df32f4 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Sat, 5 Jul 2025 15:17:39 -0500 Subject: [PATCH 13/13] sbt 1.11.3 --- project/build.properties | 2 +- sbt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/project/build.properties b/project/build.properties index 6520f6981..bbb0b608c 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.11.0 +sbt.version=1.11.2 diff --git a/sbt b/sbt index 46894c7b6..46727548b 100755 --- a/sbt +++ b/sbt @@ -1,7 +1,7 @@ #!/usr/bin/env bash set +e -declare builtin_sbt_version="1.11.2" +declare builtin_sbt_version="1.11.3" declare -a residual_args declare -a java_args declare -a scalac_args