Last active
November 29, 2019 11:30
-
-
Save ajrnz/42725b883211edbeba2a64d3451c5e59 to your computer and use it in GitHub Desktop.
Skeleton for publishing to a local repo
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | |
} |
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!
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
This should really be the base and the Sonatype publisher overrides it and adds staging