aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNikolay Tatarinov <rockjam@actor.im>2016-10-03 20:19:27 +0300
committerJan Christopher Vogt <oss.nsp@cvogt.org>2016-10-03 13:19:27 -0400
commit669ef3dfc3201fffa451b47d2b629a856afc0b25 (patch)
tree7bcb64c58266a4fe2dd3965dda47dc6f8fa71b04
parent174c52b2c24b8491eef687ee5eb3c3b77c34a61c (diff)
downloadcbt-669ef3dfc3201fffa451b47d2b629a856afc0b25.tar.gz
cbt-669ef3dfc3201fffa451b47d2b629a856afc0b25.tar.bz2
cbt-669ef3dfc3201fffa451b47d2b629a856afc0b25.zip
Sonatype release plugin (#247)
-rw-r--r--examples/sonatype-release-example/README.md1
-rw-r--r--examples/sonatype-release-example/build/build.scala31
-rw-r--r--examples/sonatype-release-example/build/build/build.scala5
-rw-r--r--examples/sonatype-release-example/src/Main.scala3
-rw-r--r--plugins/sonatype-release/build/build.scala3
-rw-r--r--plugins/sonatype-release/src/SonatypeRelease.scala51
-rw-r--r--plugins/sonatype-release/src/sonatype/HttpUtils.scala65
-rw-r--r--plugins/sonatype-release/src/sonatype/SonatypeHttpApi.scala215
-rw-r--r--plugins/sonatype-release/src/sonatype/SonatypeLib.scala148
-rw-r--r--plugins/sonatype-release/src/sonatype/models.scala31
-rw-r--r--stage2/BuildBuild.scala1
-rw-r--r--stage2/Lib.scala11
-rw-r--r--stage2/PackageJars.scala1
-rw-r--r--stage2/Publish.scala34
14 files changed, 566 insertions, 34 deletions
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"""
+ |<promoteRequest>
+ | <data>
+ | <stagedRepositoryId>$repoId</stagedRepositoryId>
+ | <description>$description</description>
+ | <targetRepositoryId>$targetRepoId</targetRepositoryId>
+ | </data>
+ |</promoteRequest>
+ """.stripMargin
+
+
+ private def createRequestBody(description: String) =
+ s"""
+ |<promoteRequest>
+ | <data>
+ | <description>$description</description>
+ | </data>
+ |</promoteRequest>
+ """.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]
}