From d86ec46780e48fb48fce9c6752369e7e793ba3bb Mon Sep 17 00:00:00 2001 From: Alexandre Archambault Date: Fri, 7 Oct 2022 14:50:48 +0200 Subject: [PATCH 1/4] Pass PublishOptions to OptionChecks.defaultValue To be used in subsequent commits --- .../src/main/scala/scala/cli/commands/publish/OptionCheck.scala | 2 +- .../main/scala/scala/cli/commands/publish/PublishSetup.scala | 2 +- .../scala/cli/commands/publish/checks/ComputeVersionCheck.scala | 2 +- .../scala/cli/commands/publish/checks/DeveloperCheck.scala | 2 +- .../scala/scala/cli/commands/publish/checks/LicenseCheck.scala | 2 +- .../scala/scala/cli/commands/publish/checks/NameCheck.scala | 2 +- .../scala/cli/commands/publish/checks/OrganizationCheck.scala | 2 +- .../scala/scala/cli/commands/publish/checks/PasswordCheck.scala | 2 +- .../scala/cli/commands/publish/checks/PgpSecretKeyCheck.scala | 2 +- .../scala/cli/commands/publish/checks/RepositoryCheck.scala | 2 +- .../main/scala/scala/cli/commands/publish/checks/ScmCheck.scala | 2 +- .../main/scala/scala/cli/commands/publish/checks/UrlCheck.scala | 2 +- .../scala/scala/cli/commands/publish/checks/UserCheck.scala | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/modules/cli/src/main/scala/scala/cli/commands/publish/OptionCheck.scala b/modules/cli/src/main/scala/scala/cli/commands/publish/OptionCheck.scala index 3e5897da97..4091a4ed9f 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/publish/OptionCheck.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/publish/OptionCheck.scala @@ -22,7 +22,7 @@ trait OptionCheck { /** Provides a way to compute a default value for this option, along with extra directives and * GitHub secrets to be set */ - def defaultValue(): Either[BuildException, OptionCheck.DefaultValue] + def defaultValue(pubOpt: BPublishOptions): Either[BuildException, OptionCheck.DefaultValue] } object OptionCheck { diff --git a/modules/cli/src/main/scala/scala/cli/commands/publish/PublishSetup.scala b/modules/cli/src/main/scala/scala/cli/commands/publish/PublishSetup.scala index 86a274c60b..ff5e383abf 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/publish/PublishSetup.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/publish/PublishSetup.scala @@ -161,7 +161,7 @@ object PublishSetup extends ScalaCommand[PublishSetupOptions] { val missingFieldsWithDefaults = missingFields .map { check => - check.defaultValue().map((check, _)) + check.defaultValue(publishOptions).map((check, _)) } .sequence .left.map(CompositeBuildException(_)) diff --git a/modules/cli/src/main/scala/scala/cli/commands/publish/checks/ComputeVersionCheck.scala b/modules/cli/src/main/scala/scala/cli/commands/publish/checks/ComputeVersionCheck.scala index 0738a6d14a..ad3f4cd0f6 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/publish/checks/ComputeVersionCheck.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/publish/checks/ComputeVersionCheck.scala @@ -20,7 +20,7 @@ final case class ComputeVersionCheck( pubOpt.version.nonEmpty || pubOpt.retained(options.publishParams.setupCi).computeVersion.nonEmpty - def defaultValue(): Either[BuildException, OptionCheck.DefaultValue] = { + def defaultValue(pubOpt: BPublishOptions): Either[BuildException, OptionCheck.DefaultValue] = { def fromGitOpt = if (GitRepo.gitRepoOpt(workspace).isDefined) { logger.message("computeVersion:") diff --git a/modules/cli/src/main/scala/scala/cli/commands/publish/checks/DeveloperCheck.scala b/modules/cli/src/main/scala/scala/cli/commands/publish/checks/DeveloperCheck.scala index 55ad5fbd6e..1f2b9d58b9 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/publish/checks/DeveloperCheck.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/publish/checks/DeveloperCheck.scala @@ -21,7 +21,7 @@ final case class DeveloperCheck( def check(pubOpt: BPublishOptions): Boolean = pubOpt.developers.nonEmpty - def defaultValue(): Either[BuildException, OptionCheck.DefaultValue] = + def defaultValue(pubOpt: BPublishOptions): Either[BuildException, OptionCheck.DefaultValue] = either { // FIXME No headOption, add all of options.publishParams.developer values… val strValue = options.publishParams.developer.headOption match { diff --git a/modules/cli/src/main/scala/scala/cli/commands/publish/checks/LicenseCheck.scala b/modules/cli/src/main/scala/scala/cli/commands/publish/checks/LicenseCheck.scala index 29060d7424..a8cac7069d 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/publish/checks/LicenseCheck.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/publish/checks/LicenseCheck.scala @@ -17,7 +17,7 @@ final case class LicenseCheck( pubOpt.license.nonEmpty private def defaultLicense = "Apache-2.0" - def defaultValue(): Either[BuildException, OptionCheck.DefaultValue] = { + def defaultValue(pubOpt: BPublishOptions): Either[BuildException, OptionCheck.DefaultValue] = { val license = options.publishParams.license.getOrElse { logger.message("license:") logger.message(s" using $defaultLicense (default)") diff --git a/modules/cli/src/main/scala/scala/cli/commands/publish/checks/NameCheck.scala b/modules/cli/src/main/scala/scala/cli/commands/publish/checks/NameCheck.scala index 0de679cced..7e7755097c 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/publish/checks/NameCheck.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/publish/checks/NameCheck.scala @@ -17,7 +17,7 @@ final case class NameCheck( def check(options: BPublishOptions): Boolean = options.name.nonEmpty || options.moduleName.nonEmpty - def defaultValue(): Either[BuildException, OptionCheck.DefaultValue] = { + def defaultValue(pubOpt: BPublishOptions): Either[BuildException, OptionCheck.DefaultValue] = { def fromWorkspaceDirName = { val n = workspace.last logger.message("name:") diff --git a/modules/cli/src/main/scala/scala/cli/commands/publish/checks/OrganizationCheck.scala b/modules/cli/src/main/scala/scala/cli/commands/publish/checks/OrganizationCheck.scala index 41f3caf218..93774f14be 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/publish/checks/OrganizationCheck.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/publish/checks/OrganizationCheck.scala @@ -18,7 +18,7 @@ final case class OrganizationCheck( def check(options: BPublishOptions): Boolean = options.organization.nonEmpty - def defaultValue(): Either[BuildException, OptionCheck.DefaultValue] = { + def defaultValue(pubOpt: BPublishOptions): Either[BuildException, OptionCheck.DefaultValue] = { def viaGitHubRemoteOpt = GitRepo.ghRepoOrgName(workspace, logger) match { case Left(err) => diff --git a/modules/cli/src/main/scala/scala/cli/commands/publish/checks/PasswordCheck.scala b/modules/cli/src/main/scala/scala/cli/commands/publish/checks/PasswordCheck.scala index 752e5f03c7..e614e6d6b8 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/publish/checks/PasswordCheck.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/publish/checks/PasswordCheck.scala @@ -22,7 +22,7 @@ final case class PasswordCheck( !options.publishParams.setupCi || pubOpt.retained(options.publishParams.setupCi).repoPassword.nonEmpty - def defaultValue(): Either[BuildException, OptionCheck.DefaultValue] = + def defaultValue(pubOpt: BPublishOptions): Either[BuildException, OptionCheck.DefaultValue] = either { if (options.publishParams.setupCi) { val password = options.publishRepo.password match { diff --git a/modules/cli/src/main/scala/scala/cli/commands/publish/checks/PgpSecretKeyCheck.scala b/modules/cli/src/main/scala/scala/cli/commands/publish/checks/PgpSecretKeyCheck.scala index 813ed47754..54b939feb5 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/publish/checks/PgpSecretKeyCheck.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/publish/checks/PgpSecretKeyCheck.scala @@ -60,7 +60,7 @@ final case class PgpSecretKeyCheck( ).value.javaCommand } - def defaultValue(): Either[BuildException, OptionCheck.DefaultValue] = + def defaultValue(pubOpt: BPublishOptions): Either[BuildException, OptionCheck.DefaultValue] = either { if (options.publishParams.setupCi) { val (pubKeyOpt, secretKey, passwordOpt) = options.publishParams.secretKey match { diff --git a/modules/cli/src/main/scala/scala/cli/commands/publish/checks/RepositoryCheck.scala b/modules/cli/src/main/scala/scala/cli/commands/publish/checks/RepositoryCheck.scala index 22db1db6fc..28a1b641d3 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/publish/checks/RepositoryCheck.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/publish/checks/RepositoryCheck.scala @@ -14,7 +14,7 @@ final case class RepositoryCheck( def directivePath = "publish" + (if (options.publishParams.setupCi) ".ci" else "") + ".repository" def check(pubOpt: BPublishOptions): Boolean = pubOpt.retained(options.publishParams.setupCi).repository.nonEmpty - def defaultValue(): Either[BuildException, OptionCheck.DefaultValue] = { + def defaultValue(pubOpt: BPublishOptions): Either[BuildException, OptionCheck.DefaultValue] = { val repo = options.publishRepo.publishRepository.getOrElse { logger.message("repository:") logger.message(" using Maven Central via its s01 server") diff --git a/modules/cli/src/main/scala/scala/cli/commands/publish/checks/ScmCheck.scala b/modules/cli/src/main/scala/scala/cli/commands/publish/checks/ScmCheck.scala index a7862428b9..5e2f820768 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/publish/checks/ScmCheck.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/publish/checks/ScmCheck.scala @@ -18,7 +18,7 @@ final case class ScmCheck( def check(pubOpt: BPublishOptions): Boolean = pubOpt.versionControl.nonEmpty - def defaultValue(): Either[BuildException, OptionCheck.DefaultValue] = { + def defaultValue(pubOpt: BPublishOptions): Either[BuildException, OptionCheck.DefaultValue] = { def ghVcsOpt = GitRepo.ghRepoOrgName(workspace, logger) match { case Left(err) => logger.debug( diff --git a/modules/cli/src/main/scala/scala/cli/commands/publish/checks/UrlCheck.scala b/modules/cli/src/main/scala/scala/cli/commands/publish/checks/UrlCheck.scala index 7fdde6635a..7c0b5b5486 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/publish/checks/UrlCheck.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/publish/checks/UrlCheck.scala @@ -18,7 +18,7 @@ final case class UrlCheck( def check(pubOpt: BPublishOptions): Boolean = pubOpt.url.nonEmpty - def defaultValue(): Either[BuildException, OptionCheck.DefaultValue] = { + def defaultValue(pubOpt: BPublishOptions): Either[BuildException, OptionCheck.DefaultValue] = { def ghUrlOpt = GitRepo.ghRepoOrgName(workspace, logger) match { case Left(err) => logger.debug( diff --git a/modules/cli/src/main/scala/scala/cli/commands/publish/checks/UserCheck.scala b/modules/cli/src/main/scala/scala/cli/commands/publish/checks/UserCheck.scala index d8cecf7d31..c976294608 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/publish/checks/UserCheck.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/publish/checks/UserCheck.scala @@ -20,7 +20,7 @@ final case class UserCheck( def check(pubOpt: BPublishOptions): Boolean = !options.publishParams.setupCi || pubOpt.retained(options.publishParams.setupCi).repoUser.nonEmpty - def defaultValue(): Either[BuildException, OptionCheck.DefaultValue] = + def defaultValue(pubOpt: BPublishOptions): Either[BuildException, OptionCheck.DefaultValue] = either { if (options.publishParams.setupCi) { val user0 = options.publishRepo.user match { From fb1fa1d857edb11c8dc9501205f1633d97f10aef Mon Sep 17 00:00:00 2001 From: Alexandre Archambault Date: Fri, 7 Oct 2022 14:33:46 +0200 Subject: [PATCH 2/4] Factor RepoParams from publish --- .../scala/cli/commands/publish/Publish.scala | 209 +++++------------- .../cli/commands/publish/RepoParams.scala | 145 ++++++++++++ 2 files changed, 202 insertions(+), 152 deletions(-) create mode 100644 modules/cli/src/main/scala/scala/cli/commands/publish/RepoParams.scala diff --git a/modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala b/modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala index 1b6c04728d..f679de3205 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala @@ -661,16 +661,6 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers { (fileSet, (mod, ver)) } - private final case class RepoParams( - repo: PublishRepository, - targetRepoOpt: Option[String], - hooks: Hooks, - isIvy2LocalLike: Boolean, - defaultParallelUpload: Boolean, - supportsSig: Boolean, - acceptsChecksums: Boolean - ) - private def doPublish( builds: Seq[Build.Successful], docBuilds: Seq[Build.Successful], @@ -697,150 +687,64 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers { val ec = builds.head.options.finalCache.ec - val repoParams = { - - lazy val es = - Executors.newSingleThreadScheduledExecutor(Util.daemonThreadFactory("publish-retry")) - - def authOpt(repo: String): Either[BuildException, Option[Authentication]] = either { - val hostOpt = { - val uri = new URI(repo) - if (uri.getScheme == "https") Some(uri.getHost) - else None - } - val isSonatype = - hostOpt.exists(host => host == "oss.sonatype.org" || host.endsWith(".oss.sonatype.org")) - val passwordOpt = publishOptions.contextual(isCi).repoPassword match { - case None if isSonatype => - value(configDb().get(Keys.sonatypePassword).wrapConfigException) - case other => other.map(_.toConfig) - } - passwordOpt.map(_.get()) match { - case None => None - case Some(password) => - val userOpt = publishOptions.contextual(isCi).repoUser match { - case None if isSonatype => - value(configDb().get(Keys.sonatypeUser).wrapConfigException) - case other => other.map(_.toConfig) - } - val realmOpt = publishOptions.contextual(isCi).repoRealm match { - case None if isSonatype => - Some("Sonatype Nexus Repository Manager") - case other => other - } - val auth = Authentication(userOpt.fold("")(_.get().value), password.value) - Some(realmOpt.fold(auth)(auth.withRealm)) - } + def authOpt(repo: String): Either[BuildException, Option[Authentication]] = either { + val hostOpt = { + val uri = new URI(repo) + if (uri.getScheme == "https") Some(uri.getHost) + else None } - - def centralRepo(base: String) = either { - val authOpt0 = value(authOpt(base)) - val repo0 = { - val r = PublishRepository.Sonatype(MavenRepository(base)) - authOpt0.fold(r)(r.withAuthentication) - } - val backend = ScalaCliSttpBackend.httpURLConnection(logger) - val api = SonatypeApi(backend, base + "/service/local", authOpt0, logger.verbosity) - val hooks0 = Hooks.sonatype( - repo0, - api, - logger.compilerOutputStream, // meh - logger.verbosity, - batch = coursier.paths.Util.useAnsiOutput(), // FIXME Get via logger - es - ) - RepoParams(repo0, Some("https://repo1.maven.org/maven2"), hooks0, false, true, true, true) + val isSonatype = + hostOpt.exists(host => host == "oss.sonatype.org" || host.endsWith(".oss.sonatype.org")) + val passwordOpt = publishOptions.contextual(isCi).repoPassword match { + case None if isSonatype => + value(configDb().get(Keys.sonatypePassword).wrapConfigException) + case other => other.map(_.toConfig) } - - def gitHubRepoFor(org: String, name: String) = - RepoParams( - PublishRepository.Simple(MavenRepository(s"https://maven.pkg.github.com/$org/$name")), - None, - Hooks.dummy, - false, - false, - false, - false - ) - - def gitHubRepo = either { - val orgNameFromVcsOpt = publishOptions.versionControl - .map(_.url) - .flatMap(url => GitRepo.maybeGhOrgName(url)) - - val (org, name) = orgNameFromVcsOpt match { - case Some(orgName) => orgName - case None => - value(GitRepo.ghRepoOrgName(builds.head.inputs.workspace, logger)) - } - - gitHubRepoFor(org, name) + passwordOpt.map(_.get()) match { + case None => None + case Some(password) => + val userOpt = publishOptions.contextual(isCi).repoUser match { + case None if isSonatype => + value(configDb().get(Keys.sonatypeUser).wrapConfigException) + case other => other.map(_.toConfig) + } + val realmOpt = publishOptions.contextual(isCi).repoRealm match { + case None if isSonatype => + Some("Sonatype Nexus Repository Manager") + case other => other + } + val auth = Authentication(userOpt.fold("")(_.get().value), password.value) + Some(realmOpt.fold(auth)(auth.withRealm)) } + } - def ivy2Local = { - val home = ivy2HomeOpt.getOrElse(os.home / ".ivy2") - val base = home / "local" - // not really a Maven repo… - RepoParams( - PublishRepository.Simple(MavenRepository(base.toNIO.toUri.toASCIIString)), - None, - Hooks.dummy, - true, - true, - true, - true - ) - } + val repoParams = { + + lazy val es = + Executors.newSingleThreadScheduledExecutor(Util.daemonThreadFactory("publish-retry")) if (publishLocal) - ivy2Local + RepoParams.ivy2Local(ivy2HomeOpt) else - publishOptions.contextual(isCi).repository match { - case None => - value(Left(new MissingPublishOptionError( - "repository", - "--publish-repository", - "publish.repository" - ))) - case Some("ivy2-local") => - ivy2Local - case Some("central" | "maven-central" | "mvn-central") => - value(centralRepo("https://oss.sonatype.org")) - case Some("central-s01" | "maven-central-s01" | "mvn-central-s01") => - value(centralRepo("https://s01.oss.sonatype.org")) - case Some("github") => - value(gitHubRepo) - case Some(repoStr) if repoStr.startsWith("github:") && repoStr.count(_ == '/') == 1 => - val (org, name) = repoStr.stripPrefix("github:").split('/') match { - case Array(org0, name0) => (org0, name0) - case other => sys.error(s"Cannot happen ('$repoStr' -> ${other.toSeq})") - } - gitHubRepoFor(org, name) - case Some(repoStr) => - val repo0 = { - val r = RepositoryParser.repositoryOpt(repoStr) - .collect { - case m: MavenRepository => - m - } - .getOrElse { - val url = - if (repoStr.contains("://")) repoStr - else os.Path(repoStr, Os.pwd).toNIO.toUri.toASCIIString - MavenRepository(url) - } - r.withAuthentication(value(authOpt(r.root))) - } - - RepoParams( - PublishRepository.Simple(repo0), - None, - Hooks.dummy, - publishOptions.contextual(isCi).repositoryIsIvy2LocalLike.getOrElse(false), - true, - true, - true - ) + value { + publishOptions.contextual(isCi).repository match { + case None => + Left(new MissingPublishOptionError( + "repository", + "--publish-repository", + "publish.repository" + )) + case Some(repo) => + RepoParams( + repo, + publishOptions.versionControl.map(_.url), + builds.head.inputs.workspace, + ivy2HomeOpt, + publishOptions.contextual(isCi).repositoryIsIvy2LocalLike.getOrElse(false), + es, + logger + ) + } } } @@ -988,10 +892,11 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers { else fileSet2.order(ec).unsafeRun()(ec) val isSnapshot0 = modVersionOpt.exists(_._2.endsWith("SNAPSHOT")) - val hooksData = repoParams.hooks.beforeUpload(finalFileSet, isSnapshot0).unsafeRun()(ec) + val repoParams0 = repoParams.withAuth(value(authOpt(repoParams.repo.repo(isSnapshot0).root))) + val hooksData = repoParams0.hooks.beforeUpload(finalFileSet, isSnapshot0).unsafeRun()(ec) - val retainedRepo = repoParams.hooks.repository(hooksData, repoParams.repo, isSnapshot0) - .getOrElse(repoParams.repo.repo(isSnapshot0)) + val retainedRepo = repoParams0.hooks.repository(hooksData, repoParams0.repo, isSnapshot0) + .getOrElse(repoParams0.repo.repo(isSnapshot0)) val upload = if (retainedRepo.root.startsWith("http://") || retainedRepo.root.startsWith("https://")) @@ -1015,9 +920,9 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers { case h :: t => value(Left(new UploadError(::(h, t)))) case Nil => - repoParams.hooks.afterUpload(hooksData).unsafeRun()(ec) + repoParams0.hooks.afterUpload(hooksData).unsafeRun()(ec) for ((mod, version) <- modVersionOpt) { - val checkRepo = repoParams.repo.checkResultsRepo(isSnapshot0) + val checkRepo = repoParams0.repo.checkResultsRepo(isSnapshot0) val relPath = { val elems = if (repoParams.isIvy2LocalLike) diff --git a/modules/cli/src/main/scala/scala/cli/commands/publish/RepoParams.scala b/modules/cli/src/main/scala/scala/cli/commands/publish/RepoParams.scala new file mode 100644 index 0000000000..d563bc4c7f --- /dev/null +++ b/modules/cli/src/main/scala/scala/cli/commands/publish/RepoParams.scala @@ -0,0 +1,145 @@ +package scala.cli.commands.publish + +import coursier.core.Authentication +import coursier.maven.MavenRepository +import coursier.publish.sonatype.SonatypeApi +import coursier.publish.{Hooks, PublishRepository} + +import java.util.concurrent.ScheduledExecutorService + +import scala.build.EitherCps.{either, value} +import scala.build.Logger +import scala.build.errors.BuildException +import scala.cli.commands.util.ScalaCliSttpBackend + +final case class RepoParams( + repo: PublishRepository, + targetRepoOpt: Option[String], + hooks: Hooks, + isIvy2LocalLike: Boolean, + defaultParallelUpload: Boolean, + supportsSig: Boolean, + acceptsChecksums: Boolean +) { + def withAuth(auth: Authentication): RepoParams = + copy( + repo = repo.withAuthentication(auth), + hooks = hooks match { + case s: Hooks.Sonatype => + s.copy( + repo = s.repo.withAuthentication(auth), + api = s.api.copy( + authentication = Some(auth) + ) + ) + case other => other + } + ) + def withAuth(authOpt: Option[Authentication]): RepoParams = + authOpt.fold(this)(withAuth(_)) +} + +object RepoParams { + + def apply( + repo: String, + vcsUrlOpt: Option[String], + workspace: os.Path, + ivy2HomeOpt: Option[os.Path], + isIvy2LocalLike: Boolean, + es: ScheduledExecutorService, + logger: Logger + ): Either[BuildException, RepoParams] = either { + repo match { + case "ivy2-local" => + RepoParams.ivy2Local(ivy2HomeOpt) + case "sonatype" | "central" | "maven-central" | "mvn-central" => + RepoParams.centralRepo("https://oss.sonatype.org", es, logger) + case "sonatype-s01" | "central-s01" | "maven-central-s01" | "mvn-central-s01" => + RepoParams.centralRepo("https://s01.oss.sonatype.org", es, logger) + case "github" => + value(RepoParams.gitHubRepo(vcsUrlOpt, workspace, logger)) + case repoStr if repoStr.startsWith("github:") && repoStr.count(_ == '/') == 1 => + val (org, name) = repoStr.stripPrefix("github:").split('/') match { + case Array(org0, name0) => (org0, name0) + case other => sys.error(s"Cannot happen ('$repoStr' -> ${other.toSeq})") + } + RepoParams.gitHubRepoFor(org, name) + case repoStr => + val repo0 = RepositoryParser.repositoryOpt(repoStr) + .collect { + case m: MavenRepository => + m + } + .getOrElse { + val url = + if (repoStr.contains("://")) repoStr + else os.Path(repoStr, os.pwd).toNIO.toUri.toASCIIString + MavenRepository(url) + } + + RepoParams( + PublishRepository.Simple(repo0), + None, + Hooks.dummy, + isIvy2LocalLike, + true, + true, + true + ) + } + } + + def centralRepo(base: String, es: ScheduledExecutorService, logger: Logger) = { + val repo0 = PublishRepository.Sonatype(MavenRepository(base)) + val backend = ScalaCliSttpBackend.httpURLConnection(logger) + val api = SonatypeApi(backend, base + "/service/local", None, logger.verbosity) + val hooks0 = Hooks.sonatype( + repo0, + api, + logger.compilerOutputStream, // meh + logger.verbosity, + batch = coursier.paths.Util.useAnsiOutput(), // FIXME Get via logger + es + ) + RepoParams(repo0, Some("https://repo1.maven.org/maven2"), hooks0, false, true, true, true) + } + + def gitHubRepoFor(org: String, name: String) = + RepoParams( + PublishRepository.Simple(MavenRepository(s"https://maven.pkg.github.com/$org/$name")), + None, + Hooks.dummy, + false, + false, + false, + false + ) + + def gitHubRepo(vcsUrlOpt: Option[String], workspace: os.Path, logger: Logger) = either { + val orgNameFromVcsOpt = vcsUrlOpt.flatMap(GitRepo.maybeGhOrgName) + + val (org, name) = orgNameFromVcsOpt match { + case Some(orgName) => orgName + case None => value(GitRepo.ghRepoOrgName(workspace, logger)) + } + + gitHubRepoFor(org, name) + } + + def ivy2Local(ivy2HomeOpt: Option[os.Path]) = { + val home = ivy2HomeOpt.getOrElse(os.home / ".ivy2") + val base = home / "local" + // not really a Maven repo… + RepoParams( + PublishRepository.Simple(MavenRepository(base.toNIO.toUri.toASCIIString)), + None, + Hooks.dummy, + true, + true, + true, + true + ) + } + +} From 3b837e149bc21aef44549f34d08046b3ba0bcce7 Mon Sep 17 00:00:00 2001 From: Alexandre Archambault Date: Fri, 7 Oct 2022 15:19:12 +0200 Subject: [PATCH 3/4] Factor default repository for publishing --- .../cli/commands/publish/checks/RepositoryCheck.scala | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/modules/cli/src/main/scala/scala/cli/commands/publish/checks/RepositoryCheck.scala b/modules/cli/src/main/scala/scala/cli/commands/publish/checks/RepositoryCheck.scala index 28a1b641d3..d531125bc4 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/publish/checks/RepositoryCheck.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/publish/checks/RepositoryCheck.scala @@ -17,9 +17,14 @@ final case class RepositoryCheck( def defaultValue(pubOpt: BPublishOptions): Either[BuildException, OptionCheck.DefaultValue] = { val repo = options.publishRepo.publishRepository.getOrElse { logger.message("repository:") - logger.message(" using Maven Central via its s01 server") - "central-s01" + logger.message(s" using ${RepositoryCheck.defaultRepositoryDescription}") + RepositoryCheck.defaultRepository } Right(OptionCheck.DefaultValue.simple(repo, Nil, Nil)) } } + +object RepositoryCheck { + def defaultRepository = "central-s01" + def defaultRepositoryDescription = "Maven Central via its s01 server" +} From e75d2840c8d74e80cb524337671688948d3ad8a7 Mon Sep 17 00:00:00 2001 From: Alexandre Archambault Date: Fri, 7 Oct 2022 12:16:39 +0200 Subject: [PATCH 4/4] Add publish.credentials config key, use it to publish --- .../scala/cli/commands/config/Config.scala | 35 ++++- .../cli/commands/publish/OptionChecks.scala | 4 +- .../scala/cli/commands/publish/Publish.scala | 35 +++-- .../publish/checks/PasswordCheck.scala | 104 +++++++++++---- .../commands/publish/checks/UserCheck.scala | 104 +++++++++++---- .../main/scala/scala/cli/config/Keys.scala | 121 +++++++++++++++++- .../scala/cli/config/PublishCredentials.scala | 9 ++ .../scala/cli/integration/ConfigTests.scala | 15 ++- .../cli/integration/PublishSetupTests.scala | 12 +- .../docs/commands/publishing/publish-setup.md | 14 +- website/docs/commands/publishing/publish.md | 6 +- 11 files changed, 373 insertions(+), 86 deletions(-) create mode 100644 modules/config/src/main/scala/scala/cli/config/PublishCredentials.scala diff --git a/modules/cli/src/main/scala/scala/cli/commands/config/Config.scala b/modules/cli/src/main/scala/scala/cli/commands/config/Config.scala index 558fa3d52b..d01b2aa63f 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/config/Config.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/config/Config.scala @@ -12,7 +12,14 @@ import scala.cli.commands.ScalaCommand import scala.cli.commands.publish.ConfigUtil.* import scala.cli.commands.util.CommonOps.* import scala.cli.commands.util.JvmUtils -import scala.cli.config.{ConfigDb, Keys, PasswordOption, RepositoryCredentials, Secret} +import scala.cli.config.{ + ConfigDb, + Keys, + PasswordOption, + PublishCredentials, + RepositoryCredentials, + Secret +} object Config extends ScalaCommand[ConfigOptions] { override def hidden = true @@ -188,6 +195,32 @@ object Config extends ScalaCommand[ConfigOptions] { val newValue = credentials :: previousValueOpt.getOrElse(Nil) db.set(Keys.repositoryCredentials, newValue) } + + case Keys.publishCredentials => + val (host, rawUser, rawPassword, realmOpt) = values match { + case Seq(host, rawUser, rawPassword) => (host, rawUser, rawPassword, None) + case Seq(host, rawUser, rawPassword, realm) => + (host, rawUser, rawPassword, Some(realm)) + case _ => + System.err.println( + s"Usage: $progName config ${Keys.publishCredentials.fullName} host user password [realm]" + ) + System.err.println( + "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" + ) + sys.exit(1) + } + val (userOpt, passwordOpt) = (parseSecret(rawUser), parseSecret(rawPassword)) + .traverseN + .left.map(CompositeBuildException(_)) + .orExit(logger) + val credentials = + PublishCredentials(host, userOpt, passwordOpt, realm = realmOpt) + val previousValueOpt = + db.get(Keys.publishCredentials).wrapConfigException.orExit(logger) + val newValue = credentials :: previousValueOpt.getOrElse(Nil) + db.set(Keys.publishCredentials, newValue) + case _ => val finalValues = if (options.passwordValue && entry.isPasswordOption) diff --git a/modules/cli/src/main/scala/scala/cli/commands/publish/OptionChecks.scala b/modules/cli/src/main/scala/scala/cli/commands/publish/OptionChecks.scala index 0d7f6fbd11..135def5a08 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/publish/OptionChecks.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/publish/OptionChecks.scala @@ -23,8 +23,8 @@ object OptionChecks { NameCheck(options, workspace, logger), ComputeVersionCheck(options, workspace, logger), RepositoryCheck(options, logger), - UserCheck(options, () => configDb, logger), - PasswordCheck(options, () => configDb, logger), + UserCheck(options, () => configDb, workspace, logger), + PasswordCheck(options, () => configDb, workspace, logger), PgpSecretKeyCheck(options, coursierCache, () => configDb, logger, backend), LicenseCheck(options, logger), UrlCheck(options, workspace, logger), diff --git a/modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala b/modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala index f679de3205..3a8903cda0 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala @@ -48,7 +48,7 @@ import scala.cli.commands.{ SharedPythonOptions, WatchUtil } -import scala.cli.config.{ConfigDb, Keys} +import scala.cli.config.{ConfigDb, Keys, PublishCredentials} import scala.cli.errors.{ FailedToSignFileError, MalformedChecksumsError, @@ -688,29 +688,44 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers { val ec = builds.head.options.finalCache.ec def authOpt(repo: String): Either[BuildException, Option[Authentication]] = either { - val hostOpt = { + val isHttps = { val uri = new URI(repo) - if (uri.getScheme == "https") Some(uri.getHost) - else None + uri.getScheme == "https" + } + val hostOpt = Option.when(isHttps)(new URI(repo).getHost) + val maybeCredentials: Either[BuildException, Option[PublishCredentials]] = hostOpt match { + case None => Right(None) + case Some(host) => + configDb().get(Keys.publishCredentials).wrapConfigException.map { credListOpt => + credListOpt.flatMap { credList => + credList.find { cred => + cred.host == host && + (isHttps || cred.httpsOnly.contains(false)) + } + } + } } val isSonatype = hostOpt.exists(host => host == "oss.sonatype.org" || host.endsWith(".oss.sonatype.org")) val passwordOpt = publishOptions.contextual(isCi).repoPassword match { - case None if isSonatype => - value(configDb().get(Keys.sonatypePassword).wrapConfigException) + case None => value(maybeCredentials).flatMap(_.password) case other => other.map(_.toConfig) } passwordOpt.map(_.get()) match { case None => None case Some(password) => val userOpt = publishOptions.contextual(isCi).repoUser match { - case None if isSonatype => - value(configDb().get(Keys.sonatypeUser).wrapConfigException) + case None => value(maybeCredentials).flatMap(_.user) case other => other.map(_.toConfig) } val realmOpt = publishOptions.contextual(isCi).repoRealm match { - case None if isSonatype => - Some("Sonatype Nexus Repository Manager") + case None => + value(maybeCredentials) + .flatMap(_.realm) + .orElse { + if (isSonatype) Some("Sonatype Nexus Repository Manager") + else None + } case other => other } val auth = Authentication(userOpt.fold("")(_.get().value), password.value) diff --git a/modules/cli/src/main/scala/scala/cli/commands/publish/checks/PasswordCheck.scala b/modules/cli/src/main/scala/scala/cli/commands/publish/checks/PasswordCheck.scala index e614e6d6b8..b33da34e9b 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/publish/checks/PasswordCheck.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/publish/checks/PasswordCheck.scala @@ -1,39 +1,88 @@ package scala.cli.commands.publish.checks +import java.net.URI + import scala.build.EitherCps.{either, value} import scala.build.Logger import scala.build.errors.BuildException import scala.build.options.{PublishOptions => BPublishOptions} import scala.cli.commands.publish.ConfigUtil._ -import scala.cli.commands.publish.{OptionCheck, PublishSetupOptions, SetSecret} +import scala.cli.commands.publish.{OptionCheck, PublishSetupOptions, RepoParams, SetSecret} import scala.cli.config.{ConfigDb, Keys} import scala.cli.errors.MissingPublishOptionError final case class PasswordCheck( options: PublishSetupOptions, configDb: () => ConfigDb, + workspace: os.Path, logger: Logger ) extends OptionCheck { def kind = OptionCheck.Kind.Repository def fieldName = "password" def directivePath = "publish" + (if (options.publishParams.setupCi) ".ci" else "") + ".password" + private def hostOpt(pubOpt: BPublishOptions): Option[String] = { + val repo = pubOpt.contextual(options.publishParams.setupCi).repository.getOrElse( + RepositoryCheck.defaultRepository + ) + RepoParams( + repo, + pubOpt.versionControl.map(_.url), + workspace, + None, + false, + null, + logger + ) match { + case Left(ex) => + logger.debug("Caught exception when trying to compute host to check user credentials") + logger.debug(ex) + None + case Right(params) => + Some(new URI(params.repo.snapshotRepo.root).getHost) + } + } + + private def passwordOpt(pubOpt: BPublishOptions) = hostOpt(pubOpt) match { + case None => Right(None) + case Some(host) => + configDb().get(Keys.publishCredentials).wrapConfigException.map { credListOpt => + credListOpt.flatMap { credList => + credList + .iterator + .filter(_.host == host) + .map(_.password) + .collectFirst { + case Some(p) => p + } + } + } + } + def check(pubOpt: BPublishOptions): Boolean = - !options.publishParams.setupCi || - pubOpt.retained(options.publishParams.setupCi).repoPassword.nonEmpty + pubOpt.retained(options.publishParams.setupCi).repoPassword.nonEmpty || { + !options.publishParams.setupCi && (passwordOpt(pubOpt) match { + case Left(ex) => + logger.debug("Ignoring error while trying to get password from config") + logger.debug(ex) + true + case Right(valueOpt) => + valueOpt.isDefined + }) + } def defaultValue(pubOpt: BPublishOptions): Either[BuildException, OptionCheck.DefaultValue] = either { + if (options.publishParams.setupCi) { val password = options.publishRepo.password match { case Some(password0) => password0.toConfig case None => - val passwordOpt = value(configDb().get(Keys.sonatypePassword).wrapConfigException) - passwordOpt match { + value(passwordOpt(pubOpt)) match { case Some(password0) => - logger.message("publish.password:") + logger.message("publish.credentials:") logger.message( - s" using ${Keys.sonatypePassword.fullName} from Scala CLI configuration" + s" using ${Keys.publishCredentials.fullName} from Scala CLI configuration" ) password0 case None => @@ -42,8 +91,8 @@ final case class PasswordCheck( new MissingPublishOptionError( "publish password", "--password", - "publish.password", - configKeys = Seq(Keys.sonatypePassword.fullName) + "publish.credentials", + configKeys = Seq(Keys.publishCredentials.fullName) ) } } @@ -56,21 +105,30 @@ final case class PasswordCheck( Seq(SetSecret("PUBLISH_PASSWORD", password.get(), force = true)) ) } - else if (value(configDb().get(Keys.sonatypePassword).wrapConfigException).isDefined) { - logger.message("publish.password:") - logger.message(s" found ${Keys.sonatypePassword.fullName} in Scala CLI configuration") - OptionCheck.DefaultValue.empty - } else - value { - Left { - new MissingPublishOptionError( - "publish password", - "", - "publish.password", - configKeys = Seq(Keys.sonatypePassword.fullName) - ) - } + hostOpt(pubOpt) match { + case None => + logger.debug("No host, not checking for publish repository password") + OptionCheck.DefaultValue.empty + case Some(host) => + if (value(passwordOpt(pubOpt)).isDefined) { + logger.message("publish.password:") + logger.message( + s" found password for $host in ${Keys.publishCredentials.fullName} in Scala CLI configuration" + ) + OptionCheck.DefaultValue.empty + } + else + value { + Left { + new MissingPublishOptionError( + "publish password", + "", + "publish.credentials", + configKeys = Seq(Keys.publishCredentials.fullName) + ) + } + } } } } diff --git a/modules/cli/src/main/scala/scala/cli/commands/publish/checks/UserCheck.scala b/modules/cli/src/main/scala/scala/cli/commands/publish/checks/UserCheck.scala index c976294608..b4ee2c5daa 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/publish/checks/UserCheck.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/publish/checks/UserCheck.scala @@ -1,37 +1,88 @@ package scala.cli.commands.publish.checks +import java.net.URI + import scala.build.EitherCps.{either, value} import scala.build.Logger import scala.build.errors.BuildException import scala.build.options.{PublishOptions => BPublishOptions} import scala.cli.commands.publish.ConfigUtil._ -import scala.cli.commands.publish.{OptionCheck, PublishSetupOptions, SetSecret} +import scala.cli.commands.publish.{OptionCheck, PublishSetupOptions, RepoParams, SetSecret} import scala.cli.config.{ConfigDb, Keys} import scala.cli.errors.MissingPublishOptionError final case class UserCheck( options: PublishSetupOptions, configDb: () => ConfigDb, + workspace: os.Path, logger: Logger ) extends OptionCheck { def kind = OptionCheck.Kind.Repository def fieldName = "user" def directivePath = "publish" + (if (options.publishParams.setupCi) ".ci" else "") + ".user" + + private def hostOpt(pubOpt: BPublishOptions): Option[String] = { + val repo = pubOpt.contextual(options.publishParams.setupCi).repository.getOrElse( + RepositoryCheck.defaultRepository + ) + RepoParams( + repo, + pubOpt.versionControl.map(_.url), + workspace, + None, + false, + null, + logger + ) match { + case Left(ex) => + logger.debug("Caught exception when trying to compute host to check user credentials") + logger.debug(ex) + None + case Right(params) => + Some(new URI(params.repo.snapshotRepo.root).getHost) + } + } + + private def userOpt(pubOpt: BPublishOptions) = hostOpt(pubOpt) match { + case None => Right(None) + case Some(host) => + configDb().get(Keys.publishCredentials).wrapConfigException.map { credListOpt => + credListOpt.flatMap { credList => + credList + .iterator + .filter(_.host == host) + .map(_.user) + .collectFirst { + case Some(p) => + p + } + } + } + } + def check(pubOpt: BPublishOptions): Boolean = - !options.publishParams.setupCi || - pubOpt.retained(options.publishParams.setupCi).repoUser.nonEmpty + pubOpt.retained(options.publishParams.setupCi).repoUser.nonEmpty || { + !options.publishParams.setupCi && (userOpt(pubOpt) match { + case Left(ex) => + logger.debug("Ignoring error while trying to get user from config") + logger.debug(ex) + true + case Right(valueOpt) => + valueOpt.isDefined + }) + } + def defaultValue(pubOpt: BPublishOptions): Either[BuildException, OptionCheck.DefaultValue] = either { if (options.publishParams.setupCi) { val user0 = options.publishRepo.user match { case Some(value0) => value0.toConfig case None => - val userOpt = value(configDb().get(Keys.sonatypeUser).wrapConfigException) - userOpt match { + value(userOpt(pubOpt)) match { case Some(user) => logger.message("publish.user:") logger.message( - s" using ${Keys.sonatypeUser.fullName} from Scala CLI configuration" + s" using ${Keys.publishCredentials.fullName} from Scala CLI configuration" ) user case None => @@ -40,8 +91,8 @@ final case class UserCheck( new MissingPublishOptionError( "publish user", "--user", - "publish.user", - configKeys = Seq(Keys.sonatypeUser.fullName) + "publish.credentials", + configKeys = Seq(Keys.publishCredentials.fullName) ) } } @@ -54,21 +105,30 @@ final case class UserCheck( Seq(SetSecret("PUBLISH_USER", user0.get(), force = true)) ) } - else if (value(configDb().get(Keys.sonatypeUser).wrapConfigException).isDefined) { - logger.message("publish.user:") - logger.message(s" found ${Keys.sonatypeUser.fullName} in Scala CLI configuration") - OptionCheck.DefaultValue.empty - } else - value { - Left { - new MissingPublishOptionError( - "publish user", - "", - "publish.user", - configKeys = Seq(Keys.sonatypeUser.fullName) - ) - } + hostOpt(pubOpt) match { + case None => + logger.debug("No host, not checking for publish repository user") + OptionCheck.DefaultValue.empty + case Some(host) => + if (value(userOpt(pubOpt).wrapConfigException).isDefined) { + logger.message("publish.credentials:") + logger.message( + s" found user for $host in ${Keys.publishCredentials.fullName} in Scala CLI configuration" + ) + OptionCheck.DefaultValue.empty + } + else + value { + Left { + new MissingPublishOptionError( + "publish user", + "", + "publish.credentials", + configKeys = Seq(Keys.publishCredentials.fullName) + ) + } + } } } } diff --git a/modules/config/src/main/scala/scala/cli/config/Keys.scala b/modules/config/src/main/scala/scala/cli/config/Keys.scala index 80a6a18d64..2dccc4df31 100644 --- a/modules/config/src/main/scala/scala/cli/config/Keys.scala +++ b/modules/config/src/main/scala/scala/cli/config/Keys.scala @@ -17,9 +17,6 @@ object Keys { val pgpSecretKeyPassword = new Key.PasswordEntry(Seq("pgp"), "secret-key-password") val pgpPublicKey = new Key.PasswordEntry(Seq("pgp"), "public-key") - val sonatypeUser = new Key.PasswordEntry(Seq("sonatype"), "user") - val sonatypePassword = new Key.PasswordEntry(Seq("sonatype"), "password") - val actions = new Key.BooleanEntry(Seq.empty, "actions") val interactive = new Key.BooleanEntry(Seq.empty, "interactive") @@ -49,10 +46,9 @@ object Keys { proxyAddress, proxyPassword, proxyUser, + publishCredentials, repositoryCredentials, repositoryMirrors, - sonatypePassword, - sonatypeUser, userEmail, userName, userUrl @@ -184,4 +180,119 @@ object Keys { Right("Inline credentials not accepted, please manually edit the config file") )) } + + private final case class PublishCredentialsAsJson( + host: String, + user: Option[String] = None, + password: Option[String] = None, + realm: Option[String] = None, + httpsOnly: Option[Boolean] = None + ) { + def credentials: Either[::[String], PublishCredentials] = { + val maybeUser = user + .map { u => + PasswordOption.parse(u) match { + case Left(error) => + Left( + s"Malformed publish credentials user value (expected 'value:…', or 'file:/path', or 'env:ENV_VAR_NAME'): $error" + ) + case Right(value) => Right(Some(value)) + } + } + .getOrElse(Right(None)) + val maybePassword = password + .filter(_.nonEmpty) + .map { p => + PasswordOption.parse(p) match { + case Left(error) => + Left( + s"Malformed publish credentials password value (expected 'value:…', or 'file:/path', or 'env:ENV_VAR_NAME'): $error" + ) + case Right(value) => Right(Some(value)) + } + } + .getOrElse(Right(None)) + (maybeUser, maybePassword) match { + case (Right(userOpt), Right(passwordOpt)) => + Right( + PublishCredentials( + host = host, + user = userOpt, + password = passwordOpt, + realm = realm, + httpsOnly = httpsOnly + ) + ) + case _ => + val errors = + (maybeUser.left.toOption.toList ::: maybePassword.left.toOption.toList) match { + case Nil => sys.error("Cannot happen") + case h :: t => ::(h, t) + } + Left(errors) + } + } + } + + val publishCredentials: Key[List[PublishCredentials]] = new Key[List[PublishCredentials]] { + + private def asJson(credentials: PublishCredentials): PublishCredentialsAsJson = + PublishCredentialsAsJson( + credentials.host, + credentials.user.map(_.asString.value), + credentials.password.map(_.asString.value), + credentials.realm, + credentials.httpsOnly + ) + private val codec: JsonValueCodec[List[PublishCredentialsAsJson]] = + JsonCodecMaker.make + + def prefix = Seq("publish") + def name = "credentials" + + def parse(json: Array[Byte]): Either[Key.EntryError, List[PublishCredentials]] = + try { + val list = readFromArray(json)(codec).map(_.credentials) + val errors = list.collect { case Left(errors) => errors }.flatten + errors match { + case Nil => + Right(list.collect { case Right(v) => v }) + case h :: t => + Left(new Key.MalformedEntry(this, ::(h, t))) + } + } + catch { + case e: JsonReaderException => + Left(new Key.JsonReaderError(e)) + } + def write(value: List[PublishCredentials]): Array[Byte] = + writeToArray(value.map(asJson))(codec) + + def asString(value: List[PublishCredentials]): Seq[String] = + value.map { cred => + val prefix = cred.httpsOnly match { + case Some(true) => "https://" + case Some(false) => "http://" + case None => "//" + } + // FIXME We're getting secrets and putting them in a non-Secret guarded string here + val credentialsPart = { + val realmPart = cred.realm.map("(" + _ + ")").getOrElse("") + val userPart = cred.user.map(_.get().value).getOrElse("") + val passwordPart = cred.password.map(":" + _.get().value).getOrElse("") + if (realmPart.nonEmpty || userPart.nonEmpty || passwordPart.nonEmpty) + realmPart + userPart + passwordPart + "@" + else + "" + } + prefix + credentialsPart + cred.host + } + def fromString(values: Seq[String]): Either[Key.MalformedValue, List[PublishCredentials]] = + Left(new Key.MalformedValue( + this, + values, + Right("Inline credentials not accepted, please manually edit the config file") + )) + } + } diff --git a/modules/config/src/main/scala/scala/cli/config/PublishCredentials.scala b/modules/config/src/main/scala/scala/cli/config/PublishCredentials.scala new file mode 100644 index 0000000000..7c4f486b4c --- /dev/null +++ b/modules/config/src/main/scala/scala/cli/config/PublishCredentials.scala @@ -0,0 +1,9 @@ +package scala.cli.config + +final case class PublishCredentials( + host: String = "", + user: Option[PasswordOption] = None, + password: Option[PasswordOption] = None, + realm: Option[String] = None, + httpsOnly: Option[Boolean] = None +) diff --git a/modules/integration/src/test/scala/scala/cli/integration/ConfigTests.scala b/modules/integration/src/test/scala/scala/cli/integration/ConfigTests.scala index 4316188a4a..e89f6b40e3 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/ConfigTests.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/ConfigTests.scala @@ -33,39 +33,40 @@ class ConfigTests extends ScalaCliSuite { val homeDir = os.rel / "home" val dirOptions = Seq[os.Shellable]("--home-directory", homeDir) val password = "1234" + val key = "httpProxy.password" TestInputs.empty.fromRoot { root => def emptyCheck(): Unit = { - val value = os.proc(TestUtil.cli, "config", dirOptions, "sonatype.password") + val value = os.proc(TestUtil.cli, "config", dirOptions, key) .call(cwd = root) expect(value.out.trim().isEmpty) } def unset(): Unit = - os.proc(TestUtil.cli, "config", dirOptions, "sonatype.password", "--unset") + os.proc(TestUtil.cli, "config", dirOptions, key, "--unset") .call(cwd = root) def read(): String = { - val res = os.proc(TestUtil.cli, "config", dirOptions, "sonatype.password") + val res = os.proc(TestUtil.cli, "config", dirOptions, key) .call(cwd = root) res.out.trim() } def readDecoded(env: Map[String, String] = null): String = { - val res = os.proc(TestUtil.cli, "config", dirOptions, "sonatype.password", "--password") + val res = os.proc(TestUtil.cli, "config", dirOptions, key, "--password") .call(cwd = root, env = env) res.out.trim() } emptyCheck() - os.proc(TestUtil.cli, "config", dirOptions, "sonatype.password", s"value:$password") + os.proc(TestUtil.cli, "config", dirOptions, key, s"value:$password") .call(cwd = root) expect(read() == s"value:$password") expect(readDecoded() == password) unset() emptyCheck() - os.proc(TestUtil.cli, "config", dirOptions, "sonatype.password", "env:MY_PASSWORD") + os.proc(TestUtil.cli, "config", dirOptions, key, "env:MY_PASSWORD") .call(cwd = root) expect(read() == "env:MY_PASSWORD") expect(readDecoded(env = Map("MY_PASSWORD" -> password)) == password) @@ -76,7 +77,7 @@ class ConfigTests extends ScalaCliSuite { TestUtil.cli, "config", dirOptions, - "sonatype.password", + key, "env:MY_PASSWORD", "--password-value" ) diff --git a/modules/integration/src/test/scala/scala/cli/integration/PublishSetupTests.scala b/modules/integration/src/test/scala/scala/cli/integration/PublishSetupTests.scala index 3e2a36486d..a93f4fa678 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/PublishSetupTests.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/PublishSetupTests.scala @@ -26,9 +26,15 @@ class PublishSetupTests extends ScalaCliSuite { .call(cwd = root, stdout = os.Inherit) os.proc(TestUtil.cli, "config", dirOptions, "user.url", devUrl) .call(cwd = root, stdout = os.Inherit) - os.proc(TestUtil.cli, "config", dirOptions, "sonatype.user", "value:uSeR") - .call(cwd = root, stdout = os.Inherit) - os.proc(TestUtil.cli, "config", dirOptions, "sonatype.password", "value:1234") + os.proc( + TestUtil.cli, + "config", + dirOptions, + "publish.credentials", + "s01.oss.sonatype.org", + "value:uSeR", + "value:1234" + ) .call(cwd = root, stdout = os.Inherit) os.proc(TestUtil.cli, "config", dirOptions, "--create-pgp-key") .call(cwd = root, stdout = os.Inherit) diff --git a/website/docs/commands/publishing/publish-setup.md b/website/docs/commands/publishing/publish-setup.md index 750e50a1bf..4811d6af22 100644 --- a/website/docs/commands/publishing/publish-setup.md +++ b/website/docs/commands/publishing/publish-setup.md @@ -67,15 +67,14 @@ under specific organizations. You can follow the [sbt-ci-release Sonatype instructions](https://github.com/sbt/sbt-ci-release#sonatype) to create an account there. Either your real Sonatype username and password, or Sonatype tokens, can be used -in Scala CLI (using the `sonatype.user` and `sonatype.password` keys in both cases). +in Scala CLI (via the `publish.credentials` config key in both cases). These can be written in the Scala CLI configuration the following way: ```sh -SONATYPE_USER=me scala-cli config sonatype.user env:SONATYPE_USER --password-value -SONATYPE_PASSWORD=1234 scala-cli config sonatype.password env:SONATYPE_PASSWORD --password-value +SONATYPE_USER=me SONATYPE_PASSWORD=1234 scala-cli config publish.credentials s01.oss.sonatype.org env:SONATYPE_USER env:SONATYPE_PASSWORD --password-value ``` -Note that both `sonatype.user` and `sonatype.password` are assumed to be secrets, and +Note that both user and password arguments are assumed to be secrets, and accept the format documented [here](../../reference/password-options.md). Beyond environment variables, commands or paths to files can provide those values. They can also be passed as is on the command line, although this is not recommended for security reasons. @@ -87,8 +86,7 @@ ask the `config` sub-command to read environment variables and persist the passw If you'd rather persist the environment variable names in the Scala CLI configuration, rather than their values, you can do ```sh -scala-cli config sonatype.user env:SONATYPE_USER -scala-cli config sonatype.password env:SONATYPE_PASSWORD +scala-cli config publish.credentials s01.oss.sonatype.org env:SONATYPE_USER env:SONATYPE_PASSWORD ``` Note that in this case, both `SONATYPE_USER` and `SONATYPE_PASSWORD` will need to be available @@ -207,9 +205,9 @@ computeVersion: repository: using Maven Central via its s01 server publish.user: - using sonatype.user from Scala CLI configuration + using publish.credentials from Scala CLI configuration publish.password: - using sonatype.password from Scala CLI configuration + using publish.credentials from Scala CLI configuration license: using Apache-2.0 (default) url: diff --git a/website/docs/commands/publishing/publish.md b/website/docs/commands/publishing/publish.md index 54c0db98a7..1003da0f3d 100644 --- a/website/docs/commands/publishing/publish.md +++ b/website/docs/commands/publishing/publish.md @@ -76,17 +76,13 @@ setting those settings via using directives. When publishing from your local mac we recommend setting the repository via a `publish.repository` directive, and keeping your Sonatype credentials in the Scala CLI settings, via commands such as ```bash -SONATYPE_USER=… scala-cli config sonatype.user env:SONATYPE_USER --password-value -SONATYPE_PASSWORD=… scala-cli config sonatype.password env:SONATYPE_PASSWORD +SONATYPE_USER=… SONATYPE_PASSWORD=… scala-cli config publish.credentials s01.oss.sonatype.org env:SONATYPE_USER env:SONATYPE_PASSWORD ``` | | `using` directive | Command-line option | Example values | Notes | |----------|-------------------|---------------------|----------------|-------| | Repository | `publish.repository` | `--publish-repository` | `central`, `central-s01`, `github`, `https://artifacts.company.com/maven` | | -| Repository User | `publish.user` | `--user` | `env:SONATYPE_USER` | Password value format | -| Repository Password | `publish.password` | `--password` | `env:SONATYPE_PASSWORD` | -| Repository Realm | `publish.realm` | `--realm` | | | ## Other settings