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", _) }