Skip to content

Commit

Permalink
feat!: rework the Nexus interaction, now multiple publications can be…
Browse files Browse the repository at this point in the history
… uploaded to the same staging repository before closing and releasing
  • Loading branch information
DanySK committed Jun 17, 2022
1 parent b7c2f2e commit fe725b7
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 167 deletions.
141 changes: 90 additions & 51 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,61 +20,54 @@ and preparing tasks to upload, close, and release the artifact.
This plugin supports both targets that use Sonatype Nexus (such as Maven Central)
and targets that do not, such as GitHub Packages.

### Provided tasks
## Configuration

* `sourcesJar`: a `Jar` task preconfigured to collect and pack `allSource` from the `main` source set
* `javadocJar`: a `Jar` task preconfigured to
1. Detect if a javadoc tasks exists, and in case depend on it, and pack its output folder
2. Detect if a dokkaJavadoc tasks exists, and to the same as above
* One task for each combination of `SoftwareComponent` and repository,
unless manually deactivated, a `MavenCentral` repository is created by default.
* One task for publishing `All` software components to any target repository
* For every repository with an associated Sonatype Nexus instance, additional tasks are generated to control the
upload into a new staging repository, its closure, and its release.
### Minimal

In short, if you have (for instance) a mixed Java-Kotlin project,
you should find the following tasks:
Add `MAVEN_CENTRAL_USERNAME` and `MAVEN_CENTRAL_PASSWORD` to your environment

* `closeJavaMavenOnMavenCentralNexus`
* `closeKotlinMavenOnMavenCentralNexus`
* `publishJavaMavenPublicationToMavenCentralRepository`
* `publishKotlinMavenPublicationToMavenCentralRepository`
* `publishAllPublicationsToMavenCentralRepository`
* `publishJavaMavenPublicationToMavenLocalRepository`
* `publishKotlinMavenPublicationToMavenLocalRepository`
* `publishAllPublicationsToMavenLocalRepository`
* `releaseJavaMavenOnMavenCentralNexus`
* `releaseKotlinMavenOnMavenCentralNexus`
* `uploadJavaMavenToMavenCentralNexus`
* `uploadKotlinMavenToMavenCentralNexus`

If you add a custom repository, say `myRepo`, you would also find the following tasks:

* `publishJavaMavenPublicationToMyRepoRepository`
* `publishKotlinMavenPublicationToMyRepoRepository`
* `publishAllPublicationsToMyRepoRepository`

and if `myRepo` has configured an URL for an associated Nexus instance, the following ones:

* `closeJavaMavenOnMyRepoNexus`
* `closeKotlinMavenOnMyRepoNexus`
* `releaseJavaMavenOnMyRepoNexus`
* `releaseKotlinMavenOnMyRepoNexus`
* `uploadJavaMavenToMyRepoNexus`
* `uploadKotlinMavenToMyRepoNexus`
```kotlin
plugins {
id ("org.danilopianini.publish-on-central") version "<pick the latest>"
}
group = "your.group.id" // This must be configured for the generated pom.xml to work correctly
publishOnCentral {
projectUrl.set("https://github.com/DanySK/${project.name}")
scmConnection.set("git:[email protected]:DanySK/${project.name}")
}
publishing {
publications {
withType<MavenPublication> {
pom {
developers {
developer {
name.set("Danilo Pianini")
email.set("[email protected]")
url.set("http://www.danilopianini.org/")
}
}
}
}
}
}
signing {
val signingKey: String? by project
val signingPassword: String? by project
useInMemoryPgpKeys(signingKey, signingPassword)
}
```

which is what needs to get called to have your artifacts uploaded on OSSRH Nexus instance.
### Complete

### Importing the plugin

```kotlin
plugins {
id ("org.danilopianini.publish-on-central") version "<pick the latest>"
}
```
The plugin is configured to react to the application of the `java` plugin, and to apply the `maven-publish` and `signing` plugin if they are not applied.

### Configuring the plugin
The plugin is configured to react to the application of the `java` plugin,
and to apply the `maven-publish` and `signing` plugin if they are not applied.

```kotlin
// The package name is equal to the project name
Expand All @@ -83,11 +76,6 @@ group = "your.group.id" // This must be configured for the generated pom.xml to
* The plugin comes with defaults that are useful to myself. You should configure it to behave as you please:
*/
publishOnCentral {
/*
* By default, this plugin aggressively configures all Maven Publications to be compatible with Central.
* If it is not the desired behavior, turn this setting to false.
*/
autoConfigureAllPublications.set(true)
// Set to false if you do not want the MavenCentral repository to be automatically configured
configureMavenCentral.set(true)
// The following values are the default, if they are ok with you, just omit them
Expand Down Expand Up @@ -132,9 +120,9 @@ publishOnCentral {
*/
if (project.version.endsWith("-SNAPSHOT")) { // Avoid stable versions being pushed there...
mavenCentralSnapshotsRepository() // Imports user and password from the configuration for Maven Central
// mavenCentralSnapshotsRepository() {
// ...but they can be customized as per any other repository
// }
// mavenCentralSnapshotsRepository() {
// ...but they can be customized as per any other repository
// }
}
}

Expand Down Expand Up @@ -170,6 +158,57 @@ signing {
}
```

## Tasks

* `sourcesJar`: a `Jar` task preconfigured to collect and pack `allSource` from the `main` source set
* `javadocJar`: a `Jar` task preconfigured to
1. Detect if a javadoc tasks exists, and in case depend on it, and pack its output folder
2. Detect if a dokkaJavadoc tasks exists, and to the same as above
* One publishing task for each combination of `SoftwareComponent` and repository,
unless manually deactivated, a `MavenCentral` repository is created by default.
* One publishing task for publishing `All` software components to any target repository
* For every repository with an associated Sonatype Nexus instance, additional tasks are generated to control the
creation, upload, closure, and release of staging repositories.

### Non-Nexus publishing

Launching the `publish[PublicationName]PublicationTo[RepositoryName]Repository` triggers the creation of the required components,
their signing, and the upload on the target repository.

```mermaid
flowchart LR
jar --o signPublicationNamePublication
sourcesJar --o signPublicationNamePublication
javadocJar --o signPublicationNamePublication
signPublicationNamePublication --o publishPublicationNamePublicationToRepositoryNameRepository
generatePomFileForPublicationNamePublication --o signPublicationNamePublication
```

### Nexus publishing

Nexus publishing is a bit more elaborate.
It requires to select:
1. the operation that must be performed (among simple upload, repository closure, and repository release), and
2. the packages that will be uploaded

Typical invocations could be:

* `./gradlew uploadAllPublicationsToMavenCentralNexus`
* Simply uploads all publications on a single staging repository
* `./gradlew uploadAllPublicationsToMavenCentralNexus closeStagingRepositoryOnMavenCentral`
* Uploads all artifacts and closes the repository
* `./gradlew uploadAllPublicationsToMavenCentralNexus releaseStagingRepositoryOnMavenCentral`
* Uploads all artifacts, closes, and releases the repository

The idea is that the packages to be uploaded must be selected by picking the right set of
`upload[Publication]To[Repo]Nexus` tasks,
and then if further operations are required, either `closeStagingRepositoryOnMavenCentral` or
`releaseStagingRepositoryOnMavenCentral` can be used to close/release.

## Usage examples

T.B.D.

## Contributing to the project

I gladly review pull requests and I'm happy to improve the work.
Expand Down
163 changes: 58 additions & 105 deletions src/main/kotlin/org/danilopianini/gradle/mavencentral/Configuration.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
package org.danilopianini.gradle.mavencentral

import org.gradle.api.DomainObjectCollection
import org.gradle.api.Project
import org.gradle.api.provider.Property
import org.gradle.api.publish.PublishingExtension
import org.gradle.api.publish.maven.MavenPublication
import org.gradle.api.publish.maven.tasks.PublishToMavenRepository
import org.gradle.api.publish.plugins.PublishingPlugin
import org.gradle.jvm.tasks.Jar
import org.gradle.configurationcache.extensions.capitalized
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.create
import org.gradle.kotlin.dsl.property
import org.gradle.kotlin.dsl.register
import org.gradle.kotlin.dsl.the
import org.gradle.kotlin.dsl.withType
import org.gradle.plugins.signing.SigningExtension
Expand All @@ -29,80 +29,13 @@ internal inline fun <reified T> Project.propertyWithDefaultProvider(noinline def
* - a javadoc jar file
*/
fun MavenPublication.configureForMavenCentral(extension: PublishOnCentralExtension) {
val project = extension.project
configurePomForMavenCentral(extension)
if (pom.packaging == "jar") {
val jarTasks = project.tasks.withType<Jar>()
// Required artifacts
if (artifacts.none { it.extension == "jar" && it.classifier.isNullOrEmpty() }) {
project.logger.debug("Publication '{}' has no pre-configured classifier-less jar", name)
artifact(jarTasks.findJarTaskWithClassifier("", "jar", this))
}
if (artifacts.none { it.classifier == "sources" }) {
project.logger.debug("Publication '{}' has no pre-configured source jar", name)
artifact(jarTasks.findJarTaskWithClassifier("sources", "sourcesJar", this))
}
if (artifacts.none { it.classifier == "javadoc" }) {
project.logger.debug("Publication '{}' has no pre-configured javadoc jar", name)
artifact(jarTasks.findJarTaskWithClassifier("javadoc", "javadocJar", this))
}
}
val project = extension.project
// Signing
project.configure<SigningExtension> {
sign(this@configureForMavenCentral)
}
}

private fun DomainObjectCollection<Jar>.findJarTaskWithClassifier(
classifier: String,
preferredName: String,
publication: MavenPublication,
): Jar {
val withClassifier = filter {
with(it.archiveClassifier.orNull) {
if (classifier.isEmpty()) isNullOrEmpty() else this == classifier
}
}
fun instructions() = """
|You can either:
| - create a task that generates a jar without the correct classifier for publish-on-central to bind to, or
| - bind yourself a jar with the right classifier as artifact for the publication, or
| - disable the automatic configuration of all Maven publications for Maven Central with:
| publishOnCentral {
| autoConfigureAllPublications.set(false)
| }
| then manually configure the publications you want on Central with
| publishing.publications.withType<MavenPublication>/* filter the ones you want*/ {
| configureForMavenCentral(publishOnCentral)
| }
""".trimMargin()
check(withClassifier.isNotEmpty()) {
"""
|Publication '${
publication.name
}' with packaging type '${
publication.pom.packaging
}' has no jar with ${
if (classifier.isEmpty()) "no classifier" else "classifier '$classifier'"
}, which is required for Maven Central.
|${instructions()}
""".trimMargin()
}
return when (withClassifier.size) {
1 -> withClassifier.first()
else -> {
val best = withClassifier.find { it.name == preferredName }
check(best != null) {
"""
|Publication $publication needs a jar with classifier '$classifier', and these tasks are available:
|${withClassifier.map { it.name }}
|Publish-on-central tried to find one named '$preferredName' among them, but there was none.
|${instructions()}
""".trimMargin()
}
best
project.tasks.findByName("sign${name.capitalized()}Publication")
?: project.configure<SigningExtension> {
sign(this@configureForMavenCentral)
}
}
}

/**
Expand Down Expand Up @@ -159,21 +92,50 @@ fun Project.configureRepository(repoToConfigure: Repository) {
}

private fun Project.configureNexusRepository(repoToConfigure: Repository, nexusUrl: String) {
the<PublishingExtension>().publications.withType<MavenPublication>().forEach { publication ->
val nexus = NexusStatefulOperation(
project = project,
nexusUrl = nexusUrl,
group = project.group.toString(),
user = repoToConfigure.user,
password = repoToConfigure.password,
timeOut = repoToConfigure.nexusTimeOut,
connectionTimeOut = repoToConfigure.nexusConnectTimeOut,
)
the<PublishingExtension>().publications.configureEach {
println("Publication ${it.name}: ${it::class}")
}
val nexus = NexusStatefulOperation(
project = project,
nexusUrl = nexusUrl,
group = project.group.toString(),
user = repoToConfigure.user,
password = repoToConfigure.password,
timeOut = repoToConfigure.nexusTimeOut,
connectionTimeOut = repoToConfigure.nexusConnectTimeOut,
)
val nexusUploadUrl: Property<URI> = project.objects.property()
val createStagingRepository = tasks.register("createStagingRepositoryOn${repoToConfigure.name}") {
it.doLast {
project.warnIfCredentialsAreMissing(repoToConfigure)
nexusUploadUrl.set(nexus.repoUrl)
}
it.group = PublishingPlugin.PUBLISH_TASK_GROUP
it.description = "Creates a new Nexus staging repository on ${repoToConfigure.name}."
}
val uploadAllPublications = tasks.register("uploadAllPublicationsTo${repoToConfigure.name}Nexus") {
it.dependsOn(createStagingRepository)
it.group = PublishingPlugin.PUBLISH_TASK_GROUP
it.description = "Uploads all publications to a staging repository on ${repoToConfigure.name}."
}
val closeStagingRepository = tasks.register("closeStagingRepositoryOn${repoToConfigure.name}") {
it.doLast { nexus.close() }
it.dependsOn(createStagingRepository)
it.mustRunAfter(uploadAllPublications)
it.group = PublishingPlugin.PUBLISH_TASK_GROUP
it.description = "Closes the Nexus repository on ${repoToConfigure.name}."
}
tasks.register("releaseStagingRepositoryOn${repoToConfigure.name}") {
it.doLast { nexus.release() }
it.dependsOn(closeStagingRepository)
it.group = PublishingPlugin.PUBLISH_TASK_GROUP
it.description = "Releases the Nexus repo on ${repoToConfigure.name}."
}
the<PublishingExtension>().publications.withType<MavenPublication>().configureEach { publication ->
val publicationName = publication.name.replaceFirstChar(Char::titlecase)
val uploadArtifacts = project.tasks.create(
project.tasks.register<PublishToMavenRepository>(
"upload${publicationName}To${repoToConfigure.name}Nexus",
PublishToMavenRepository::class,
) { publishTask ->
).configure { publishTask ->
publishTask.repository = project.repositories.maven { repo ->
repo.name = repoToConfigure.name
repo.url = project.uri(repoToConfigure.url)
Expand All @@ -182,28 +144,19 @@ private fun Project.configureNexusRepository(repoToConfigure: Repository, nexusU
it.password = repoToConfigure.password.orNull
}
}
publishTask.dependsOn(createStagingRepository)
uploadAllPublications.get().dependsOn(publishTask)
closeStagingRepository.get().mustRunAfter(publishTask)
publishTask.publication = publication
publishTask.doFirst {
warnIfCredentialsAreMissing(repoToConfigure)
publishTask.repository.url = nexus.repoUrl
}
publishTask.publication = publication
publishTask.doLast {
publishTask.repository.url = nexusUploadUrl.get()
}
publishTask.group = PublishingPlugin.PUBLISH_TASK_GROUP
publishTask.description = "Initializes a new Nexus repository on ${repoToConfigure.name} " +
"and uploads the $publicationName publication."
}
val closeRepository = tasks.create("close${publicationName}On${repoToConfigure.name}Nexus") {
it.doLast { nexus.close() }
it.dependsOn(uploadArtifacts)
it.group = PublishingPlugin.PUBLISH_TASK_GROUP
it.description = "Closes the Nexus repository on ${repoToConfigure.name} with the " +
"$publicationName publication."
}
tasks.create("release${publicationName}On${repoToConfigure.name}Nexus") {
it.doLast { nexus.release() }
it.dependsOn(closeRepository)
it.group = PublishingPlugin.PUBLISH_TASK_GROUP
it.description = "Releases the Nexus repo on ${repoToConfigure.name} " +
"with the $publicationName publication."
publishTask.description = "Uploads the $publicationName publication " +
"to a staging repository on ${repoToConfigure.name}."
}
}
}
Expand Down
Loading

0 comments on commit fe725b7

Please sign in to comment.