@@ -12,7 +12,7 @@ import coursier.publish.signing.logger.InteractiveSignerLogger
12
12
import coursier .publish .signing .{GpgSigner , NopSigner , Signer }
13
13
import coursier .publish .sonatype .SonatypeApi
14
14
import coursier .publish .upload .logger .InteractiveUploadLogger
15
- import coursier .publish .upload .{DummyUpload , FileUpload , HttpURLConnectionUpload }
15
+ import coursier .publish .upload .{DummyUpload , FileUpload , HttpURLConnectionUpload , Upload }
16
16
import coursier .publish .{Content , Hooks , Pom , PublishRepository }
17
17
18
18
import java .io .{File , OutputStreamWriter }
@@ -58,10 +58,11 @@ import scala.cli.commands.{ScalaCommand, SpecificationLevel, WatchUtil}
58
58
import scala .cli .config .{ConfigDb , Keys , PasswordOption , PublishCredentials }
59
59
import scala .cli .errors .{
60
60
FailedToSignFileError ,
61
+ InvalidSonatypePublishCredentials ,
61
62
MalformedChecksumsError ,
62
- MissingConfigEntryError ,
63
63
MissingPublishOptionError ,
64
- UploadError
64
+ UploadError ,
65
+ WrongSonatypeServerError
65
66
}
66
67
import scala .cli .packaging .Library
67
68
import scala .cli .publish .BouncycastleSignerMaker
@@ -76,12 +77,16 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers {
76
77
override def scalaSpecificationLevel : SpecificationLevel = SpecificationLevel .EXPERIMENTAL
77
78
78
79
import scala .cli .commands .shared .HelpGroup .*
80
+
79
81
val primaryHelpGroups : Seq [HelpGroup ] = Seq (Publishing , Signing , PGP )
80
82
val hiddenHelpGroups : Seq [HelpGroup ] = Seq (Scala , Java , Entrypoint , Dependency , Watch )
83
+
81
84
override def helpFormat : HelpFormat = super .helpFormat
82
85
.withHiddenGroups(hiddenHelpGroups)
83
86
.withPrimaryGroups(primaryHelpGroups)
87
+
84
88
override def group : String = HelpCommandGroup .Main .toString
89
+
85
90
override def sharedOptions (options : PublishOptions ): Option [SharedOptions ] =
86
91
Some (options.shared)
87
92
@@ -370,18 +375,22 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers {
370
375
" publish.organization"
371
376
))
372
377
}
378
+
373
379
private def defaultName (workspace : os.Path , logger : Logger ): String = {
374
380
val name = workspace.last
375
381
logger.message(
376
382
s " Using directive publish.name not specified, using workspace directory name $name as default name "
377
383
)
378
384
name
379
385
}
386
+
380
387
def defaultComputeVersion (mayDefaultToGitTag : Boolean ): Option [ComputeVersion ] =
381
388
if (mayDefaultToGitTag) Some (ComputeVersion .GitTag (os.rel, dynVer = false , positions = Nil ))
382
389
else None
390
+
383
391
def defaultVersionError =
384
392
new MissingPublishOptionError (" version" , " --project-version" , " publish.version" )
393
+
385
394
def defaultVersion : Either [BuildException , String ] =
386
395
Left (defaultVersionError)
387
396
@@ -496,7 +505,8 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers {
496
505
case None =>
497
506
val computeVer = publishOptions.contextual(isCi).computeVersion.orElse {
498
507
def isGitRepo = GitRepo .gitRepoOpt(workspace).isDefined
499
- val default = defaultComputeVersion(! isCi && isGitRepo)
508
+
509
+ val default = defaultComputeVersion(! isCi && isGitRepo)
500
510
if (default.isDefined)
501
511
logger.message(
502
512
s " Using directive ${defaultVersionError.directiveName} not set, assuming git:tag as publish.computeVersion "
@@ -757,51 +767,50 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers {
757
767
758
768
val ec = builds.head.options.finalCache.ec
759
769
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
+ }
774
786
}
775
787
}
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
+ }
803
813
}
804
- }
805
814
806
815
val repoParams = {
807
816
@@ -837,32 +846,28 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers {
837
846
}
838
847
}
839
848
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
+
840
856
val now = Instant .now()
841
857
val (fileSet0, modVersionOpt) = value {
842
858
it
843
859
// TODO Allow to add test JARs to the main build artifacts
844
860
.filter(_._1.scope != Scope .Test )
845
861
.map {
846
862
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
- }
858
863
buildFileSet(
859
864
build,
860
865
docBuildOpt,
861
866
workingDir,
862
867
now,
863
868
isIvy2LocalLike = repoParams.isIvy2LocalLike,
864
869
isCi = isCi,
865
- isSonatype = isSonatype ,
870
+ isSonatype,
866
871
logger
867
872
)
868
873
}
@@ -1029,17 +1034,32 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers {
1029
1034
if (repoParams.isIvy2LocalLike) fileSet2
1030
1035
else fileSet2.order(ec).unsafeRun()(ec)
1031
1036
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
+
1034
1043
if (repoParams.shouldAuthenticate && authOpt0.isEmpty)
1035
1044
logger.diagnostic(
1036
1045
" Publishing to a repository that needs authentication, but no credentials are available." ,
1037
1046
Severity .Warning
1038
1047
)
1039
- val repoParams0 = repoParams.withAuth(authOpt0)
1048
+ val repoParams0 : RepoParams = repoParams.withAuth(authOpt0)
1049
+ val isLegacySonatype = isSonatype && ! repoParams0.repo.releaseRepo.root.contains(" s01" )
1040
1050
val hooksDataOpt = Option .when(! dummy) {
1041
1051
try repoParams0.hooks.beforeUpload(finalFileSet, isSnapshot0).unsafeRun()(ec)
1042
1052
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))
1043
1063
case NonFatal (e) =>
1044
1064
throw new Exception (e)
1045
1065
}
@@ -1087,6 +1107,37 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers {
1087
1107
}
1088
1108
1089
1109
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
+ )
1090
1141
case h :: t =>
1091
1142
value(Left (new UploadError (:: (h, t))))
1092
1143
case Nil =>
0 commit comments