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 26, 2019

This should really be the base and the Sonatype publisher overrides it and adds staging

@james64
Copy link

james64 commented Nov 29, 2019

I am confused. I compared this to original PublishModule source and it is almost identical. Both publish and publishLocal delegates to same LocalPublisher and SonatypePublisher respectively. Or is import on line 13 referring to some custom publisher implementation? Thanks!

@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