Skip to content
Merged
85 changes: 85 additions & 0 deletions libs/javalib/src/mill/javalib/MavenPublish.scala
Original file line number Diff line number Diff line change
@@ -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")
}
}

}
42 changes: 42 additions & 0 deletions libs/javalib/src/mill/javalib/MavenPublishModule.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package mill.javalib

import mill.*
import mill.api.*
import mill.util.Tasks

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add some scaladoc to this trait and its companion object, and explain what is the difference between this and the normal trait PublishModule and object SonatypeCentralPublishModule we typically tell people to use?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Also, do we need this trait at all? Ideally we should just have everyone extend PublishModule and use different companion object external modules to publish them to different places

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding "do we need this trait at all": I was just exactly following what is already done in trait SonatypeCentralPublishModule and its companion object. Therefore it seems clean to me to not break with that design?

Regarding what the difference is: What SonatypeCentralPublishModule does for Sonatype Central, MavenPublishModule does for "plain" Maven. I can certainly make this distinction clear by providing a scaladoc for both modules.

To try to explain my understanding as best as possible: SonatypeCentralPublishModule is specifically for publishing to the service https://central.sonatype.com/. Because of at least staging releases (not sure if something else), this is not exactly the same as publishing to a repository that "just" follows the Maven2 Repository Layout (even if that repo is managed by the Sonatype software). That is the reason why MavenPublishModule has been brought to life. Now to go even deeper into this, I believe it should be possible and in fact probably also cleaner and closer to reality, to have all of this functionality in one module and/or publish task, which can be configured in a multitude of ways (e.g. publish to central or private; specify credentials or not; cryptographically sign or not). This is what was actually done in the deprecated publishAll task of the PublishModule itself and is stated in the Mill docs (though - if I remember my testing correctly - was not 100 % compliant for "plain" Maven publishing because it did not create maven-metadata.xml). Implementing this unified approach is something that I consider both out-of-scope for this PR and a bit out of my comfort zone. Though I'd certainly be open to try if you agree :)

Copy link
Member

@lihaoyi lihaoyi Nov 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the important thing is to make sure ./mill mill.javalib.MavenPublishModule/ works for publishing normal mill.javalib.publish.PublishModules, and not just this particular trait. The same way that ./mill mill.javalib.SonatypeCentralPublishModule/ works on normal mill.javalib.publish.PublishModules without requiring the individual modules extend mill.javalib.SonatypeCentralPublishModule

If that works, then we can probably do without this trait and just ask people to use the companion object

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my understanding ./mill mill.javalib.MavenPublishModule/ works for publishing normal mill.javalib.publish.PublishModules with the current implementation in this PR, because MavenPublishModule is an ExternalModule, it's default task is publishAll and publishAlls publishArtifacts parameter by default resolves to __:PublishModule.publishArtifacts (following exactly what is done in SonatypeCentralPublishModule). Or am I missing something here?

I would propose to still keep the MavenPublishModule trait for two reasons:

  1. This follows the design of SonatypeCentralPublishModule and therefore is uniform
  2. It gives users the option to either run ./mill mill.javalib.MavenPublishModule/ or (for example) ./mill __.publishMaven. I think giving users the second option is important, because of the following scenario: What if I want to publish some of my modules to Sonatype Central and some to a private repo? Or what if I want to publish to different private repos? When keeping the MavenPublishModule trait, this is easy to express in the build by extending it in different modules and supplying different parameters. I think with the external module alone this would be more tricky?

Copy link
Member

@lefou lefou Nov 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to clarify, the external module's command accepting a --publish-artifacts option itself accepting a task query like __.publishArtifacts or <my-module>.publishArtifacts should already allow to publish selected modules via different methods.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I was missing this part, thanks! So what I could do to put this in - say - a shared module in a build is something like the below right?

package millbuild

import mill.*
import mill.api.Task.Simple
import mill.javalib.MavenPublishModule
import mill.scalalib.*
import mill.scalalib.publish.*
import mill.util.Tasks

trait FooModule extends ScalaModule, PublishModule:

  def publishMaven = MavenPublishModule.publishAll(
    publishArtifacts = Tasks(Seq(publishArtifacts)),
    username = "",
    password = "",
    bundleName = "",
    releaseUri = "",
    snapshotUri = "",
  )

  def publishVersion = "0.1.0"

  def pomSettings = PomSettings(
    description = "",
    organization = "",
    url = "",
    licenses = Seq.empty,
    versionControl = VersionControl.github("", ""),
    developers = Seq.empty,
  )

In this case should I provide this option in the docs? And related: Is there a different way than using the Tasks util for this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think expecting people to pass the values via the CLI or env variables would be fine for now, that's what we mostly recommend for SonatypePublishModule and it seems to work

/**
* 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]

}
42 changes: 42 additions & 0 deletions libs/javalib/src/mill/javalib/PublishCredentialsModule.scala
Original file line number Diff line number Diff line change
@@ -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"
)
)
}
}
}
112 changes: 31 additions & 81 deletions libs/javalib/src/mill/javalib/SonatypeCentralPublishModule.scala
Original file line number Diff line number Diff line change
@@ -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] =
Expand Down Expand Up @@ -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())

Expand Down Expand Up @@ -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 = "<user did not override this method>"

def self = this
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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]
}
5 changes: 3 additions & 2 deletions libs/javalib/src/mill/javalib/publish/SonatypeHelpers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions libs/scalalib/src/mill/scalalib/aliases.scala
Original file line number Diff line number Diff line change
@@ -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)
Loading
Loading