All Downloads are FREE. Search and download functionalities are using the official Maven repository.

mill.scalalib.PublishModule.scala Maven / Gradle / Ivy

The newest version!
package mill
package scalalib

import coursier.core.{Configuration, DependencyManagement}
import mill.define.{Command, ExternalModule, Task}
import mill.api.{JarManifest, PathRef, Result}
import mill.main.Tasks
import mill.scalalib.PublishModule.checkSonatypeCreds
import mill.scalalib.publish.SonatypeHelpers.{
  PASSWORD_ENV_VARIABLE_NAME,
  USERNAME_ENV_VARIABLE_NAME
}
import mill.scalalib.publish.{Artifact, SonatypePublisher}
import os.Path

/**
 * Configuration necessary for publishing a Scala module to Maven Central or similar
 */
trait PublishModule extends JavaModule { outer =>
  import mill.scalalib.publish._

  override def moduleDeps: Seq[PublishModule] = super.moduleDeps.map {
    case m: PublishModule => m
    case other =>
      throw new Exception(
        s"PublishModule moduleDeps need to be also PublishModules. $other is not a PublishModule"
      )
  }

  // TODO Add this when we can break bin-compat. See also below in publishXmlBomDeps.
  // override def bomModuleDeps: Seq[BomModule with PublishModule] = super.bomModuleDeps.map {
  //   case m: BomModule with PublishModule => m
  //   case other =>
  //     throw new Exception(
  //       s"PublishModule bomModuleDeps need to be also PublishModules. $other is not a PublishModule"
  //     )
  // }

  /**
   * The packaging type. See [[PackagingType]] for specially handled values.
   */
  def pomPackagingType: String =
    this match {
      case _: BomModule => PackagingType.Pom
      case _ => PackagingType.Jar
    }

  /**
   * POM parent project.
   *
   * @see [[https://maven.apache.org/guides/introduction/introduction-to-the-pom.html#Project_Inheritance Project Inheritance]]
   */
  def pomParentProject: T[Option[Artifact]] = None

  /**
   * Configuration for the `pom.xml` metadata file published with this module
   */
  def pomSettings: T[PomSettings]

  /**
   * The artifact version that this module would be published as
   */
  def publishVersion: T[String]

  /**
   * Optional information about the used version scheme.
   * This may enable dependency resolvers to properly resolve version ranges and version mismatches (conflicts).
   * This information will be written as `info.versionScheme` property in the `pom.xml`.
   * See [[VersionScheme]] for possible values.
   *
   * You can find more info under these links:
   * - https://docs.scala-lang.org/overviews/core/binary-compatibility-for-library-authors.html#recommended-versioning-scheme
   * - https://www.scala-lang.org/blog/2021/02/16/preventing-version-conflicts-with-versionscheme.html
   * - https://www.scala-sbt.org/1.x/docs/Publishing.html#Version+scheme
   * - https://semver.org
   *
   * @since Mill after 0.10.0-M5
   */
  def versionScheme: T[Option[VersionScheme]] = Task { None }

  def publishSelfDependency: T[Artifact] = Task {
    Artifact(pomSettings().organization, artifactId(), publishVersion())
  }

  def publishIvyDeps
      : Task[(Map[coursier.core.Module, String], DependencyManagement.Map) => Agg[Dependency]] =
    Task.Anon {
      (rootDepVersions: Map[coursier.core.Module, String], bomDepMgmt: DependencyManagement.Map) =>
        val bindDependency0 = bindDependency()
        val resolvePublishDependency0 = resolvePublishDependency.apply()

        // Ivy doesn't support BOM, so we try to add versions and exclusions from BOMs
        // to the dependencies themselves.
        def process(dep: mill.scalalib.Dep) = {
          var dep0 = bindDependency0(dep).dep

          if (dep0.version.isEmpty)
            for (version <- rootDepVersions.get(dep0.module))
              dep0 = dep0.withVersion(version)

          for (
            values <- bomDepMgmt.get(DependencyManagement.Key.from(dep0))
            if values.minimizedExclusions.nonEmpty
          )
            dep0 = dep0.withMinimizedExclusions(
              dep0.minimizedExclusions.join(values.minimizedExclusions)
            )

          resolvePublishDependency0(BoundDep(dep0, force = false).toDep)
        }

        val ivyPomDeps = allIvyDeps().map(process)

        val runIvyPomDeps = runIvyDeps().map(process)
          .filter(!ivyPomDeps.contains(_))

        val compileIvyPomDeps = compileIvyDeps().map(process)
          .filter(!ivyPomDeps.contains(_))

        val modulePomDeps = Task.sequence(moduleDepsChecked.collect {
          case m: PublishModule => m.publishSelfDependency
        })()
        val compileModulePomDeps = Task.sequence(compileModuleDepsChecked.collect {
          case m: PublishModule => m.publishSelfDependency
        })()
        val runModulePomDeps = Task.sequence(runModuleDepsChecked.collect {
          case m: PublishModule => m.publishSelfDependency
        })()

        ivyPomDeps ++
          compileIvyPomDeps.map(_.copy(scope = Scope.Provided)) ++
          runIvyPomDeps.map(_.copy(scope = Scope.Runtime)) ++
          modulePomDeps.map(Dependency(_, Scope.Compile)) ++
          compileModulePomDeps.map(Dependency(_, Scope.Provided)) ++
          runModulePomDeps.map(Dependency(_, Scope.Runtime))
    }

  def publishXmlDeps: Task[Agg[Dependency]] = Task.Anon {
    val ivyPomDeps =
      allIvyDeps()
        .map(resolvePublishDependency.apply().apply(_))

    val runIvyPomDeps = runIvyDeps()
      .map(resolvePublishDependency.apply().apply(_))
      .filter(!ivyPomDeps.contains(_))

    val compileIvyPomDeps = compileIvyDeps()
      .map(resolvePublishDependency.apply().apply(_))
      .filter(!ivyPomDeps.contains(_))

    val modulePomDeps = Task.sequence(moduleDepsChecked.collect {
      case m: PublishModule => m.publishSelfDependency
    })()
    val compileModulePomDeps = Task.sequence(compileModuleDepsChecked.collect {
      case m: PublishModule => m.publishSelfDependency
    })()
    val runModulePomDeps = Task.sequence(runModuleDepsChecked.collect {
      case m: PublishModule => m.publishSelfDependency
    })()

    ivyPomDeps ++
      compileIvyPomDeps.map(_.copy(scope = Scope.Provided)) ++
      runIvyPomDeps.map(_.copy(scope = Scope.Runtime)) ++
      modulePomDeps.map(Dependency(_, Scope.Compile)) ++
      compileModulePomDeps.map(Dependency(_, Scope.Provided)) ++
      runModulePomDeps.map(Dependency(_, Scope.Runtime))
  }

  /**
   * BOM dependency to specify in the POM
   */
  def publishXmlBomDeps: Task[Agg[Dependency]] = Task.Anon {
    val fromBomMods = Task.traverse(
      bomModuleDepsChecked
        // TODO When we can break bin-compat, add the bomModuleDeps override above,
        // and change the .map to this .collect:
        // .collect { case p: PublishModule => p }
        .map {
          case p: PublishModule => p
          case other =>
            throw new Exception(
              s"PublishModule bomModuleDeps need to be also PublishModules. $other is not a PublishModule"
            )
        }
    )(_.artifactMetadata)().map { a =>
      Dependency(a, Scope.Import)
    }
    Agg(fromBomMods: _*) ++
      bomIvyDeps().map(resolvePublishDependency.apply().apply(_))
  }

  /**
   * Dependency management to specify in the POM
   */
  def publishXmlDepMgmt: Task[Agg[Dependency]] = Task.Anon {
    depManagement().map(resolvePublishDependency.apply().apply(_))
  }

  def pom: T[PathRef] = Task {
    val pom = Pom(
      artifactMetadata(),
      publishXmlDeps(),
      artifactId(),
      pomSettings(),
      publishProperties(),
      packagingType = pomPackagingType,
      parentProject = pomParentProject(),
      bomDependencies = publishXmlBomDeps(),
      dependencyManagement = publishXmlDepMgmt()
    )
    val pomPath = Task.dest / s"${artifactId()}-${publishVersion()}.pom"
    os.write.over(pomPath, pom)
    PathRef(pomPath)
  }

  /**
   * Dependencies with version placeholder filled from BOMs, alongside with BOM data
   */
  @deprecated("Unused by Mill", "Mill after 0.12.5")
  def bomDetails: T[(Map[coursier.core.Module, String], coursier.core.DependencyManagement.Map)] =
    Task {
      val res = millResolver().resolution(Seq(coursierDependency))
      val processedDeps = res.finalDependenciesCache.getOrElse(
        coursierDependency,
        sys.error(
          s"Should not happen - could not find root dependency $coursierDependency in Resolution#finalDependenciesCache"
        )
      )
      val depMgmt = res.projectCache
        .get(coursierDependency.moduleVersion)
        .map(_._2.overrides.flatten.toMap)
        .getOrElse {
          sys.error(
            s"Should not happen - could not find root dependency ${coursierDependency.moduleVersion} in Resolution#projectCache"
          )
        }
      (processedDeps.map(_.moduleVersion).toMap, depMgmt)
    }

  /**
   * Path to the ivy.xml file for this module
   */
  def ivy: T[PathRef] = Task {
    val content = ivy(hasJar = pomPackagingType != PackagingType.Pom)()
    val ivyPath = Task.dest / "ivy.xml"
    os.write.over(ivyPath, content)
    PathRef(ivyPath)
  }

  /**
   * ivy.xml content for this module
   *
   * @param hasJar Whether this module has a JAR or not
   * @return
   */
  private def ivy(hasJar: Boolean): Task[String] = Task.Anon {
    val dep = coursierDependency.withConfiguration(Configuration.runtime)
    val resolution = millResolver().resolution(Seq(BoundDep(dep, force = false)))

    val (results, bomDepMgmt) =
      (
        resolution.finalDependenciesCache.getOrElse(
          dep,
          sys.error(
            s"Should not happen - could not find root dependency $dep in Resolution#finalDependenciesCache"
          )
        ),
        resolution.projectCache
          .get(dep.moduleVersion)
          .map(_._2.overrides.flatten.toMap)
          .getOrElse {
            sys.error(
              s"Should not happen - could not find root dependency ${dep.moduleVersion} in Resolution#projectCache"
            )
          }
      )
    val publishXmlDeps0 = {
      val rootDepVersions = results.map(_.moduleVersion).toMap
      publishIvyDeps.apply().apply(rootDepVersions, bomDepMgmt)
    }
    val overrides = {
      val bomDepMgmt0 = {
        // Ensure we don't override versions of root dependencies with overrides from the BOM
        val rootDepsAdjustment = publishXmlDeps0.iterator.flatMap { dep =>
          val key = coursier.core.DependencyManagement.Key(
            coursier.core.Organization(dep.artifact.group),
            coursier.core.ModuleName(dep.artifact.id),
            coursier.core.Type.jar,
            coursier.core.Classifier.empty
          )
          bomDepMgmt.get(key).flatMap { values =>
            if (values.version.nonEmpty && values.version != dep.artifact.version)
              Some(key -> values.withVersion(""))
            else
              None
          }
        }
        bomDepMgmt ++ rootDepsAdjustment
      }
      lazy val moduleSet = publishXmlDeps0.map(dep => (dep.artifact.group, dep.artifact.id)).toSet
      val depMgmtEntries = processedDependencyManagement(
        depManagement().toSeq
          .map(bindDependency())
          .map(_.dep)
          .filter(_.version.nonEmpty)
          .filter { depMgmt =>
            // Ensure we don't override versions of root dependencies with overrides from the BOM
            !moduleSet.contains((depMgmt.module.organization.value, depMgmt.module.name.value))
          }
      )
      val entries = coursier.core.DependencyManagement.add(
        Map.empty,
        depMgmtEntries ++ bomDepMgmt0
          .filter {
            case (key, _) =>
              // Ensure we don't override versions of root dependencies with overrides from the BOM
              !moduleSet.contains((key.organization.value, key.name.value))
          }
      )
      entries.toVector
        .map {
          case (key, values) =>
            Ivy.Override(
              key.organization.value,
              key.name.value,
              values.version
            )
        }
        .sortBy(value => (value.organization, value.name, value.version))
    }
    Ivy(artifactMetadata(), publishXmlDeps0, extraPublish(), overrides, hasJar = hasJar)
  }

  def artifactMetadata: T[Artifact] = Task {
    Artifact(pomSettings().organization, artifactId(), publishVersion())
  }

  private def defaultPublishInfos: T[Seq[PublishInfo]] = {
    def defaultPublishJars: Task[Seq[(PathRef, PathRef => PublishInfo)]] = {
      pomPackagingType match {
        case PackagingType.Pom => Task.Anon(Seq())
        case _ => Task.Anon(Seq((jar(), PublishInfo.jar _)))
      }
    }
    Task {
      defaultPublishJars().map { case (jar, info) => info(jar) }
    }
  }

  /**
   * Extra artifacts to publish.
   */
  def extraPublish: T[Seq[PublishInfo]] = Task { Seq.empty[PublishInfo] }

  /**
   * Properties to be published with the published pom/ivy XML.
   * Use `super.publishProperties() ++` when overriding to avoid losing default properties.
   * @since Mill after 0.10.0-M5
   */
  def publishProperties: T[Map[String, String]] = Task {
    versionScheme().map(_.toProperty).toMap
  }

  /**
   * Publish artifacts to a local ivy repository.
   * @param localIvyRepo The local ivy repository.
   *                     If not defined, the default resolution is used (probably `$HOME/.ivy2/local`).
   * @param sources whether to generate and publish a sources JAR
   * @param doc whether to generate and publish a javadoc JAR
   * @param transitive if true, also publish locally the transitive module dependencies of this module
   *                   (this includes the runtime transitive module dependencies, but not the compile-only ones)
   */
  def publishLocal(
      localIvyRepo: String = null,
      sources: Boolean = true,
      doc: Boolean = true,
      transitive: Boolean = false
  ): define.Command[Unit] = Task.Command {
    publishLocalTask(
      Task.Anon {
        Option(localIvyRepo).map(os.Path(_, Task.workspace))
      },
      sources,
      doc,
      transitive
    )()
    Result.Success(())
  }

  // bin-compat shim
  def publishLocal(
      localIvyRepo: String
  ): define.Command[Unit] =
    publishLocal(localIvyRepo, sources = true, doc = true, transitive = false)

  /**
   * Publish artifacts the local ivy repository.
   */
  def publishLocalCached: T[Seq[PathRef]] = Task {
    val res = publishLocalTask(
      Task.Anon(None),
      sources = true,
      doc = true,
      transitive = false
    )()
    res.map(p => PathRef(p).withRevalidateOnce)
  }

  private def publishLocalTask(
      localIvyRepo: Task[Option[os.Path]],
      sources: Boolean,
      doc: Boolean,
      transitive: Boolean
  ): Task[Seq[Path]] =
    if (transitive) {
      val publishTransitiveModuleDeps = (transitiveModuleDeps ++ transitiveRunModuleDeps).collect {
        case p: PublishModule => p
      }
      Target.traverse(publishTransitiveModuleDeps.distinct) { publishMod =>
        publishMod.publishLocalTask(localIvyRepo, sources, doc, transitive = false)
      }.map(_.flatten)
    } else {
      val sourcesJarOpt =
        if (sources) Task.Anon(Some(PublishInfo.sourcesJar(sourceJar())))
        else Task.Anon(None)
      val docJarOpt =
        if (doc) Task.Anon(Some(PublishInfo.docJar(docJar())))
        else Task.Anon(None)

      Task.Anon {
        val publisher = localIvyRepo() match {
          case None => LocalIvyPublisher
          case Some(path) => new LocalIvyPublisher(path)
        }
        val publishInfos =
          defaultPublishInfos() ++ sourcesJarOpt().toSeq ++ docJarOpt().toSeq ++ extraPublish()
        publisher.publishLocal(
          pom = pom().path,
          ivy = Right(ivy().path),
          artifact = artifactMetadata(),
          publishInfos = publishInfos
        )
      }
    }

  /**
   * Publish artifacts to a local Maven repository.
   * @param m2RepoPath The path to the local repository  as string (default: `$HOME/.m2repository`).
   *                   If not set, falls back to `maven.repo.local` system property or `~/.m2/repository`
   * @return [[PathRef]]s to published files.
   */
  def publishM2Local(m2RepoPath: String = null): Command[Seq[PathRef]] = m2RepoPath match {
    case null => Task.Command { publishM2LocalTask(Task.Anon { publishM2LocalRepoPath() })() }
    case p => Task.Command { publishM2LocalTask(Task.Anon { os.Path(p, Task.workspace) })() }
  }

  /**
   * Publish artifacts to the local Maven repository.
   * @return [[PathRef]]s to published files.
   */
  def publishM2LocalCached: T[Seq[PathRef]] = Task {
    publishM2LocalTask(publishM2LocalRepoPath)()
  }

  /**
   * The default path that [[publishM2Local]] should publish its artifacts to.
   * Defaults to `~/.m2/repository`, but can be configured by setting the
   * `maven.repo.local` JVM property
   */
  def publishM2LocalRepoPath: Task[os.Path] = Task.Input {
    sys.props.get("maven.repo.local").map(os.Path(_))
      .getOrElse(os.Path(os.home / ".m2", Task.workspace)) / "repository"
  }

  private def publishM2LocalTask(m2RepoPath: Task[os.Path]): Task[Seq[PathRef]] = Task.Anon {
    val path = m2RepoPath()
    val publishInfos = defaultPublishInfos() ++
      Seq(
        PublishInfo.sourcesJar(sourceJar()),
        PublishInfo.docJar(docJar())
      ) ++
      extraPublish()

    new LocalM2Publisher(path)
      .publish(pom().path, artifactMetadata(), publishInfos)
      .map(PathRef(_).withRevalidateOnce)
  }

  def sonatypeUri: String = "https://oss.sonatype.org/service/local"

  def sonatypeSnapshotUri: String = "https://oss.sonatype.org/content/repositories/snapshots"

  def publishArtifacts: T[PublishModule.PublishData] = {
    val baseNameTask: Task[String] = Task.Anon { s"${artifactId()}-${publishVersion()}" }
    val defaultPayloadTask: Task[Seq[(PathRef, String)]] = pomPackagingType match {
      case PackagingType.Pom => Task.Anon {
          val baseName = baseNameTask()
          Seq(
            pom() -> s"$baseName.pom"
          )
        }
      case PackagingType.Jar | _ => Task.Anon {
          val baseName = baseNameTask()
          Seq(
            jar() -> s"$baseName.jar",
            sourceJar() -> s"$baseName-sources.jar",
            docJar() -> s"$baseName-javadoc.jar",
            pom() -> s"$baseName.pom"
          )
        }
    }
    Task {
      val baseName = baseNameTask()
      PublishModule.PublishData(
        meta = artifactMetadata(),
        payload = defaultPayloadTask() ++ extraPublish().map(p =>
          (p.file, s"$baseName${p.classifierPart}.${p.ext}")
        )
      )
    }
  }

  /**
   * Publish all given artifacts to Sonatype.
   * Uses environment variables MILL_SONATYPE_USERNAME and MILL_SONATYPE_PASSWORD as
   * credentials.
   *
   * @param sonatypeCreds Sonatype credentials in format username:password.
   *                      If specified, environment variables will be ignored.
   *                      Note: consider using environment variables over this argument due
   *                      to security reasons.
   * @param gpgArgs       GPG arguments. Defaults to `--batch --yes -a -b`.
   *                      Specifying this will override/remove the defaults.
   *                      Add the default args to your args to keep them.
   */
  def publish(
      sonatypeCreds: String = "",
      signed: Boolean = true,
      // mainargs wasn't handling a default value properly,
      // so we instead use the empty Seq as default.
      // see https://github.com/com-lihaoyi/mill/pull/1678
      // TODO: In mill 0.11, we may want to change to a String argument
      // which we can split at `,` symbols, as we do in `PublishModule.publishAll`.
      gpgArgs: Seq[String] = Seq.empty,
      release: Boolean = true,
      readTimeout: Int = 30 * 60 * 1000,
      connectTimeout: Int = 30 * 60 * 1000,
      awaitTimeout: Int = 30 * 60 * 1000,
      stagingRelease: Boolean = true
  ): define.Command[Unit] = Task.Command {
    val PublishModule.PublishData(artifactInfo, artifacts) = publishArtifacts()
    PublishModule.pgpImportSecretIfProvided(Task.env)
    new SonatypePublisher(
      sonatypeUri,
      sonatypeSnapshotUri,
      checkSonatypeCreds(sonatypeCreds)(),
      signed,
      if (gpgArgs.isEmpty) PublishModule.defaultGpgArgsForPassphrase(Task.env.get("PGP_PASSPHRASE"))
      else gpgArgs,
      readTimeout,
      connectTimeout,
      Task.log,
      Task.workspace,
      Task.env,
      awaitTimeout,
      stagingRelease
    ).publish(artifacts.map { case (a, b) => (a.path, b) }, artifactInfo, release)
  }

  override def manifest: T[JarManifest] = Task {
    import java.util.jar.Attributes.Name
    val pom = pomSettings()
    super.manifest().add(
      Name.IMPLEMENTATION_TITLE.toString() -> artifactName(),
      Name.IMPLEMENTATION_VERSION.toString() -> publishVersion(),
      Name.IMPLEMENTATION_VENDOR.toString() -> pom.organization,
      "Description" -> pom.description,
      "URL" -> pom.url,
      "Licenses" -> pom.licenses.map(l => s"${l.name} (${l.id})").mkString(",")
    )
  }
}

object PublishModule extends ExternalModule with TaskModule {
  def defaultCommandName(): String = "publishAll"
  val defaultGpgArgs: Seq[String] = defaultGpgArgsForPassphrase(None)
  def pgpImportSecretIfProvided(env: Map[String, String]): Unit = {
    for (secret <- env.get("MILL_PGP_SECRET_BASE64")) {
      os.call(
        ("gpg", "--import", "--no-tty", "--batch", "--yes"),
        stdin = java.util.Base64.getDecoder.decode(secret)
      )
    }
  }

  def defaultGpgArgsForPassphrase(passphrase: Option[String]): Seq[String] = {
    passphrase.map("--passphrase=" + _).toSeq ++
      Seq(
        "--no-tty",
        "--pinentry-mode",
        "loopback",
        "--batch",
        "--yes",
        "-a",
        "-b"
      )
  }

  case class PublishData(meta: Artifact, payload: Seq[(PathRef, String)]) {

    /**
     * Maps the path reference to an actual path so that it can be used in publishAll signatures
     */
    private[mill] def withConcretePath: (Seq[(Path, String)], Artifact) =
      (payload.map { case (p, f) => (p.path, f) }, meta)
  }
  object PublishData {
    import mill.scalalib.publish.artifactFormat
    implicit def jsonify: upickle.default.ReadWriter[PublishData] = upickle.default.macroRW
  }

  /**
   * Publish all given artifacts to Sonatype.
   * Uses environment variables SONATYPE_USERNAME and SONATYPE_PASSWORD as
   * credentials.
   *
   * @param publishArtifacts what artifacts you want to publish. Defaults to `__.publishArtifacts`
   *                         which selects all `PublishModule`s in your build
   * @param sonatypeCreds Sonatype credentials in format username:password.
   *                      If specified, environment variables will be ignored.
   *                      Note: consider using environment variables over this argument due
   *                      to security reasons.
   * @param signed
   * @param gpgArgs       GPG arguments. Defaults to `--passphrase=$MILL_PGP_PASSPHRASE,--no-tty,--pienty-mode,loopback,--batch,--yes,-a,-b`.
   *                      Specifying this will override/remove the defaults.
   *                      Add the default args to your args to keep them.
   * @param release Whether to release the artifacts after staging them
   * @param sonatypeUri Sonatype URI to use. Defaults to `oss.sonatype.org`, newer projects
   *                    may need to set it to https://s01.oss.sonatype.org/service/local
   * @param sonatypeSnapshotUri Sonatype snapshot URI to use. Defaults to `oss.sonatype.org`, newer projects
   *                            may need to set it to https://s01.oss.sonatype.org/content/repositories/snapshots
   * @param readTimeout How long to wait before timing out network reads
   * @param connectTimeout How long to wait before timing out network connections
   * @param awaitTimeout How long to wait before timing out on failed uploads
   * @param stagingRelease
   * @return
   */
  def publishAll(
      publishArtifacts: Tasks[PublishModule.PublishData] =
        Tasks.resolveMainDefault("__.publishArtifacts"),
      sonatypeCreds: String = "",
      signed: Boolean = true,
      gpgArgs: String = "",
      release: Boolean = true,
      sonatypeUri: String = "https://oss.sonatype.org/service/local",
      sonatypeSnapshotUri: String = "https://oss.sonatype.org/content/repositories/snapshots",
      readTimeout: Int = 30 * 60 * 1000,
      connectTimeout: Int = 30 * 60 * 1000,
      awaitTimeout: Int = 30 * 60 * 1000,
      stagingRelease: Boolean = true
  ): Command[Unit] = Task.Command {
    val x: Seq[(Seq[(os.Path, String)], Artifact)] = Task.sequence(publishArtifacts.value)().map {
      case PublishModule.PublishData(a, s) => (s.map { case (p, f) => (p.path, f) }, a)
    }

    pgpImportSecretIfProvided(Task.env)

    new SonatypePublisher(
      sonatypeUri,
      sonatypeSnapshotUri,
      checkSonatypeCreds(sonatypeCreds)(),
      signed,
      if (gpgArgs.isEmpty) defaultGpgArgsForPassphrase(Task.env.get("MILL_PGP_PASSPHRASE"))
      else gpgArgs.split(','),
      readTimeout,
      connectTimeout,
      Task.log,
      Task.workspace,
      Task.env,
      awaitTimeout,
      stagingRelease
    ).publishAll(
      release,
      x: _*
    )
  }

  private def getSonatypeCredsFromEnv: Task[(String, String)] = Task.Anon {
    (for {
      // Allow legacy environment variables as well
      username <- Task.env.get(USERNAME_ENV_VARIABLE_NAME).orElse(Task.env.get("SONATYPE_USERNAME"))
      password <- Task.env.get(PASSWORD_ENV_VARIABLE_NAME).orElse(Task.env.get("SONATYPE_PASSWORD"))
    } yield {
      Result.Success((username, password))
    }).getOrElse(
      Result.Failure(
        s"Consider using ${USERNAME_ENV_VARIABLE_NAME}/${PASSWORD_ENV_VARIABLE_NAME} environment variables or passing `sonatypeCreds` argument"
      )
    )
  }

  private[scalalib] def checkSonatypeCreds(sonatypeCreds: String): Task[String] =
    if (sonatypeCreds.isEmpty) {
      for {
        (username, password) <- getSonatypeCredsFromEnv
      } yield s"$username:$password"
    } else {
      Task.Anon {
        if (sonatypeCreds.split(":").length >= 2) {
          Result.Success(sonatypeCreds)
        } else {
          Result.Failure(
            "Sonatype credentials must be set in the following format - username:password. Incorrect format received."
          )
        }
      }
    }

  lazy val millDiscover: mill.define.Discover = mill.define.Discover[this.type]
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy