Skip to content

Commit c9d3b26

Browse files
authored
Merge pull request #2007 from MaciejG604/react-to-sonatype-response-codes
React to some HTTP responses
2 parents d0901ba + 815c93a commit c9d3b26

File tree

4 files changed

+145
-63
lines changed

4 files changed

+145
-63
lines changed

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

Lines changed: 112 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import coursier.publish.signing.logger.InteractiveSignerLogger
1212
import coursier.publish.signing.{GpgSigner, NopSigner, Signer}
1313
import coursier.publish.sonatype.SonatypeApi
1414
import coursier.publish.upload.logger.InteractiveUploadLogger
15-
import coursier.publish.upload.{DummyUpload, FileUpload, HttpURLConnectionUpload}
15+
import coursier.publish.upload.{DummyUpload, FileUpload, HttpURLConnectionUpload, Upload}
1616
import coursier.publish.{Content, Hooks, Pom, PublishRepository}
1717

1818
import java.io.{File, OutputStreamWriter}
@@ -58,10 +58,11 @@ import scala.cli.commands.{ScalaCommand, SpecificationLevel, WatchUtil}
5858
import scala.cli.config.{ConfigDb, Keys, PasswordOption, PublishCredentials}
5959
import scala.cli.errors.{
6060
FailedToSignFileError,
61+
InvalidSonatypePublishCredentials,
6162
MalformedChecksumsError,
62-
MissingConfigEntryError,
6363
MissingPublishOptionError,
64-
UploadError
64+
UploadError,
65+
WrongSonatypeServerError
6566
}
6667
import scala.cli.packaging.Library
6768
import scala.cli.publish.BouncycastleSignerMaker
@@ -76,12 +77,16 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers {
7677
override def scalaSpecificationLevel: SpecificationLevel = SpecificationLevel.EXPERIMENTAL
7778

7879
import scala.cli.commands.shared.HelpGroup.*
80+
7981
val primaryHelpGroups: Seq[HelpGroup] = Seq(Publishing, Signing, PGP)
8082
val hiddenHelpGroups: Seq[HelpGroup] = Seq(Scala, Java, Entrypoint, Dependency, Watch)
83+
8184
override def helpFormat: HelpFormat = super.helpFormat
8285
.withHiddenGroups(hiddenHelpGroups)
8386
.withPrimaryGroups(primaryHelpGroups)
87+
8488
override def group: String = HelpCommandGroup.Main.toString
89+
8590
override def sharedOptions(options: PublishOptions): Option[SharedOptions] =
8691
Some(options.shared)
8792

@@ -370,18 +375,22 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers {
370375
"publish.organization"
371376
))
372377
}
378+
373379
private def defaultName(workspace: os.Path, logger: Logger): String = {
374380
val name = workspace.last
375381
logger.message(
376382
s"Using directive publish.name not specified, using workspace directory name $name as default name"
377383
)
378384
name
379385
}
386+
380387
def defaultComputeVersion(mayDefaultToGitTag: Boolean): Option[ComputeVersion] =
381388
if (mayDefaultToGitTag) Some(ComputeVersion.GitTag(os.rel, dynVer = false, positions = Nil))
382389
else None
390+
383391
def defaultVersionError =
384392
new MissingPublishOptionError("version", "--project-version", "publish.version")
393+
385394
def defaultVersion: Either[BuildException, String] =
386395
Left(defaultVersionError)
387396

@@ -496,7 +505,8 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers {
496505
case None =>
497506
val computeVer = publishOptions.contextual(isCi).computeVersion.orElse {
498507
def isGitRepo = GitRepo.gitRepoOpt(workspace).isDefined
499-
val default = defaultComputeVersion(!isCi && isGitRepo)
508+
509+
val default = defaultComputeVersion(!isCi && isGitRepo)
500510
if (default.isDefined)
501511
logger.message(
502512
s"Using directive ${defaultVersionError.directiveName} not set, assuming git:tag as publish.computeVersion"
@@ -757,51 +767,50 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers {
757767

758768
val ec = builds.head.options.finalCache.ec
759769

760-
def authOpt(repo: String): Either[BuildException, Option[Authentication]] = either {
761-
val isHttps = {
762-
val uri = new URI(repo)
763-
uri.getScheme == "https"
764-
}
765-
val hostOpt = Option.when(isHttps)(new URI(repo).getHost)
766-
val maybeCredentials: Either[BuildException, Option[PublishCredentials]] = hostOpt match {
767-
case None => Right(None)
768-
case Some(host) =>
769-
configDb().get(Keys.publishCredentials).wrapConfigException.map { credListOpt =>
770-
credListOpt.flatMap { credList =>
771-
credList.find { cred =>
772-
cred.host == host &&
773-
(isHttps || cred.httpsOnly.contains(false))
770+
def authOpt(repo: String, isSonatype: Boolean): Either[BuildException, Option[Authentication]] =
771+
either {
772+
val isHttps = {
773+
val uri = new URI(repo)
774+
uri.getScheme == "https"
775+
}
776+
val hostOpt = Option.when(isHttps)(new URI(repo).getHost)
777+
val maybeCredentials: Either[BuildException, Option[PublishCredentials]] = hostOpt match {
778+
case None => Right(None)
779+
case Some(host) =>
780+
configDb().get(Keys.publishCredentials).wrapConfigException.map { credListOpt =>
781+
credListOpt.flatMap { credList =>
782+
credList.find { cred =>
783+
cred.host == host &&
784+
(isHttps || cred.httpsOnly.contains(false))
785+
}
774786
}
775787
}
776-
}
777-
}
778-
val isSonatype =
779-
hostOpt.exists(host => host == "oss.sonatype.org" || host.endsWith(".oss.sonatype.org"))
780-
val passwordOpt = publishOptions.contextual(isCi).repoPassword match {
781-
case None => value(maybeCredentials).flatMap(_.password)
782-
case other => other.map(_.toConfig)
783-
}
784-
passwordOpt.map(_.get()) match {
785-
case None => None
786-
case Some(password) =>
787-
val userOpt = publishOptions.contextual(isCi).repoUser match {
788-
case None => value(maybeCredentials).flatMap(_.user)
789-
case other => other.map(_.toConfig)
790-
}
791-
val realmOpt = publishOptions.contextual(isCi).repoRealm match {
792-
case None =>
793-
value(maybeCredentials)
794-
.flatMap(_.realm)
795-
.orElse {
796-
if (isSonatype) Some("Sonatype Nexus Repository Manager")
797-
else None
798-
}
799-
case other => other
800-
}
801-
val auth = Authentication(userOpt.fold("")(_.get().value), password.value)
802-
Some(realmOpt.fold(auth)(auth.withRealm))
788+
}
789+
val passwordOpt = publishOptions.contextual(isCi).repoPassword match {
790+
case None => value(maybeCredentials).flatMap(_.password)
791+
case other => other.map(_.toConfig)
792+
}
793+
passwordOpt.map(_.get()) match {
794+
case None => None
795+
case Some(password) =>
796+
val userOpt = publishOptions.contextual(isCi).repoUser match {
797+
case None => value(maybeCredentials).flatMap(_.user)
798+
case other => other.map(_.toConfig)
799+
}
800+
val realmOpt = publishOptions.contextual(isCi).repoRealm match {
801+
case None =>
802+
value(maybeCredentials)
803+
.flatMap(_.realm)
804+
.orElse {
805+
if (isSonatype) Some("Sonatype Nexus Repository Manager")
806+
else None
807+
}
808+
case other => other
809+
}
810+
val auth = Authentication(userOpt.fold("")(_.get().value), password.value)
811+
Some(realmOpt.fold(auth)(auth.withRealm))
812+
}
803813
}
804-
}
805814

806815
val repoParams = {
807816

@@ -837,32 +846,28 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers {
837846
}
838847
}
839848

849+
val isSonatype: Boolean = {
850+
val uri = new URI(repoParams.repo.snapshotRepo.root)
851+
val hostOpt = Option.when(uri.getScheme == "https")(uri.getHost)
852+
853+
hostOpt.exists(host => host == "oss.sonatype.org" || host.endsWith(".oss.sonatype.org"))
854+
}
855+
840856
val now = Instant.now()
841857
val (fileSet0, modVersionOpt) = value {
842858
it
843859
// TODO Allow to add test JARs to the main build artifacts
844860
.filter(_._1.scope != Scope.Test)
845861
.map {
846862
case (build, docBuildOpt) =>
847-
val isSonatype = {
848-
val hostOpt = {
849-
val repo = repoParams.repo.snapshotRepo.root
850-
val uri = new URI(repo)
851-
if (uri.getScheme == "https") Some(uri.getHost)
852-
else None
853-
}
854-
hostOpt.exists(host =>
855-
host == "oss.sonatype.org" || host.endsWith(".oss.sonatype.org")
856-
)
857-
}
858863
buildFileSet(
859864
build,
860865
docBuildOpt,
861866
workingDir,
862867
now,
863868
isIvy2LocalLike = repoParams.isIvy2LocalLike,
864869
isCi = isCi,
865-
isSonatype = isSonatype,
870+
isSonatype,
866871
logger
867872
)
868873
}
@@ -1029,17 +1034,32 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers {
10291034
if (repoParams.isIvy2LocalLike) fileSet2
10301035
else fileSet2.order(ec).unsafeRun()(ec)
10311036

1032-
val isSnapshot0 = modVersionOpt.exists(_._2.endsWith("SNAPSHOT"))
1033-
val authOpt0 = value(authOpt(repoParams.repo.repo(isSnapshot0).root))
1037+
val isSnapshot0 = modVersionOpt.exists(_._2.endsWith("SNAPSHOT"))
1038+
val authOpt0 = value(authOpt(repoParams.repo.repo(isSnapshot0).root, isSonatype))
1039+
val asciiRegex = """[\u0000-\u007f]*""".r
1040+
val usernameOnlyAscii = authOpt0.exists(auth => asciiRegex.matches(auth.user))
1041+
val passwordOnlyAscii = authOpt0.exists(_.passwordOpt.exists(pass => asciiRegex.matches(pass)))
1042+
10341043
if (repoParams.shouldAuthenticate && authOpt0.isEmpty)
10351044
logger.diagnostic(
10361045
"Publishing to a repository that needs authentication, but no credentials are available.",
10371046
Severity.Warning
10381047
)
1039-
val repoParams0 = repoParams.withAuth(authOpt0)
1048+
val repoParams0: RepoParams = repoParams.withAuth(authOpt0)
1049+
val isLegacySonatype = isSonatype && !repoParams0.repo.releaseRepo.root.contains("s01")
10401050
val hooksDataOpt = Option.when(!dummy) {
10411051
try repoParams0.hooks.beforeUpload(finalFileSet, isSnapshot0).unsafeRun()(ec)
10421052
catch {
1053+
case NonFatal(e)
1054+
if "Failed to get .*oss\\.sonatype\\.org.*/staging/profiles \\(http status: 403,".r.unanchored.matches(
1055+
e.getMessage
1056+
) =>
1057+
logger.exit(new WrongSonatypeServerError(isLegacySonatype))
1058+
case NonFatal(e)
1059+
if "Failed to get .*oss\\.sonatype\\.org.*/staging/profiles \\(http status: 401,".r.unanchored.matches(
1060+
e.getMessage
1061+
) =>
1062+
logger.exit(new InvalidSonatypePublishCredentials(usernameOnlyAscii, passwordOnlyAscii))
10431063
case NonFatal(e) =>
10441064
throw new Exception(e)
10451065
}
@@ -1087,6 +1107,37 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers {
10871107
}
10881108

10891109
errors.toList match {
1110+
case (h @ (_, _, e: Upload.Error.HttpError)) :: _
1111+
if isSonatype && errors.distinctBy(_._3.getMessage()).size == 1 =>
1112+
val httpCodeRegex = "HTTP (\\d+)\n.*".r
1113+
e.getMessage() match {
1114+
case httpCodeRegex("403") =>
1115+
logger.error(
1116+
s"""
1117+
|Uploading files failed!
1118+
|Possible causes:
1119+
|- no rights to publish under this organization
1120+
|- organization name is misspelled
1121+
| -> have you registered your organisation yet?
1122+
|""".stripMargin
1123+
)
1124+
case _ => throw new UploadError(::(h, Nil))
1125+
}
1126+
case _ :: _ if isSonatype && errors.forall {
1127+
case (_, _, _: Upload.Error.Unauthorized) => true
1128+
case _ => false
1129+
} =>
1130+
logger.error(
1131+
s"""
1132+
|Uploading files failed!
1133+
|Possible causes:
1134+
|- incorrect Sonatype credentials
1135+
|- incorrect Sonatype server was used, try ${
1136+
if isLegacySonatype then "'central-s01'" else "'central'"
1137+
}
1138+
| -> consult publish subcommand documentation
1139+
|""".stripMargin
1140+
)
10901141
case h :: t =>
10911142
value(Left(new UploadError(::(h, t))))
10921143
case Nil =>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package scala.cli.errors
2+
3+
import scala.build.errors.BuildException
4+
5+
final class InvalidSonatypePublishCredentials(usernameIsAscii: Boolean, passwordIsAscii: Boolean)
6+
extends BuildException(
7+
if (usernameIsAscii && passwordIsAscii)
8+
"Username or password to the publish repository are incorrect"
9+
else
10+
s"Your Sonatype ${InvalidSonatypePublishCredentials.isUsernameOrPassword(
11+
usernameIsAscii,
12+
passwordIsAscii
13+
)} unsupported characters"
14+
)
15+
16+
object InvalidSonatypePublishCredentials {
17+
def isUsernameOrPassword(usernameIsAscii: Boolean, passwordIsAscii: Boolean): String =
18+
if (!usernameIsAscii && !passwordIsAscii)
19+
"password and username contain"
20+
else if (!usernameIsAscii)
21+
"username contains"
22+
else
23+
"password contains"
24+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package scala.cli.errors
2+
3+
import scala.build.errors.BuildException
4+
5+
final class WrongSonatypeServerError(legacyChosen: Boolean)
6+
extends BuildException(
7+
s"Wrong Sonatype server, try ${if legacyChosen then "'central-s01'" else "'central'"}"
8+
)

modules/core/src/main/scala/scala/build/internals/OsLibc.scala

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,8 @@ object OsLibc {
7373
else s"temurin:$jvmVersion"
7474
}
7575

76-
def defaultJvm(os: String): String = {
76+
def defaultJvm(os: String): String =
7777
baseDefaultJvm(os, defaultJvmVersion)
78-
}
7978

8079
def javaVersion(javaCmd: String): Int = {
8180
val javaVersionOutput = os.proc(javaCmd, "-version").call(

0 commit comments

Comments
 (0)