diff --git a/libs/javalib/src/mill/javalib/MavenPublish.scala b/libs/javalib/src/mill/javalib/MavenPublish.scala new file mode 100644 index 000000000000..dad54ffc96ba --- /dev/null +++ b/libs/javalib/src/mill/javalib/MavenPublish.scala @@ -0,0 +1,85 @@ +package mill.javalib + +import com.lumidion.sonatype.central.client.core.SonatypeCredentials +import mill.api.daemon.Logger +import mill.javalib.PublishModule.PublishData +import mill.javalib.internal.MavenWorkerSupport as InternalMavenWorkerSupport + +private[mill] trait MavenPublish { + + def mavenPublishDatas( + publishDatas: Seq[PublishData], + credentials: SonatypeCredentials, + releaseUri: String, + snapshotUri: String, + taskDest: os.Path, + log: Logger, + env: Map[String, String], + worker: InternalMavenWorkerSupport.Api + ): Unit = { + val dryRun = env.get("MILL_TESTS_PUBLISH_DRY_RUN").contains("1") + + val (snapshots, releases) = publishDatas.partition(_.meta.isSnapshot) + + releases.map(_ -> false).appendedAll(snapshots.map(_ -> true)).foreach { (data, isSnapshot) => + mavenPublishData( + dryRun = dryRun, + publishData = data, + isSnapshot = isSnapshot, + credentials = credentials, + releaseUri = releaseUri, + snapshotUri = snapshotUri, + taskDest = taskDest, + log = log, + worker = worker + ) + } + } + + def mavenPublishData( + dryRun: Boolean, + publishData: PublishData, + isSnapshot: Boolean, + credentials: SonatypeCredentials, + releaseUri: String, + snapshotUri: String, + taskDest: os.Path, + log: Logger, + worker: InternalMavenWorkerSupport.Api + ): Unit = { + val uri = if (isSnapshot) snapshotUri else releaseUri + val artifacts = MavenWorkerSupport.RemoteM2Publisher.asM2ArtifactsFromPublishDatas( + publishData.meta, + publishData.payloadAsMap + ) + + if (isSnapshot) { + log.info( + s"Detected a 'SNAPSHOT' version for ${publishData.meta}, publishing to Maven Repository at '$uri'" + ) + } + + /** Maven uses this as a workspace for file manipulation. */ + val mavenWorkspace = taskDest / "maven" + + if (dryRun) { + val publishTo = taskDest / "repository" + val result = worker.publishToLocal( + publishTo = publishTo, + workspace = mavenWorkspace, + artifacts + ) + log.info(s"Dry-run publishing to '$publishTo' finished with result: $result") + } else { + val result = worker.publishToRemote( + uri = uri, + workspace = mavenWorkspace, + username = credentials.username, + password = credentials.password, + artifacts + ) + log.info(s"Publishing to '$uri' finished with result: $result") + } + } + +} diff --git a/libs/javalib/src/mill/javalib/MavenPublishModule.scala b/libs/javalib/src/mill/javalib/MavenPublishModule.scala new file mode 100644 index 000000000000..147da2af9068 --- /dev/null +++ b/libs/javalib/src/mill/javalib/MavenPublishModule.scala @@ -0,0 +1,42 @@ +package mill.javalib + +import mill.* +import mill.api.* +import mill.util.Tasks + +/** + * External module to publish artifactes to Maven repositories other than `central.sonatype.org` + * (e.g. a private Maven repository). + */ +object MavenPublishModule extends ExternalModule, DefaultTaskModule, MavenWorkerSupport, + PublishCredentialsModule, MavenPublish { + + def defaultTask(): String = "publishAll" + + def publishAll( + publishArtifacts: mill.util.Tasks[PublishModule.PublishData] = + Tasks.resolveMainDefault("__:PublishModule.publishArtifacts"), + username: String = "", + password: String = "", + releaseUri: String, + snapshotUri: String + ): Command[Unit] = Task.Command { + val artifacts = Task.sequence(publishArtifacts.value)() + + val credentials = getPublishCredentials("MILL_MAVEN", username, password)() + + mavenPublishDatas( + artifacts, + credentials, + releaseUri = releaseUri, + snapshotUri = snapshotUri, + taskDest = Task.dest, + log = Task.log, + env = Task.env, + worker = mavenWorker() + ) + } + + lazy val millDiscover: Discover = Discover[this.type] + +} diff --git a/libs/javalib/src/mill/javalib/PublishCredentialsModule.scala b/libs/javalib/src/mill/javalib/PublishCredentialsModule.scala new file mode 100644 index 000000000000..9ea4d6bea9b1 --- /dev/null +++ b/libs/javalib/src/mill/javalib/PublishCredentialsModule.scala @@ -0,0 +1,42 @@ +package mill.javalib + +import com.lumidion.sonatype.central.client.core.SonatypeCredentials +import mill.api.* + +/** + * Internal module to Retrieve credentials for publishing to Maven repositories + * (e.g. `central.sonatype.org` or a private Maven repository). + */ +private[mill] trait PublishCredentialsModule extends Module { + def getPublishCredentials( + envVariablePrefix: String, + usernameParameterValue: String, + passwordParameterValue: String + ): Task[SonatypeCredentials] = Task.Anon { + val username = + getPublishCredential(usernameParameterValue, "username", s"${envVariablePrefix}_USERNAME")() + val password = + getPublishCredential(passwordParameterValue, "password", s"${envVariablePrefix}_PASSWORD")() + Result.Success(SonatypeCredentials(username, password)) + } + + private def getPublishCredential( + credentialParameterValue: String, + credentialName: String, + envVariableName: String + ): Task[String] = Task.Anon { + if (credentialParameterValue.nonEmpty) { + Result.Success(credentialParameterValue) + } else { + (for { + credential <- Task.env.get(envVariableName) + } yield { + Result.Success(credential) + }).getOrElse( + Result.Failure( + s"No $credentialName set. Consider using the $envVariableName environment variable or passing `$credentialName` argument" + ) + ) + } + } +} diff --git a/libs/javalib/src/mill/javalib/SonatypeCentralPublishModule.scala b/libs/javalib/src/mill/javalib/SonatypeCentralPublishModule.scala index 5101ffa72f7b..f8710ff57589 100644 --- a/libs/javalib/src/mill/javalib/SonatypeCentralPublishModule.scala +++ b/libs/javalib/src/mill/javalib/SonatypeCentralPublishModule.scala @@ -1,29 +1,30 @@ package mill.javalib import com.lihaoyi.unroll -import com.lumidion.sonatype.central.client.core.{PublishingType, SonatypeCredentials} +import com.lumidion.sonatype.central.client.core.PublishingType +import com.lumidion.sonatype.central.client.core.SonatypeCredentials import mill.* -import javalib.* -import mill.api.{ExternalModule, Task} -import mill.util.Tasks +import mill.api.BuildCtx import mill.api.DefaultTaskModule +import mill.api.ExternalModule import mill.api.Result -import mill.javalib.SonatypeCentralPublishModule.{ - defaultAwaitTimeout, - defaultConnectTimeout, - defaultCredentials, - defaultReadTimeout, - getPublishingTypeFromReleaseFlag, - getSonatypeCredentials -} -import mill.javalib.publish.Artifact -import mill.javalib.publish.SonatypeHelpers.{PASSWORD_ENV_VARIABLE_NAME, USERNAME_ENV_VARIABLE_NAME} -import mill.api.BuildCtx +import mill.api.Task import mill.api.daemon.Logger import mill.javalib.PublishModule.PublishData +import mill.javalib.SonatypeCentralPublishModule.defaultAwaitTimeout +import mill.javalib.SonatypeCentralPublishModule.defaultConnectTimeout +import mill.javalib.SonatypeCentralPublishModule.defaultCredentials +import mill.javalib.SonatypeCentralPublishModule.defaultReadTimeout +import mill.javalib.SonatypeCentralPublishModule.getPublishingTypeFromReleaseFlag import mill.javalib.internal.PublishModule.GpgArgs +import mill.javalib.publish.Artifact +import mill.javalib.publish.SonatypeHelpers.CREDENTIALS_ENV_VARIABLE_PREFIX +import mill.util.Tasks -trait SonatypeCentralPublishModule extends PublishModule, MavenWorkerSupport { +import javalib.* + +trait SonatypeCentralPublishModule extends PublishModule, MavenWorkerSupport, + PublishCredentialsModule { @deprecated("Use `sonatypeCentralGpgArgsForKey` instead.", "Mill 1.0.1") def sonatypeCentralGpgArgs: T[String] = @@ -63,7 +64,7 @@ trait SonatypeCentralPublishModule extends PublishModule, MavenWorkerSupport { @unroll docs: Boolean = true ): Task.Command[Unit] = Task.Command { val artifact = artifactMetadata() - val credentials = getSonatypeCredentials(username, password)() + val credentials = getPublishCredentials(CREDENTIALS_ENV_VARIABLE_PREFIX, username, password)() val publishData = publishArtifactsPayload(sources = sources, docs = docs)() val publishingType = getPublishingTypeFromReleaseFlag(sonatypeCentralShouldRelease()) @@ -97,8 +98,8 @@ trait SonatypeCentralPublishModule extends PublishModule, MavenWorkerSupport { /** * External module to publish artifacts to `central.sonatype.org` */ -object SonatypeCentralPublishModule extends ExternalModule with DefaultTaskModule - with MavenWorkerSupport { +object SonatypeCentralPublishModule extends ExternalModule, DefaultTaskModule, MavenWorkerSupport, + PublishCredentialsModule, MavenPublish { private final val sonatypeCentralGpgArgsSentinelValue = "" def self = this @@ -127,7 +128,7 @@ object SonatypeCentralPublishModule extends ExternalModule with DefaultTaskModul val artifacts = Task.sequence(publishArtifacts.value)() val finalBundleName = if (bundleName.isEmpty) None else Some(bundleName) - val credentials = getSonatypeCredentials(username, password)() + val credentials = getPublishCredentials(CREDENTIALS_ENV_VARIABLE_PREFIX, username, password)() def makeGpgArgs() = internal.PublishModule.pgpImportSecretIfProvidedAndMakeGpgArgs( Task.env, GpgArgs.fromUserProvided(gpgArgs) @@ -169,37 +170,17 @@ object SonatypeCentralPublishModule extends ExternalModule with DefaultTaskModul val dryRun = env.get("MILL_TESTS_PUBLISH_DRY_RUN").contains("1") def publishSnapshot(publishData: PublishData): Unit = { - val uri = sonatypeCentralSnapshotUri - val artifacts = MavenWorkerSupport.RemoteM2Publisher.asM2ArtifactsFromPublishDatas( - publishData.meta, - publishData.payloadAsMap - ) - - log.info( - s"Detected a 'SNAPSHOT' version for ${publishData.meta}, publishing to Sonatype Central Snapshots at '$uri'" + mavenPublishData( + dryRun = dryRun, + publishData = publishData, + isSnapshot = true, + credentials = credentials, + releaseUri = sonatypeCentralSnapshotUri, + snapshotUri = sonatypeCentralSnapshotUri, + taskDest = taskDest, + log = log, + worker = worker ) - - /** Maven uses this as a workspace for file manipulation. */ - val mavenWorkspace = taskDest / "maven" - - if (dryRun) { - val publishTo = taskDest / "repository" - val result = worker.publishToLocal( - publishTo = publishTo, - workspace = mavenWorkspace, - artifacts - ) - log.info(s"Dry-run publishing to '$publishTo' finished with result: $result") - } else { - val result = worker.publishToRemote( - uri = uri, - workspace = mavenWorkspace, - username = credentials.username, - password = credentials.password, - artifacts - ) - log.info(s"Publishing to '$uri' finished with result: $result") - } } def publishReleases(artifacts: Seq[PublishData], gpgArgs: GpgArgs): Unit = { @@ -258,36 +239,5 @@ object SonatypeCentralPublishModule extends ExternalModule with DefaultTaskModul } } - private def getSonatypeCredential( - credentialParameterValue: String, - credentialName: String, - envVariableName: String - ): Task[String] = Task.Anon { - if (credentialParameterValue.nonEmpty) { - Result.Success(credentialParameterValue) - } else { - (for { - credential <- Task.env.get(envVariableName) - } yield { - Result.Success(credential) - }).getOrElse( - Result.Failure( - s"No $credentialName set. Consider using the $envVariableName environment variable or passing `$credentialName` argument" - ) - ) - } - } - - private def getSonatypeCredentials( - usernameParameterValue: String, - passwordParameterValue: String - ): Task[SonatypeCredentials] = Task.Anon { - val username = - getSonatypeCredential(usernameParameterValue, "username", USERNAME_ENV_VARIABLE_NAME)() - val password = - getSonatypeCredential(passwordParameterValue, "password", PASSWORD_ENV_VARIABLE_NAME)() - Result.Success(SonatypeCredentials(username, password)) - } - lazy val millDiscover: mill.api.Discover = mill.api.Discover[this.type] } diff --git a/libs/javalib/src/mill/javalib/publish/SonatypeHelpers.scala b/libs/javalib/src/mill/javalib/publish/SonatypeHelpers.scala index 40be70f0a0fd..2a10ceb00c9d 100644 --- a/libs/javalib/src/mill/javalib/publish/SonatypeHelpers.scala +++ b/libs/javalib/src/mill/javalib/publish/SonatypeHelpers.scala @@ -8,8 +8,9 @@ import java.security.MessageDigest object SonatypeHelpers { // http://central.sonatype.org/pages/working-with-pgp-signatures.html#signing-a-file - val USERNAME_ENV_VARIABLE_NAME = "MILL_SONATYPE_USERNAME" - val PASSWORD_ENV_VARIABLE_NAME = "MILL_SONATYPE_PASSWORD" + val CREDENTIALS_ENV_VARIABLE_PREFIX = "MILL_SONATYPE" + val USERNAME_ENV_VARIABLE_NAME = s"${CREDENTIALS_ENV_VARIABLE_PREFIX}_USERNAME" + val PASSWORD_ENV_VARIABLE_NAME = s"${CREDENTIALS_ENV_VARIABLE_PREFIX}_PASSWORD" private[mill] def getArtifactMappings( isSigned: Boolean, diff --git a/libs/scalalib/src/mill/scalalib/aliases.scala b/libs/scalalib/src/mill/scalalib/aliases.scala index cabb2e381c5e..5fc2458b4fcd 100644 --- a/libs/scalalib/src/mill/scalalib/aliases.scala +++ b/libs/scalalib/src/mill/scalalib/aliases.scala @@ -1,5 +1,7 @@ package mill.scalalib object Dependency extends mill.api.ExternalModule.Alias(mill.javalib.Dependency) +object MavenPublishModule + extends mill.api.ExternalModule.Alias(mill.javalib.MavenPublishModule) object SonatypeCentralPublishModule extends mill.api.ExternalModule.Alias(mill.javalib.SonatypeCentralPublishModule) diff --git a/website/docs/modules/ROOT/partials/Publishing_Footer.adoc b/website/docs/modules/ROOT/partials/Publishing_Footer.adoc index 431f7bed20cd..5888c1e08a88 100644 --- a/website/docs/modules/ROOT/partials/Publishing_Footer.adoc +++ b/website/docs/modules/ROOT/partials/Publishing_Footer.adoc @@ -12,7 +12,7 @@ has the instructions on how to do this === Publishing Secrets Mill uses the following environment variables as a way to pass the necessary secrets -for publishing: +for publishing to Maven Central: [,sh] @@ -158,6 +158,40 @@ workflows. See https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions[Using Secrets in Github Actions] for more details. +== Instructions for Publishing to Generic Maven Repositories + +Apart from publishing your project's modules to Maven Central, you may also want to publish them to a generic +Maven repository (such as a privately hosted one). + +=== Publishing Secrets + +Mill uses the following environment variables as a way to pass the necessary secrets for publishing: + +[,sh] +---- +# Credentials for publishing to the repository +export MILL_MAVEN_USERNAME=... +export MILL_MAVEN_PASSWORD=... +---- + +=== Publishing + +You can publish all eligible modules in your Mill project using the +xref:fundamentals/modules.adoc#_default_tasks[Default Task] of the +xref:fundamentals/modules.adoc#_external_modules[External Module] `mill.javalib.MavenPublishModule`: + +[source,console] +---- +> mill mill.javalib.MavenPublishModule/ \ + --username myusername \ + --password mypassword \ + --releaseUri https://example.company.com/release \ + --snapshotUri https://example.company.com/snapshot +---- + +When you do not provide the username and password parameters, Mill will instead look for the `MILL_MAVEN_USERNAME` and +`MILL_MAVEN_PASSWORD` environment variables. Providing the credentials via environment variables is the recommended +approach due to security considerations. == Instructions for Publishing to Maven Central via Legacy OSSHR (Deprecated) @@ -174,7 +208,7 @@ publishing via the legacy OSSHR(OSS Repository Hosting) also requires them. === Publishing You can publish all eligible modules in your Mill project using -the xref:fundamentals/modules.adoc#_default_tasks[default task] of the +the xref:fundamentals/modules.adoc#_default_tasks[Default Task] of the xref:fundamentals/modules.adoc#_external_modules[External Module] `mill.scalalib.PublishModule`: [source,console]