Skip to content

Commit fcb6877

Browse files
Merge pull request #1533 from alexarchambault/publish-credentials
Add publish.credentials config key, use it to publish
2 parents b7c1c57 + e75d284 commit fcb6877

File tree

23 files changed

+585
-243
lines changed

23 files changed

+585
-243
lines changed

modules/cli/src/main/scala/scala/cli/commands/config/Config.scala

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,14 @@ import scala.cli.commands.ScalaCommand
1212
import scala.cli.commands.publish.ConfigUtil.*
1313
import scala.cli.commands.util.CommonOps.*
1414
import scala.cli.commands.util.JvmUtils
15-
import scala.cli.config.{ConfigDb, Keys, PasswordOption, RepositoryCredentials, Secret}
15+
import scala.cli.config.{
16+
ConfigDb,
17+
Keys,
18+
PasswordOption,
19+
PublishCredentials,
20+
RepositoryCredentials,
21+
Secret
22+
}
1623

1724
object Config extends ScalaCommand[ConfigOptions] {
1825
override def hidden = true
@@ -188,6 +195,32 @@ object Config extends ScalaCommand[ConfigOptions] {
188195
val newValue = credentials :: previousValueOpt.getOrElse(Nil)
189196
db.set(Keys.repositoryCredentials, newValue)
190197
}
198+
199+
case Keys.publishCredentials =>
200+
val (host, rawUser, rawPassword, realmOpt) = values match {
201+
case Seq(host, rawUser, rawPassword) => (host, rawUser, rawPassword, None)
202+
case Seq(host, rawUser, rawPassword, realm) =>
203+
(host, rawUser, rawPassword, Some(realm))
204+
case _ =>
205+
System.err.println(
206+
s"Usage: $progName config ${Keys.publishCredentials.fullName} host user password [realm]"
207+
)
208+
System.err.println(
209+
"Note that user and password are assumed to be secrets, specified like value:... or env:ENV_VAR_NAME, see https://scala-cli.virtuslab.org/docs/reference/password-options for more details"
210+
)
211+
sys.exit(1)
212+
}
213+
val (userOpt, passwordOpt) = (parseSecret(rawUser), parseSecret(rawPassword))
214+
.traverseN
215+
.left.map(CompositeBuildException(_))
216+
.orExit(logger)
217+
val credentials =
218+
PublishCredentials(host, userOpt, passwordOpt, realm = realmOpt)
219+
val previousValueOpt =
220+
db.get(Keys.publishCredentials).wrapConfigException.orExit(logger)
221+
val newValue = credentials :: previousValueOpt.getOrElse(Nil)
222+
db.set(Keys.publishCredentials, newValue)
223+
191224
case _ =>
192225
val finalValues =
193226
if (options.passwordValue && entry.isPasswordOption)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ trait OptionCheck {
2222
/** Provides a way to compute a default value for this option, along with extra directives and
2323
* GitHub secrets to be set
2424
*/
25-
def defaultValue(): Either[BuildException, OptionCheck.DefaultValue]
25+
def defaultValue(pubOpt: BPublishOptions): Either[BuildException, OptionCheck.DefaultValue]
2626
}
2727

2828
object OptionCheck {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ object OptionChecks {
2323
NameCheck(options, workspace, logger),
2424
ComputeVersionCheck(options, workspace, logger),
2525
RepositoryCheck(options, logger),
26-
UserCheck(options, () => configDb, logger),
27-
PasswordCheck(options, () => configDb, logger),
26+
UserCheck(options, () => configDb, workspace, logger),
27+
PasswordCheck(options, () => configDb, workspace, logger),
2828
PgpSecretKeyCheck(options, coursierCache, () => configDb, logger, backend),
2929
LicenseCheck(options, logger),
3030
UrlCheck(options, workspace, logger),

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

Lines changed: 72 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ import scala.cli.commands.{
4848
SharedPythonOptions,
4949
WatchUtil
5050
}
51-
import scala.cli.config.{ConfigDb, Keys}
51+
import scala.cli.config.{ConfigDb, Keys, PublishCredentials}
5252
import scala.cli.errors.{
5353
FailedToSignFileError,
5454
MalformedChecksumsError,
@@ -661,16 +661,6 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers {
661661
(fileSet, (mod, ver))
662662
}
663663

664-
private final case class RepoParams(
665-
repo: PublishRepository,
666-
targetRepoOpt: Option[String],
667-
hooks: Hooks,
668-
isIvy2LocalLike: Boolean,
669-
defaultParallelUpload: Boolean,
670-
supportsSig: Boolean,
671-
acceptsChecksums: Boolean
672-
)
673-
674664
private def doPublish(
675665
builds: Seq[Build.Successful],
676666
docBuilds: Seq[Build.Successful],
@@ -697,150 +687,79 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers {
697687

698688
val ec = builds.head.options.finalCache.ec
699689

700-
val repoParams = {
701-
702-
lazy val es =
703-
Executors.newSingleThreadScheduledExecutor(Util.daemonThreadFactory("publish-retry"))
704-
705-
def authOpt(repo: String): Either[BuildException, Option[Authentication]] = either {
706-
val hostOpt = {
707-
val uri = new URI(repo)
708-
if (uri.getScheme == "https") Some(uri.getHost)
709-
else None
710-
}
711-
val isSonatype =
712-
hostOpt.exists(host => host == "oss.sonatype.org" || host.endsWith(".oss.sonatype.org"))
713-
val passwordOpt = publishOptions.contextual(isCi).repoPassword match {
714-
case None if isSonatype =>
715-
value(configDb().get(Keys.sonatypePassword).wrapConfigException)
716-
case other => other.map(_.toConfig)
717-
}
718-
passwordOpt.map(_.get()) match {
719-
case None => None
720-
case Some(password) =>
721-
val userOpt = publishOptions.contextual(isCi).repoUser match {
722-
case None if isSonatype =>
723-
value(configDb().get(Keys.sonatypeUser).wrapConfigException)
724-
case other => other.map(_.toConfig)
725-
}
726-
val realmOpt = publishOptions.contextual(isCi).repoRealm match {
727-
case None if isSonatype =>
728-
Some("Sonatype Nexus Repository Manager")
729-
case other => other
690+
def authOpt(repo: String): Either[BuildException, Option[Authentication]] = either {
691+
val isHttps = {
692+
val uri = new URI(repo)
693+
uri.getScheme == "https"
694+
}
695+
val hostOpt = Option.when(isHttps)(new URI(repo).getHost)
696+
val maybeCredentials: Either[BuildException, Option[PublishCredentials]] = hostOpt match {
697+
case None => Right(None)
698+
case Some(host) =>
699+
configDb().get(Keys.publishCredentials).wrapConfigException.map { credListOpt =>
700+
credListOpt.flatMap { credList =>
701+
credList.find { cred =>
702+
cred.host == host &&
703+
(isHttps || cred.httpsOnly.contains(false))
704+
}
730705
}
731-
val auth = Authentication(userOpt.fold("")(_.get().value), password.value)
732-
Some(realmOpt.fold(auth)(auth.withRealm))
733-
}
706+
}
734707
}
735-
736-
def centralRepo(base: String) = either {
737-
val authOpt0 = value(authOpt(base))
738-
val repo0 = {
739-
val r = PublishRepository.Sonatype(MavenRepository(base))
740-
authOpt0.fold(r)(r.withAuthentication)
741-
}
742-
val backend = ScalaCliSttpBackend.httpURLConnection(logger)
743-
val api = SonatypeApi(backend, base + "/service/local", authOpt0, logger.verbosity)
744-
val hooks0 = Hooks.sonatype(
745-
repo0,
746-
api,
747-
logger.compilerOutputStream, // meh
748-
logger.verbosity,
749-
batch = coursier.paths.Util.useAnsiOutput(), // FIXME Get via logger
750-
es
751-
)
752-
RepoParams(repo0, Some("https://repo1.maven.org/maven2"), hooks0, false, true, true, true)
708+
val isSonatype =
709+
hostOpt.exists(host => host == "oss.sonatype.org" || host.endsWith(".oss.sonatype.org"))
710+
val passwordOpt = publishOptions.contextual(isCi).repoPassword match {
711+
case None => value(maybeCredentials).flatMap(_.password)
712+
case other => other.map(_.toConfig)
753713
}
754-
755-
def gitHubRepoFor(org: String, name: String) =
756-
RepoParams(
757-
PublishRepository.Simple(MavenRepository(s"https://maven.pkg.github.com/$org/$name")),
758-
None,
759-
Hooks.dummy,
760-
false,
761-
false,
762-
false,
763-
false
764-
)
765-
766-
def gitHubRepo = either {
767-
val orgNameFromVcsOpt = publishOptions.versionControl
768-
.map(_.url)
769-
.flatMap(url => GitRepo.maybeGhOrgName(url))
770-
771-
val (org, name) = orgNameFromVcsOpt match {
772-
case Some(orgName) => orgName
773-
case None =>
774-
value(GitRepo.ghRepoOrgName(builds.head.inputs.workspace, logger))
775-
}
776-
777-
gitHubRepoFor(org, name)
714+
passwordOpt.map(_.get()) match {
715+
case None => None
716+
case Some(password) =>
717+
val userOpt = publishOptions.contextual(isCi).repoUser match {
718+
case None => value(maybeCredentials).flatMap(_.user)
719+
case other => other.map(_.toConfig)
720+
}
721+
val realmOpt = publishOptions.contextual(isCi).repoRealm match {
722+
case None =>
723+
value(maybeCredentials)
724+
.flatMap(_.realm)
725+
.orElse {
726+
if (isSonatype) Some("Sonatype Nexus Repository Manager")
727+
else None
728+
}
729+
case other => other
730+
}
731+
val auth = Authentication(userOpt.fold("")(_.get().value), password.value)
732+
Some(realmOpt.fold(auth)(auth.withRealm))
778733
}
734+
}
779735

780-
def ivy2Local = {
781-
val home = ivy2HomeOpt.getOrElse(os.home / ".ivy2")
782-
val base = home / "local"
783-
// not really a Maven repo…
784-
RepoParams(
785-
PublishRepository.Simple(MavenRepository(base.toNIO.toUri.toASCIIString)),
786-
None,
787-
Hooks.dummy,
788-
true,
789-
true,
790-
true,
791-
true
792-
)
793-
}
736+
val repoParams = {
737+
738+
lazy val es =
739+
Executors.newSingleThreadScheduledExecutor(Util.daemonThreadFactory("publish-retry"))
794740

795741
if (publishLocal)
796-
ivy2Local
742+
RepoParams.ivy2Local(ivy2HomeOpt)
797743
else
798-
publishOptions.contextual(isCi).repository match {
799-
case None =>
800-
value(Left(new MissingPublishOptionError(
801-
"repository",
802-
"--publish-repository",
803-
"publish.repository"
804-
)))
805-
case Some("ivy2-local") =>
806-
ivy2Local
807-
case Some("central" | "maven-central" | "mvn-central") =>
808-
value(centralRepo("https://oss.sonatype.org"))
809-
case Some("central-s01" | "maven-central-s01" | "mvn-central-s01") =>
810-
value(centralRepo("https://s01.oss.sonatype.org"))
811-
case Some("github") =>
812-
value(gitHubRepo)
813-
case Some(repoStr) if repoStr.startsWith("github:") && repoStr.count(_ == '/') == 1 =>
814-
val (org, name) = repoStr.stripPrefix("github:").split('/') match {
815-
case Array(org0, name0) => (org0, name0)
816-
case other => sys.error(s"Cannot happen ('$repoStr' -> ${other.toSeq})")
817-
}
818-
gitHubRepoFor(org, name)
819-
case Some(repoStr) =>
820-
val repo0 = {
821-
val r = RepositoryParser.repositoryOpt(repoStr)
822-
.collect {
823-
case m: MavenRepository =>
824-
m
825-
}
826-
.getOrElse {
827-
val url =
828-
if (repoStr.contains("://")) repoStr
829-
else os.Path(repoStr, Os.pwd).toNIO.toUri.toASCIIString
830-
MavenRepository(url)
831-
}
832-
r.withAuthentication(value(authOpt(r.root)))
833-
}
834-
835-
RepoParams(
836-
PublishRepository.Simple(repo0),
837-
None,
838-
Hooks.dummy,
839-
publishOptions.contextual(isCi).repositoryIsIvy2LocalLike.getOrElse(false),
840-
true,
841-
true,
842-
true
843-
)
744+
value {
745+
publishOptions.contextual(isCi).repository match {
746+
case None =>
747+
Left(new MissingPublishOptionError(
748+
"repository",
749+
"--publish-repository",
750+
"publish.repository"
751+
))
752+
case Some(repo) =>
753+
RepoParams(
754+
repo,
755+
publishOptions.versionControl.map(_.url),
756+
builds.head.inputs.workspace,
757+
ivy2HomeOpt,
758+
publishOptions.contextual(isCi).repositoryIsIvy2LocalLike.getOrElse(false),
759+
es,
760+
logger
761+
)
762+
}
844763
}
845764
}
846765

@@ -988,10 +907,11 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers {
988907
else fileSet2.order(ec).unsafeRun()(ec)
989908

990909
val isSnapshot0 = modVersionOpt.exists(_._2.endsWith("SNAPSHOT"))
991-
val hooksData = repoParams.hooks.beforeUpload(finalFileSet, isSnapshot0).unsafeRun()(ec)
910+
val repoParams0 = repoParams.withAuth(value(authOpt(repoParams.repo.repo(isSnapshot0).root)))
911+
val hooksData = repoParams0.hooks.beforeUpload(finalFileSet, isSnapshot0).unsafeRun()(ec)
992912

993-
val retainedRepo = repoParams.hooks.repository(hooksData, repoParams.repo, isSnapshot0)
994-
.getOrElse(repoParams.repo.repo(isSnapshot0))
913+
val retainedRepo = repoParams0.hooks.repository(hooksData, repoParams0.repo, isSnapshot0)
914+
.getOrElse(repoParams0.repo.repo(isSnapshot0))
995915

996916
val upload =
997917
if (retainedRepo.root.startsWith("http://") || retainedRepo.root.startsWith("https://"))
@@ -1015,9 +935,9 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers {
1015935
case h :: t =>
1016936
value(Left(new UploadError(::(h, t))))
1017937
case Nil =>
1018-
repoParams.hooks.afterUpload(hooksData).unsafeRun()(ec)
938+
repoParams0.hooks.afterUpload(hooksData).unsafeRun()(ec)
1019939
for ((mod, version) <- modVersionOpt) {
1020-
val checkRepo = repoParams.repo.checkResultsRepo(isSnapshot0)
940+
val checkRepo = repoParams0.repo.checkResultsRepo(isSnapshot0)
1021941
val relPath = {
1022942
val elems =
1023943
if (repoParams.isIvy2LocalLike)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ object PublishSetup extends ScalaCommand[PublishSetupOptions] {
161161

162162
val missingFieldsWithDefaults = missingFields
163163
.map { check =>
164-
check.defaultValue().map((check, _))
164+
check.defaultValue(publishOptions).map((check, _))
165165
}
166166
.sequence
167167
.left.map(CompositeBuildException(_))

0 commit comments

Comments
 (0)