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

Add FindStagingRepository task to find staging repository by its description #145

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 31 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,37 @@ publishing {

Finally, call `publishToSonatype closeAndReleaseSonatypeStagingRepository` to publish all publications to Sonatype's OSSRH Nexus and subsequently close and release the corresponding staging repository, effectively making the artifacts available in Maven Central (usually after a few minutes).

Note that until [#19](https://github.com/gradle-nexus/publish-plugin/issues/19) is done, the `publishToSonatype closeAndReleaseSonatypeStagingRepository` tasks have to be executed in the same Gradle invocation because `closeAndRelease` relies on information that is not persisted between calls to Gradle. Failing to do so will result in an error like `No staging repository with name sonatype created`.

Please bear in mind that - especially on the initial project publishing to Maven Central - it might be wise to call just `publishToSonatype closeSonatypeStagingRepository` and manually verify that the artifacts placed in the closed staging repository in Nexus looks ok. After that, the staging repository might be dropped (if needed) or manually released from the Nexus UI.

#### Publishing and closing in different Gradle invocations

You might want to publish and close in different Gradle invocations. For example, you might want to publish from CI
and close and release from your local machine.
An alternative use case is to publish and close the repository and let others review and preview the publication before
the release.

The use case should work automatically most of the time since `InitializeNexusStagingRepository`
attempts to find an existing staging repository before creating a new one.

However, when running `closeSonatypeStagingRepository`, it is expected that the task fails
if no matching repository is missing, so `closeSonatypeStagingRepository` depends on
`find${repository.name.capitalize()}StagingRepository` task to find the repository or fail.

The description can be customized via:
* `io.github.gradlenexus.publishplugin.NexusPublishExtension.getRepositoryDescription` property (default: `$group:$module:$version` of the root project)
* `io.github.gradlenexus.publishplugin.InitializeNexusStagingRepository.repositoryDescription` property

The search regular expression can be customized with
* `io.github.gradlenexus.publishplugin.InitializeNexusStagingRepository.repositorySearchRegex` property (regex, default: `"\\b" + Regex.escape(repositoryDescription) + "(\\s|$)"`)

So the steps to publish and release in different Gradle invocations are:
1. Publish the artifacts to the staging repository: `./gradlew publishToSonatype`
2. Close the staging repository: `./gradlew closeSonatypeStagingRepository`
3. Release the staging repository: `./gradlew releaseSonatypeStagingRepository`

Note: `closeSonatypeStagingRepository` and `releaseSonatypeStagingRepository` will automatically find
the staging repository id unless, however, you can optionally override it with `--staging-repository-id repo-42` argument.

### Full example

#### Groovy DSL
Expand Down Expand Up @@ -290,7 +317,8 @@ 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.
- create a `initialize${repository.name.capitalize()}StagingRepository` task that reuses an existing or creates a new 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.
- create a `find${repository.name.capitalize()}StagingRepository` task that searches an existing staging repository (e.g. in case `close...` or `release...` tasks executed in an isolated Gradle invocation).
- 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
- create `close${repository.name.capitalize()}StagingRepository` and `release${repository.name.capitalize()}StagingRepository` tasks that must run after the all publishing tasks
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ class NexusPublishPluginTests {
private const val STAGING_PROFILE_ID = "someProfileId"
private const val STAGED_REPOSITORY_ID = "orgexample-42"
private const val OVERRIDDEN_STAGED_REPOSITORY_ID = "orgexample-42o"
private val DUMMY_REPOSITORY = StagingRepository(
id = "orgexample-42-dummy",
state = StagingRepository.State.OPEN,
transitioning = false,
description = "dummy"
)
}

private enum class StagingRepoTransitionOperation(val urlSufix: String, val desiredState: StagingRepository.State) {
Expand Down Expand Up @@ -231,6 +237,7 @@ class NexusPublishPluginTests {
)

stubStagingProfileRequest("/staging/profiles", mapOf("id" to STAGING_PROFILE_ID, "name" to "org.example"))
stubGetStagingReposForStagingProfileWithIdAndReturnOne(DUMMY_REPOSITORY)
stubCreateStagingRepoRequest("/staging/profiles/$STAGING_PROFILE_ID/start", STAGED_REPOSITORY_ID)
expectArtifactUploads("/staging/deployByRepositoryId/$STAGED_REPOSITORY_ID")

Expand Down Expand Up @@ -344,6 +351,8 @@ class NexusPublishPluginTests {
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)
stubGetStagingReposForStagingProfileWithIdAndReturnOne(DUMMY_REPOSITORY)
stubGetStagingReposForStagingProfileWithIdAndReturnOne(DUMMY_REPOSITORY, stagingProfileId = otherStagingProfileId, 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")
Expand Down Expand Up @@ -433,6 +442,7 @@ class NexusPublishPluginTests {
"""
)

stubGetStagingReposForStagingProfileWithIdAndReturnOne(DUMMY_REPOSITORY)
stubCreateStagingRepoRequest("/staging/profiles/$STAGING_PROFILE_ID/start", STAGED_REPOSITORY_ID)
expectArtifactUploads("/staging/deployByRepositoryId/$STAGED_REPOSITORY_ID")

Expand Down Expand Up @@ -662,20 +672,34 @@ class NexusPublishPluginTests {
}

@Test
fun `should close staging repository`() {
fun `should close existing staging repository`() {
writeDefaultSingleProjectConfiguration()
writeMockedSonatypeNexusPublishingConfiguration()

stubCreateStagingRepoRequest("/staging/profiles/$STAGING_PROFILE_ID/start", STAGED_REPOSITORY_ID)
stubGetStagingReposForStagingProfileWithIdAndReturnOne()

stubCloseStagingRepoRequestWithSubsequentQueryAboutItsState(STAGED_REPOSITORY_ID)

val result = run("initializeSonatypeStagingRepository", "closeSonatypeStagingRepository")
val result = run("closeSonatypeStagingRepository")

assertSuccess(result, ":initializeSonatypeStagingRepository")
assertSuccess(result, ":findSonatypeStagingRepository")
assertSuccess(result, ":closeSonatypeStagingRepository")
assertCloseOfStagingRepo()
}

@Test
fun `should fail closing staging repository if missing`() {
writeDefaultSingleProjectConfiguration()
writeMockedSonatypeNexusPublishingConfiguration()

stubGetStagingReposForStagingProfileWithIdAndReturnOne(DUMMY_REPOSITORY)

val result = runAndFail("closeSonatypeStagingRepository")

assertThat(result.output).contains("No staging repositories found for stagingProfileId: someProfileId, descriptionRegex: \\b\\Qorg.example:sample:0.0.1\\E(\\s|\$). Here are all the repositories: [ReadStagingRepository(repositoryId=orgexample-42-dummy, type=open, transitioning=false, description=dummy)]")
assertFailure(result, ":findSonatypeStagingRepository")
}

private fun stubCloseStagingRepoRequestWithSubsequentQueryAboutItsState(stagingRepositoryId: String = STAGED_REPOSITORY_ID) {
stubTransitToDesiredStateStagingRepoRequestWithSubsequentQueryAboutItsState(StagingRepoTransitionOperation.CLOSE, stagingRepositoryId)
}
Expand Down Expand Up @@ -715,12 +739,12 @@ class NexusPublishPluginTests {
writeDefaultSingleProjectConfiguration()
writeMockedSonatypeNexusPublishingConfiguration()

stubCreateStagingRepoRequest("/staging/profiles/$STAGING_PROFILE_ID/start", STAGED_REPOSITORY_ID)
stubGetStagingReposForStagingProfileWithIdAndReturnOne()
stubReleaseStagingRepoRequestWithSubsequentQueryAboutItsState(STAGED_REPOSITORY_ID)

val result = run("tasks", "initializeSonatypeStagingRepository", "releaseSonatypeStagingRepository")
val result = run("releaseSonatypeStagingRepository")

assertSuccess(result, ":initializeSonatypeStagingRepository")
assertSuccess(result, ":findSonatypeStagingRepository")
assertSuccess(result, ":releaseSonatypeStagingRepository")
assertReleaseOfStagingRepo()
}
Expand Down Expand Up @@ -782,6 +806,7 @@ class NexusPublishPluginTests {
)
// and
stubGetStagingProfilesForOneProfileIdGivenId(STAGING_PROFILE_ID)
stubGetStagingReposForStagingProfileWithIdAndReturnOne(DUMMY_REPOSITORY)
stubCreateStagingRepoRequest("/staging/profiles/$STAGING_PROFILE_ID/start", STAGED_REPOSITORY_ID)
stubCloseStagingRepoRequestWithSubsequentQueryAboutItsState()

Expand Down Expand Up @@ -863,6 +888,7 @@ class NexusPublishPluginTests {
"""
)

stubGetStagingReposForStagingProfileWithIdAndReturnOne(DUMMY_REPOSITORY)
stubStagingProfileRequest("/staging/profiles", mapOf("id" to STAGING_PROFILE_ID, "name" to "org.example"))
stubCreateStagingRepoRequest("/staging/profiles/$STAGING_PROFILE_ID/start", STAGED_REPOSITORY_ID)
expectArtifactUploads("/staging/deployByRepositoryId/$STAGED_REPOSITORY_ID")
Expand All @@ -879,16 +905,111 @@ class NexusPublishPluginTests {
.withRequestBody(matchingJsonPath("\$.data[?(@.description == 'Some custom description')]"))
)

server.resetAll()

// Assume the staged repo has been created by "publishToSonatype" task above
stubGetStagingReposForStagingProfileWithIdAndReturnOne(repositoryDescription = "Some custom description")
stubReleaseStagingRepoRequestWithSubsequentQueryAboutItsState(STAGED_REPOSITORY_ID)

run("releaseSonatypeStagingRepository", "--staging-repository-id=$STAGED_REPOSITORY_ID")
run("releaseSonatypeStagingRepository")

server.verify(
postRequestedFor(urlEqualTo("/staging/bulk/promote"))
.withRequestBody(matchingJsonPath("\$.data[?(@.description == 'Some custom description')]"))
)
}

@Test
fun `should find staging repository by description`() {
// given
writeDefaultSingleProjectConfiguration()
writeMockedSonatypeNexusPublishingConfiguration()
// and
stubGetStagingReposForStagingProfileWithIdAndReturnOne()

val result = run("findSonatypeStagingRepository")
vlsi marked this conversation as resolved.
Show resolved Hide resolved

assertSuccess(result, ":findSonatypeStagingRepository")
assertThat(result.output).containsPattern(Regex("Staging repository for .* '$STAGED_REPOSITORY_ID'").toPattern())
// and
assertGetStagingRepositoriesForStatingProfile(STAGING_PROFILE_ID)
}

@Test
fun `should not find staging repository by wrong description`() {
vlsi marked this conversation as resolved.
Show resolved Hide resolved
// given
writeDefaultSingleProjectConfiguration()
writeMockedSonatypeNexusPublishingConfiguration()
// and
stubGetStagingReposForStagingProfileWithIdAndReturnOne(DUMMY_REPOSITORY)

val result = runAndFail("findSonatypeStagingRepository")

assertThat(result.output).contains("No staging repositories found for stagingProfileId: someProfileId, descriptionRegex: \\b\\Qorg.example:sample:0.0.1\\E(\\s|\$). Here are all the repositories: [ReadStagingRepository(repositoryId=orgexample-42-dummy, type=open, transitioning=false, description=dummy)]")
assertFailure(result, ":findSonatypeStagingRepository")
}

private fun stubGetStagingReposForStagingProfileWithIdAndReturnOne(
stagingRepository: StagingRepository,
stagingProfileId: String = STAGING_PROFILE_ID,
wireMockServer: WireMockServer = server
) = stubGetStagingReposForStagingProfileWithIdAndReturnOne(
stagingRepository.id,
stagingRepository.description,
stagingProfileId,
wireMockServer
)

private fun stubGetStagingReposForStagingProfileWithIdAndReturnOne(
stagingRepositoryId: String = STAGED_REPOSITORY_ID,
repositoryDescription: String = StagingRepository.DEFAULT_DESCRIPTION,
stagingProfileId: String = STAGING_PROFILE_ID,
wireMockServer: WireMockServer = server
) {
val stagingRepository = StagingRepository(
id = stagingRepositoryId,
state = StagingRepository.State.OPEN,
transitioning = false,
description = repositoryDescription
)
val responseBody = getStagingReposWithOneStagingRepoWithGivenIdJsonResponseAsString(stagingRepository, stagingProfileId)
stubGetStagingReposForStagingProfileIdWithResponseStatusCodeAndResponseBody(
stagingProfileId,
200,
responseBody,
wireMockServer = wireMockServer
)
}

@Test
fun `should fail when multiple repositories exist`() {
// given
writeDefaultSingleProjectConfiguration()
writeMockedSonatypeNexusPublishingConfiguration()
// and
val stagingRepository = StagingRepository(STAGED_REPOSITORY_ID, StagingRepository.State.OPEN, false)
val stagingRepository2 = StagingRepository(OVERRIDDEN_STAGED_REPOSITORY_ID, StagingRepository.State.OPEN, false)
// Return two repositories with the same description, so the find call would get both, and it should fail
val responseBody = """
{
"data": [
${getOneStagingRepoWithGivenIdJsonResponseAsString(stagingRepository)},
${getOneStagingRepoWithGivenIdJsonResponseAsString(stagingRepository2)}
]
}
""".trimIndent()
stubGetStagingReposForStagingProfileIdWithResponseStatusCodeAndResponseBody(
STAGING_PROFILE_ID,
200,
responseBody
)

val result = runAndFail("findSonatypeStagingRepository")

assertFailure(result, ":findSonatypeStagingRepository")
assertThat(result.output).contains("Too many repositories found for stagingProfileId: someProfileId, descriptionRegex: \\b\\Qorg.example:sample:0.0.1\\E(\\s|\$). If some of the repositories are not needed, consider deleting them manually. Here are the repositories matching the regular expression: [ReadStagingRepository(repositoryId=orgexample-42, type=open, transitioning=false, description=org.example:sample:0.0.1), ReadStagingRepository(repositoryId=orgexample-42o, type=open, transitioning=false, description=org.example:sample:0.0.1)]")
}

// TODO: To be used also in other tests
private fun writeDefaultSingleProjectConfiguration() {
projectDir.resolve("settings.gradle").write(
Expand Down Expand Up @@ -1062,6 +1183,24 @@ class NexusPublishPluginTests {
)
}

private fun stubGetStagingReposForStagingProfileIdWithResponseStatusCodeAndResponseBody(
stagingProfileId: String,
statusCode: Int,
responseBody: String,
wireMockServer: WireMockServer = server
) {
wireMockServer.stubFor(
get(urlEqualTo("/staging/profile_repositories/$stagingProfileId"))
.withHeader("Accept", containing("application/json"))
.willReturn(
aResponse()
.withStatus(statusCode)
.withHeader("Content-Type", "application/json")
.withBody(responseBody)
)
)
}

private fun expectArtifactUploads(prefix: String, wireMockServer: WireMockServer = server) {
wireMockServer.stubFor(
put(urlMatching("$prefix/.+"))
Expand Down Expand Up @@ -1127,6 +1266,10 @@ class NexusPublishPluginTests {
server.verify(count, getRequestedFor(urlMatching("/staging/repository/$stagingRepositoryId")))
}

private fun assertGetStagingRepositoriesForStatingProfile(stagingProfileId: String = STAGING_PROFILE_ID, count: Int = 1) {
server.verify(count, getRequestedFor(urlMatching("/staging/profile_repositories/$stagingProfileId")))
}

private fun getOneStagingProfileWithGivenIdShrunkJsonResponseAsString(stagingProfileId: String): String {
return """
{
Expand All @@ -1148,6 +1291,19 @@ class NexusPublishPluginTests {
""".trimIndent()
}

private fun getStagingReposWithOneStagingRepoWithGivenIdJsonResponseAsString(
stagingRepository: StagingRepository,
stagingProfileId: String = STAGING_PROFILE_ID
): String {
return """
{
"data": [
${getOneStagingRepoWithGivenIdJsonResponseAsString(stagingRepository, stagingProfileId)}
]
}
""".trimIndent()
}

private fun getOneStagingRepoWithGivenIdJsonResponseAsString(
stagingRepository: StagingRepository,
stagingProfileId: String = STAGING_PROFILE_ID
Expand All @@ -1170,7 +1326,7 @@ class NexusPublishPluginTests {
"updated": "2020-01-28T10:23:49.616Z",
"updatedDate": "Tue Jan 28 10:23:49 UTC 2020",
"updatedTimestamp": 1580207029616,
"description": "Closed by io.github.gradle-nexus.publish-plugin Gradle plugin",
"description": "${stagingRepository.description}",
"provider": "maven2",
"releaseRepositoryId": "no-sync-releases",
"releaseRepositoryName": "No Sync Releases",
Expand Down
Loading