mirror of https://github.com/halo-dev/halo
Migrate maven publish with publishing by using the Portal Publisher API
parent
981d6d1c4a
commit
033764ea63
|
@ -22,6 +22,18 @@ java {
|
|||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(21)
|
||||
}
|
||||
withJavadocJar()
|
||||
withSourcesJar()
|
||||
}
|
||||
|
||||
jar {
|
||||
manifest {
|
||||
attributes(
|
||||
'Implementation-Title': project.name,
|
||||
'Implementation-Version': project.version,
|
||||
'Implementation-Vendor': 'Halo Project',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
|
@ -99,11 +111,6 @@ dependencies {
|
|||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
java {
|
||||
withSourcesJar()
|
||||
withJavadocJar()
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications.named('mavenJava', MavenPublication) {
|
||||
from components.java
|
||||
|
|
|
@ -0,0 +1,200 @@
|
|||
import groovy.json.JsonSlurper
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.artifacts.repositories.PasswordCredentials
|
||||
import org.gradle.api.file.RegularFileProperty
|
||||
import org.gradle.api.provider.Property
|
||||
import org.gradle.api.tasks.Input
|
||||
import org.gradle.api.tasks.InputFile
|
||||
import org.gradle.api.tasks.Internal
|
||||
import org.gradle.api.tasks.Optional
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
|
||||
import java.net.http.HttpClient
|
||||
import java.net.http.HttpRequest
|
||||
import java.net.http.HttpResponse
|
||||
import java.time.Duration
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
import static java.net.http.HttpRequest.BodyPublishers.ofFile
|
||||
import static java.net.http.HttpRequest.BodyPublishers.ofString
|
||||
import static java.nio.charset.StandardCharsets.UTF_8
|
||||
|
||||
/**
|
||||
* Task to upload a bundle to Sonatype Central Repository.
|
||||
* @author JohnNiang
|
||||
*/
|
||||
abstract class UploadBundleTask extends DefaultTask {
|
||||
|
||||
@InputFile
|
||||
abstract RegularFileProperty getBundleFile();
|
||||
|
||||
@Input
|
||||
@Optional
|
||||
abstract Property<PublishingType> getPublishingType();
|
||||
|
||||
@Input
|
||||
abstract Property<PasswordCredentials> getCredentials();
|
||||
|
||||
UploadBundleTask() {
|
||||
getPublishingType().convention(PublishingType.AUTOMATIC)
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
void upload() {
|
||||
if (project.version.toString().endsWith("-SNAPSHOT")) {
|
||||
logger.lifecycle("Detected SNAPSHOT version, uploading to maven-snapshots repository...")
|
||||
uploadSnapshotBundle()
|
||||
logger.lifecycle("Snapshot bundle uploaded successfully.")
|
||||
return
|
||||
}
|
||||
def deploymentId = uploadReleaseBundle()
|
||||
def maxWait = Duration.ofMinutes(12)
|
||||
def pollingInterval = Duration.ofSeconds(10)
|
||||
def maxRetry = (int) (maxWait.toMillis() / pollingInterval.toMillis())
|
||||
def retry = 0
|
||||
logger.lifecycle("Preparing to check deployment state for deployment ID: ${deploymentId}, max retries: ${maxRetry}, polling interval: ${pollingInterval.toSeconds()} seconds")
|
||||
while (retry++ <= maxRetry) {
|
||||
def state = checkDeploymentState(deploymentId)
|
||||
if (state == null) {
|
||||
throw new RuntimeException("Failed to check deployment state for deployment ID: ${deploymentId}")
|
||||
}
|
||||
if (state == DeploymentState.PUBLISHED) {
|
||||
println("Bundle published successfully with deployment ID: ${deploymentId}")
|
||||
break
|
||||
}
|
||||
if (state == DeploymentState.VALIDATED) {
|
||||
println("Bundle validated successfully with deployment ID: ${deploymentId}")
|
||||
break
|
||||
}
|
||||
if (state == DeploymentState.FAILED) {
|
||||
throw new RuntimeException("Bundle deployment failed with deployment ID: ${deploymentId}")
|
||||
}
|
||||
if (state == DeploymentState.VALIDATING) {
|
||||
logger.lifecycle('Bundle is being validated, please wait...')
|
||||
} else if (state == DeploymentState.PENDING) {
|
||||
logger.lifecycle('Bundle is pending, please wait...')
|
||||
}
|
||||
println("Deployment state: ${state}, retrying(${retry}) in ${pollingInterval.toSeconds()} seconds...")
|
||||
sleep(pollingInterval.toMillis())
|
||||
}
|
||||
}
|
||||
|
||||
DeploymentState checkDeploymentState(String deploymentId) {
|
||||
createHttpClient().withCloseable { client ->
|
||||
def endpoint = "https://central.sonatype.com/api/v1/publisher/status?id=${deploymentId}"
|
||||
|
||||
def request = HttpRequest.newBuilder(URI.create(endpoint))
|
||||
.header("Authorization", bearerAuthorizationHeader)
|
||||
.POST(HttpRequest.BodyPublishers.noBody())
|
||||
.build()
|
||||
def response = client.send(request, HttpResponse.BodyHandlers.ofString(UTF_8))
|
||||
if (response.statusCode() != 200) {
|
||||
throw new RuntimeException("Failed to check deployment status: status: ${response.statusCode()} - body: ${response.body()}")
|
||||
}
|
||||
logger.debug("Response body: ${response.body()}")
|
||||
def status = new JsonSlurper().parseText(response.body())
|
||||
logger.debug("Deployment status: ${status}")
|
||||
return status.properties.deploymentState as DeploymentState
|
||||
}
|
||||
}
|
||||
|
||||
void uploadSnapshotBundle() {
|
||||
logger.lifecycle('Starting to upload snapshot bundle: {}', bundleFile.asFile.get())
|
||||
createHttpClient().withCloseable { client ->
|
||||
new ZipInputStream(bundleFile.asFile.get().newInputStream()).withCloseable { zis ->
|
||||
ZipEntry entry
|
||||
while ((entry = zis.nextEntry) != null) {
|
||||
if (entry.directory) {
|
||||
continue
|
||||
}
|
||||
def relativePath = entry.name
|
||||
def endpoint = "https://central.sonatype.com/repository/maven-snapshots/$relativePath"
|
||||
def request = HttpRequest.newBuilder(URI.create(endpoint))
|
||||
.header("Authorization", basicAuthorizationHeader)
|
||||
.header("Content-Type", "application/octet-stream")
|
||||
.PUT(HttpRequest.BodyPublishers.ofByteArray(zis.readAllBytes()))
|
||||
.build()
|
||||
def response = client.send(request, HttpResponse.BodyHandlers.ofString(UTF_8))
|
||||
logger.debug('Response status code: {}, body: {}', response.statusCode(), response.body())
|
||||
if (response.statusCode() != 200 && response.statusCode() != 201) {
|
||||
throw new RuntimeException("Failed to upload snapshot bundle: status: ${response.statusCode()} - entry: ${relativePath} - body : ${response.body()}")
|
||||
}
|
||||
logger.lifecycle('Uploaded snapshot entry: {}, status: {}', relativePath, response.statusCode())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String uploadReleaseBundle() {
|
||||
logger.lifecycle('Starting to upload release bundle: {}', bundleFile.asFile.get())
|
||||
createHttpClient().withCloseable { client ->
|
||||
// See https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html for more
|
||||
def boundary = "------haloformboundary${UUID.randomUUID().toString().replace('-', '')}"
|
||||
def crlf = "\r\n"
|
||||
def delimiter = "${crlf}--${boundary}"
|
||||
def endpoint = "https://central.sonatype.com/api/v1/publisher/upload?publishingType=${publishingType}"
|
||||
|
||||
def publishers = new ArrayList<HttpRequest.BodyPublisher>()
|
||||
publishers.add(ofString("${delimiter}${crlf}"))
|
||||
publishers.add(ofString("""\
|
||||
Content-Disposition: form-data; name="bundle"; filename="${bundleFile.get().asFile.name}"\
|
||||
${crlf}\
|
||||
"""))
|
||||
publishers.add(ofString("Content-Type: application/octet-stream${crlf}${crlf}"))
|
||||
publishers.add(ofFile(bundleFile.get().asFile.toPath()))
|
||||
publishers.add(ofString("${delimiter}--${crlf}"))
|
||||
|
||||
def request = HttpRequest.newBuilder(URI.create(endpoint))
|
||||
.header("Authorization", bearerAuthorizationHeader)
|
||||
.header("Content-Type", "multipart/form-data; boundary=${boundary}")
|
||||
.POST(HttpRequest.BodyPublishers.concat(publishers.toArray(HttpRequest.BodyPublisher[]::new)))
|
||||
.build()
|
||||
def response = client.send(request, HttpResponse.BodyHandlers.ofString(UTF_8))
|
||||
if (logger.debugEnabled) {
|
||||
logger.debug('Response status code: {}, body: {}', response.statusCode(), response.body())
|
||||
}
|
||||
if (response.statusCode() != 201 && response.statusCode() != 200) {
|
||||
throw new RuntimeException("Failed to upload bundle: status: ${response.statusCode()} - body: ${response.body()}")
|
||||
}
|
||||
def deploymentId = response.body().trim()
|
||||
logger.lifecycle('Uploaded release bundle successfully, deployment ID: {}', deploymentId)
|
||||
return deploymentId
|
||||
}
|
||||
}
|
||||
|
||||
@Internal
|
||||
String getBearerAuthorizationHeader() {
|
||||
def encoded = Base64.encoder.encodeToString("${credentials.get().username}:${credentials.get().password}".getBytes(UTF_8))
|
||||
return "Bearer ${encoded}"
|
||||
}
|
||||
|
||||
@Internal
|
||||
String getBasicAuthorizationHeader() {
|
||||
def encoded = Base64.encoder.encodeToString("${credentials.get().username}:${credentials.get().password}".getBytes(UTF_8))
|
||||
return "Basic ${encoded}"
|
||||
}
|
||||
|
||||
static HttpClient createHttpClient() {
|
||||
return HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofMinutes(1))
|
||||
.build()
|
||||
}
|
||||
|
||||
enum PublishingType {
|
||||
AUTOMATIC,
|
||||
USER_MANAGED,
|
||||
;
|
||||
}
|
||||
|
||||
enum DeploymentState {
|
||||
PENDING,
|
||||
VALIDATING,
|
||||
VALIDATED,
|
||||
PUBLISHING,
|
||||
PUBLISHED,
|
||||
FAILED,
|
||||
;
|
||||
}
|
||||
|
||||
}
|
|
@ -3,6 +3,8 @@ plugins {
|
|||
id 'signing'
|
||||
}
|
||||
|
||||
def internalRepo = layout.buildDirectory.dir('repos/internal')
|
||||
|
||||
publishing {
|
||||
publications.register('mavenJava', MavenPublication) {
|
||||
pom {
|
||||
|
@ -29,15 +31,9 @@ publishing {
|
|||
}
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
if (project.hasProperty("release")) {
|
||||
maven {
|
||||
name = 'ossrh'
|
||||
def releasesRepoUrl = 'https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/'
|
||||
def snapshotsRepoUrl = 'https://s01.oss.sonatype.org/content/repositories/snapshots/'
|
||||
url = version.endsWith('-SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl
|
||||
credentials(PasswordCredentials)
|
||||
}
|
||||
maven {
|
||||
name = 'internal'
|
||||
url = internalRepo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -45,3 +41,29 @@ publishing {
|
|||
signing {
|
||||
sign publishing.publications.mavenJava
|
||||
}
|
||||
|
||||
tasks.register('createBundle', Zip) {
|
||||
group = PublishingPlugin.PUBLISH_TASK_GROUP
|
||||
dependsOn tasks.named('publishAllPublicationsToInternalRepository')
|
||||
from(internalRepo)
|
||||
archiveBaseName = "${project.group}.${project.name}"
|
||||
}
|
||||
|
||||
tasks.register('uploadBundle', UploadBundleTask) {
|
||||
group = PublishingPlugin.PUBLISH_TASK_GROUP
|
||||
credentials = providers.credentials(PasswordCredentials, "portal")
|
||||
bundleFile = tasks.named('createBundle', Zip).map { it.archiveFile }.get()
|
||||
}
|
||||
|
||||
tasks.register('cleanInternalRepo', Delete) {
|
||||
group = PublishingPlugin.PUBLISH_TASK_GROUP
|
||||
delete internalRepo
|
||||
}
|
||||
|
||||
tasks.named('publishAllPublicationsToInternalRepository') {
|
||||
dependsOn tasks.named('cleanInternalRepo')
|
||||
}
|
||||
|
||||
tasks.named('publish') {
|
||||
dependsOn tasks.named('uploadBundle')
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue