Skip to content

Commit

Permalink
Merge pull request #62 from ryandens/getStagingProfileTask
Browse files Browse the repository at this point in the history
✨ Get staging profile task
  • Loading branch information
szpak authored Mar 8, 2021
2 parents 364fdf6 + df6cd66 commit f3c24a7
Show file tree
Hide file tree
Showing 5 changed files with 250 additions and 13 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ You can configure the `connectTimeout` and `clientTimeout` properties on the `ne
The plugin does the following:

- configure a Maven artifact repository for each repository defined in the `nexusPublishing { repositories { ... } }` block in each subproject that applies the `maven-publish` plugin
- creates a `retrieve{repository.name.capitalize()}StagingProfile` task that retrieves the staging profile id from the remote Nexus repository. This is a diagnostic task to enable setting the configuration property `stagingProfileId` in `nexusPublishing { repositories { myRepository { ... } } }`. Specifying the configuration property rather than relying on the API call is considered a performance optimization.
- create a `initialize${repository.name.capitalize()}StagingRepository` task that starts a new staging repository in case the project's version does not end with `-SNAPSHOT` (customizable via the `useStaging` property) and sets the URL of the corresponding Maven artifact repository accordingly. In case of a multi-project build, all subprojects with the same `nexusUrl` will use the same staging repository.
- make all publishing tasks for each configured repository depend on the `initialize${repository.name.capitalize()}StagingRepository` task
- create a `publishTo${repository.name.capitalize()}` lifecycle task that depends on all publishing tasks for the corresponding Maven artifact repository
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright 2021 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 com.github.tomakehurst.wiremock.WireMockServer
import org.junit.jupiter.api.extension.AfterEachCallback
import org.junit.jupiter.api.extension.ExtensionContext
import org.junit.jupiter.api.extension.ParameterContext
import org.junit.jupiter.api.extension.ParameterResolver
import ru.lanwen.wiremock.ext.WiremockResolver
import java.lang.IllegalArgumentException

/**
* Composes a [WiremockResolver] and uses that by default. But, if a parameter is annotated
* with [MethodScopeWiremockResolver] it creates a new instance of the [WiremockResolver] extension and
* manages its lifecycle in the scope of that [ExtensionContext]
*/
class MethodScopeWiremockResolver(
private val inner: WiremockResolver = WiremockResolver()
) : ParameterResolver by inner, AfterEachCallback {

/**
* Checks to see if a method the parameter is annotated with [MethodScopedWiremockServer]. If it is, we first verify
*/
override fun resolveParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Any {
return if (parameterContext.isAnnotated(MethodScopedWiremockServer::class.java)) {
if (!parameterContext.parameter.type.isAssignableFrom(WireMockServer::class.java)) {
throw IllegalArgumentException("Annotated type must be a WireMockServer")
}
return getStore(extensionContext)
.getOrComputeIfAbsent(Keys.LOCAL_RESOLVER, { WiremockResolver() }, WiremockResolver::class.java)
.resolveParameter(parameterContext, extensionContext)
} else {
inner.resolveParameter(parameterContext, extensionContext)
}
}

override fun afterEach(context: ExtensionContext) {
getStore(context).get(Keys.LOCAL_RESOLVER, WiremockResolver::class.java)?.afterEach(context)
inner.afterEach(context)
}

/**
* helper method for get getting a [ExtensionContext.Store] specific to a test method
*/
private fun getStore(context: ExtensionContext): ExtensionContext.Store {
return context.getStore(ExtensionContext.Namespace.create(javaClass))
}

/**
* Keys for storing and accessing [MethodScopeWiremockResolver]s
*/
enum class Keys {
LOCAL_RESOLVER
}

/**
* Decorates a parameter also annotated with [WiremockResolver.Wiremock]
*/
@Target(allowedTargets = [AnnotationTarget.VALUE_PARAMETER])
@Retention(AnnotationRetention.RUNTIME)
annotation class MethodScopedWiremockServer
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,12 @@ 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
import ru.lanwen.wiremock.ext.WiremockResolver
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)
@ExtendWith(MethodScopeWiremockResolver::class)
class NexusPublishPluginTests {

companion object {
Expand Down Expand Up @@ -110,6 +109,32 @@ class NexusPublishPluginTests {
assertThat(result.output).contains("Plugin must be applied to the root project but was applied to :sub")
}

@Test
fun `can get StagingProfileId from Nexus`() {
writeDefaultSingleProjectConfiguration()
//and
buildGradle.append("""
nexusPublishing {
repositories {
sonatype {
nexusUrl = uri('${server.baseUrl()}')
allowInsecureProtocol = true
//No staging profile defined
}
}
}
""")
//and
stubGetStagingProfilesForOneProfileIdGivenId(STAGING_PROFILE_ID)

val result = run("retrieveSonatypeStagingProfile")

assertSuccess(result, ":retrieveSonatypeStagingProfile")
assertThat(result.output).containsOnlyOnce("Received staging profile id: '$STAGING_PROFILE_ID' for package org.example")
// and
assertGetStagingProfile(1)
}

@Test
fun `publish task depends on correct tasks`() {
projectDir.resolve("settings.gradle").write("""
Expand Down Expand Up @@ -206,6 +231,71 @@ class NexusPublishPluginTests {
assertUploadedToStagingRepo("/org/example/sample/0.0.1/sample-0.0.1.jar")
}

@Test
fun `publishes to two Nexus repositories`(@MethodScopeWiremockResolver.MethodScopedWiremockServer @Wiremock otherServer: WireMockServer) {
projectDir.resolve("settings.gradle").write("""
rootProject.name = 'sample'
""")
projectDir.resolve("build.gradle").write("""
plugins {
id('java-library')
id('maven-publish')
id('io.github.gradle-nexus.publish-plugin')
}
group = 'org.example'
version = '0.0.1'
publishing {
publications {
mavenJava(MavenPublication) {
from(components.java)
}
}
}
nexusPublishing {
repositories {
myNexus {
nexusUrl = uri('${server.baseUrl()}')
snapshotRepositoryUrl = uri('${server.baseUrl()}/snapshots/')
allowInsecureProtocol = true
username = 'username'
password = 'password'
}
someOtherNexus {
nexusUrl = uri('${otherServer.baseUrl()}')
snapshotRepositoryUrl = uri('${otherServer.baseUrl()}/snapshots/')
allowInsecureProtocol = true
username = 'someUsername'
password = 'somePassword'
}
}
}
""")

val otherStagingProfileId = "otherStagingProfileId"
val otherStagingRepositoryId = "orgexample-43"
stubStagingProfileRequest("/staging/profiles", mapOf("id" to STAGING_PROFILE_ID, "name" to "org.example"))
stubStagingProfileRequest("/staging/profiles", mapOf("id" to otherStagingProfileId, "name" to "org.example"), wireMockServer = otherServer)
stubCreateStagingRepoRequest("/staging/profiles/$STAGING_PROFILE_ID/start", STAGED_REPOSITORY_ID)
stubCreateStagingRepoRequest("/staging/profiles/$otherStagingProfileId/start", otherStagingRepositoryId, wireMockServer = otherServer)
expectArtifactUploads("/staging/deployByRepositoryId/$STAGED_REPOSITORY_ID")
expectArtifactUploads("/staging/deployByRepositoryId/$otherStagingRepositoryId", wireMockServer = otherServer)

val result = run("publishToMyNexus", "publishToSomeOtherNexus")

assertSuccess(result, ":initializeMyNexusStagingRepository")
assertSuccess(result, ":initializeSomeOtherNexusStagingRepository")
assertThat(result.output).containsOnlyOnce("Created staging repository '$STAGED_REPOSITORY_ID' at ${server.baseUrl()}/repositories/$STAGED_REPOSITORY_ID/content/")
assertThat(result.output).containsOnlyOnce("Created staging repository '$otherStagingRepositoryId' at ${otherServer.baseUrl()}/repositories/$otherStagingRepositoryId/content/")
server.verify(postRequestedFor(urlEqualTo("/staging/profiles/$STAGING_PROFILE_ID/start"))
.withRequestBody(matchingJsonPath("\$.data[?(@.description == 'org.example:sample:0.0.1')]")))
otherServer.verify(postRequestedFor(urlEqualTo("/staging/profiles/$otherStagingProfileId/start"))
.withRequestBody(matchingJsonPath("\$.data[?(@.description == 'org.example:sample:0.0.1')]")))
assertUploadedToStagingRepo("/org/example/sample/0.0.1/sample-0.0.1.pom")
assertUploadedToStagingRepo("/org/example/sample/0.0.1/sample-0.0.1.jar")
assertUploadedToStagingRepo("/org/example/sample/0.0.1/sample-0.0.1.pom", stagingRepositoryId = otherStagingRepositoryId, wireMockServer = otherServer)
assertUploadedToStagingRepo("/org/example/sample/0.0.1/sample-0.0.1.jar", stagingRepositoryId = otherStagingRepositoryId, wireMockServer = otherServer)
}

@Test
fun `can be used with lazily applied Gradle Plugin Development Plugin`() {
projectDir.resolve("settings.gradle").write("""
Expand Down Expand Up @@ -352,6 +442,7 @@ class NexusPublishPluginTests {

val result = gradleRunner("initializeMyNexusStagingRepository").buildAndFail()

// we assert that the first task that sends an HTTP request to server fails as expected
assertOutcome(result, ":initializeMyNexusStagingRepository", FAILED)
assertThat(result.output).contains("SocketTimeoutException")
}
Expand Down Expand Up @@ -732,14 +823,18 @@ class NexusPublishPluginTests {
}

@SafeVarargs
private fun stubStagingProfileRequest(url: String, vararg stagingProfiles: Map<String, String>) {
server.stubFor(get(urlEqualTo(url))
private fun stubStagingProfileRequest(
url: String,
vararg stagingProfiles: Map<String, String>,
wireMockServer: WireMockServer = server
) {
wireMockServer.stubFor(get(urlEqualTo(url))
.withHeader("User-Agent", matching("gradle-nexus-publish-plugin/.*"))
.willReturn(aResponse().withBody(gson.toJson(mapOf("data" to listOf(*stagingProfiles))))))
}

private fun stubCreateStagingRepoRequest(url: String, stagedRepositoryId: String) {
server.stubFor(post(urlEqualTo(url))
private fun stubCreateStagingRepoRequest(url: String, stagedRepositoryId: String, wireMockServer: WireMockServer = server) {
wireMockServer.stubFor(post(urlEqualTo(url))
.willReturn(aResponse().withBody(gson.toJson(mapOf("data" to mapOf("stagedRepositoryId" to stagedRepositoryId))))))
}

Expand Down Expand Up @@ -797,10 +892,10 @@ class NexusPublishPluginTests {
.withBody(getOneStagingRepoWithGivenIdJsonResponseAsString(stagingRepository2))))
}

private fun expectArtifactUploads(prefix: String) {
server.stubFor(put(urlMatching("$prefix/.+"))
private fun expectArtifactUploads(prefix: String, wireMockServer: WireMockServer = server) {
wireMockServer.stubFor(put(urlMatching("$prefix/.+"))
.willReturn(aResponse().withStatus(201)))
server.stubFor(get(urlMatching("$prefix/.+/maven-metadata.xml"))
wireMockServer.stubFor(get(urlMatching("$prefix/.+/maven-metadata.xml"))
.willReturn(aResponse().withStatus(404)))
}

Expand All @@ -827,12 +922,12 @@ class NexusPublishPluginTests {
server.verify(count, getRequestedFor(urlMatching("/staging/profiles")))
}

private fun assertUploadedToStagingRepo(path: String) {
assertUploaded("/staging/deployByRepositoryId/$STAGED_REPOSITORY_ID$path")
private fun assertUploadedToStagingRepo(path: String, stagingRepositoryId: String = STAGED_REPOSITORY_ID, wireMockServer: WireMockServer = server) {
assertUploaded("/staging/deployByRepositoryId/$stagingRepositoryId$path", wireMockServer = wireMockServer)
}

private fun assertUploaded(testUrl: String) {
server.verify(putRequestedFor(urlMatching(testUrl)))
private fun assertUploaded(testUrl: String, wireMockServer: WireMockServer = server) {
wireMockServer.verify(putRequestedFor(urlMatching(testUrl)))
}

private fun assertCloseOfStagingRepo(stagingRepositoryId: String = STAGED_REPOSITORY_ID) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ class NexusPublishPlugin : Plugin<Project> {
private fun configureNexusTasks(rootProject: Project, extension: NexusPublishExtension, registry: Provider<StagingRepositoryDescriptorRegistry>) {
extension.repositories.all {
val repository = this
val retrieveStagingProfileTask = rootProject.tasks.register<RetrieveStagingProfile>("retrieve${capitalizedName}StagingProfile", rootProject.objects, extension, repository)
val initializeTask = rootProject.tasks.register<InitializeNexusStagingRepository>(
"initialize${capitalizedName}StagingRepository", rootProject.objects, extension, repository, registry)
val closeTask = rootProject.tasks.register<CloseNexusStagingRepository>(
Expand All @@ -76,6 +77,9 @@ class NexusPublishPlugin : Plugin<Project> {
"release${capitalizedName}StagingRepository", rootProject.objects, extension, repository, registry)
val closeAndReleaseTask = rootProject.tasks.register<Task>(
"closeAndRelease${capitalizedName}StagingRepository")
retrieveStagingProfileTask {
description = "Gets and displays a staging profile id for a given repository and package group. This is a diagnostic task to get the value and put it into the NexusRepository configuration closure as stagingProfileId."
}
closeTask {
description = "Closes open staging repository in '${repository.name}' Nexus instance."
group = PublishingPlugin.PUBLISH_TASK_GROUP
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright 2021 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.GradleException
import org.gradle.api.Incubating
import org.gradle.api.model.ObjectFactory
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction
import org.gradle.kotlin.dsl.property
import javax.inject.Inject

/**
* Diagnostic task for retrieving the [NexusRepository.stagingProfileId] for the [packageGroup] from the provided [NexusRepository] and logging it
*/
@Incubating
open class RetrieveStagingProfile @Inject constructor(
objects: ObjectFactory,
extension: NexusPublishExtension,
repository: NexusRepository
) : AbstractNexusStagingRepositoryTask(objects, extension, repository) {

@Input
val packageGroup = objects.property<String>().apply {
set(extension.packageGroup)
}

@TaskAction
fun retrieveStagingProfile() {
val repository = repository.get()

val client = NexusClient(
repository.nexusUrl.get(),
repository.username.orNull,
repository.password.orNull,
clientTimeout.orNull,
connectTimeout.orNull
)

val packageGroup = packageGroup.get()
val stagingProfileId = client.findStagingProfileId(packageGroup)
?: throw GradleException("Failed to find staging profile for package group: $packageGroup")
logger.lifecycle("Received staging profile id: '{}' for package {}", stagingProfileId, packageGroup)
}
}

0 comments on commit f3c24a7

Please sign in to comment.