Skip to content

Instantly share code, notes, and snippets.

@ajrnz
Last active November 29, 2019 11:30
Show Gist options
  • Save ajrnz/42725b883211edbeba2a64d3451c5e59 to your computer and use it in GitHub Desktop.
Save ajrnz/42725b883211edbeba2a64d3451c5e59 to your computer and use it in GitHub Desktop.
Skeleton for publishing to a local repo
trait CommonPublishModule extends LocalRepoPublishModule {
def sonatypeUri: String = "http://artifactory.mysite.dev/artifactory/libs-release/"
def publishVersion = "0.9.9"
... etc ...
}
import mill.define.{ExternalModule, Task}
import mill.api.PathRef
import mill.scalalib.publish.Artifact
import mill.scalalib.JavaModule
import $file.SonatypePublisher, SonatypePublisher.SonatypePublisher
/**
* Configuration necessary for publishing a Scala module to Maven Central or similar
*/
trait LocalRepoPublishModule extends JavaModule { outer =>
import mill.scalalib.publish.{Artifact => _, SonatypePublisher => _, _}
override def moduleDeps = Seq.empty[LocalRepoPublishModule]
def pomSettings: T[PomSettings]
def publishVersion: T[String]
def publishSelfDependency = T {
Artifact(pomSettings().organization, artifactId(), publishVersion())
}
def publishXmlDeps = T.task {
val ivyPomDeps = ivyDeps().map(resolvePublishDependency().apply(_))
val compileIvyPomDeps = compileIvyDeps()
.map(resolvePublishDependency().apply(_))
.filter(!ivyPomDeps.contains(_))
.map(_.copy(scope = Scope.Provided))
val modulePomDeps = Task.sequence(moduleDeps.map(_.publishSelfDependency))()
ivyPomDeps ++ compileIvyPomDeps ++ modulePomDeps.map(Dependency(_, Scope.Compile))
}
def pom = T {
val pom = Pom(artifactMetadata(), publishXmlDeps(), artifactId(), pomSettings())
val pomPath = T.ctx().dest / s"${artifactId()}-${publishVersion()}.pom"
os.write.over(pomPath, pom)
PathRef(pomPath)
}
def ivy = T {
val ivy = Ivy(artifactMetadata(), publishXmlDeps())
val ivyPath = T.ctx().dest / "ivy.xml"
os.write.over(ivyPath, ivy)
PathRef(ivyPath)
}
def artifactMetadata: T[Artifact] = T {
Artifact(pomSettings().organization, artifactId(), publishVersion())
}
def publishLocal(): define.Command[Unit] = T.command {
LocalPublisher.publish(
jar = jar().path,
sourcesJar = sourceJar().path,
docJar = docJar().path,
pom = pom().path,
ivy = ivy().path,
artifact = artifactMetadata()
)
}
def sonatypeUri: String = "https://oss.sonatype.org/service/local"
def sonatypeSnapshotUri: String = "https://oss.sonatype.org/content/repositories/snapshots"
def publishArtifacts = T {
val baseName = s"${artifactId()}-${publishVersion()}"
PublishModule.PublishData(
artifactMetadata(),
Seq(
jar() -> s"$baseName.jar",
sourceJar() -> s"$baseName-sources.jar",
docJar() -> s"$baseName-javadoc.jar",
pom() -> s"$baseName.pom"
)
)
}
def publish(sonatypeCreds: String,
gpgPassphrase: String = null,
gpgKeyName: String = null,
signed: Boolean = true,
readTimeout: Int = 60000,
connectTimeout: Int = 5000,
release: Boolean,
awaitTimeout: Int = 120 * 1000): define.Command[Unit] = T.command {
println("publish")
val PublishModule.PublishData(artifactInfo, artifacts) = publishArtifacts()
new SonatypePublisher(
sonatypeUri,
sonatypeSnapshotUri,
sonatypeCreds,
Option(gpgPassphrase),
Option(gpgKeyName),
signed,
readTimeout,
connectTimeout,
T.ctx().log,
awaitTimeout
).publish(artifacts.map{case (a, b) => (a.path, b)}, artifactInfo, release)
}
}
// Modified SonatypePublisher
import java.math.BigInteger
import java.security.MessageDigest
import mill.api.Logger
import os.Shellable
import mill.scalalib.publish.{SonatypeHttpApi, Artifact}
class SonatypePublisher(uri: String,
snapshotUri: String,
credentials: String,
gpgPassphrase: Option[String],
gpgKeyName: Option[String],
signed: Boolean,
readTimeout: Int,
connectTimeout: Int,
log: Logger,
awaitTimeout: Int) {
private val api = new SonatypeHttpApi(uri, credentials, readTimeout = readTimeout, connectTimeout = connectTimeout)
def publish(fileMapping: Seq[(os.Path, String)], artifact: Artifact, release: Boolean): Unit = {
publishAll(release, fileMapping -> artifact)
}
def publishAll(release: Boolean, artifacts: (Seq[(os.Path, String)], Artifact)*): Unit = {
val mappings = for ((fileMapping0, artifact) <- artifacts) yield {
val publishPath = Seq(
artifact.group.replace(".", "/"),
artifact.id,
artifact.version
).mkString("/")
val fileMapping = fileMapping0.map { case (file, name) => (file, publishPath + "/" + name) }
val signedArtifacts = if (signed) fileMapping.map {
case (file, name) => poorMansSign(file, gpgPassphrase, gpgKeyName) -> s"$name.asc"
} else Seq()
artifact -> (fileMapping ++ signedArtifacts).flatMap {
case (file, name) =>
val content = os.read.bytes(file)
Seq(
name -> content,
(name + ".md5") -> md5hex(content),
(name + ".sha1") -> sha1hex(content)
)
}
}
val (snapshots, releases) = mappings.partition(_._1.isSnapshot)
if (snapshots.nonEmpty) {
publishSnapshot(snapshots.flatMap(_._2), snapshots.map(_._1))
}
val releaseGroups = releases.groupBy(_._1.group)
for ((group, groupReleases) <- releaseGroups) {
publishRelease(
release,
groupReleases.flatMap(_._2),
group,
releases.map(_._1),
awaitTimeout
)
}
}
private def publishSnapshot(payloads: Seq[(String, Array[Byte])],
artifacts: Seq[Artifact]): Unit = {
val publishResults = payloads.map {
case (fileName, data) =>
log.info(s"Uploading $fileName")
val resp = api.upload(s"$snapshotUri/$fileName", data)
resp
}
reportPublishResults(publishResults, artifacts)
}
private def publishRelease(release: Boolean,
payloads: Seq[(String, Array[Byte])],
stagingProfile: String,
artifacts: Seq[Artifact],
awaitTimeout: Int): Unit = {
// val profileUri = api.getStagingProfileUri(stagingProfile)
// val stagingRepoId = api.createStagingRepo(profileUri, stagingProfile)
val baseUri = s"$uri" ///staging/deployByRepositoryId/$stagingRepoId/"
val publishResults = payloads.map {
case (fileName, data) =>
log.info(s"Uploading ${fileName} to ${s"$baseUri/$fileName"}")
api.upload(s"$baseUri/$fileName", data)
}
reportPublishResults(publishResults, artifacts)
// if (release) {
// log.info("Closing staging repository")
// api.closeStagingRepo(profileUri, stagingRepoId)
// log.info("Waiting for staging repository to close")
// awaitRepoStatus("closed", stagingRepoId, awaitTimeout)
// log.info("Promoting staging repository")
// api.promoteStagingRepo(profileUri, stagingRepoId)
// log.info("Waiting for staging repository to release")
// awaitRepoStatus("released", stagingRepoId, awaitTimeout)
// log.info("Dropping staging repository")
// api.dropStagingRepo(profileUri, stagingRepoId)
// log.info(s"Published ${artifacts.map(_.id).mkString(", ")} successfully")
// }
}
private def reportPublishResults(publishResults: Seq[requests.Response],
artifacts: Seq[Artifact]) = {
if (publishResults.forall(_.is2xx)) {
log.info(s"Published ${artifacts.map(_.id).mkString(", ")} to Sonatype")
} else {
val errors = publishResults.filterNot(_.is2xx).map { response =>
s"Code: ${response.statusCode}, message: ${response.data.text}"
}
throw new RuntimeException(
s"Failed to publish ${artifacts.map(_.id).mkString(", ")} to Sonatype. Errors: \n${errors.mkString("\n")}"
)
}
}
private def awaitRepoStatus(status: String,
stagingRepoId: String,
awaitTimeout: Int): Unit = {
def isRightStatus =
api.getStagingRepoState(stagingRepoId).equalsIgnoreCase(status)
var attemptsLeft = awaitTimeout / 3000
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: os.Path, maybePassphrase: Option[String], maybeKeyName: Option[String]): os.Path = {
val fileName = file.toString
val optionFlag = (flag: String, ov: Option[String]) => ov.map(flag :: _ :: Nil).getOrElse(Nil)
val command = "gpg" ::
optionFlag("--passphrase", maybePassphrase) ++ optionFlag("-u", maybeKeyName) ++
Seq("--batch", "--yes", "-a", "-b", fileName)
os.proc(command.map(v => v: Shellable))
.call(stdin = os.Inherit, stdout = os.Inherit, stderr = os.Inherit)
os.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))
}
@ajrnz
Copy link
Author

ajrnz commented Nov 29, 2019

Sorry, you are right I had copied the SonatypePublisher and commented things out. I've added that below for reference. It works but as you can see it needs a lot of work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment