summaryrefslogtreecommitdiff
path: root/scalaplugin/src/main/scala/mill/scalaplugin/publish/SonatypePublisher.scala
diff options
context:
space:
mode:
authorrockjam <5min4eq.unity@gmail.com>2017-12-10 23:33:46 +0300
committerNikolay Tatarinov <5min4eq.unity@gmail.com>2017-12-20 01:17:16 +0300
commit9d19d740c5b387704e08fb89412b8318549a4fc5 (patch)
tree203c7ede2212c916195a82c2cf6578e9c755de7b /scalaplugin/src/main/scala/mill/scalaplugin/publish/SonatypePublisher.scala
parent6a7f6cf910362cadf6b39f8c0795c46745e08742 (diff)
downloadmill-9d19d740c5b387704e08fb89412b8318549a4fc5.tar.gz
mill-9d19d740c5b387704e08fb89412b8318549a4fc5.tar.bz2
mill-9d19d740c5b387704e08fb89412b8318549a4fc5.zip
Add Sonatypype publishing;
now you can publish your module with `mill run MyModule.publish --credentials $SONATYPE_CREDENTIALS --gpgPassphrase $GPG_PASSPHRASE`
Diffstat (limited to 'scalaplugin/src/main/scala/mill/scalaplugin/publish/SonatypePublisher.scala')
-rw-r--r--scalaplugin/src/main/scala/mill/scalaplugin/publish/SonatypePublisher.scala148
1 files changed, 148 insertions, 0 deletions
diff --git a/scalaplugin/src/main/scala/mill/scalaplugin/publish/SonatypePublisher.scala b/scalaplugin/src/main/scala/mill/scalaplugin/publish/SonatypePublisher.scala
new file mode 100644
index 00000000..90a39745
--- /dev/null
+++ b/scalaplugin/src/main/scala/mill/scalaplugin/publish/SonatypePublisher.scala
@@ -0,0 +1,148 @@
+package mill.scalaplugin.publish
+
+import java.math.BigInteger
+import java.security.MessageDigest
+
+import ammonite.ops._
+import mill.util.Logger
+
+import scalaj.http.HttpResponse
+
+class SonatypePublisher(uri: String,
+ snapshotUri: String,
+ credentials: String,
+ gpgPassphrase: String,
+ log: Logger) {
+
+ private val api = new SonatypeHttpApi(uri, credentials)
+
+ def publish(artifacts: Seq[(Path, String)], artifact: Artifact): Unit = {
+ val signedArtifacts = artifacts ++ artifacts.map {
+ case (file, name) =>
+ poorMansSign(file, gpgPassphrase) -> s"${name}.asc"
+ }
+
+ val signedArtifactsWithDigest = signedArtifacts.flatMap {
+ case (file, name) =>
+ val content = read.bytes(file)
+
+ Seq(
+ name -> content,
+ (name + ".md5") -> md5hex(content),
+ (name + ".sha1") -> sha1hex(content)
+ )
+ }
+
+ val publishPath = Seq(
+ artifact.group.replace(".", "/"),
+ artifact.id,
+ artifact.version
+ ).mkString("/")
+
+ if (artifact.isSnapshot)
+ publishSnapshot(publishPath, signedArtifactsWithDigest, artifact)
+ else
+ publishRelease(publishPath, signedArtifactsWithDigest, artifact)
+ }
+
+ private def publishSnapshot(publishPath: String,
+ payloads: Seq[(String, Array[Byte])],
+ artifact: Artifact): Unit = {
+ val baseUri: String = snapshotUri + "/" + publishPath
+
+ val publishResults = payloads.map {
+ case (fileName, data) =>
+ log.info(s"Uploading ${fileName}")
+ val resp = api.upload(s"${baseUri}/${fileName}", data)
+ resp
+ }
+ reportPublishResults(publishResults, artifact)
+ }
+
+ private def publishRelease(publishPath: String,
+ payloads: Seq[(String, Array[Byte])],
+ artifact: Artifact): Unit = {
+ val profileUri = api.getStagingProfileUri(artifact.group)
+ val stagingRepoId =
+ api.createStagingRepo(profileUri, artifact.group)
+ val baseUri =
+ s"${uri}/staging/deployByRepositoryId/${stagingRepoId}/${publishPath}"
+
+ val publishResults = payloads.map {
+ case (fileName, data) =>
+ log.info(s"Uploading ${fileName}")
+ api.upload(s"${baseUri}/${fileName}", data)
+ }
+ reportPublishResults(publishResults, artifact)
+
+ log.info("Closing staging repository")
+ api.closeStagingRepo(profileUri, stagingRepoId)
+
+ log.info("Waiting for staging repository to close")
+ awaitRepoStatus("closed", stagingRepoId)
+
+ log.info("Promoting staging repository")
+ api.promoteStagingRepo(profileUri, stagingRepoId)
+
+ log.info("Waiting for staging repository to release")
+ awaitRepoStatus("released", stagingRepoId)
+
+ log.info("Dropping staging repository")
+ api.dropStagingRepo(profileUri, stagingRepoId)
+
+ log.info(s"Published ${artifact.id} successfully")
+ }
+
+ private def reportPublishResults(publishResults: Seq[HttpResponse[String]],
+ artifact: Artifact) = {
+ if (publishResults.forall(_.is2xx)) {
+ log.info(s"Published ${artifact.id} to Sonatype")
+ } else {
+ val errors = publishResults.filterNot(_.is2xx).map { response =>
+ s"Code: ${response.code}, message: ${response.body}"
+ }
+ throw new RuntimeException(
+ s"Failed to publish ${artifact.id} to Sonatype. Errors: \n${errors.mkString("\n")}"
+ )
+ }
+ }
+
+ private def awaitRepoStatus(status: String,
+ stagingRepoId: String,
+ attempts: Int = 20): Unit = {
+ def isRightStatus =
+ api.getStagingRepoState(stagingRepoId).equalsIgnoreCase(status)
+ var attemptsLeft = attempts
+
+ while (attemptsLeft > 0 && !isRightStatus) {
+ Thread.sleep(3000)
+ attemptsLeft -= 1
+ if (attemptsLeft == 0) {
+ throw new RuntimeException(
+ s"Couldn't wait for staging repository to be ${status}. Failing")
+ }
+ }
+ }
+
+ // http://central.sonatype.org/pages/working-with-pgp-signatures.html#signing-a-file
+ private def poorMansSign(file: Path, passphrase: String): Path = {
+ val fileName = file.toString
+ import ammonite.ops.ImplicitWd._
+ %("gpg", "--yes", "-a", "-b", "--passphrase", passphrase, fileName)
+ Path(fileName + ".asc")
+ }
+
+ private def md5hex(bytes: Array[Byte]): Array[Byte] =
+ hexArray(md5.digest(bytes)).getBytes
+
+ private def sha1hex(bytes: Array[Byte]): Array[Byte] =
+ hexArray(sha1.digest(bytes)).getBytes
+
+ private def md5 = MessageDigest.getInstance("md5")
+
+ private def sha1 = MessageDigest.getInstance("sha1")
+
+ private def hexArray(arr: Array[Byte]) =
+ String.format("%0" + (arr.length << 1) + "x", new BigInteger(1, arr))
+
+}