From ace6f057c057693f5a56e9f412a95fe4f03c60f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Zaj=C4=85czkowski?= Date: Fri, 18 Oct 2019 18:52:49 +0200 Subject: [PATCH] [#4] Close and release tasks Initial version. --- .stutter/java11.lock | 4 + .stutter/java8.lock | 7 +- build.gradle.kts | 6 +- .../publishplugin/NexusPublishPluginTests.kt | 111 +++++++++++++++++- .../publishplugin/TestExtensions.kt | 9 +- .../BaseOperationOnNexusStagingRepository.kt | 13 ++ .../CloseNexusStagingRepository.kt | 47 ++++++++ .../InitializeNexusStagingRepository.kt | 31 +++-- .../publishplugin/NexusPublishPlugin.kt | 43 +++++-- .../publishplugin/NexusRepository.kt | 6 + .../ReleaseNexusStagingRepository.kt | 47 ++++++++ .../publishplugin/internal/NexusClient.kt | 36 ++++++ .../StagingPluginIntegrationTest.kt | 1 + .../publishplugin/TaskOrchestrationTest.kt | 109 +++++++++++++++++ 14 files changed, 434 insertions(+), 36 deletions(-) create mode 100644 .stutter/java11.lock create mode 100644 src/main/kotlin/io/github/gradlenexus/publishplugin/CloseNexusStagingRepository.kt create mode 100644 src/main/kotlin/io/github/gradlenexus/publishplugin/ReleaseNexusStagingRepository.kt create mode 100644 src/test/kotlin/io/github/gradlenexus/publishplugin/TaskOrchestrationTest.kt diff --git a/.stutter/java11.lock b/.stutter/java11.lock new file mode 100644 index 00000000..13ae7251 --- /dev/null +++ b/.stutter/java11.lock @@ -0,0 +1,4 @@ +# DO NOT MODIFY: Generated by Stutter plugin. +4.10.3 +5.0 +5.6.3 diff --git a/.stutter/java8.lock b/.stutter/java8.lock index 04cefca3..13ae7251 100644 --- a/.stutter/java8.lock +++ b/.stutter/java8.lock @@ -1,9 +1,4 @@ # DO NOT MODIFY: Generated by Stutter plugin. 4.10.3 5.0 -5.1.1 -5.2.1 -5.3.1 -5.4.1 -5.5.1 -5.6.2 +5.6.3 diff --git a/build.gradle.kts b/build.gradle.kts index 4ea31c51..ee812d3f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -51,7 +51,7 @@ repositories { val licenseHeaderFile = file("gradle/license-header.txt") spotless { kotlin { - ktlint() + ktlint().userData(mapOf("disabled_rules" to "comment-spacing")) licenseHeaderFile(licenseHeaderFile) } } @@ -98,9 +98,13 @@ dependencies { } stutter { + isSparse = (findProperty("stutter.sparce")?.toString()?.toBoolean()) ?: true java(8) { compatibleRange("4.10") } + java(11) { + compatibleRange("4.10") + } } configurations { diff --git a/src/compatTest/kotlin/io/github/gradlenexus/publishplugin/NexusPublishPluginTests.kt b/src/compatTest/kotlin/io/github/gradlenexus/publishplugin/NexusPublishPluginTests.kt index 772c1632..53d03469 100644 --- a/src/compatTest/kotlin/io/github/gradlenexus/publishplugin/NexusPublishPluginTests.kt +++ b/src/compatTest/kotlin/io/github/gradlenexus/publishplugin/NexusPublishPluginTests.kt @@ -35,8 +35,11 @@ import org.gradle.testkit.runner.TaskOutcome import org.gradle.testkit.runner.TaskOutcome.FAILED import org.gradle.testkit.runner.TaskOutcome.SKIPPED import org.gradle.testkit.runner.TaskOutcome.SUCCESS +import org.gradle.util.GradleVersion import org.gradle.util.VersionNumber import org.junit.jupiter.api.Assumptions.assumeTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.io.TempDir @@ -45,6 +48,7 @@ import ru.lanwen.wiremock.ext.WiremockResolver.Wiremock import java.nio.file.Files import java.nio.file.Path +@Suppress("FunctionName") //TODO: How to suppress "kotlin:S100" from SonarLint? @ExtendWith(WiremockResolver::class) class NexusPublishPluginTests { @@ -55,7 +59,7 @@ class NexusPublishPluginTests { private val gson = Gson() - private val gradleVersion = System.getProperty("compat.gradle.version") + private val gradleVersion = System.getProperty("compat.gradle.version") ?: GradleVersion.current().version private val gradleRunner = GradleRunner.create() .withPluginClasspath() @@ -67,6 +71,13 @@ class NexusPublishPluginTests { @TempDir lateinit var projectDir: Path + lateinit var buildGradle: Path + + @BeforeEach + internal fun setUp() { + buildGradle = projectDir.resolve("build.gradle") + } + @Test fun `publish task depends on correct tasks`() { projectDir.resolve("settings.gradle").write(""" @@ -615,6 +626,94 @@ class NexusPublishPluginTests { .contains("b.snapshotRepositoryUrl = https://oss.sonatype.org/content/repositories/snapshots/") } + @Test + fun `should close staging repository`(@Wiremock server: WireMockServer) { + writeDefaultSingleProjectConfiguration() + buildGradle.append(""" + nexusPublishing { + repositories { + sonatype { + nexusUrl = uri('${server.baseUrl()}') + stagingProfileId = '$STAGING_PROFILE_ID' + } + } + } + """) + + stubCreateStagingRepoRequest(server, "/staging/profiles/$STAGING_PROFILE_ID/start", STAGED_REPOSITORY_ID) + server.stubFor(post(urlEqualTo("/staging/bulk/close")) + .withRequestBody(matchingJsonPath("\$.data[?(@.stagedRepositoryIds[0] == '$STAGED_REPOSITORY_ID')]")) + .withRequestBody(matchingJsonPath("\$.data[?(@.autoDropAfterRelease == true)]")) + .willReturn(aResponse().withHeader("Content-Type", "application/json").withBody("{}"))) + server.stubFor(get(urlEqualTo("/staging/repository/$STAGED_REPOSITORY_ID")) + .willReturn(aResponse().withHeader("Content-Type", "application/json").withBody("{\"transitioning\":false,\"type\":\"CLOSED\"}"))) + + val result = run("initializeSonatypeStagingRepository", "closeSonatypeStagingRepository") + + assertSuccess(result, ":initializeSonatypeStagingRepository") + assertSuccess(result, ":closeSonatypeStagingRepository") + assertCloseOfStagingRepo(server) + } + + @Test + fun `should close and release staging repository`(@Wiremock server: WireMockServer) { + writeDefaultSingleProjectConfiguration() + buildGradle.append(""" + nexusPublishing { + repositories { + sonatype { + nexusUrl = uri('${server.baseUrl()}') + stagingProfileId = '$STAGING_PROFILE_ID' + } + } + } + """) + + stubCreateStagingRepoRequest(server, "/staging/profiles/$STAGING_PROFILE_ID/start", STAGED_REPOSITORY_ID) + server.stubFor(post(urlEqualTo("/staging/bulk/promote")) + .withRequestBody(matchingJsonPath("\$.data[?(@.stagedRepositoryIds[0] == '$STAGED_REPOSITORY_ID')]")) + .withRequestBody(matchingJsonPath("\$.data[?(@.autoDropAfterRelease == true)]")) + .willReturn(aResponse().withHeader("Content-Type", "application/json").withBody("{}"))) + server.stubFor(get(urlEqualTo("/staging/repository/$STAGED_REPOSITORY_ID")) + .willReturn(aResponse().withHeader("Content-Type", "application/json").withBody("{\"transitioning\":false,\"type\":\"RELEASED\"}"))) + + val result = run("tasks", "initializeSonatypeStagingRepository", "releaseSonatypeStagingRepository") + + assertSuccess(result, ":initializeSonatypeStagingRepository") + assertSuccess(result, ":releaseSonatypeStagingRepository") + assertReleaseOfStagingRepo(server) + } + + // TODO: To be used also in other tests + private fun writeDefaultSingleProjectConfiguration() { + projectDir.resolve("settings.gradle").write(""" + rootProject.name = 'sample' + """) + projectDir.resolve("build.gradle").write(""" + buildscript { + repositories { + gradlePluginPortal() + } + dependencies { + classpath files($pluginClasspathAsString) + } + } + plugins { + id('java-library') + } + apply plugin: 'io.github.gradle-nexus.publish-plugin' + group = 'org.example' + version = '0.0.1' + publishing { + publications { + mavenJava(MavenPublication) { + from(components.java) + } + } + } + """) + } + private fun run(vararg arguments: String): BuildResult { return gradleRunner(*arguments).build() } @@ -673,7 +772,15 @@ class NexusPublishPluginTests { } private fun assertCloseOfStagingRepo(server: WireMockServer) { - server.verify(postRequestedFor(urlMatching("/staging/bulk/close")) + assertGivenTransitionOperationOfStagingRepo(server, "close") + } + + private fun assertReleaseOfStagingRepo(server: WireMockServer) { + assertGivenTransitionOperationOfStagingRepo(server, "promote") + } + + private fun assertGivenTransitionOperationOfStagingRepo(server: WireMockServer, transitionOperation: String) { + server.verify(postRequestedFor(urlMatching("/staging/bulk/$transitionOperation")) .withRequestBody(matchingJsonPath("\$.data[?(@.stagedRepositoryIds[0] == '$STAGED_REPOSITORY_ID')]"))) } } diff --git a/src/compatTest/kotlin/io/github/gradlenexus/publishplugin/TestExtensions.kt b/src/compatTest/kotlin/io/github/gradlenexus/publishplugin/TestExtensions.kt index 484e57f4..c25e3bfa 100644 --- a/src/compatTest/kotlin/io/github/gradlenexus/publishplugin/TestExtensions.kt +++ b/src/compatTest/kotlin/io/github/gradlenexus/publishplugin/TestExtensions.kt @@ -19,7 +19,14 @@ package io.github.gradlenexus.publishplugin import java.nio.file.Files import java.nio.file.Path -fun Path.write(text: String) { +fun Path.write(text: String): Path { Files.createDirectories(parent) toFile().writeText(text) + return this +} + +fun Path.append(text: String): Path { + Files.createDirectories(parent) + toFile().appendText(text) + return this } diff --git a/src/main/kotlin/io/github/gradlenexus/publishplugin/BaseOperationOnNexusStagingRepository.kt b/src/main/kotlin/io/github/gradlenexus/publishplugin/BaseOperationOnNexusStagingRepository.kt index 7665ae5a..6d0f6383 100644 --- a/src/main/kotlin/io/github/gradlenexus/publishplugin/BaseOperationOnNexusStagingRepository.kt +++ b/src/main/kotlin/io/github/gradlenexus/publishplugin/BaseOperationOnNexusStagingRepository.kt @@ -16,7 +16,9 @@ package io.github.gradlenexus.publishplugin +import io.github.gradlenexus.publishplugin.internal.NexusClient import org.gradle.api.DefaultTask +import org.gradle.api.GradleException import org.gradle.api.model.ObjectFactory import org.gradle.api.provider.Property import org.gradle.api.tasks.Input @@ -70,4 +72,15 @@ constructor(objects: ObjectFactory, extension: NexusPublishExtension, repository connectTimeout.set(extension.connectTimeout) this.onlyIf { extension.useStaging.getOrElse(false) } } + + protected fun determineStagingProfileId(client: NexusClient): String { + var stagingProfileId = stagingProfileId.orNull + if (stagingProfileId == null) { + val packageGroup = packageGroup.get() + logger.debug("No stagingProfileId set, querying for packageGroup '{}'", packageGroup) + stagingProfileId = client.findStagingProfileId(packageGroup) + ?: throw GradleException("Failed to find staging profile for package group: $packageGroup") + } + return stagingProfileId + } } diff --git a/src/main/kotlin/io/github/gradlenexus/publishplugin/CloseNexusStagingRepository.kt b/src/main/kotlin/io/github/gradlenexus/publishplugin/CloseNexusStagingRepository.kt new file mode 100644 index 00000000..eaaec2d5 --- /dev/null +++ b/src/main/kotlin/io/github/gradlenexus/publishplugin/CloseNexusStagingRepository.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.gradlenexus.publishplugin + +import io.github.gradlenexus.publishplugin.internal.NexusClient +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.TaskAction +import org.gradle.kotlin.dsl.property +import javax.inject.Inject + +@Suppress("UnstableApiUsage") +open class CloseNexusStagingRepository @Inject +constructor(objects: ObjectFactory, extension: NexusPublishExtension, repository: NexusRepository) : + BaseOperationOnNexusStagingRepository(objects, extension, repository) { + + @get:Input + val stagingRepositoryId: Property = objects.property() + + init { + stagingRepositoryId.set(repository.stagingRepositoryId) + } + + @TaskAction + fun closeStagingRepo() { + val client = NexusClient(serverUrl.get(), username.orNull, password.orNull, clientTimeout.orNull, connectTimeout.orNull) + val stagingProfileId = determineStagingProfileId(client) + logger.info("Closing staging repository with id '{}' for stagingProfileId '{}'", stagingRepositoryId.get(), stagingProfileId) + client.closeStagingRepository(stagingRepositoryId.get()) + // TODO: Broken with real Nexus - waiting for effective execution is also required https://github.com/gradle-nexus/publish-plugin/issues/7 + } +} diff --git a/src/main/kotlin/io/github/gradlenexus/publishplugin/InitializeNexusStagingRepository.kt b/src/main/kotlin/io/github/gradlenexus/publishplugin/InitializeNexusStagingRepository.kt index 5eeed67c..8186dfbf 100644 --- a/src/main/kotlin/io/github/gradlenexus/publishplugin/InitializeNexusStagingRepository.kt +++ b/src/main/kotlin/io/github/gradlenexus/publishplugin/InitializeNexusStagingRepository.kt @@ -16,11 +16,11 @@ package io.github.gradlenexus.publishplugin -import io.github.gradlenexus.publishplugin.internal.NexusClient import io.codearte.gradle.nexus.NexusStagingExtension -import org.gradle.api.GradleException +import io.github.gradlenexus.publishplugin.internal.NexusClient import org.gradle.api.artifacts.repositories.MavenArtifactRepository import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property import org.gradle.api.publish.PublishingExtension import org.gradle.api.tasks.TaskAction import org.gradle.kotlin.dsl.the @@ -32,6 +32,8 @@ open class InitializeNexusStagingRepository @Inject constructor(objects: ObjectFactory, extension: NexusPublishExtension, repository: NexusRepository, private val serverUrlToStagingRepoUrl: MutableMap) : BaseOperationOnNexusStagingRepository(objects, extension, repository) { + private val stagingRepositoryId: Property = repository.stagingRepositoryId + @TaskAction fun createStagingRepoAndReplacePublishingRepoUrl() { val url = createStagingRepo() @@ -41,13 +43,15 @@ constructor(objects: ObjectFactory, extension: NexusPublishExtension, repository internal fun createStagingRepo(): URI { return serverUrlToStagingRepoUrl.computeIfAbsent(serverUrl.get()) { serverUrl -> val client = NexusClient(serverUrl, username.orNull, password.orNull, clientTimeout.orNull, connectTimeout.orNull) - val stagingProfileId = determineStagingProfileId(client) + val stagingProfileId = determineStagingProfileId(client) // TODO: It would be good to keep/cache value in Extension/Repository logger.info("Creating staging repository for stagingProfileId '{}'", stagingProfileId) - val stagingRepositoryId = client.createStagingRepository(stagingProfileId) + val stagingRepositoryIdAsString = client.createStagingRepository(stagingProfileId) + keppStagingRepositoyIdInExtension(stagingRepositoryIdAsString) + project.rootProject.plugins.withId("io.codearte.nexus-staging") { val nexusStagingExtension = project.rootProject.the() try { - nexusStagingExtension.stagingRepositoryId.set(stagingRepositoryId) + nexusStagingExtension.stagingRepositoryId.set(stagingRepositoryIdAsString) } catch (e: NoSuchMethodError) { logger.warn("For increased publishing reliability please update the io.codearte.nexus-staging plugin to at least version 0.20.0.\n" + "If your version is at least 0.20.0, try to update the io.github.gradle-nexus.publish-plugin plugin to its latest version.\n" + @@ -55,25 +59,18 @@ constructor(objects: ObjectFactory, extension: NexusPublishExtension, repository logger.debug("getStagingRepositoryId method not found on nexusStagingExtension", e) } } - client.getStagingRepositoryUri(stagingRepositoryId) + client.getStagingRepositoryUri(stagingRepositoryIdAsString) } } - private fun determineStagingProfileId(client: NexusClient): String { - var stagingProfileId = stagingProfileId.orNull - if (stagingProfileId == null) { - val packageGroup = packageGroup.get() - logger.debug("No stagingProfileId set, querying for packageGroup '{}'", packageGroup) - stagingProfileId = client.findStagingProfileId(packageGroup) - ?: throw GradleException("Failed to find staging profile for package group: $packageGroup") - } - return stagingProfileId - } - private fun replacePublishingRepoUrl(url: URI) { val publishing = project.the() val repository = publishing.repositories.getByName(repositoryName.get()) as MavenArtifactRepository logger.info("Updating URL of publishing repository '{}' to '{}'", repository.name, url) repository.setUrl(url.toString()) } + + private fun keppStagingRepositoyIdInExtension(stagingRepositoryIdAsString: String) { + stagingRepositoryId.set(stagingRepositoryIdAsString) + } } diff --git a/src/main/kotlin/io/github/gradlenexus/publishplugin/NexusPublishPlugin.kt b/src/main/kotlin/io/github/gradlenexus/publishplugin/NexusPublishPlugin.kt index 09fcf7bb..2510efce 100644 --- a/src/main/kotlin/io/github/gradlenexus/publishplugin/NexusPublishPlugin.kt +++ b/src/main/kotlin/io/github/gradlenexus/publishplugin/NexusPublishPlugin.kt @@ -30,7 +30,6 @@ import org.gradle.api.publish.plugins.PublishingPlugin import org.gradle.api.tasks.TaskProvider import org.gradle.kotlin.dsl.apply import org.gradle.kotlin.dsl.create -import org.gradle.kotlin.dsl.named import org.gradle.kotlin.dsl.register import org.gradle.kotlin.dsl.the import org.gradle.kotlin.dsl.withType @@ -56,25 +55,35 @@ class NexusPublishPlugin : Plugin { val extension = project.extensions.create(NexusPublishExtension.NAME, project) extension.repositories.all { - project.tasks.register("publishTo${name.capitalize()}") { + project.tasks.register("publishTo${capitalizedName()}") { description = "Publishes all Maven publications produced by this project to the '${this@all.name}' Nexus repository." group = PublishingPlugin.PUBLISH_TASK_GROUP } project.tasks - .register("initialize${name.capitalize()}StagingRepository", project.objects, extension, this, serverUrlToStagingRepoUrl) + .register("initialize${capitalizedName()}StagingRepository", project.objects, extension, this, serverUrlToStagingRepoUrl) + project.tasks + .register("close${capitalizedName()}StagingRepository", project.objects, extension, this) + project.tasks + .register("release${capitalizedName()}StagingRepository", project.objects, extension, this) } extension.repositories.whenObjectRemoved { - project.tasks.remove(project.tasks.named("publishTo${name.capitalize()}") as Any) - project.tasks.remove(project.tasks.named("initialize${name.capitalize()}StagingRepository") as Any) + project.tasks.remove(project.tasks.named("publishTo${capitalizedName()}") as Any) + project.tasks.remove(project.tasks.named("initialize${capitalizedName()}StagingRepository") as Any) + project.tasks.remove(project.tasks.named("close${capitalizedName()}StagingRepository") as Any) + project.tasks.remove(project.tasks.named("release${capitalizedName()}StagingRepository") as Any) } project.afterEvaluate { val nexusRepositories = addMavenRepositories(project, extension) nexusRepositories.forEach { (nexusRepo, mavenRepo) -> - val publishToNexusTask = project.tasks.named("publishTo${nexusRepo.name.capitalize()}") + val publishToNexusTask = project.tasks.named("publishTo${nexusRepo.capitalizedName()}") val initializeTask = project.tasks.withType(InitializeNexusStagingRepository::class) - .named("initialize${nexusRepo.name.capitalize()}StagingRepository") - configureTaskDependencies(project, publishToNexusTask, initializeTask, mavenRepo) + .named("initialize${nexusRepo.capitalizedName()}StagingRepository") + val closeTask = project.tasks.withType(CloseNexusStagingRepository::class) + .named("close${nexusRepo.capitalizedName()}StagingRepository") + val releaseTask = project.tasks.withType(ReleaseNexusStagingRepository::class) + .named("release${nexusRepo.capitalizedName()}StagingRepository") + configureTaskDependencies(project, publishToNexusTask, initializeTask, closeTask, releaseTask, mavenRepo) } } @@ -109,7 +118,14 @@ class NexusPublishPlugin : Plugin { } } - private fun configureTaskDependencies(project: Project, publishToNexusTask: TaskProvider, initializeTask: TaskProvider, nexusRepository: MavenArtifactRepository) { + private fun configureTaskDependencies( + project: Project, + publishToNexusTask: TaskProvider, + initializeTask: TaskProvider, + closeTask: TaskProvider, + releaseTask: TaskProvider, + nexusRepository: MavenArtifactRepository + ) { val publishTasks = project.tasks .withType() .matching { it.repository == nexusRepository } @@ -121,6 +137,15 @@ class NexusPublishPlugin : Plugin { doFirst { logger.info("Uploading to {}", repository.url) } } } + closeTask.configure { + mustRunAfter(initializeTask) + mustRunAfter(publishTasks) + } + releaseTask.configure { + mustRunAfter(initializeTask) + mustRunAfter(closeTask) + mustRunAfter(publishTasks) + } } private fun getRepoUrl(nexusRepo: NexusRepository, extension: NexusPublishExtension): URI { diff --git a/src/main/kotlin/io/github/gradlenexus/publishplugin/NexusRepository.kt b/src/main/kotlin/io/github/gradlenexus/publishplugin/NexusRepository.kt index 0562fcc8..b7b37696 100644 --- a/src/main/kotlin/io/github/gradlenexus/publishplugin/NexusRepository.kt +++ b/src/main/kotlin/io/github/gradlenexus/publishplugin/NexusRepository.kt @@ -38,4 +38,10 @@ open class NexusRepository @Inject constructor(val name: String, project: Projec } val stagingProfileId: Property = project.objects.property() + + val stagingRepositoryId: Property = project.objects.property() + + fun capitalizedName(): String { + return name.capitalize() + } } diff --git a/src/main/kotlin/io/github/gradlenexus/publishplugin/ReleaseNexusStagingRepository.kt b/src/main/kotlin/io/github/gradlenexus/publishplugin/ReleaseNexusStagingRepository.kt new file mode 100644 index 00000000..3bf4cca4 --- /dev/null +++ b/src/main/kotlin/io/github/gradlenexus/publishplugin/ReleaseNexusStagingRepository.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.gradlenexus.publishplugin + +import io.github.gradlenexus.publishplugin.internal.NexusClient +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.TaskAction +import org.gradle.kotlin.dsl.property +import javax.inject.Inject + +@Suppress("UnstableApiUsage") +open class ReleaseNexusStagingRepository @Inject +constructor(objects: ObjectFactory, extension: NexusPublishExtension, repository: NexusRepository) : + BaseOperationOnNexusStagingRepository(objects, extension, repository) { + + @get:Input + val stagingRepositoryId: Property = objects.property() + + init { + stagingRepositoryId.set(repository.stagingRepositoryId) + } + + @TaskAction + fun releaseStagingRepo() { + val client = NexusClient(serverUrl.get(), username.orNull, password.orNull, clientTimeout.orNull, connectTimeout.orNull) + val stagingProfileId = determineStagingProfileId(client) // TODO: Will it update value in extension? + logger.info("Releasing staging repository with id '{}' for stagingProfileId '{}'", stagingRepositoryId.get(), stagingProfileId) + client.releaseStagingRepository(stagingRepositoryId.get()) + // TODO: Broken with real Nexus - waiting for effective execution is also required https://github.com/gradle-nexus/publish-plugin/issues/7 + } +} diff --git a/src/main/kotlin/io/github/gradlenexus/publishplugin/internal/NexusClient.kt b/src/main/kotlin/io/github/gradlenexus/publishplugin/internal/NexusClient.kt index e698ebe8..1ca5184d 100644 --- a/src/main/kotlin/io/github/gradlenexus/publishplugin/internal/NexusClient.kt +++ b/src/main/kotlin/io/github/gradlenexus/publishplugin/internal/NexusClient.kt @@ -97,6 +97,28 @@ class NexusClient(private val baseUrl: URI, username: String?, password: String? } } + fun closeStagingRepository(stagingRepositoryId: String) { + try { + val response = api.closeStagingRepo(Dto(StagingRepositoryToTransit(listOf(stagingRepositoryId), "Closed by io.github.gradle-nexus.publish-plugin Gradle plugin"))).execute() + if (!response.isSuccessful) { + throw failure("close staging repository", response) + } + } catch (e: IOException) { + throw UncheckedIOException(e) + } + } + + fun releaseStagingRepository(stagingRepositoryId: String) { + try { + val response = api.releaseStagingRepo(Dto(StagingRepositoryToTransit(listOf(stagingRepositoryId), "Release by io.github.gradle-nexus.publish-plugin Gradle plugin"))).execute() + if (!response.isSuccessful) { + throw failure("release staging repository", response) + } + } catch (e: IOException) { + throw UncheckedIOException(e) + } + } + fun getStagingRepositoryUri(stagingRepositoryId: String): URI = URI.create("${baseUrl.toString().removeSuffix("/")}/staging/deployByRepositoryId/$stagingRepositoryId") @@ -115,6 +137,10 @@ class NexusClient(private val baseUrl: URI, username: String?, password: String? private interface NexusApi { + companion object { + private const val RELEASE_OPERATION_NAME_IN_NEXUS = "promote" // promote and release use the same operation, used body parameters matter + } + @get:Headers("Accept: application/json") @get:GET("staging/profiles") val stagingProfiles: Call>> @@ -122,6 +148,14 @@ class NexusClient(private val baseUrl: URI, username: String?, password: String? @Headers("Content-Type: application/json") @POST("staging/profiles/{stagingProfileId}/start") fun startStagingRepo(@Path("stagingProfileId") stagingProfileId: String, @Body description: Dto): Call> + + @Headers("Content-Type: application/json") + @POST("staging/bulk/close") + fun closeStagingRepo(@Body stagingRepositoryToClose: Dto): Call + + @Headers("Content-Type: application/json") + @POST("staging/bulk/$RELEASE_OPERATION_NAME_IN_NEXUS") + fun releaseStagingRepo(@Body stagingRepositoryToClose: Dto): Call } data class Dto(var data: T) @@ -131,4 +165,6 @@ class NexusClient(private val baseUrl: URI, username: String?, password: String? data class Description(val description: String) data class StagingRepository(var stagedRepositoryId: String) + + data class StagingRepositoryToTransit(val stagedRepositoryIds: List, val description: String, val autoDropAfterRelease: Boolean = true) } diff --git a/src/test/kotlin/io/github/gradlenexus/publishplugin/StagingPluginIntegrationTest.kt b/src/test/kotlin/io/github/gradlenexus/publishplugin/StagingPluginIntegrationTest.kt index f52eb4fc..3fead2cb 100644 --- a/src/test/kotlin/io/github/gradlenexus/publishplugin/StagingPluginIntegrationTest.kt +++ b/src/test/kotlin/io/github/gradlenexus/publishplugin/StagingPluginIntegrationTest.kt @@ -37,6 +37,7 @@ import ru.lanwen.wiremock.ext.WiremockResolver import ru.lanwen.wiremock.ext.WiremockResolver.Wiremock import java.net.URI +@Suppress("UnstableApiUsage") @ExtendWith(WiremockResolver::class) class StagingPluginIntegrationTest { diff --git a/src/test/kotlin/io/github/gradlenexus/publishplugin/TaskOrchestrationTest.kt b/src/test/kotlin/io/github/gradlenexus/publishplugin/TaskOrchestrationTest.kt new file mode 100644 index 00000000..fedf0df6 --- /dev/null +++ b/src/test/kotlin/io/github/gradlenexus/publishplugin/TaskOrchestrationTest.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.gradlenexus.publishplugin + +import org.assertj.core.api.Assertions.assertThat +import org.gradle.api.Action +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.publish.PublishingExtension +import org.gradle.api.publish.maven.MavenPublication +import org.gradle.testfixtures.ProjectBuilder +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import java.nio.file.Path + +class TaskOrchestrationTest { + + @TempDir + lateinit var projectDir: Path + + private lateinit var project: Project + + @BeforeEach + internal fun setUp() { + project = ProjectBuilder.builder().withProjectDir(projectDir.toFile()).build() + } + + @Test + internal fun `close task should run after init`() { + initSingleProjectWithDefaultConfiguration() + assertGivenTaskMustRunAfterAnother("closeSonatypeStagingRepository", "initializeSonatypeStagingRepository") + } + + @Test + @Disabled("Broken - must run only after publishMavenJavaPublicationToSonatypeRepository not publishToSonatypeRepository") + internal fun `close task should run after related publish`() { + initSingleProjectWithDefaultConfiguration() + assertGivenTaskMustRunAfterAnother("closeSonatypeStagingRepository", "publishToSonatypeRepository") + } + + @Test + @Disabled("TODO") + internal fun `close task should not run after non-related publish`() {} + + @Test + @Disabled("TODO") + internal fun `close task should run after all related publish tasks in multi-project build`() {} + + @Test + internal fun `release task should run after init`() { + initSingleProjectWithDefaultConfiguration() + assertGivenTaskMustRunAfterAnother("releaseSonatypeStagingRepository", "initializeSonatypeStagingRepository") + } + + @Test + @Disabled("Broken - must run only after publishMavenJavaPublicationToSonatypeRepository not publishToSonatypeRepository") + internal fun `release task should run after related publish`() { + initSingleProjectWithDefaultConfiguration() + assertGivenTaskMustRunAfterAnother("releaseSonatypeStagingRepository", "publishToSonatypeRepository") + } + + @Test + internal fun `release task should run after close`() { + initSingleProjectWithDefaultConfiguration() + assertGivenTaskMustRunAfterAnother("releaseSonatypeStagingRepository", "closeSonatypeStagingRepository") + } + + private fun initSingleProjectWithDefaultConfiguration() { + project.apply(mapOf("plugin" to NexusPublishPlugin::class.java)) + val extension = project.extensions.findByType(NexusPublishExtension::class.java) + extension?.repositories(Action { + sonatype() + }) + val publishingExtension = project.extensions.findByType(PublishingExtension::class.java) + publishingExtension?.publications(Action { + create("mavenJava", MavenPublication::class.java) { +// from(components.java) //fails to compile + } + }) + } + + private fun assertGivenTaskMustRunAfterAnother(taskName: String, expectedPredecessorName: String) { + val task = getJustOneTaskByNameOrFail(taskName) + val expectedPredecessor = getJustOneTaskByNameOrFail(expectedPredecessorName) + assertThat(task.mustRunAfter.getDependencies(task)).contains(expectedPredecessor) + } + + private fun getJustOneTaskByNameOrFail(taskName: String): Task { + val tasks = project.getTasksByName(taskName, true) // forces project evaluation" + assertThat(tasks.size).describedAs("Expected just one task: $taskName. Found: ${project.tasks}").isOne() + return tasks.first() + } +}