@@ -12,7 +12,7 @@ import coursier.publish.signing.logger.InteractiveSignerLogger
1212import coursier .publish .signing .{GpgSigner , NopSigner , Signer }
1313import coursier .publish .sonatype .SonatypeApi
1414import coursier .publish .upload .logger .InteractiveUploadLogger
15- import coursier .publish .upload .{DummyUpload , FileUpload , HttpURLConnectionUpload }
15+ import coursier .publish .upload .{DummyUpload , FileUpload , HttpURLConnectionUpload , Upload }
1616import coursier .publish .{Content , Hooks , Pom , PublishRepository }
1717
1818import java .io .{File , OutputStreamWriter }
@@ -58,10 +58,11 @@ import scala.cli.commands.{ScalaCommand, SpecificationLevel, WatchUtil}
5858import scala .cli .config .{ConfigDb , Keys , PasswordOption , PublishCredentials }
5959import scala .cli .errors .{
6060 FailedToSignFileError ,
61+ InvalidSonatypePublishCredentials ,
6162 MalformedChecksumsError ,
62- MissingConfigEntryError ,
6363 MissingPublishOptionError ,
64- UploadError
64+ UploadError ,
65+ WrongSonatypeServerError
6566}
6667import scala .cli .packaging .Library
6768import 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 =>
0 commit comments