From fbfe56cb7c4bad51f9b65937406b0f39e7ec977c Mon Sep 17 00:00:00 2001 From: Chris Kilian Date: Tue, 14 Oct 2025 22:32:11 -0700 Subject: [PATCH] Add custom publishing target to publish to Maven Central With the sunsetting of OSSRH, the current version of `gradle-maven-publish-plugin` can no longer successfully publish using `gradlew publish`. The minimum version of the plugin that can publish to Maven Central requires an upgrade to Gradle 8, which is a large upgrade from the current Gradle 6 and will require upgrades to several other plugins as well. In order to unblock the ability to publish, we implement a custom publishing task in this commit that uses the same set of credentials to use a POST command to publish to maven central. This will allow publishing of new upgrades of okbuck while other upgrades are being scoped. --- RELEASING.md | 52 ++++++++++++++++-- buildSrc/build.gradle | 106 +++++++++++++++++++++++++++++++++++++ buildSrc/gradle.properties | 13 +++++ 3 files changed, 166 insertions(+), 5 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index 5b1bf523..cb27ac9f 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -12,11 +12,53 @@ Releasing ## Push New Release -1. Using github UI to create a release from a tag (https://github.com/uber/okbuck/releases/new?tag=vX.Y.Z) - 1. Click on Tags - 2. Find your new tag and select "Create Release" from the context menu. - 3. Auto-generate and edit release notes as necessary. -2. `./gradlew clean publish --no-daemon --no-parallel && ./gradlew closeAndReleaseRepository` + +### 1. Create GitHub Release +Using github UI to create a release from a tag (https://github.com/uber/okbuck/releases/new?tag=vX.Y.Z) +1. Click on Tags +2. Find your new tag and select "Create Release" from the context menu. +3. Auto-generate and edit release notes as necessary. + +### 2. Publish to Maven Central + +The plugin uses a custom publishing task that uploads to Maven Central Portal. + +**Prerequisites:** +- Maven Central Portal credentials (username and token) +- Get credentials from: https://central.sonatype.com/account +- Ensure PGP signing is configured (see buildSrc/gradle.properties) + +**Publish command:** +```bash +./gradlew publishToCentralPortal \ + -PmavenCentralUsername= \ + -PmavenCentralPassword= +``` + +Alternatively, set environment variables to avoid passing credentials on command line: +```bash +export MAVEN_CENTRAL_USERNAME= +export MAVEN_CENTRAL_PASSWORD= +./gradlew publishToCentralPortal +``` + +**What happens:** +- Builds all artifacts (JAR, sources, javadoc, POM, module metadata) +- Signs all artifacts with PGP +- Generates MD5 and SHA1 checksums +- Creates a deployment bundle (ZIP file) +- Uploads to Maven Central Portal + +**After upload:** +- Log into https://central.sonatype.com/publishing +- Review and publish the deployment (Manual step required) +- Publication typically takes 15-30 minutes to sync to Maven Central + +**Verify locally before uploading:** +```bash +./gradlew publishToCentralPortal -PskipUpload=true +``` +This creates the bundle at `buildSrc/build/okbuck-X.Y.Z.zip` without uploading. ## Prepare for Next Release diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index f0b8963d..3f07354b 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -112,3 +112,109 @@ tasks.withType(com.fizzed.rocker.gradle.RockerTask).each { task -> } } } + +// Custom task to publish to Maven Central Portal +/** + * Custom publishing task for Maven Central Portal. + * + * This task creates a deployment bundle with all required artifacts (JAR, sources, javadoc, POM, + * module metadata) along with their PGP signatures and checksums (MD5/SHA1), then uploads to + * Maven Central Portal's REST API. + * + * Usage: + * ./gradlew publishToCentralPortal -PmavenCentralUsername= -PmavenCentralPassword= + * + * Or set environment variables: MAVEN_CENTRAL_USERNAME, MAVEN_CENTRAL_PASSWORD + * + * To build without uploading: ./gradlew publishToCentralPortal -PskipUpload=true + * + * See RELEASING.md for complete publishing instructions. + */ +task publishToCentralPortal { + dependsOn 'generatePomFileForMavenPublication', 'jar', 'javaSourcesJar', 'simpleJavadocJar', 'signMavenPublication' + + doLast { + def version = findProperty('VERSION_NAME') + if (!version) { + throw new GradleException("VERSION_NAME property is required. Set it in buildSrc/gradle.properties") + } + def targetDir = file("${buildDir}/deployment/com/uber/okbuck/${version}") + targetDir.mkdirs() + + // Copy artifacts + copy { + from "${buildDir}/libs" + into targetDir + include "plugin-${version}*.jar", "plugin-${version}*.jar.asc" + rename "plugin-${version}", "okbuck-${version}" + } + copy { + from "${buildDir}/publications/maven" + into targetDir + include 'pom-default.xml*', 'module.json*' + rename 'pom-default.xml', "okbuck-${version}.pom" + rename 'pom-default.xml.asc', "okbuck-${version}.pom.asc" + rename 'module.json', "okbuck-${version}.module" + rename 'module.json.asc', "okbuck-${version}.module.asc" + } + + // Generate checksums + targetDir.listFiles().each { file -> + if (file.isFile() && !file.name.endsWith('.md5') && !file.name.endsWith('.sha1')) { + ['MD5', 'SHA-1'].each { algorithm -> + def digest = java.security.MessageDigest.getInstance(algorithm) + file.withInputStream { is -> + def buffer = new byte[8192] + int read + while ((read = is.read(buffer)) != -1) digest.update(buffer, 0, read) + } + def ext = algorithm == 'MD5' ? '.md5' : '.sha1' + new File(targetDir, "${file.name}${ext}").text = digest.digest().encodeHex().toString() + } + } + } + + // Create bundle + def bundleFile = file("${buildDir}/okbuck-${version}.zip") + ant.zip(destfile: bundleFile) { fileset(dir: file("${buildDir}/deployment")) } + println "Created: ${bundleFile.absolutePath} (${bundleFile.length()} bytes)" + + // Upload if credentials provided + def username = findProperty("mavenCentralUsername") ?: System.getenv("MAVEN_CENTRAL_USERNAME") + def password = findProperty("mavenCentralPassword") ?: System.getenv("MAVEN_CENTRAL_PASSWORD") + + if (!username || !password || findProperty("skipUpload") == "true") { + println "Skipping upload. Use -PmavenCentralUsername=... -PmavenCentralPassword=... to upload" + return + } + + // Upload + println "Uploading to Maven Central Portal..." + def connection = new URL("https://central.sonatype.com/api/v1/publisher/upload?publishingType=USER_MANAGED") + .openConnection() as HttpURLConnection + connection.requestMethod = "POST" + connection.doOutput = true + connection.setRequestProperty("Authorization", "UserToken ${"${username}:${password}".bytes.encodeBase64().toString()}") + connection.connectTimeout = 60000 + connection.readTimeout = 60000 + + def boundary = "----WebKitFormBoundary${System.currentTimeMillis()}" + connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=${boundary}") + + connection.outputStream.withWriter("UTF-8") { w -> + w << "--${boundary}\r\n" + w << "Content-Disposition: form-data; name=\"bundle\"; filename=\"${bundleFile.name}\"\r\n" + w << "Content-Type: application/octet-stream\r\n\r\n" + w.flush() + connection.outputStream << bundleFile.bytes + w << "\r\n--${boundary}--\r\n" + } + + def code = connection.responseCode + if (code >= 200 && code < 300) { + println "✓ Upload successful! Response: ${connection.inputStream.text}" + } else { + throw new GradleException("Upload failed (${code}): ${connection.errorStream?.text ?: connection.inputStream?.text}") + } + } +} diff --git a/buildSrc/gradle.properties b/buildSrc/gradle.properties index ef77d666..832f5ab9 100644 --- a/buildSrc/gradle.properties +++ b/buildSrc/gradle.properties @@ -17,3 +17,16 @@ POM_PACKAGING=jar SONATYPE_HOST=DEFAULT RELEASE_SIGNING_ENABLED=true + +# Publishing Configuration: +# The publishToCentralPortal task requires PGP signing to be configured. +# Set these properties in your ~/.gradle/gradle.properties file: +# signing.keyId= +# signing.password= +# signing.secretKeyRingFile= +# +# Maven Central credentials (required for upload): +# mavenCentralUsername= +# mavenCentralPassword= +# +# See RELEASING.md for complete instructions.