From c3badf74c15228602af5dc26dacca0fec90aaf9e Mon Sep 17 00:00:00 2001 From: Lari Hotari Date: Fri, 12 Jun 2026 02:44:38 +0300 Subject: [PATCH 1/2] [improve][build] Add ASF Nexus publishing repositories to publish conventions - Add apacheReleases and apacheSnapshots Maven repositories (named after the ASF parent POM repositories), publishable with publishAllPublicationsToApacheReleasesRepository / publishAllPublicationsToApacheSnapshotsRepository - Resolve credentials at execution time from the apacheReleasesUsername / apacheReleasesPassword (and apacheSnapshots*) Gradle properties via credentials(PasswordCredentials::class), keeping the publish tasks configuration-cache compatible; pass them as ORG_GRADLE_PROJECT_- prefixed environment variables on the publish command line - Allow overriding the repository URLs with the apacheReleasesRepoUrl / apacheSnapshotsRepoUrl Gradle properties (e.g. a file:// URL for testing the publication layout; credentials are skipped for the file transport, which rejects them) - Validate the version against the target repository before any upload: only -SNAPSHOT versions may go to apacheSnapshots and only release versions to apacheReleases - Enable signing with -PuseGpgCmd=true alone: the gpg command uses its default key unless -Psigning.gnupg.keyName= selects one Assisted-by: Claude Fable 5 (Claude Code) --- .../pulsar.publish-conventions.gradle.kts | 92 ++++++++++++++++++- 1 file changed, 89 insertions(+), 3 deletions(-) diff --git a/build-logic/conventions/src/main/kotlin/pulsar.publish-conventions.gradle.kts b/build-logic/conventions/src/main/kotlin/pulsar.publish-conventions.gradle.kts index eae72273f310f..a5d2a337eb3b3 100644 --- a/build-logic/conventions/src/main/kotlin/pulsar.publish-conventions.gradle.kts +++ b/build-logic/conventions/src/main/kotlin/pulsar.publish-conventions.gradle.kts @@ -19,7 +19,8 @@ // Convention plugin for publishing Pulsar modules to Maven repositories. // Configures maven-publish, GPG signing, POM metadata, sources/javadoc JARs, -// and a local deploy repository for testing. +// the ASF Nexus release/snapshot repositories, and a local deploy repository +// for testing. plugins { `maven-publish` @@ -153,6 +154,88 @@ run { } } +// --- Apache distribution repositories (ASF Nexus) --- +// Repository names follow the ASF parent POM (apache.releases.https / apache.snapshots.https). +// Publish with one of: +// ./gradlew publishAllPublicationsToApacheSnapshotsRepository (for -SNAPSHOT versions) +// ./gradlew publishAllPublicationsToApacheReleasesRepository (for release versions) +// Releases must be published with --no-parallel: when uploading to the Apache staging +// repository, Nexus creates an implicit staging repository, and concurrent per-module uploads +// can end up split across multiple implicitly-created staging repositories instead of being +// collected into a single one. +// Credentials are resolved by Gradle at execution time from the apacheReleasesUsername / +// apacheReleasesPassword and apacheSnapshotsUsername / apacheSnapshotsPassword Gradle properties +// (the credentials(PasswordCredentials::class) form, which keeps the publish tasks +// configuration-cache compatible — explicitly assigned credentials would not be). Pass them as +// ORG_GRADLE_PROJECT_-prefixed environment variables on the publish command line so the password +// doesn't have to be stored in ~/.gradle/gradle.properties where it could leak to unrelated +// builds; start the command line with a space to keep the password out of shell history: +// ORG_GRADLE_PROJECT_apacheReleasesUsername=$APACHE_USER \ +// ORG_GRADLE_PROJECT_apacheReleasesPassword="" \ +// ./gradlew publishAllPublicationsToApacheReleasesRepository --no-parallel ... +// The URLs can be overridden with the apacheReleasesRepoUrl / apacheSnapshotsRepoUrl Gradle +// properties (e.g. a file:// URL for testing the publication layout). +run { + fun MavenArtifactRepository.configureApacheRepository(urlProperty: String, defaultUrl: String) { + val repositoryUrl = uri(providers.gradleProperty(urlProperty).getOrElse(defaultUrl)) + url = repositoryUrl + // The file transport (an URL overridden to file:// for testing) rejects credentials, + // and Gradle's credentials validation would fail when the properties aren't set. + if (repositoryUrl.scheme != "file") { + credentials(PasswordCredentials::class) + } + } + + publishing { + repositories { + maven { + name = "apacheReleases" + configureApacheRepository( + "apacheReleasesRepoUrl", + "https://repository.apache.org/service/local/staging/deploy/maven2" + ) + } + maven { + name = "apacheSnapshots" + configureApacheRepository( + "apacheSnapshotsRepoUrl", + "https://repository.apache.org/content/repositories/snapshots" + ) + } + } + } + + // Validate before any upload: only -SNAPSHOT versions may go to apacheSnapshots and only + // release versions to apacheReleases. (Maven's deploy picks the repository from the version; + // in Gradle the task name picks the repository, so the version must be checked instead.) + // The task's repository property is discarded by configuration cache serialization, so + // capture the repository name at configuration time and only register the validation + // action (capturing plain strings/booleans) for the Apache repositories. + val projectVersion = version.toString() + val isSnapshotVersion = projectVersion.endsWith("-SNAPSHOT") + tasks.withType().configureEach { + val repositoryName = repository.name + if (repositoryName == "apacheReleases" || repositoryName == "apacheSnapshots") { + doFirst { + if (repositoryName == "apacheSnapshots" && !isSnapshotVersion) { + throw GradleException( + "Refusing to publish non-snapshot version '$projectVersion' to the " + + "'apacheSnapshots' repository. Use " + + "publishAllPublicationsToApacheReleasesRepository for release versions." + ) + } + if (repositoryName == "apacheReleases" && isSnapshotVersion) { + throw GradleException( + "Refusing to publish snapshot version '$projectVersion' to the " + + "'apacheReleases' repository. Use " + + "publishAllPublicationsToApacheSnapshotsRepository for -SNAPSHOT versions." + ) + } + } + } + } +} + // --- GPG signing --- signing { isRequired = !version.toString().endsWith("-SNAPSHOT") @@ -165,10 +248,13 @@ signing { sign(publishing.publications) } -// Disable signing tasks when no key is configured (local dev without signing) +// Disable signing tasks when no signing configuration is present (local dev without signing). +// With -PuseGpgCmd=true, an explicit key isn't needed: the gpg command uses its default key +// unless -Psigning.gnupg.keyName= selects one. tasks.withType().configureEach { enabled = providers.gradleProperty("signing.keyId").isPresent || - providers.gradleProperty("signing.gnupg.keyName").isPresent + providers.gradleProperty("signing.gnupg.keyName").isPresent || + (providers.gradleProperty("useGpgCmd").orNull?.toBoolean() ?: false) } // Suppress enforced-platform validation: all java-library modules use From 13861a21d0231b92d8d4e89fa595ab9848d2b7af Mon Sep 17 00:00:00 2001 From: Lari Hotari Date: Fri, 12 Jun 2026 04:21:45 +0300 Subject: [PATCH 2/2] [improve][build] Read POM description in afterEvaluate in publish conventions Read project.description in afterEvaluate so that a description assigned in a module's build script body is picked up (the plugin previously read it at apply time, before the script body runs), and capture it as a plain string so the pom configuration stays configuration-cache compatible. Assisted-by: Claude Fable 5 (Claude Code) --- .../pulsar.publish-conventions.gradle.kts | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/build-logic/conventions/src/main/kotlin/pulsar.publish-conventions.gradle.kts b/build-logic/conventions/src/main/kotlin/pulsar.publish-conventions.gradle.kts index a5d2a337eb3b3..5da26aaac6438 100644 --- a/build-logic/conventions/src/main/kotlin/pulsar.publish-conventions.gradle.kts +++ b/build-logic/conventions/src/main/kotlin/pulsar.publish-conventions.gradle.kts @@ -87,25 +87,33 @@ run { // Capture values in a local scope so withXml closures don't capture the script object // (which would break configuration cache serialization) val projectName = project.name - val projectDescription = project.description val archivesNameValue = the().archivesName.get() val isPlatformProject = plugins.hasPlugin("java-platform") val isRootProject = project == rootProject val pulsarVersion = version.toString() val localDeployRepoDir = rootProject.layout.buildDirectory.dir("local-deploy-repo") + // Per-module POM name and description. Read in afterEvaluate so that a description + // assigned in a module's build script body is picked up, and captured as plain strings + // so the pom configuration stays configuration-cache compatible. + if (!isRootProject) { + afterEvaluate { + val projectDescription = project.description ?: "Apache Pulsar :: $projectName" + publishing.publications.withType().configureEach { + pom { + name.set(projectDescription) + description.set(projectDescription) + } + } + } + } + publishing { publications { withType().configureEach { artifactId = archivesNameValue pom { - // Per-module name and description - if (!isRootProject) { - name.set("Apache Pulsar :: $projectName") - description.set(projectDescription ?: "Apache Pulsar :: $projectName") - } - // Clean up POM XML and inject reference withXml { val sb = asString()