From 669ef3dfc3201fffa451b47d2b629a856afc0b25 Mon Sep 17 00:00:00 2001 From: Nikolay Tatarinov Date: Mon, 3 Oct 2016 20:19:27 +0300 Subject: Sonatype release plugin (#247) --- examples/sonatype-release-example/README.md | 1 + .../sonatype-release-example/build/build.scala | 31 +++ .../build/build/build.scala | 5 + examples/sonatype-release-example/src/Main.scala | 3 + plugins/sonatype-release/build/build.scala | 3 + plugins/sonatype-release/src/SonatypeRelease.scala | 51 +++++ .../sonatype-release/src/sonatype/HttpUtils.scala | 65 +++++++ .../src/sonatype/SonatypeHttpApi.scala | 215 +++++++++++++++++++++ .../src/sonatype/SonatypeLib.scala | 148 ++++++++++++++ plugins/sonatype-release/src/sonatype/models.scala | 31 +++ stage2/BuildBuild.scala | 1 + stage2/Lib.scala | 11 +- stage2/PackageJars.scala | 1 + stage2/Publish.scala | 34 +--- 14 files changed, 566 insertions(+), 34 deletions(-) create mode 100644 examples/sonatype-release-example/README.md create mode 100644 examples/sonatype-release-example/build/build.scala create mode 100644 examples/sonatype-release-example/build/build/build.scala create mode 100644 examples/sonatype-release-example/src/Main.scala create mode 100644 plugins/sonatype-release/build/build.scala create mode 100644 plugins/sonatype-release/src/SonatypeRelease.scala create mode 100644 plugins/sonatype-release/src/sonatype/HttpUtils.scala create mode 100644 plugins/sonatype-release/src/sonatype/SonatypeHttpApi.scala create mode 100644 plugins/sonatype-release/src/sonatype/SonatypeLib.scala create mode 100644 plugins/sonatype-release/src/sonatype/models.scala diff --git a/examples/sonatype-release-example/README.md b/examples/sonatype-release-example/README.md new file mode 100644 index 0000000..a099036 --- /dev/null +++ b/examples/sonatype-release-example/README.md @@ -0,0 +1 @@ +TBD diff --git a/examples/sonatype-release-example/build/build.scala b/examples/sonatype-release-example/build/build.scala new file mode 100644 index 0000000..6af452d --- /dev/null +++ b/examples/sonatype-release-example/build/build.scala @@ -0,0 +1,31 @@ +import java.net.URL + +import cbt._ + +class Build(val context: Context) extends SonatypeRelease { + def groupId: String = "com.github.rockjam" + def defaultVersion: String = "0.0.15" + def name: String = "cbt-sonatype" + + def description: String = "Plugin for CBT to release artifacts to sonatype OSS" + def developers: Seq[Developer] = Seq( + Developer( + "rockjam", + "Nikolay Tatarinov", + "GMT+3", + new URL("https://github.com/rockjam") + ) + ) + def inceptionYear: Int = 2016 + def licenses: Seq[cbt.License] = Seq(License.Apache2) + def organization: Option[cbt.Organization] = None + def scmConnection: String = "" + def scmUrl: String = "https://github.com/rockjam/cbt-sonatype.git" + def url: java.net.URL = new URL("https://github.com/rockjam/cbt-sonatype") + + override def dependencies = + super.dependencies ++ + Resolver( mavenCentral ).bind( + ScalaDependency("com.chuusai", "shapeless", "2.3.2") + ) +} diff --git a/examples/sonatype-release-example/build/build/build.scala b/examples/sonatype-release-example/build/build/build.scala new file mode 100644 index 0000000..a47d3e1 --- /dev/null +++ b/examples/sonatype-release-example/build/build/build.scala @@ -0,0 +1,5 @@ +import cbt._ + +class Build(val context: Context) extends BuildBuild { + override def dependencies = super.dependencies :+ plugins.sonatypeRelease +} diff --git a/examples/sonatype-release-example/src/Main.scala b/examples/sonatype-release-example/src/Main.scala new file mode 100644 index 0000000..5e03d27 --- /dev/null +++ b/examples/sonatype-release-example/src/Main.scala @@ -0,0 +1,3 @@ +object Main extends App { + println("This is serious app that does nothing, but has shapeless dependency") +} diff --git a/plugins/sonatype-release/build/build.scala b/plugins/sonatype-release/build/build.scala new file mode 100644 index 0000000..0205cf8 --- /dev/null +++ b/plugins/sonatype-release/build/build.scala @@ -0,0 +1,3 @@ +import cbt._ + +class Build(val context: Context) extends Plugin diff --git a/plugins/sonatype-release/src/SonatypeRelease.scala b/plugins/sonatype-release/src/SonatypeRelease.scala new file mode 100644 index 0000000..cb32417 --- /dev/null +++ b/plugins/sonatype-release/src/SonatypeRelease.scala @@ -0,0 +1,51 @@ +package cbt + +import cbt.sonatype.SonatypeLib + +/** + * Sonatype release plugin. + * It provides ability to release your artifacts to Sonatype OSSRH + * and publish to Central repository (aka Maven Central). + * + * Release proccess is executed in two steps: + * • `sonatypePublishSigned` + * - creates staging repository to publish artifacts; + * - publishes signed artifacts(jars) to staging repository. + * • `sonatypeRelease` + * - closes staging repository; + * - promotes staging repository to Central repository; + * - drops staging repository after release. + */ +trait SonatypeRelease extends Publish { + + def profileName: String = groupId + + def sonatypeServiceURI: String = SonatypeLib.sonatypeServiceURI + + def sonatypeSnapshotsURI: String = SonatypeLib.sonatypeSnapshotsURI + + def sonatypeCredentials: String = SonatypeLib.sonatypeCredentials + + def sonatypePublishSigned: ExitCode = + sonatypeLib.sonatypePublishSigned( + sourceFiles, + `package` :+ pom, + groupId, + artifactId, + version, + isSnapshot, + scalaMajorVersion + ) + + def sonatypePublishSignedSnapshot: ExitCode = { + copy(context.copy(version = Some(version + "-SNAPSHOT"))).sonatypePublishSigned + } + + def sonatypeRelease: ExitCode = + sonatypeLib.sonatypeRelease(groupId, artifactId, version) + + private def sonatypeLib = + new SonatypeLib(sonatypeServiceURI, sonatypeSnapshotsURI, sonatypeCredentials, profileName)(lib) + + override def copy(context: Context) = super.copy(context).asInstanceOf[SonatypeRelease] +} diff --git a/plugins/sonatype-release/src/sonatype/HttpUtils.scala b/plugins/sonatype-release/src/sonatype/HttpUtils.scala new file mode 100644 index 0000000..9d23744 --- /dev/null +++ b/plugins/sonatype-release/src/sonatype/HttpUtils.scala @@ -0,0 +1,65 @@ +package cbt.sonatype + +import java.net.URL + +import cbt.Stage0Lib + +import scala.annotation.tailrec +import scala.util.{ Failure, Success, Try } + +private[sonatype] object HttpUtils { + // Make http GET. On failure request will be retried with exponential backoff. + def GET(uri: String, headers: Map[String, String]): (Int, String) = + withRetry(httpRequest("GET", uri, headers)) + + // Make http POST. On failure request will be retried with exponential backoff. + def POST(uri: String, body: Array[Byte], headers: Map[String, String]): (Int, String) = + withRetry(httpRequest("POST", uri, headers, body)) + + private def httpRequest(method: String, uri: String, headers: Map[String, String], body: Array[Byte] = Array.emptyByteArray): (Int, String) = { + val conn = Stage0Lib.openConnectionConsideringProxy(new URL(uri)) + conn.setReadTimeout(60000) // 1 minute + conn.setConnectTimeout(30000) // 30 seconds + + headers foreach { case (k,v) => + conn.setRequestProperty(k, v) + } + conn.setRequestMethod(method) + if(method == "POST" || method == "PUT") { // PATCH too? + conn.setDoOutput(true) + conn.getOutputStream.write(body) + } + + val arr = new Array[Byte](conn.getContentLength) + conn.getInputStream.read(arr) + + conn.getResponseCode -> new String(arr) + } + + // ============== General utilities + + def withRetry[T](f: => T): T = withRetry(4000, 5)(f) + + /** + * Retry execution of `f` `retriesLeft` times + * with `delay` doubled between attempts. + */ + @tailrec + def withRetry[T](delay: Int, retriesLeft: Int)(f: ⇒ T): T = { + Try(f) match { + case Success(result) ⇒ + result + case Failure(e) ⇒ + if (retriesLeft == 0) { + throw new Exception(e) + } else { + val newDelay = delay * 2 + val newRetries = retriesLeft - 1 +// log(s"Failed with exception: $e, will retry $newRetries times; waiting: $delay") + Thread.sleep(delay) + + withRetry(newDelay, newRetries)(f) + } + } + } +} diff --git a/plugins/sonatype-release/src/sonatype/SonatypeHttpApi.scala b/plugins/sonatype-release/src/sonatype/SonatypeHttpApi.scala new file mode 100644 index 0000000..e90b81d --- /dev/null +++ b/plugins/sonatype-release/src/sonatype/SonatypeHttpApi.scala @@ -0,0 +1,215 @@ +package cbt.sonatype + +import java.util.Base64 + +import scala.xml.XML + +/** + * Interface for Sonatype staging plugin HTTP API. + * All resources are described here: + * https://oss.sonatype.org/nexus-staging-plugin/default/docs/index.html + * + * Publish proccess via HTTP API described here: + * https://support.sonatype.com/hc/en-us/articles/213465868-Uploading-to-a-Staging-Repository-via-REST-API?page=1#comment_204178478 + */ +private final class SonatypeHttpApi(sonatypeURI: String, sonatypeCredentials: String, profileName: String)(log: String => Unit) { + import HttpUtils._ + + private val base64Credentials = new String(Base64.getEncoder.encode(sonatypeCredentials.getBytes)) + + // https://oss.sonatype.org/nexus-staging-plugin/default/docs/path__staging_profiles.html + def getStagingProfile: StagingProfile = { + log(s"Retrieving info for profile: $profileName") + val (_, response) = GET( + uri = s"$sonatypeURI/staging/profiles", + headers = Map("Authorization" -> s"Basic $base64Credentials") + ) + + val currentProfile = (XML.loadString(response) \\ "stagingProfile" find { profile => + (profile \ "name").headOption.exists(_.text == profileName) + }).getOrElse(throw new Exception(s"Failed to get profile with name: $profileName")) + + StagingProfile( + id = (currentProfile \ "id").head.text, + name = (currentProfile \ "name").head.text, + repositoryTargetId = (currentProfile \ "repositoryTargetId").head.text, + resourceURI = (currentProfile \ "resourceURI").head.text + ) + } + + // https://oss.sonatype.org/nexus-staging-plugin/default/docs/path__staging_profile_repositories_-profileIdKey-.html + def getStagingRepos(profile: StagingProfile): Seq[StagingRepository] = { + log(s"Retrieving staging repositories for profile: $profileName") + val (_, response) = GET( + uri = s"$sonatypeURI/staging/profile_repositories/${profile.id}", + headers = Map( + "Authorization" -> s"Basic $base64Credentials" + ) + ) + + (XML.loadString(response) \\ "stagingProfileRepository") map extractStagingRepository + } + + // https://oss.sonatype.org/nexus-staging-plugin/default/docs/path__staging_repository_-repositoryIdKey-.html + private def getStagingRepoById(repoId: StagingRepositoryId): StagingRepository = { + log(s"Retrieving staging repo with id: ${repoId.repositoryId}") + val (_, response) = GET( + uri = s"$sonatypeURI/staging/repository/${repoId.repositoryId}", + headers = Map( + "Authorization" -> s"Basic $base64Credentials" + ) + ) + + extractStagingRepository(XML.loadString(response)) + } + + // https://oss.sonatype.org/nexus-staging-plugin/default/docs/path__staging_profiles_-profileIdKey-_start.html + def createStagingRepo(profile: StagingProfile): StagingRepositoryId = { + log(s"Creating staging repositories for profile: $profileName") + val (responseCode, response) = POST( + uri = profile.resourceURI + "/start", + body = createRequestBody("Create staging repository [CBT]").getBytes, + headers = Map( + "Authorization" -> s"Basic $base64Credentials", + "Content-Type" -> "application/xml" + ) + ) + + require(responseCode == 201, s"Create staging repo response code. Expected: 201, got: $responseCode") + + val optRepositoryId = (XML.loadString(response) \ "data" \ "stagedRepositoryId").headOption.map(e => StagingRepositoryId(e.text)) + + optRepositoryId.getOrElse(throw new Exception(s"Malformed response. Failed to get id of created staging repo")) + } + + def finishRelease(repo: StagingRepository, profile: StagingProfile): Unit = { + val repoId = StagingRepositoryId(repo.repositoryId) + repo.state match { + case Open => + closeStagingRepo(profile, repoId) + promoteStagingRepo(profile, repoId) + dropStagingRepo(profile, repoId) + case Closed => + promoteStagingRepo(profile, repoId) + dropStagingRepo(profile, repoId) + case Released => + dropStagingRepo(profile, repoId) + case Unknown(status) => + throw new Exception(s"Got repo in status: ${status}, can't finish release.") + } + } + + // https://oss.sonatype.org/nexus-staging-plugin/default/docs/path__staging_profiles_-profileIdKey-_finish.html + private def closeStagingRepo(profile: StagingProfile, repoId: StagingRepositoryId): Unit = { + log(s"Closing staging repo: ${repoId.repositoryId}") + val (responseCode, _) = POST( + uri = profile.resourceURI + "/finish", + body = promoteRequestBody( + repoId.repositoryId, + "Close staging repository [CBT]", + profile.repositoryTargetId + ).getBytes, + headers = Map( + "Authorization" -> s"Basic $base64Credentials", + "Content-Type" -> "application/xml" + ) + ) + + require(responseCode == 201, s"Close staging repo response code. Expected: 201, got: $responseCode") + } + + // https://oss.sonatype.org/nexus-staging-plugin/default/docs/path__staging_profiles_-profileIdKey-_promote.html + // You can promote repository only when it is in "closed" state. + private def promoteStagingRepo(profile: StagingProfile, repoId: StagingRepositoryId): Unit = { + log(s"Promoting staging repo: ${repoId.repositoryId}") + val responseCode = withRetry { + // need to get fresh info about this repo + val repoState = try getStagingRepoById(repoId) catch { + case e: Exception => + throw new Exception(s"Repository with id ${repoId.repositoryId} not found. Maybe it was dropped already", e) + } + + if(repoState.state == Closed) { + val (code, _) = POST( + uri = profile.resourceURI + "/promote", + body = promoteRequestBody( + repoId.repositoryId, + "Promote staging repository [CBT]", + profile.repositoryTargetId + ).getBytes, + headers = Map( + "Authorization" -> s"Basic $base64Credentials", + "Content-Type" -> "application/xml" + ) + ) + code + } else { + throw new Exception(s"Can't promote, repository ${repoId.repositoryId} is not in closed state yet!") + } + } + + require(responseCode == 201, s"Promote staging repo response code. Expected: 201, got: $responseCode") + } + + // https://oss.sonatype.org/nexus-staging-plugin/default/docs/path__staging_profiles_-profileIdKey-_drop.html + // It's safe to drop repository in "released" state. + private def dropStagingRepo(profile: StagingProfile, repoId: StagingRepositoryId): Unit = { + log(s"Dropping staging repo: ${repoId.repositoryId}") + val responseCode = withRetry { + // need to get fresh info about this repo + val repoState = try getStagingRepoById(repoId) catch { + case e: Exception => + throw new Exception(s"Repository with id ${repoId.repositoryId} not found. Maybe it was dropped already", e) + } + + if (repoState.state == Released) { + val (code, _) = POST( + uri = profile.resourceURI + "/drop", + body = promoteRequestBody( + repoId.repositoryId, + "Drop staging repository [CBT]", + profile.repositoryTargetId + ).getBytes, + headers = Map( + "Authorization" -> s"Basic $base64Credentials", + "Content-Type" -> "application/xml" + ) + ) + code + } else { + throw new Exception(s"Can't drop, repository ${repoId.repositoryId} is not in released state yet!") + } + } + require(responseCode == 201, s"Drop staging repo response code. Expected: 201, got: $responseCode") + } + + private def promoteRequestBody(repoId: String, description: String, targetRepoId: String) = + s""" + | + | + | $repoId + | $description + | $targetRepoId + | + | + """.stripMargin + + + private def createRequestBody(description: String) = + s""" + | + | + | $description + | + | + """.stripMargin + + private def extractStagingRepository(repo: xml.Node): StagingRepository = + StagingRepository( + (repo \ "profileId").head.text, + (repo \ "profileName").head.text, + (repo \ "repositoryId").head.text, + RepositoryState.fromString((repo \ "type").head.text) + ) +} + diff --git a/plugins/sonatype-release/src/sonatype/SonatypeLib.scala b/plugins/sonatype-release/src/sonatype/SonatypeLib.scala new file mode 100644 index 0000000..9aab9f5 --- /dev/null +++ b/plugins/sonatype-release/src/sonatype/SonatypeLib.scala @@ -0,0 +1,148 @@ +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) { + + 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", _) + +} diff --git a/plugins/sonatype-release/src/sonatype/models.scala b/plugins/sonatype-release/src/sonatype/models.scala new file mode 100644 index 0000000..4446c53 --- /dev/null +++ b/plugins/sonatype-release/src/sonatype/models.scala @@ -0,0 +1,31 @@ +package cbt.sonatype + +case class StagingProfile( + id: String, + name: String, + repositoryTargetId: String, + resourceURI: String + ) + +case class StagingRepositoryId(repositoryId: String) + +object RepositoryState { + val fromString: String => RepositoryState = { + case "open" => Open + case "closed" => Closed + case "released" => Released + case other => Unknown(other) + } +} +sealed trait RepositoryState +case object Open extends RepositoryState +case object Closed extends RepositoryState +case object Released extends RepositoryState +case class Unknown(state: String) extends RepositoryState + +case class StagingRepository( + profileId: String, + profileName: String, + repositoryId: String, + state: RepositoryState // stands as `type` in XML response + ) diff --git a/stage2/BuildBuild.scala b/stage2/BuildBuild.scala index f444803..b183745 100644 --- a/stage2/BuildBuild.scala +++ b/stage2/BuildBuild.scala @@ -15,6 +15,7 @@ trait BuildBuild extends BaseBuild{ final lazy val scalafmt = DirectoryDependency( managedContext.cbtHome ++ "/plugins/scalafmt" ) final lazy val wartremover = DirectoryDependency( managedContext.cbtHome ++ "/plugins/wartremover" ) final lazy val uberJar = DirectoryDependency( managedContext.cbtHome ++ "/plugins/uber-jar" ) + final lazy val sonatypeRelease = DirectoryDependency( managedContext.cbtHome ++ "/plugins/sonatype-release" ) } override def dependencies = diff --git a/stage2/Lib.scala b/stage2/Lib.scala index c18bf2e..25183a3 100644 --- a/stage2/Lib.scala +++ b/stage2/Lib.scala @@ -272,6 +272,7 @@ final class Lib(logger: Logger) extends Stage1Lib(logger) with Scaffold{ } yield file } + // FIXME: for some reason it includes full path in docs def jarFile( jarFile: File, files: Seq[File], mainClass: Option[String] = None ): Option[File] = { Files.deleteIfExists(jarFile.toPath) if( files.isEmpty ){ @@ -430,11 +431,9 @@ final class Lib(logger: Logger) extends Stage1Lib(logger) with Scaffold{ } } - def publishSigned( sourceFiles: Seq[File], artifacts: Seq[File], url: URL, credentials: Option[String] = None ): Unit = { + def publishSigned( artifacts: Seq[File], url: URL, credentials: Option[String] = None ): Unit = { // TODO: make concurrency configurable here - if(sourceFiles.nonEmpty){ - publish( artifacts ++ artifacts.map(sign), url, credentials ) - } + publish( artifacts ++ artifacts.map(sign), url, credentials ) } private def publish(artifacts: Seq[File], url: URL, credentials: Option[String]): Unit = { @@ -450,12 +449,12 @@ final class Lib(logger: Logger) extends Stage1Lib(logger) with Scaffold{ } def uploadAll(url: URL, nameAndContents: Seq[(String, Array[Byte])], credentials: Option[String] = None ): Unit = - nameAndContents.map{ case(name, content) => upload(name, content, url, credentials ) } + nameAndContents.foreach { case (name, content) => upload(name, content, url, credentials ) } def upload(fileName: String, fileContents: Array[Byte], baseUrl: URL, credentials: Option[String] = None): Unit = { import java.net._ import java.io._ - val url = baseUrl ++ fileName + val url = baseUrl ++ "/" ++ fileName System.err.println(blue("uploading ") ++ url.toString) val httpCon = Stage0Lib.openConnectionConsideringProxy(url) httpCon.setDoOutput(true) diff --git a/stage2/PackageJars.scala b/stage2/PackageJars.scala index 10e4c3a..a101993 100644 --- a/stage2/PackageJars.scala +++ b/stage2/PackageJars.scala @@ -1,5 +1,6 @@ package cbt import java.io.File + // would love to call this just `Package` but that conflicts with scala package objects. trait PackageJars extends BaseBuild with ArtifactInfo{ def name: String diff --git a/stage2/Publish.scala b/stage2/Publish.scala index 96e856b..7e00620 100644 --- a/stage2/Publish.scala +++ b/stage2/Publish.scala @@ -35,37 +35,15 @@ trait Publish extends PackageJars{ ) // ========== publish ========== - final protected val releaseFolder = s"/${groupId.replace(".","/")}/${artifactId}_$scalaMajorVersion/$version/" - private def snapshotUrl = new URL("https://oss.sonatype.org/content/repositories/snapshots") - private def releaseUrl = new URL("https://oss.sonatype.org/service/local/staging/deploy/maven2") - def publishUrl = if(version.endsWith("-SNAPSHOT")) snapshotUrl else releaseUrl - override def copy(context: Context) = super.copy(context).asInstanceOf[Publish] - - protected def sonatypeCredentials: Option[String] = { - // FIXME: this should probably not use cbtHome, but some reference to the system's host cbt - Some(new String(readAllBytes((context.cbtRootHome ++ "/sonatype.login").toPath)).trim) - } - - def publishSnapshot: Unit = { - copy( context.copy(version = Some(version+"-SNAPSHOT")) ).publishUnsigned - } + private val releaseFolder = s"/${groupId.replace(".","/")}/${artifactId}_$scalaMajorVersion/$version/" - def publishLocal: Unit = { + def publishLocal: Unit = lib.publishLocal( sourceFiles, `package` :+ pom, context.paths.mavenCache, releaseFolder ) - } - def publishSnapshotLocal: Unit = { + def publishSnapshotLocal: Unit = copy( context.copy(version = Some(version+"-SNAPSHOT")) ).publishLocal - } - def publishUnsigned: Unit = { - lib.publishUnsigned( - sourceFiles, `package` :+ pom, publishUrl ++ releaseFolder, sonatypeCredentials - ) - } - def publishSigned: Unit = { - lib.publishSigned( - sourceFiles, `package` :+ pom, publishUrl ++ releaseFolder, sonatypeCredentials - ) - } + def isSnapshot: Boolean = version.endsWith("-SNAPSHOT") + + override def copy(context: Context) = super.copy(context).asInstanceOf[Publish] } -- cgit v1.2.3