Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: add central portal support #1224

Merged
merged 12 commits into from
Dec 7, 2024
61 changes: 57 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ and targets that do not, such as GitHub Packages.

### Minimal

Add `MAVEN_CENTRAL_USERNAME` and `MAVEN_CENTRAL_PASSWORD` to your environment
If you use the legacy Nexus-based publishing,
add `MAVEN_CENTRAL_USERNAME` and `MAVEN_CENTRAL_PASSWORD` to your environment.
If you use the new Maven Central Portal,
add `MAVEN_CENTRAL_PORTAL_USERNAME` and `MAVEN_CENTRAL_PORTAL_PASSWORD`
(obtain these credentials from the portal user settings page).
The plugin can also work in mixed mode, with some publications going to the portal and some to Nexus.

```kotlin
plugins {
Expand Down Expand Up @@ -216,6 +221,56 @@ Consequently, at the moment, the plugin only preconfigures the POM file and the
* For every repository with an associated Sonatype Nexus instance, additional tasks are generated to control the
creation, upload, closure, and release of staging repositories.

### Maven Central Portal publishing

Maven Central recently introduced a new portal for managing the artifacts,
which will progressively replace the Nexus-based publishing.
Publishing on the Central Portal goes through the following steps:
1. the publication is uploaded to project-local maven repository
2. the local repository is zipped
3. the bundle is then uploaded to the portal and validated
4. once the validation is complete, the bundle is released or dropped

The lifecycle is summarized in the following diagram:
```mermaid
flowchart LR
jar --o signPublication1Publication
sourcesJar --o signPublication1Publication
javadocJar --o signPublication1Publication
jar --o signPublication2Publication
sourcesJar --o signPublication2Publication
javadocJar --o signPublication2Publication
signPublication1Publication --o publishPublication1PublicationToProjectLocalRepository
signPublication2Publication --o publishPublication2PublicationToProjectLocalRepository
generatePomFileForPublication1Publication --o signPublication1Publication
generatePomFileForPublication2Publication --o signPublication2Publication
publishPublication1PublicationToProjectLocalRepository --o zipMavenCentralPortalPublication
publishPublication2PublicationToProjectLocalRepository --o zipMavenCentralPortalPublication
zipMavenCentralPortalPublication --o validateMavenCentralPortalPublication
validateMavenCentralPortalPublication --o dropMavenCentralPortalPublication
validateMavenCentralPortalPublication --o releaseMavenCentralPortalPublication
```

In short, select the publications you wish to publish,
and use the `upload<PublicationName>PublicationToProjectLocalRepository` task to enqueue them for upload,
then use the `zipMavenCentralPortalPublication` to create a bundle.
Now, you can interact with the portal using the (`validate`/`release`/`drop`)`MavenCentralPortalPublication` tasks.
A typical invocation could be:

```console
$ ./gradlew uploadAllPublicationsToProjectLocalRepository zipMavenCentralPortalPublication releaseMavenCentralPortalPublication
```

If you already have an uploaded bundle and want to manage it using this plugin,
set the `publishDeploymentId` property to the deployment ID of the bundle you want to manage, e.g.:

```console
$ ./gradlew -PpublishDeploymentId=8697a629-c07d-4349-9a3f-0f52f3ba74fb dropMavenCentralPortalPublication
```

If `publishDeploymentId` is set,
no upload will be performed.

### Non-Nexus publishing

Launching the `publish[PublicationName]PublicationTo[RepositoryName]Repository` triggers the creation of the required components,
Expand All @@ -230,7 +285,7 @@ flowchart LR
generatePomFileForPublicationNamePublication --o signPublicationNamePublication
```

### Nexus publishing
### Sonatype Nexus publishing

Nexus publishing is a bit more elaborate.
It requires to select:
Expand Down Expand Up @@ -326,8 +381,6 @@ jobs:
If you use publish-on-central in your project, please consider providing a pull request with a link to your project:
it will provide useful use cases for newcomers to look at.



### Java, simple project, kts build file
* [**gson-extras**](https://github.com/DanySK/gson-extras): extra goodies for Google Gson
* [**JIRF**](https://github.com/DanySK/jirf/): an implicit reflective factory
Expand Down
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ dependencies {
api(gradleKotlinDsl())
api(libs.kotlin.gradlePlugin)
api(libs.nexus.publish)
api(libs.maven.central.api)
implementation(libs.kotlinx.coroutines)
implementation(libs.fuel)
testImplementation(libs.testkit)
Expand Down
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ kotest-junit5-jvm = { module = "io.kotest:kotest-runner-junit5-jvm", version.ref
kotest-assertions-core-jvm = { module = "io.kotest:kotest-assertions-core-jvm", version.ref = "kotest" }
kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
kotlinx-coroutines = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0"
maven-central-api = "org.danilopianini:maven-central-portal-kotlin-api-jvm:2.2.1"
nexus-publish = { module = "io.github.gradle-nexus:publish-plugin", version.ref = "nexus-publish" }
testkit = { module = "io.github.mirko-felice.testkit:core", version.ref = "testkit" }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ open class InitializeNexusClient
NexusStatefulOperation(
project = project,
nexusUrl = nexusUrl,
group = project.group.toString(),
user = repoToConfigure.user,
password = repoToConfigure.password,
timeOut = repoToConfigure.nexusTimeOut,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,20 @@ data class NexusStatefulOperation(
private val password: Provider<String>,
private val timeOut: Duration,
private val connectionTimeOut: Duration,
private val group: String,
) {
/**
* Repository description.
*/
val description by lazy { project.run { "$group:$name:$version" } }

private val group: String by lazy {
project.group.toString().apply {
check(isNotBlank()) {
"Project $project has no group set"
}
}
}

/**
* The NexusClient.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package org.danilopianini.gradle.mavencentral

import io.github.gradlenexus.publishplugin.internal.StagingRepository.State.CLOSED
import kotlinx.coroutines.runBlocking
import org.danilopianini.gradle.mavencentral.MavenPublicationExtensions.signingTasks
import org.danilopianini.gradle.mavencentral.PublishPortalDeployment.Companion.DROP_TASK_NAME
import org.danilopianini.gradle.mavencentral.PublishPortalDeployment.Companion.RELEASE_TASK_NAME
import org.gradle.api.Action
import org.gradle.api.DefaultTask
import org.gradle.api.Plugin
Expand All @@ -21,7 +24,6 @@ import org.gradle.kotlin.dsl.the
import org.gradle.kotlin.dsl.withType
import org.gradle.plugins.signing.Sign
import org.jetbrains.kotlin.gradle.dsl.KotlinJsProjectExtension
import java.net.URI
import kotlin.reflect.KClass

internal object ProjectExtensions {
Expand Down Expand Up @@ -82,7 +84,7 @@ internal object ProjectExtensions {
publishing.repositories { repository ->
repository.maven { mavenArtifactRepository ->
mavenArtifactRepository.name = repoToConfigure.name
mavenArtifactRepository.url = repoToConfigure.url.map { URI(it) }.get()
mavenArtifactRepository.url = repoToConfigure.url.get()
if (mavenArtifactRepository.url.scheme != "file") {
mavenArtifactRepository.credentials { credentials ->
credentials.username = repoToConfigure.user.orNull
Expand Down Expand Up @@ -320,4 +322,83 @@ internal object ProjectExtensions {
)
}
}

internal fun Project.setupMavenCentralPortal() {
configureRepository(Repository.projectLocalRepository(project))
val zipMavenCentralPortal =
tasks.register<ZipMavenCentralPortalPublication>(
checkNotNull(ZipMavenCentralPortalPublication::class.simpleName)
.replaceFirstChar { it.lowercase() },
)
val portalDeployment =
PublishPortalDeployment(
project = project,
baseUrl = "https://central.sonatype.com/",
user =
project.propertyWithDefaultProvider {
System.getenv("MAVEN_CENTRAL_PORTAL_USERNAME")
?: project.properties["mavenCentralPortalUsername"]?.toString()
?: project.properties["centralPortalUsername"]?.toString()
?: project.properties["centralUsername"]?.toString()
},
password =
project.propertyWithDefaultProvider {
System.getenv("MAVEN_CENTRAL_PORTAL_PASSWORD")
?: project.properties["mavenCentralPortalPassword"]?.toString()
?: project.properties["centralPortalPassword"]?.toString()
?: project.properties["centralPassword"]?.toString()
},
zipTask = zipMavenCentralPortal,
)
tasks.register("saveMavenCentralPortalDeploymentId") { save ->
val fileName = "maven-central-portal-bundle-id"
val file = rootProject.layout.buildDirectory.map { it.asFile.resolve(fileName) }
save.group = PublishingPlugin.PUBLISH_TASK_GROUP
save.description = "Saves the Maven Central Portal deployment ID locally in ${file.get().absolutePath}"
save.dependsOn(zipMavenCentralPortal)
save.outputs.file(file)
save.doLast {
file.get().writeText("${portalDeployment.fileToUpload}=${portalDeployment.deploymentId}\n")
}
}
val validate =
tasks.register(PublishPortalDeployment.VALIDATE_TASK_NAME) { validate ->
validate.group = PublishingPlugin.PUBLISH_TASK_GROUP
validate.description = "Validates the Maven Central Portal publication, uploading if needed"
validate.mustRunAfter(zipMavenCentralPortal)
validate.doLast {
runBlocking {
portalDeployment.validate()
}
}
}
tasks.register(DROP_TASK_NAME) { drop ->
drop.group = PublishingPlugin.PUBLISH_TASK_GROUP
drop.description = "Drops the Maven Central Portal publication"
drop.mustRunAfter(validate)
drop.mustRunAfter(zipMavenCentralPortal)
drop.doLast {
runBlocking {
portalDeployment.drop()
}
}
}
tasks.register(RELEASE_TASK_NAME) { release ->
release.group = PublishingPlugin.PUBLISH_TASK_GROUP
release.description = "Releases the Maven Central Portal publication"
release.mustRunAfter(validate)
release.mustRunAfter(zipMavenCentralPortal)
release.doLast {
runBlocking {
portalDeployment.release()
}
}
}
gradle.taskGraph.whenReady { taskGraph ->
val allTasks = taskGraph.allTasks.map { it.name }.toSet()
check(RELEASE_TASK_NAME !in allTasks || DROP_TASK_NAME !in allTasks) {
"Task $RELEASE_TASK_NAME and $DROP_TASK_NAME cannot be executed together"
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import org.danilopianini.gradle.mavencentral.ProjectExtensions.addSourcesArtifac
import org.danilopianini.gradle.mavencentral.ProjectExtensions.configureJavadocJarTaskForKtJs
import org.danilopianini.gradle.mavencentral.ProjectExtensions.configureRepository
import org.danilopianini.gradle.mavencentral.ProjectExtensions.registerTaskIfNeeded
import org.danilopianini.gradle.mavencentral.ProjectExtensions.setupMavenCentralPortal
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.publish.PublishingExtension
Expand Down Expand Up @@ -47,14 +48,13 @@ class PublishOnCentral : Plugin<Project> {
publications { publications ->
val name = "${component.name}$PUBLICATION_NAME"
if (publications.none { it.name == name }) {
publications.create(name, MavenPublication::class.java) { publication ->
publications.register(name, MavenPublication::class.java) { publication ->
createdPublications += publication
publication.from(component)
project.addSourcesArtifactIfNeeded(publication, sourcesJarTask)
if (javadocJarTask is JavadocJar) {
publication.artifact(javadocJarTask)
}
publication.configurePomForMavenCentral(extension)
publication.pom.packaging = "jar"
project.configure<SigningExtension> {
sign(publication)
Expand All @@ -64,20 +64,31 @@ class PublishOnCentral : Plugin<Project> {
}
}
}
publications.withType<MavenPublication>().configureEach { publication ->
if (extension.autoConfigureAllPublications.getOrElse(true) && publication !in createdPublications) {
publication.configurePomForMavenCentral(extension)
if (publication.signingTasks(project).isEmpty()) {
project.configure<SigningExtension> {
sign(publication)
publications
.withType<MavenPublication>()
.configureEach { publication ->
if (extension.autoConfigureAllPublications.getOrElse(true) || publication in createdPublications) {
project.logger.info(
"Populating data of publication {} in {}, group {}",
publication.name,
project,
project.group,
)
publication.configurePomForMavenCentral(extension)
if (publication.signingTasks(project).isEmpty()) {
project.configure<SigningExtension> {
sign(publication)
}
}
}
}
}
}
project.tasks.withType<PublishToMavenRepository>().configureEach { publish ->
publish.mustRunAfter(project.tasks.withType<Sign>())
}
// Maven Central Portal
project.setupMavenCentralPortal()
// Initialize Central if needed
project.afterEvaluate {
if (extension.configureMavenCentral.getOrElse(true)) {
project.configureRepository(extension.mavenCentral)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,7 @@ open class PublishOnCentralExtension(
name: String = repositoryNameFromURL(url),
configurator: Repository.() -> Unit = { },
) {
val repo = Repository.fromProject(project, name)
repo.url.set(url)
val repo = Repository.fromProject(project, name, url)
repo.apply(configurator)
project.afterEvaluate { it.configureRepository(repo) }
}
Expand Down
Loading
Loading