Skip to content
Open
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
5 changes: 3 additions & 2 deletions plugins/stage-vote-release-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,13 +116,13 @@ releaseParams {
}
}
nexus { // Configures Nexus repository
mavenCentral() // By default the plugin publishes to repository.apache.org, and it can be overridden
mavenCentral() // Publish to Maven Central via the Central Portal OSSRH Staging API compatibility host (defaults to repository.apache.org)
prodUrl.set(uri("https://nexus.url")) // An alternative option to override Nexus URL
credentials {
username.set(String) // -PasfNexusUsername or -PasfTestNexusUsername
password.set(String) // -PasfNexusPassword or -PasfTestNexusPassword
}
stagingProfileId.set(String) // Specify Nexus staging profile to save Nexus roundtrip on publishing
stagingProfileId.set(String) // Nexus staging profile id; setting it skips the profile lookup. mavenCentral() defaults it to ${project.group}
}
source { // This is a Git configuration for the source code Git repository (e.g. for pushing the release tag)
credentials {
Expand Down Expand Up @@ -166,6 +166,7 @@ releaseParams { // ReleaseExtension
connectTimeout.set(Duration.ofMinutes(10))
operationTimeout.set(Duration.ofMinutes(10))
url.set(URI) // Defaults to https://repository.apache.org (prod) or http://127.0.0.1:8080 (test)
snapshotUrl.set(URI) // Defaults to the snapshot path derived from url; mavenCentral() points it at central.sonatype.com
packageGroup.set(String) // Defaults to ${project.group}
}
source {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,8 +272,42 @@ open class NexusConfig @Inject constructor(
val prodUrl = objects.property<URI>().convention(project.uri("https://repository.apache.org"))
val testUrl = objects.property<URI>().convention(project.uri("http://127.0.0.1:8080"))

/**
* Snapshot repository URL. When unset, the plugin derives it from [url] using the Nexus 2
* layout. The Central Portal serves snapshots from a different host, so [mavenCentral] sets
* it for production releases.
*/
val snapshotUrl = objects.property<URI>()

/**
* Publishes to Maven Central through the Central Portal OSSRH Staging API compatibility host.
*
* The legacy OSSRH host (`oss.sonatype.org`) was sunset in 2025. The compatibility host keeps
* Nexus-based publishing working without pointing the build at it by hand, and it identifies
* staging profiles by namespace, so [stagingProfileId] defaults to the project group.
*
* Like [apacheRepository], this only affects production releases; the test environment keeps
* using the local Nexus stub.
*/
fun mavenCentral() {
prodUrl.set(project.uri("https://oss.sonatype.org"))
prodUrl.set(project.uri("https://ossrh-staging-api.central.sonatype.com"))
snapshotUrl.convention(
ext.repositoryType.flatMap {
when (it) {
RepositoryType.PROD ->
project.provider { project.uri("https://central.sonatype.com/repository/maven-snapshots/") }
RepositoryType.TEST -> project.objects.property<URI>()
}
}
)
stagingProfileId.convention(
ext.repositoryType.flatMap {
when (it) {
RepositoryType.PROD -> project.provider { project.group.toString() }
RepositoryType.TEST -> project.objects.property<String>()
}
}
)
}

fun apacheRepository() {
Expand All @@ -286,6 +320,12 @@ open class NexusConfig @Inject constructor(
project.provider {
project.group.toString()
})

/**
* Nexus staging profile id. Setting it lets the publish plugin skip the staging-profile lookup.
* [mavenCentral] defaults it to the project group, which the Central Portal treats as the
* namespace.
*/
val stagingProfileId = objects.property<String>()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -579,15 +579,20 @@ class StageVoteReleasePlugin @Inject constructor(
validateBeforeBuildingReleaseArtifacts: TaskProvider<*>
) {
configure<NexusPublishExtension> {
val nexus = releaseExt.nexus
val repo = repositories.create(REPOSITORY_NAME) {
nexusUrl.set(releaseExt.nexus.url.map { it.replacePath("/service/local/") })
snapshotRepositoryUrl.set(releaseExt.nexus.url.map { it.replacePath("/content/repositories/snapshots/") })
username.set(project.provider { releaseExt.nexus.credentials.username(project) })
password.set(project.provider { releaseExt.nexus.credentials.password(project) })
nexusUrl.set(nexus.url.map { it.replacePath("/service/local/") })
snapshotRepositoryUrl.set(
nexus.snapshotUrl.orElse(
nexus.url.map { it.replacePath("/content/repositories/snapshots/") }
)
)
username.set(project.provider { nexus.credentials.username(project) })
password.set(project.provider { nexus.credentials.password(project) })
stagingProfileId.set(nexus.stagingProfileId)
}
connectTimeout.set(releaseExt.nexus.connectTimeout)
clientTimeout.set(releaseExt.nexus.operationTimeout)
val nexus = releaseExt.nexus
connectTimeout.set(nexus.connectTimeout)
clientTimeout.set(nexus.operationTimeout)
packageGroup.set(nexus.packageGroup)
transitionCheckOptions {
delayBetween.set(Duration.ofSeconds(10))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
* Copyright 2024 Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
*
* 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 com.github.vlsi.gradle.release

import com.github.vlsi.gradle.BaseGradleTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.parallel.Execution
import org.junit.jupiter.api.parallel.ExecutionMode
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.MethodSource

@Execution(ExecutionMode.SAME_THREAD)
class NexusConfigTest : BaseGradleTest() {
private fun buildScript(nexusConfig: String): String =
/* language=gradle */
"""
plugins {
id 'com.github.vlsi.stage-vote-release'
}

group = 'org.example.demo'
version = '1.0.0'

releaseParams {
nexus {
$nexusConfig
}
}

def repo = nexusPublishing.repositories.getByName('nexus')
tasks.register('printNexus') {
def nexusUrl = repo.nexusUrl
def snapshotUrl = repo.snapshotRepositoryUrl
def stagingProfileId = repo.stagingProfileId
doLast {
println("nexusUrl=" + nexusUrl.get())
println("snapshotUrl=" + snapshotUrl.get())
println("stagingProfileId=" + stagingProfileId.getOrElse('<absent>'))
}
}
""".trimIndent()

private fun BuildResultOf(output: String): Map<String, String> =
output.lineSequence()
.mapNotNull { line ->
KEYS.firstOrNull { line.startsWith("$it=") }?.let { it to line.substringAfter('=') }
}
.toMap()

@ParameterizedTest
@MethodSource("disabledConfigurationCacheGradleVersionAndSettings")
fun `mavenCentral targets the Central Portal compatibility host in production`(testCase: TestCase) {
createSettings(testCase)
projectDir.resolve("build.gradle").write(buildScript("mavenCentral()"))

val values = BuildResultOf(prepare(testCase, "printNexus", "-Pasf").build().output)

assertEquals("https://ossrh-staging-api.central.sonatype.com/service/local/", values["nexusUrl"]) {
"mavenCentral() should point nexusUrl at the Central Portal OSSRH Staging API host"
}
assertEquals("https://central.sonatype.com/repository/maven-snapshots/", values["snapshotUrl"]) {
"mavenCentral() should publish snapshots to the Central Portal snapshot host"
}
assertEquals("org.example.demo", values["stagingProfileId"]) {
"stagingProfileId should default to the project group"
}
}

@ParameterizedTest
@MethodSource("disabledConfigurationCacheGradleVersionAndSettings")
fun `mavenCentral keeps the local stub in the test environment`(testCase: TestCase) {
createSettings(testCase)
projectDir.resolve("build.gradle").write(buildScript("mavenCentral()"))

val values = BuildResultOf(prepare(testCase, "printNexus").build().output)

assertEquals("http://127.0.0.1:8080/service/local/", values["nexusUrl"]) {
"the test environment should keep using the local Nexus stub"
}
assertEquals("http://127.0.0.1:8080/content/repositories/snapshots/", values["snapshotUrl"]) {
"the test environment should derive the snapshot URL from the local stub"
}
assertEquals("<absent>", values["stagingProfileId"]) {
"the test environment should not default the staging profile id"
}
}

@ParameterizedTest
@MethodSource("disabledConfigurationCacheGradleVersionAndSettings")
fun `explicit stagingProfileId reaches the nexus repository`(testCase: TestCase) {
createSettings(testCase)
projectDir.resolve("build.gradle").write(
buildScript(
/* language=gradle */
"""
mavenCentral()
stagingProfileId.set('custom-profile')
""".trimIndent()
)
)

val values = BuildResultOf(prepare(testCase, "printNexus", "-Pasf").build().output)

assertEquals("custom-profile", values["stagingProfileId"]) {
"an explicit stagingProfileId should reach the publish plugin"
}
}

companion object {
private val KEYS = listOf("nexusUrl", "snapshotUrl", "stagingProfileId")
}
}
Loading