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 {
|
toolchain {
|
||||||
languageVersion = JavaLanguageVersion.of(21)
|
languageVersion = JavaLanguageVersion.of(21)
|
||||||
}
|
}
|
||||||
|
withJavadocJar()
|
||||||
|
withSourcesJar()
|
||||||
|
}
|
||||||
|
|
||||||
|
jar {
|
||||||
|
manifest {
|
||||||
|
attributes(
|
||||||
|
'Implementation-Title': project.name,
|
||||||
|
'Implementation-Version': project.version,
|
||||||
|
'Implementation-Vendor': 'Halo Project',
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
|
@ -99,11 +111,6 @@ dependencies {
|
||||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||||
}
|
}
|
||||||
|
|
||||||
java {
|
|
||||||
withSourcesJar()
|
|
||||||
withJavadocJar()
|
|
||||||
}
|
|
||||||
|
|
||||||
publishing {
|
publishing {
|
||||||
publications.named('mavenJava', MavenPublication) {
|
publications.named('mavenJava', MavenPublication) {
|
||||||
from components.java
|
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'
|
id 'signing'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def internalRepo = layout.buildDirectory.dir('repos/internal')
|
||||||
|
|
||||||
publishing {
|
publishing {
|
||||||
publications.register('mavenJava', MavenPublication) {
|
publications.register('mavenJava', MavenPublication) {
|
||||||
pom {
|
pom {
|
||||||
|
@ -29,15 +31,9 @@ publishing {
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenLocal()
|
|
||||||
if (project.hasProperty("release")) {
|
|
||||||
maven {
|
maven {
|
||||||
name = 'ossrh'
|
name = 'internal'
|
||||||
def releasesRepoUrl = 'https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/'
|
url = internalRepo
|
||||||
def snapshotsRepoUrl = 'https://s01.oss.sonatype.org/content/repositories/snapshots/'
|
|
||||||
url = version.endsWith('-SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl
|
|
||||||
credentials(PasswordCredentials)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,3 +41,29 @@ publishing {
|
||||||
signing {
|
signing {
|
||||||
sign publishing.publications.mavenJava
|
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