Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ jobs:
- name: Publish Artifacts
uses: ./actions/run-gradle
with:
gradle_command: publish closeAndReleaseStagingRepositories -PreleaseBuild=true -PpublishBuild=true -PgithubPublish=true -PcentralPublish=true
gradle_command: publish publishMavenCentralBundle -PreleaseBuild=true -PpublishBuild=true -PgithubPublish=true -PcentralPublish=true
env:
ORG_GRADLE_PROJECT_signingKey: ${{ secrets.GPG_PRIVATE_KEY }}
ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.GPG_PASSPHRASE }}
Expand Down
106 changes: 98 additions & 8 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -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}/*"
Copy link
Collaborator

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/foundationdb we 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.

Copy link
Collaborator Author

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

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()}"
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 uuid other this to help ensure that the zip file itself doesn't have this sequence of bytes? (at least if I understand multipart correctly)
Or would it be worthwhile to validate on our end that the string we use for the boundary is not in the file?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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()
}
}
}
Expand Down
31 changes: 0 additions & 31 deletions docs/sphinx/source/ReleaseNotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,37 +7,6 @@ As the [versioning guide](Versioning.md) details, it cannot always be determined

## 4.8

### 4.8.9.0


<details>
<summary>

<h4> Build/Test/Documentation/Style Improvements (click to expand) </h4>

</summary>

* Increase heap size for JReleaser - [PR #3720](https://github.com/FoundationDB/fdb-record-layer/pull/3720)
* Clean up 4.8.4.0 and 4.8.7.0 release notes - [PR #3719](https://github.com/FoundationDB/fdb-record-layer/pull/3719)
* Upload the jreleaser trace logs & config - [PR #3718](https://github.com/FoundationDB/fdb-record-layer/pull/3718)
* Reduce test load to reduce flakiness - [PR #3712](https://github.com/FoundationDB/fdb-record-layer/pull/3712)
* Update the release plugin to use the central publishing API directly - [PR #3710](https://github.com/FoundationDB/fdb-record-layer/pull/3710)

</details>


**[Full Changelog (4.8.6.0...4.8.9.0)](https://github.com/FoundationDB/fdb-record-layer/compare/4.8.6.0...4.8.9.0)**

#### Mixed Mode Test Results

Mixed mode testing run against the following previous versions:

❌`4.6.4.0`, ❌`4.6.5.0`, ❌`4.7.1.0`, ❌`4.7.2.0`, ✅`4.7.3.0`, ✅`4.8.1.0`, ✅`4.8.2.0`, ✅`4.8.3.0`, ✅`4.8.5.0`, ✅`4.8.6.0`

[See full test run](https://github.com/FoundationDB/fdb-record-layer/actions/runs/19075271823)



### 4.8.6.0

<h4> New Features </h4>
Expand Down
1 change: 0 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,6 @@ test-compileOnly = [ "autoService", "jsr305" ]
download = { id = "de.undercouch.download", version = "5.6.0" }
gitversion = { id = "com.palantir.git-version", version = "3.1.0" }
jmh = { id = "me.champeau.jmh", version = "0.7.2" }
nexus = { id = "io.github.gradle-nexus.publish-plugin", version = "2.0.0" }
protobuf = { id = "com.google.protobuf", version = "0.9.4" }
serviceloader = { id = "com.github.harbby.gradle.serviceloader", version = "1.1.8" }
shadow = { id = "com.gradleup.shadow", version = "8.3.5" }
Expand Down
8 changes: 8 additions & 0 deletions gradle/publishing.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,14 @@ publishing {
}
}
}

if (Boolean.parseBoolean(centralPublish)) {
// Publish this project to the root project's staging repo. This will be
// bundled up by createMavenCentralBundle for publishing
maven {
url = rootProject.layout.buildDirectory.dir("staging-repo")
}
}
}
}
}
Loading