aboutsummaryrefslogblamecommitdiff
path: root/plugins/sonatype-release/src/sonatype/SonatypeLib.scala
blob: 4666950f047e4e6ba237f4c815e49cf3d84b9e21 (plain) (tree)





















































                                                                                                                        




                                    









                                                                                                                     







                             
































                                                                                         



                       


































                                                                                                                        
package cbt.sonatype

import java.io.File
import java.net.URL
import java.nio.file.Files._
import java.nio.file.Paths

import cbt.{ ExitCode, Lib }

/**
  * Sonatype release process is:
  * • get your profile info to publish artifacts
  * • open staging repository to publish artifacts
  * • publish signed artifacts and signatures to staging repository
  * • close staging repository
  * • promote staging repository
  * • drop staging repository
  */

object SonatypeLib {

  val sonatypeServiceURI: String = "https://oss.sonatype.org/service/local"

  val sonatypeSnapshotsURI: String = "https://oss.sonatype.org/content/repositories/snapshots"

  /**
    * login:password for Sonatype access.
    * Order of credentials lookup:
    * • environment variables SONATYPE_USERNAME and SONATYPE_PASSWORD
    * • ~/.cbt/sonatype-credentials
    */
  def sonatypeCredentials: String = {
    def fromEnv = for {
      username <- Option(System.getenv("SONATYPE_USERNAME"))
      password <- Option(System.getenv("SONATYPE_PASSWORD"))
    } yield s"$username:$password"

    def fromFile = {
      for {
        home <- Option(System.getProperty("user.home"))
        credsPath = Paths.get(home, ".cbt", "sonatype-credentials")
      } yield new String(readAllBytes(credsPath)).trim
    }

    fromEnv
      .orElse(fromFile)
      .getOrElse(throw new Exception(
        "No Sonatype credentials found! You can provide them via SONATYPE_USERNAME, SONATYPE_PASSWORD env variables, " +
          "or in ~/.cbt/sonatype-credentials file as login:password"
      ))
  }
}

final class SonatypeLib(
  sonatypeServiceURI: String,
  sonatypeSnapshotsURI: String,
  sonatypeCredentials: String,
  profileName: String
)( lib: Lib, log: String => Unit ) {

  private val sonatypeApi = new SonatypeHttpApi(sonatypeServiceURI, sonatypeCredentials, profileName)(sonatypeLogger)

  /*
   * Signed publish steps:
   * • create new staging repo
   * • create artifacts and sign them
   * • publish jars to created repo
  */
  def sonatypePublishSigned(
    sourceFiles: Seq[File],
    artifacts: Seq[File],
    groupId: String,
    artifactId: String,
    version: String,
    isSnapshot: Boolean,
    scalaMajorVersion: String
  ): ExitCode = {
    if(sourceFiles.nonEmpty) {
      System.err.println(lib.blue("Staring publishing to Sonatype."))

      val profile = getStagingProfile()

      val deployURI = (if (isSnapshot) {
        sonatypeSnapshotsURI
      } else {
        val repoId = sonatypeApi.createStagingRepo(profile)
        s"${sonatypeServiceURI}/staging/deployByRepositoryId/${repoId.repositoryId}"
      }) + s"/${groupId.replace(".", "/")}/${artifactId}_${scalaMajorVersion}/${version}"

      lib.publishSigned(
        artifacts = artifacts,
        url = new URL(deployURI),
        credentials = Some(sonatypeCredentials)
      )
      System.err.println(lib.green("Successfully published artifacts to Sonatype."))
      ExitCode.Success
    } else {
      System.err.println(lib.red("Sources are empty, won't publish empty jar."))
      ExitCode.Failure
    }
  }

  /**
    * Release is:
    * • find staging repo related to current profile;
    * • close this staging repo;
    * • wait until this repo is released;
    * • drop this repo.
    */
  def sonatypeRelease(
    groupId: String,
    artifactId: String,
    version: String
  ): ExitCode = {
    val profile = getStagingProfile()

    sonatypeApi.getStagingRepos(profile).toList match {
      case Nil =>
        System.err.println(lib.red("No staging repositories found, you need to publish artifacts first."))
        ExitCode.Failure
      case repo :: Nil =>
        sonatypeApi.finishRelease(repo, profile)
        System.err.println(lib.green(s"Successfully released ${groupId}/${artifactId} v:${version}"))
        ExitCode.Success
      case repos =>
        val showRepo = { r: StagingRepository => s"${r.repositoryId} in state: ${r.state}" }
        val toRelease = lib.pickOne(lib.blue(s"More than one staging repo found. Select one of them:"), repos)(showRepo)

        toRelease map { repo =>
          sonatypeApi.finishRelease(repo, profile)
          System.err.println(lib.green(s"Successfully released ${groupId}/${artifactId} v:${version}"))
          ExitCode.Success
        } getOrElse {
          System.err.println(lib.red("Wrong repository number, try again please."))
          ExitCode.Failure
        }
    }
  }

  private def getStagingProfile() =
    try {
      sonatypeApi.getStagingProfile
    } catch {
      case e: Exception => throw new Exception(s"Failed to get info for profile: $profileName", e)
    }

  private def sonatypeLogger: String => Unit = lib.logger.log("Sonatype", _)

}