Skip to content

Commit 8277d39

Browse files
authored
Merge pull request #3774 from Gedochao/feature/sonatype-central-ossrh-staging-api
`publish` command with Sonatype Central Portal OSSRH Staging API
2 parents 61fadd8 + 09ed46f commit 8277d39

File tree

3 files changed

+120
-79
lines changed

3 files changed

+120
-79
lines changed

modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,7 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers {
425425
now: Instant,
426426
isIvy2LocalLike: Boolean,
427427
isCi: Boolean,
428-
isLegacySonatype: Boolean,
428+
isSonatype: Boolean,
429429
withTestScope: Boolean,
430430
logger: Logger
431431
): Either[BuildException, (FileSet, (coursier.core.Module, String))] = either {
@@ -540,7 +540,7 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers {
540540
developers = developers
541541
)
542542

543-
if isLegacySonatype then {
543+
if isSonatype then {
544544
if url.isEmpty then
545545
logger.diagnostic(
546546
"Publishing to Sonatype, but project URL is empty (set it with the '//> using publish.url' directive)."
@@ -750,7 +750,7 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers {
750750
now = now,
751751
isIvy2LocalLike = repoParams.isIvy2LocalLike,
752752
isCi = isCi,
753-
isLegacySonatype = repoParams.isLegacySonatype,
753+
isSonatype = repoParams.isSonatype,
754754
withTestScope = withTestScope,
755755
logger = logger
756756
)
@@ -878,7 +878,7 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers {
878878
val isSnapshot0 = modVersionOpt.exists(_._2.endsWith("SNAPSHOT"))
879879
val authOpt0 = value(authOpt(
880880
repo = repoParams.repo.repo(isSnapshot0).root,
881-
isLegacySonatype = repoParams.isLegacySonatype
881+
isLegacySonatype = repoParams.isSonatype
882882
))
883883
val asciiRegex = """[\u0000-\u007f]*""".r
884884
val usernameOnlyAscii = authOpt0.exists(auth => asciiRegex.matches(auth.user))
@@ -891,7 +891,7 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers {
891891
)
892892
val repoParams0: RepoParams = repoParams.withAuth(authOpt0)
893893
val isLegacySonatype =
894-
repoParams0.isLegacySonatype && !repoParams0.repo.releaseRepo.root.contains("s01")
894+
repoParams0.isSonatype && !repoParams0.repo.releaseRepo.root.contains("s01")
895895
val hooksDataOpt = Option.when(!dummy) {
896896
try repoParams0.hooks.beforeUpload(finalFileSet, isSnapshot0).unsafeRun()(using ec)
897897
catch {
@@ -953,7 +953,7 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers {
953953

954954
errors.toList match {
955955
case (h @ (_, _, e: Upload.Error.HttpError)) :: _
956-
if repoParams0.isLegacySonatype && errors.distinctBy(_._3.getMessage()).size == 1 =>
956+
if repoParams0.isSonatype && errors.distinctBy(_._3.getMessage()).size == 1 =>
957957
val httpCodeRegex = "HTTP (\\d+)\n.*".r
958958
e.getMessage match {
959959
case httpCodeRegex("403") =>
@@ -968,7 +968,7 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers {
968968
)
969969
case _ => throw new UploadError(::(h, Nil))
970970
}
971-
case _ :: _ if repoParams0.isLegacySonatype && errors.forall {
971+
case _ :: _ if repoParams0.isSonatype && errors.forall {
972972
case (_, _, _: Upload.Error.Unauthorized) => true
973973
case _ => false
974974
} =>

modules/cli/src/main/scala/scala/cli/commands/publish/RepoParams.scala

Lines changed: 112 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import java.util.concurrent.ScheduledExecutorService
1212
import scala.build.EitherCps.{either, value}
1313
import scala.build.Logger
1414
import scala.build.errors.BuildException
15+
import scala.build.internals.ConsoleUtils.ScalaCliConsole.warnPrefix
1516
import scala.cli.commands.util.ScalaCliSttpBackend
1617

1718
final case class RepoParams(
@@ -25,6 +26,8 @@ final case class RepoParams(
2526
shouldSign: Boolean,
2627
shouldAuthenticate: Boolean
2728
) {
29+
import RepoParams.*
30+
2831
def withAuth(auth: Authentication): RepoParams =
2932
copy(
3033
repo = repo.withAuthentication(auth),
@@ -41,14 +44,25 @@ final case class RepoParams(
4144
)
4245
def withAuth(authOpt: Option[Authentication]): RepoParams = authOpt.fold(this)(withAuth)
4346

44-
lazy val isLegacySonatype: Boolean =
47+
lazy val isSonatype: Boolean =
4548
Option(new URI(repo.snapshotRepo.root))
4649
.filter(_.getScheme == "https")
4750
.map(_.getHost)
48-
.exists(host => host == "oss.sonatype.org" || host.endsWith(".oss.sonatype.org"))
51+
.exists(sonatypeHosts.contains)
4952
}
5053

5154
object RepoParams {
55+
private val sonatypeOssrhStagingApiBase = "https://ossrh-staging-api.central.sonatype.com"
56+
private val sonatypeSnapshotsBase = "https://central.sonatype.com/repository/maven-snapshots/"
57+
private val sonatypeLegacyBase = "https://oss.sonatype.org"
58+
private val sonatypeS01LegacyBase = "https://s01.oss.sonatype.org"
59+
private def sonatypeHosts: Seq[String] =
60+
Seq(
61+
sonatypeLegacyBase,
62+
sonatypeSnapshotsBase,
63+
sonatypeS01LegacyBase,
64+
sonatypeOssrhStagingApiBase
65+
).map(new URI(_).getHost)
5266

5367
def apply(
5468
repo: String,
@@ -67,24 +81,42 @@ object RepoParams {
6781
case "ivy2-local" =>
6882
RepoParams.ivy2Local(ivy2HomeOpt)
6983
case "sonatype" | "central" | "maven-central" | "mvn-central" =>
84+
logger.message(s"Using Portal OSSRH Staging API: $sonatypeOssrhStagingApiBase")
85+
RepoParams.centralRepo(
86+
base = sonatypeOssrhStagingApiBase,
87+
useLegacySnapshots = false,
88+
connectionTimeoutRetries = connectionTimeoutRetries,
89+
connectionTimeoutSeconds = connectionTimeoutSeconds,
90+
stagingRepoRetries = stagingRepoRetries,
91+
stagingRepoWaitTimeMilis = stagingRepoWaitTimeMilis,
92+
es = es,
93+
logger = logger
94+
)
95+
case "sonatype-legacy" | "central-legacy" | "maven-central-legacy" | "mvn-central-legacy" =>
96+
logger.message(s"$warnPrefix $sonatypeLegacyBase is EOL since 2025-06-30.")
97+
logger.message(s"$warnPrefix $sonatypeLegacyBase publishing is expected to fail.")
7098
RepoParams.centralRepo(
71-
"https://oss.sonatype.org",
72-
connectionTimeoutRetries,
73-
connectionTimeoutSeconds,
74-
stagingRepoRetries,
75-
stagingRepoWaitTimeMilis,
76-
es,
77-
logger
99+
base = sonatypeLegacyBase,
100+
useLegacySnapshots = true,
101+
connectionTimeoutRetries = connectionTimeoutRetries,
102+
connectionTimeoutSeconds = connectionTimeoutSeconds,
103+
stagingRepoRetries = stagingRepoRetries,
104+
stagingRepoWaitTimeMilis = stagingRepoWaitTimeMilis,
105+
es = es,
106+
logger = logger
78107
)
79108
case "sonatype-s01" | "central-s01" | "maven-central-s01" | "mvn-central-s01" =>
109+
logger.message(s"$warnPrefix $sonatypeS01LegacyBase is EOL since 2025-06-30.")
110+
logger.message(s"$warnPrefix it's expected publishing will fail.")
80111
RepoParams.centralRepo(
81-
"https://s01.oss.sonatype.org",
82-
connectionTimeoutRetries,
83-
connectionTimeoutSeconds,
84-
stagingRepoRetries,
85-
stagingRepoWaitTimeMilis,
86-
es,
87-
logger
112+
base = sonatypeS01LegacyBase,
113+
useLegacySnapshots = true,
114+
connectionTimeoutRetries = connectionTimeoutRetries,
115+
connectionTimeoutSeconds = connectionTimeoutSeconds,
116+
stagingRepoRetries = stagingRepoRetries,
117+
stagingRepoWaitTimeMilis = stagingRepoWaitTimeMilis,
118+
es = es,
119+
logger = logger
88120
)
89121
case "github" =>
90122
value(RepoParams.gitHubRepo(vcsUrlOpt, workspace, logger))
@@ -103,77 +135,86 @@ object RepoParams {
103135
}
104136

105137
RepoParams(
106-
PublishRepository.Simple(repo0),
107-
None,
108-
Hooks.dummy,
109-
isIvy2LocalLike,
110-
true,
111-
true,
112-
true,
113-
false,
114-
false
138+
repo = PublishRepository.Simple(repo0),
139+
targetRepoOpt = None,
140+
hooks = Hooks.dummy,
141+
isIvy2LocalLike = isIvy2LocalLike,
142+
defaultParallelUpload = true,
143+
supportsSig = true,
144+
acceptsChecksums = true,
145+
shouldSign = false,
146+
shouldAuthenticate = false
115147
)
116148
}
117149
}
118150

119151
def centralRepo(
120152
base: String,
153+
useLegacySnapshots: Boolean,
121154
connectionTimeoutRetries: Option[Int],
122155
connectionTimeoutSeconds: Option[Int],
123156
stagingRepoRetries: Option[Int],
124157
stagingRepoWaitTimeMilis: Option[Int],
125158
es: ScheduledExecutorService,
126159
logger: Logger
127-
) = {
128-
val repo0 = PublishRepository.Sonatype(MavenRepository(base))
160+
): RepoParams = {
161+
val repo0 = PublishRepository.Sonatype(
162+
base = MavenRepository(base),
163+
useLegacySnapshots = useLegacySnapshots
164+
)
129165
val backend = ScalaCliSttpBackend.httpURLConnection(logger, connectionTimeoutSeconds)
130166
val api = SonatypeApi(
131-
backend,
132-
base + "/service/local",
133-
None,
134-
logger.verbosity,
167+
backend = backend,
168+
base = base + "/service/local",
169+
authentication = None,
170+
verbosity = logger.verbosity,
135171
retryOnTimeout = connectionTimeoutRetries.getOrElse(3),
136-
stagingRepoRetryParams = EmaRetryParams(
137-
stagingRepoRetries.getOrElse(3),
138-
stagingRepoWaitTimeMilis.getOrElse(10 * 1000),
139-
2.0f
140-
)
172+
stagingRepoRetryParams =
173+
EmaRetryParams(
174+
attempts = stagingRepoRetries.getOrElse(3),
175+
initialWaitDurationMs = stagingRepoWaitTimeMilis.getOrElse(10 * 1000),
176+
factor = 2.0f
177+
)
141178
)
142179
val hooks0 = Hooks.sonatype(
143-
repo0,
144-
api,
145-
logger.compilerOutputStream, // meh
146-
logger.verbosity,
180+
repo = repo0,
181+
api = api,
182+
out = logger.compilerOutputStream, // meh
183+
verbosity = logger.verbosity,
147184
batch = coursier.paths.Util.useAnsiOutput(), // FIXME Get via logger
148-
es
185+
es = es
149186
)
150187
RepoParams(
151-
repo0,
152-
Some("https://repo1.maven.org/maven2"),
153-
hooks0,
154-
false,
155-
true,
156-
true,
157-
true,
158-
true,
159-
true
188+
repo = repo0,
189+
targetRepoOpt = Some("https://repo1.maven.org/maven2"),
190+
hooks = hooks0,
191+
isIvy2LocalLike = false,
192+
defaultParallelUpload = true,
193+
supportsSig = true,
194+
acceptsChecksums = true,
195+
shouldSign = true,
196+
shouldAuthenticate = true
160197
)
161198
}
162199

163-
def gitHubRepoFor(org: String, name: String) =
200+
def gitHubRepoFor(org: String, name: String): RepoParams =
164201
RepoParams(
165-
PublishRepository.Simple(MavenRepository(s"https://maven.pkg.github.com/$org/$name")),
166-
None,
167-
Hooks.dummy,
168-
false,
169-
false,
170-
false,
171-
false,
172-
false,
173-
true
202+
repo = PublishRepository.Simple(MavenRepository(s"https://maven.pkg.github.com/$org/$name")),
203+
targetRepoOpt = None,
204+
hooks = Hooks.dummy,
205+
isIvy2LocalLike = false,
206+
defaultParallelUpload = false,
207+
supportsSig = false,
208+
acceptsChecksums = false,
209+
shouldSign = false,
210+
shouldAuthenticate = true
174211
)
175212

176-
def gitHubRepo(vcsUrlOpt: Option[String], workspace: os.Path, logger: Logger) = either {
213+
def gitHubRepo(
214+
vcsUrlOpt: Option[String],
215+
workspace: os.Path,
216+
logger: Logger
217+
): Either[BuildException, RepoParams] = either {
177218
val orgNameFromVcsOpt = vcsUrlOpt.flatMap(GitRepo.maybeGhOrgName)
178219

179220
val (org, name) = orgNameFromVcsOpt match {
@@ -184,23 +225,23 @@ object RepoParams {
184225
gitHubRepoFor(org, name)
185226
}
186227

187-
def ivy2Local(ivy2HomeOpt: Option[os.Path]) = {
228+
def ivy2Local(ivy2HomeOpt: Option[os.Path]): RepoParams = {
188229
val home = ivy2HomeOpt
189230
.orElse(sys.props.get("ivy.home").map(prop => os.Path(prop)))
190231
.orElse(sys.props.get("user.home").map(prop => os.Path(prop) / ".ivy2"))
191232
.getOrElse(os.home / ".ivy2")
192233
val base = home / "local"
193234
// not really a Maven repo…
194235
RepoParams(
195-
PublishRepository.Simple(MavenRepository(base.toNIO.toUri.toASCIIString)),
196-
None,
197-
Hooks.dummy,
198-
true,
199-
true,
200-
true,
201-
true,
202-
false,
203-
false
236+
repo = PublishRepository.Simple(MavenRepository(base.toNIO.toUri.toASCIIString)),
237+
targetRepoOpt = None,
238+
hooks = Hooks.dummy,
239+
isIvy2LocalLike = true,
240+
defaultParallelUpload = true,
241+
supportsSig = true,
242+
acceptsChecksums = true,
243+
shouldSign = false,
244+
shouldAuthenticate = false
204245
)
205246
}
206247

project/deps/package.mill.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ object Deps {
115115
def coursier = coursierDefault
116116
def coursierCli = coursierDefault
117117
def coursierM1Cli = coursierDefault
118-
def coursierPublish = "0.3.0"
118+
def coursierPublish = "0.4.0"
119119
def jmh = "1.37"
120120
def jsoniterScalaJava8 = "2.13.5.2"
121121
def jsoup = "1.21.1"

0 commit comments

Comments
 (0)