-
Notifications
You must be signed in to change notification settings - Fork 116
Replace nexus publishing plugin with direct call to central sonatype uploading URL #3723
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -41,7 +41,6 @@ plugins { | |
| alias(libs.plugins.protobuf) | ||
| alias(libs.plugins.versions) | ||
| alias(libs.plugins.spotbugs) | ||
| alias(libs.plugins.nexus) | ||
| alias(libs.plugins.download) | ||
| } | ||
|
|
||
|
|
@@ -310,13 +309,104 @@ subprojects { | |
| // Configure publishing for maven central. This is done in the top-level build.gradle, and then | ||
| // all of the subprojects configure the artifacts they want published by applying | ||
| // ${rootDir}/gradle/publishing.gradle in the project gradle. By default, we publish a library | ||
| // from each package, but this can be configured by adjusting the publishLibrary variable | ||
| if (Boolean.parseBoolean(centralPublish)) { | ||
| nexusPublishing { | ||
| repositories { | ||
| sonatype { | ||
| // Update the URL now that the OSSRH service has been sunset: https://central.sonatype.org/news/20250326_ossrh_sunset/ | ||
| nexusUrl.set(uri("https://ossrh-staging-api.central.sonatype.com/service/local/")) | ||
| // from each package, but this can be configured by adjusting the publishLibrary variable. | ||
| // Each subproject will place its artifacts into the central staging repository, | ||
| // which will then be picked up by the bundle task | ||
| if (Boolean.parseBoolean(publishBuild) && Boolean.parseBoolean(centralPublish)) { | ||
| var stagingRepo = layout.buildDirectory.dir('staging-repo') | ||
|
|
||
| // Task to create a bundle zip of all published artifacts | ||
| tasks.register('createMavenCentralBundle', Zip) { | ||
| description = "Creates a bundle zip of all artifacts for Maven Central upload" | ||
| group = "publishing" | ||
|
|
||
| dependsOn subprojects.collect { it.tasks.matching { it.name == 'publish' } } | ||
|
|
||
| archiveBaseName = "${rootProject.group}-${rootProject.name}" | ||
| archiveVersion = project.version | ||
| archiveClassifier = 'bundle' | ||
| destinationDirectory = layout.buildDirectory.dir('distributions') | ||
|
|
||
| from(stagingRepo) { | ||
| include "${rootProject.group.replaceAll('.', '/')}/**/${project.version}/*" | ||
| exclude '**/maven-metadata*' | ||
| } | ||
|
|
||
| includeEmptyDirs = false | ||
| } | ||
|
|
||
| // Task to upload the bundle to Maven Central | ||
| tasks.register('publishMavenCentralBundle') { | ||
| description = "Uploads the bundle zip to Maven Central Portal" | ||
| group = "publishing" | ||
|
|
||
| dependsOn tasks.named('createMavenCentralBundle') | ||
|
|
||
| doLast { | ||
| def sonatypeUsername = findProperty('sonatypeUsername') | ||
| def sonatypePassword = findProperty('sonatypePassword') | ||
|
|
||
| if (sonatypeUsername == null || sonatypePassword == null) { | ||
| throw new GradleException("sonatypeUsername and sonatypePassword properties must be set") | ||
| } | ||
|
|
||
| // Create base64 encoded credentials | ||
| def credentials = "${sonatypeUsername}:${sonatypePassword}" | ||
| def encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes('UTF-8')) | ||
|
|
||
| // Get the bundle file | ||
| def bundleTask = tasks.named('createMavenCentralBundle').get() | ||
| def bundleFile = bundleTask.archiveFile.get().asFile | ||
|
|
||
| if (!bundleFile.exists()) { | ||
| throw new GradleException("Bundle file does not exist: ${bundleFile}") | ||
| } | ||
|
|
||
| logger.lifecycle("Uploading bundle: ${bundleFile.name} (${bundleFile.length() / 1024 / 1024} MB)") | ||
|
|
||
| // Upload using HttpURLConnection for multipart/form-data | ||
| def url = new URL('https://central.sonatype.com/api/v1/publisher/upload?publishingType=AUTOMATIC') | ||
| def connection = url.openConnection() as HttpURLConnection | ||
|
|
||
| try { | ||
| connection.setRequestMethod('POST') | ||
| connection.setDoOutput(true) | ||
| connection.setRequestProperty('Authorization', "Bearer ${encodedCredentials}") | ||
|
|
||
| def boundary = "----GradleBoundary${System.currentTimeMillis()}" | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Given that we are uploading a zip, would it be worthwhile to add a
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I could see that. It may be overkill, and that is sort of what the current time millis is for. I think a UUID is probably safe enough, though we could do a search for it, I suppose |
||
| connection.setRequestProperty('Content-Type', "multipart/form-data; boundary=${boundary}") | ||
|
|
||
| connection.outputStream.withWriter('UTF-8') { writer -> | ||
| writer.write("--${boundary}\r\n") | ||
| writer.write("Content-Disposition: form-data; name=\"bundle\"; filename=\"${bundleFile.name}\"\r\n") | ||
| writer.write("Content-Type: application/octet-stream\r\n") | ||
| writer.write("\r\n") | ||
| writer.flush() | ||
|
|
||
| // Write the file bytes | ||
| bundleFile.withInputStream { input -> | ||
| connection.outputStream << input | ||
| } | ||
|
|
||
| writer.write("\r\n") | ||
| writer.write("--${boundary}--\r\n") | ||
| writer.flush() | ||
| } | ||
|
|
||
| def responseCode = connection.responseCode | ||
| def responseMessage = connection.responseMessage | ||
|
|
||
| if (responseCode >= 200 && responseCode < 300) { | ||
| def deploymentId = connection.inputStream.text.trim() | ||
| logger.lifecycle("Upload successful!") | ||
| logger.lifecycle("Deployment ID: ${deploymentId}") | ||
| logger.lifecycle("You can check the status at: https://central.sonatype.com/api/v1/publisher/status?id=${deploymentId}") | ||
| } else { | ||
| def errorResponse = connection.errorStream?.text ?: responseMessage | ||
| throw new GradleException("Upload failed with status ${responseCode}: ${errorResponse}") | ||
| } | ||
| } finally { | ||
| connection.disconnect() | ||
| } | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This means if a sub-project publishes to the staging repo, but doesn't put the results in
org/foundationdbwe just won't publish it, right? Probably ok, at least for the time being to not protect against that, I can't foresee us messing that up, and I think the only improvement would be to error.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, though that's somewhat deliberate, as the bundle we produce is tagged with the group ID of the project, and I believe that central may actually reject our artifacts if it's in a different group