Browse Source

Migrate to new Maven Central API (#5344)

- new API requires creating a zip
- API code is copied and adapted from
[compose-hot-reload](799b90b76f/buildSrc/src/main/kotlin/PublishToMavenCentralTask.kt (L39))
- [CI change](https://jetbrains.team/p/ui/reviews/31/timeline)

## Testing
1. Configure Maven Space Token in
https://public.jetbrains.space/p/compose/edit/applications, write it in
cli/build.gradle.kts

2. Use Maven Central token, write it in cli/gradle.properties

3.
```
..\gradlew reuploadArtifactsToMavenCentral --info --stacktrace -Pmaven.central.coordinates=org.jetbrains.compose*:*:1.8.0,org.jetbrains.compose.material:material-navigation*:2.9.0-beta02,org.jetbrains.compose.material3.adaptive:*:1.1.0 -Pmaven.central.deployName="Compose1.8.0 and associated libs" --rerun-tasks

..\gradlew reuploadArtifactsToMavenCentral --info --stacktrace -Pmaven.central.coordinates=org.jetbrains.skiko*:*:0.9.16 -Pmaven.central.deployName="Skiko 0.9.16" --rerun-tasks
```
downloads packages from Space, creates a zip, and uploads as a new
(non-published) deployment.

With failed state, because signing was disabled:
<img width="305" alt="image"
src="https://github.com/user-attachments/assets/0a32e185-e43b-4783-9145-84aba64d41c0"
/>
I will check on a valid uploading on Skiko after the merge

## Release Notes
N/A
pull/5352/head v1.9.0+dev2620
Igor Demin 6 months ago committed by GitHub
parent
commit
5f43a2afcc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 6
      ci/build-helpers/cli/build.gradle.kts
  2. 4
      ci/build-helpers/publishing/build.gradle.kts
  3. 1
      ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/DownloadFromSpaceTask.kt
  4. 11
      ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/MavenCentralProperties.kt
  5. 179
      ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/UploadToSonatypeTask.kt
  6. 77
      ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/AbstractRestApiClient.kt
  7. 12
      ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/Json.kt
  8. 115
      ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/ModuleValidator.kt
  9. 28
      ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/RequestError.kt
  10. 50
      ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/SonatypeApi.kt
  11. 89
      ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/SonatypeRestApiClient.kt
  12. 31
      ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/Xml.kt

6
ci/build-helpers/cli/build.gradle.kts

@ -49,12 +49,10 @@ val reuploadArtifactsToMavenCentral by tasks.registering(UploadToSonatypeTask::c @@ -49,12 +49,10 @@ val reuploadArtifactsToMavenCentral by tasks.registering(UploadToSonatypeTask::c
modulesToUpload.set(project.provider { readComposeModules(modulesFile, preparedArtifactsRoot) })
sonatypeServer.set("https://oss.sonatype.org")
user.set(mavenCentral.user)
password.set(mavenCentral.password)
autoCommitOnSuccess.set(mavenCentral.autoCommitOnSuccess)
stagingProfileName.set(mavenCentral.stage)
stagingDescription.set(mavenCentral.description)
deployName.set(mavenCentral.deployName)
publishAfterUploading.set(mavenCentral.publishAfterUploading)
}
fun readComposeModules(

4
ci/build-helpers/publishing/build.gradle.kts

@ -26,7 +26,9 @@ dependencies { @@ -26,7 +26,9 @@ dependencies {
val jacksonVersion = "2.12.5"
implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-xml:$jacksonVersion")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion")
implementation("io.ktor:ktor-client-okhttp:2.3.13")
implementation("io.ktor:ktor-client-core:3.1.3")
implementation("io.ktor:ktor-client-cio:3.1.3")
implementation("io.ktor:ktor-client-okhttp:3.1.3")
implementation("org.apache.tika:tika-parsers:1.24.1")
implementation("org.jsoup:jsoup:1.14.3")
implementation("org.jetbrains:space-sdk-jvm:2024.3-185883")

1
ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/DownloadFromSpaceTask.kt

@ -30,6 +30,7 @@ abstract class DownloadFromSpaceMavenRepoTask : DefaultTask() { @@ -30,6 +30,7 @@ abstract class DownloadFromSpaceMavenRepoTask : DefaultTask() {
}
private fun downloadArtifactsFromComposeDev(module: ModuleToUpload) {
logger.info("Downloading ${module.coordinate}...")
val groupUrl = module.groupId.replace(".", "/")
val filesListingDocument =

11
ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/MavenCentralProperties.kt

@ -13,11 +13,8 @@ class MavenCentralProperties(private val myProject: Project) { @@ -13,11 +13,8 @@ class MavenCentralProperties(private val myProject: Project) {
val coordinates: Provider<String> =
propertyProvider("maven.central.coordinates")
val stage: Provider<String> =
propertyProvider("maven.central.stage")
val description: Provider<String> =
propertyProvider("maven.central.description")
val deployName: Provider<String> =
propertyProvider("maven.central.deployName")
val user: Provider<String> =
propertyProvider("maven.central.user", envVar = "MAVEN_CENTRAL_USER")
@ -25,8 +22,8 @@ class MavenCentralProperties(private val myProject: Project) { @@ -25,8 +22,8 @@ class MavenCentralProperties(private val myProject: Project) {
val password: Provider<String> =
propertyProvider("maven.central.password", envVar = "MAVEN_CENTRAL_PASSWORD")
val autoCommitOnSuccess: Provider<Boolean> =
propertyProvider("maven.central.staging.close.after.upload", defaultValue = "false")
val publishAfterUploading: Provider<Boolean> =
propertyProvider("maven.central.publishAfterUploading", defaultValue = "false")
.map { it.toBoolean() }
val signArtifacts: Boolean

179
ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/UploadToSonatypeTask.kt

@ -5,18 +5,38 @@ @@ -5,18 +5,38 @@
package org.jetbrains.compose.internal.publishing
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.request.accept
import io.ktor.client.request.bearerAuth
import io.ktor.client.request.forms.*
import io.ktor.client.request.parameter
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.request.url
import io.ktor.client.statement.bodyAsText
import io.ktor.http.*
import io.ktor.http.headers
import io.ktor.utils.io.streams.asInput
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import org.gradle.api.DefaultTask
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.TaskAction
import org.jetbrains.compose.internal.publishing.utils.*
import java.io.FileInputStream
import java.io.FileOutputStream
import java.util.*
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
@Suppress("unused") // public api
abstract class UploadToSonatypeTask : DefaultTask() {
// the task must always re-run anyway, so all inputs can be declared Internal
@get:Internal
abstract val sonatypeServer: Property<String>
@get:Internal
abstract val user: Property<String>
@ -25,73 +45,134 @@ abstract class UploadToSonatypeTask : DefaultTask() { @@ -25,73 +45,134 @@ abstract class UploadToSonatypeTask : DefaultTask() {
abstract val password: Property<String>
@get:Internal
abstract val stagingProfileName: Property<String>
@get:Internal
abstract val stagingDescription: Property<String>
abstract val deployName: Property<String>
@get:Internal
abstract val autoCommitOnSuccess: Property<Boolean>
abstract val publishAfterUploading: Property<Boolean>
@get:Internal
abstract val modulesToUpload: ListProperty<ModuleToUpload>
@TaskAction
fun run() {
SonatypeRestApiClient(
sonatypeServer = sonatypeServer.get(),
user = user.get(),
password = password.get(),
logger = logger
).use { client -> run(client) }
}
private fun run(sonatype: SonatypeApi) {
val stagingProfiles = sonatype.stagingProfiles()
val stagingProfileName = stagingProfileName.get()
val stagingProfile = stagingProfiles.data.firstOrNull { it.name == stagingProfileName }
?: error(
"Cannot find staging profile '$stagingProfileName' among existing staging profiles: " +
stagingProfiles.data.joinToString { "'${it.name}'" }
)
val modules = modulesToUpload.get()
val deploymentBundle = createDeploymentBundle(modulesToUpload.get())
runBlocking {
HttpClient(CIO) {
install(HttpTimeout) {
requestTimeoutMillis = 5 * 60 * 1000 // 5 minutes
connectTimeoutMillis = 60 * 1000 // 1 minute
socketTimeoutMillis = 5 * 60 * 1000 // 5 minutes
}
install(HttpRequestRetry) {
retryOnExceptionOrServerErrors(maxRetries = 5)
exponentialDelay()
}
}.use { client ->
client.publish(deploymentBundle)
}
}
}
validate(stagingProfile, modules)
private fun createDeploymentBundle(modules: List<ModuleToUpload>): InputProvider {
val zipFile = project.buildDir.resolve("publishing/compose-deploy.zip")
zipFile.parentFile.mkdirs()
val stagingRepo = sonatype.createStagingRepo(
stagingProfile, stagingDescription.get()
)
try {
for (module in modules) {
sonatype.upload(stagingRepo, module)
ZipOutputStream(FileOutputStream(zipFile)).use { zipOut ->
val sourcesToDestinations = modules.map { it.localDir to it.mavenDirectory() }
for ((sourceDir, destDir) in sourcesToDestinations) {
val files = sourceDir.listFiles() ?: continue
for (file in files) {
if (file.isFile) {
val entryPath = "$destDir/${file.name}"
val entry = ZipEntry(entryPath)
zipOut.putNextEntry(entry)
file.inputStream().use { input ->
input.copyTo(zipOut)
}
if (autoCommitOnSuccess.get()) {
sonatype.closeStagingRepo(stagingRepo)
zipOut.closeEntry()
}
} catch (e: Exception) {
throw e
}
}
}
logger.info("Zip bundle is created at $zipFile")
private fun validate(stagingProfile: StagingProfile, modules: List<ModuleToUpload>) {
val validationIssues = arrayListOf<Pair<ModuleToUpload, ModuleValidator.Status.Error>>()
for (module in modules) {
val status = ModuleValidator(stagingProfile, module).validate()
if (status is ModuleValidator.Status.Error) {
validationIssues.add(module to status)
return InputProvider(zipFile.length()) {
FileInputStream(zipFile).asInput()
}
}
if (validationIssues.isNotEmpty()) {
val message = buildString {
appendLine("Some modules violate Maven Central requirements:")
for ((module, status) in validationIssues) {
appendLine("* ${module.coordinate} (files: ${module.localDir})")
for (error in status.errors) {
appendLine(" * $error")
// By the doc https://central.sonatype.org/publish/publish-portal-api/
private suspend fun HttpClient.publish(deploymentBundle: InputProvider) {
val publishAfterUploading = publishAfterUploading.get()
val bearerToken = Base64.getEncoder().encode(
"${user.get()}:${password.get()}".toByteArray()
).toString(Charsets.UTF_8)
logger.info("Start uploading ${deployName.get()}")
val response = submitForm {
url("https://central.sonatype.com/api/v1/publisher/upload")
parameter("name", deployName.get())
parameter("publishingType", if (publishAfterUploading) "AUTOMATIC" else "USER_MANAGED")
bearerAuth(bearerToken)
setBody(
MultiPartFormDataContent(
formData {
append("bundle", deploymentBundle, headers {
append(HttpHeaders.ContentType, ContentType.Application.OctetStream.contentType)
append(HttpHeaders.ContentDisposition, "filename=\"bundle.zip\"")
})
}
)
)
var lastUploadLogTime = 0L
onUpload { bytesSentTotal, contentLength ->
val currentTime = System.currentTimeMillis()
if (currentTime - lastUploadLogTime >= 5000) { // 5 seconds debounce
logger.info("Sent $bytesSentTotal bytes from $contentLength")
lastUploadLogTime = currentTime
}
}
error(message)
}
if (response.status != HttpStatusCode.Created) {
error("Deployment failed (${response.status}):\n ${response.bodyAsText()}")
}
val deploymentId = response.bodyAsText().trim()
logger.info("Successfully uploaded ${deploymentId.take(4)}")
val endStatus = if (publishAfterUploading) "PUBLISHED" else "VALIDATED"
while (true) {
logger.info("Checking the status of the deployment...")
val statusResponse = post {
bearerAuth(bearerToken)
accept(ContentType.Application.Json)
url("https://central.sonatype.com/api/v1/publisher/status")
parameter("id", deploymentId)
}
if (statusResponse.status != HttpStatusCode.OK) {
error("Deployment failed (${statusResponse.status}):\n ${statusResponse.bodyAsText()}")
}
if (statusResponse.bodyAsText().contains(endStatus)) break
if (statusResponse.bodyAsText().contains("FAILED")) {
error("Deployment failed (${statusResponse.status}):\n ${statusResponse.bodyAsText()}")
}
delay(5000)
}
logger.info("Successfully published")
}
private fun ModuleToUpload.mavenDirectory() =
groupId.replace(".", "/") + "/" + artifactId + "/" + version
}

77
ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/AbstractRestApiClient.kt

@ -1,77 +0,0 @@ @@ -1,77 +0,0 @@
/*
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.internal.publishing.utils
import okhttp3.*
import okhttp3.internal.http.RealResponseBody
import okio.Buffer
import org.gradle.api.logging.Logger
import java.net.URL
import java.time.Duration
import java.util.concurrent.atomic.AtomicLong
internal class RestApiClient(
private val serverUrl: String,
private val user: String,
private val password: String,
private val logger: Logger,
) : AutoCloseable {
private val okClient by lazy {
OkHttpClient.Builder()
.readTimeout(Duration.ofMinutes(1))
.build()
}
fun buildRequest(urlPath: String, configure: Request.Builder.() -> Unit): Request =
Request.Builder().apply {
addHeader("Authorization", Credentials.basic(user, password))
url(URL("$serverUrl/$urlPath"))
configure()
}.build()
fun <T> execute(
request: Request,
retries: Int = 5,
delaySec: Long = 10,
processResponse: (ResponseBody) -> T
): T {
val message = "Remote request #${globalRequestCounter.incrementAndGet()}"
val startTimeNs = System.nanoTime()
logger.info("$message: ${request.method} '${request.url}'")
val delayMs = delaySec * 1000
for (i in 1..retries) {
try {
return okClient.newCall(request).execute().use { response ->
val endTimeNs = System.nanoTime()
logger.info("$message: finished in ${(endTimeNs - startTimeNs)/1_000_000} ms")
if (!response.isSuccessful)
throw RequestError(request, response)
val responseBody = response.body ?: RealResponseBody(null, 0, Buffer())
processResponse(responseBody)
}
} catch (e: Exception) {
if (i == retries) {
throw RuntimeException("$message: failed all $retries attempts, see nested exception for details", e)
}
logger.info("$message: retry #$i of $retries failed. Retrying in $delayMs ms\n${e.message}")
Thread.sleep(delayMs)
}
}
error("Unreachable")
}
override fun close() {
okClient.connectionPool.evictAll()
}
companion object {
private val globalRequestCounter = AtomicLong()
}
}

12
ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/Json.kt

@ -1,12 +0,0 @@ @@ -1,12 +0,0 @@
/*
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.internal.publishing.utils
import okhttp3.MediaType.Companion.toMediaType
internal object Json {
val mediaType = "application/json".toMediaType()
}

115
ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/ModuleValidator.kt

@ -1,115 +0,0 @@ @@ -1,115 +0,0 @@
/*
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.internal.publishing.utils
import com.fasterxml.jackson.annotation.JsonRootName
import org.jetbrains.compose.internal.publishing.ModuleToUpload
import java.io.File
internal class ModuleValidator(
private val stagingProfile: StagingProfile,
private val module: ModuleToUpload,
) {
private val errors = arrayListOf<String>()
private var status: Status? = null
sealed class Status {
object OK : Status()
class Error(val errors: List<String>) : Status()
}
fun validate(): Status {
if (status == null) {
validateImpl()
status = if (errors.isEmpty()) Status.OK
else Status.Error(errors)
}
return status!!
}
private fun validateImpl() {
if (!module.groupId.startsWith(stagingProfile.name)) {
errors.add("Module's group id '${module.groupId}' does not match staging repo '${stagingProfile.name}'")
}
val pomFile = artifactFile(extension = "pom")
val pom = when {
pomFile.exists() ->
try {
// todo: validate POM
Xml.deserialize<Pom>(pomFile.readText())
} catch (e: Exception) {
errors.add("Cannot deserialize $pomFile: $e")
null
}
else -> null
}
val mandatoryFiles = arrayListOf(pomFile)
if (pom != null && pom.packaging != "pom") {
mandatoryFiles.add(artifactFile(extension = pom.packaging ?: "jar"))
mandatoryFiles.add(artifactFile(extension = "jar", classifier = "sources"))
mandatoryFiles.add(artifactFile(extension = "jar", classifier = "javadoc"))
}
val nonExistingFiles = mandatoryFiles.filter { !it.exists() }
if (nonExistingFiles.isNotEmpty()) {
errors.add("Some necessary files do not exist: [${nonExistingFiles.map { it.name }.joinToString()}]")
}
// signatures and checksums should not be signed themselves
val skipSignatureCheckExtensions = setOf("asc", "md5", "sha1", "sha256", "sha512")
val unsignedFiles = module.listFiles()
.filter {
it.extension !in skipSignatureCheckExtensions && !it.resolveSibling(it.name + ".asc").exists()
}
if (unsignedFiles.isNotEmpty()) {
errors.add("Some files are not signed: [${unsignedFiles.map { it.name }.joinToString()}]")
}
}
private fun artifactFile(extension: String, classifier: String? = null): File {
val fileName = buildString {
append("${module.artifactId}-${module.version}")
if (classifier != null)
append("-$classifier")
append(".$extension")
}
return module.localDir.resolve(fileName)
}
}
@JsonRootName("project")
private data class Pom(
var groupId: String? = null,
var artifactId: String? = null,
var packaging: String? = null,
var name: String? = null,
var description: String? = null,
var url: String? = null,
var scm: Scm? = null,
var licenses: List<License>? = null,
var developers: List<Developer>? = null,
) {
internal data class Scm(
var connection: String?,
var developerConnection: String?,
var url: String?,
)
internal data class License(
var name: String? = null,
var url: String? = null
)
internal data class Developer(
var name: String? = null,
var organization: String? = null,
var organizationUrl: String? = null
)
}

28
ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/RequestError.kt

@ -1,28 +0,0 @@ @@ -1,28 +0,0 @@
/*
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.internal.publishing.utils
import okhttp3.Request
import okhttp3.Response
internal class RequestError(
val request: Request,
val response: Response,
responseBody: String
) : RuntimeException("${request.url}: returned ${response.code}\n${responseBody.trim()}")
internal fun RequestError(request: Request, response: Response): RequestError {
var responseBodyException: Throwable? = null
val responseBody = try {
response.body?.string() ?: ""
} catch (t: Throwable) {
responseBodyException = t
""
}
return RequestError(request, response, responseBody).apply {
if (responseBodyException != null) addSuppressed(responseBodyException)
}
}

50
ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/SonatypeApi.kt

@ -1,50 +0,0 @@ @@ -1,50 +0,0 @@
/*
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.internal.publishing.utils
import com.fasterxml.jackson.annotation.JsonRootName
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper
import org.jetbrains.compose.internal.publishing.ModuleToUpload
interface SonatypeApi {
fun upload(repo: StagingRepo, module: ModuleToUpload)
fun stagingProfiles(): StagingProfiles
fun createStagingRepo(profile: StagingProfile, description: String): StagingRepo
fun closeStagingRepo(repo: StagingRepo)
}
@JsonRootName("stagingProfile")
data class StagingProfile(
var id: String = "",
var name: String = "",
)
@JsonRootName("stagingProfiles")
class StagingProfiles(
@JacksonXmlElementWrapper
var data: List<StagingProfile>
)
data class StagingRepo(
val id: String,
val description: String,
val profile: StagingProfile
) {
constructor(
response: PromoteResponse,
profile: StagingProfile
) : this(
id = response.data.stagedRepositoryId!!,
description = response.data.description,
profile = profile
)
@JsonRootName("promoteRequest")
data class PromoteRequest(var data: PromoteData)
@JsonRootName("promoteResponse")
data class PromoteResponse(var data: PromoteData)
data class PromoteData(var stagedRepositoryId: String? = null, var description: String)
}

89
ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/SonatypeRestApiClient.kt

@ -1,89 +0,0 @@ @@ -1,89 +0,0 @@
/*
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.internal.publishing.utils
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Request
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.ResponseBody
import org.apache.tika.Tika
import org.gradle.api.logging.Logger
import org.jetbrains.compose.internal.publishing.ModuleToUpload
import java.io.Closeable
import java.io.File
// https://support.sonatype.com/hc/en-us/articles/213465868-Uploading-to-a-Staging-Repository-via-REST-API
class SonatypeRestApiClient(
sonatypeServer: String,
user: String,
password: String,
private val logger: Logger,
) : SonatypeApi, Closeable {
private val client = RestApiClient(sonatypeServer, user, password, logger)
private fun buildRequest(urlPath: String, builder: Request.Builder.() -> Unit): Request =
client.buildRequest(urlPath, builder)
private fun <T> Request.execute(processResponse: (ResponseBody) -> T): T =
client.execute(this, processResponse = processResponse)
override fun close() {
client.close()
}
override fun upload(repo: StagingRepo, module: ModuleToUpload) {
for (file in module.localDir.listFiles()!!) {
uploadFile(repo, module, file)
}
}
private fun uploadFile(repo: StagingRepo, module: ModuleToUpload, file: File) {
val fileType = Tika().detect(file.name)
logger.info("Uploading $file (detected type='$fileType', length=${file.length()})")
val deployUrl = "service/local/staging/deployByRepositoryId/${repo.id}"
val groupUrl = module.groupId.replace(".", "/")
val coordinateUrl = "$groupUrl/${module.artifactId}/${module.version}"
val uploadUrlPath = "$deployUrl/$coordinateUrl/${file.name}"
buildRequest(uploadUrlPath) {
header("Content-type", fileType)
put(file.asRequestBody(fileType.toMediaTypeOrNull()))
}.execute { }
}
override fun stagingProfiles(): StagingProfiles =
buildRequest("service/local/staging/profiles") {
get()
}.execute { responseBody ->
Xml.deserialize(responseBody.string())
}
override fun createStagingRepo(profile: StagingProfile, description: String): StagingRepo {
logger.info("Creating sonatype staging repository for `${profile.id}` with description `$description`")
val response =
buildRequest("service/local/staging/profiles/${profile.id}/start") {
val promoteRequest = StagingRepo.PromoteRequest(
StagingRepo.PromoteData(description = description)
)
post(Xml.serialize(promoteRequest).toRequestBody(Xml.mediaType))
}.execute { responseBody ->
Xml.deserialize<StagingRepo.PromoteResponse>(responseBody.string())
}
return StagingRepo(response, profile)
}
override fun closeStagingRepo(repo: StagingRepo) {
logger.info("Closing repository '${repo.id}'")
buildRequest("service/local/staging/bulk/close") {
val request = "{\"data\":{\"stagedRepositoryIds\":[\"${repo.id}\"]}}"
post(request.toRequestBody(Json.mediaType))
.addHeader("Accept", Json.mediaType.toString())
}.execute { responseBody ->
logger.info("Finished closing repository '${repo.id}': '${responseBody.string()}'")
}
}
}

31
ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/Xml.kt

@ -1,31 +0,0 @@ @@ -1,31 +0,0 @@
/*
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.internal.publishing.utils
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.MapperFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.dataformat.xml.JacksonXmlModule
import com.fasterxml.jackson.dataformat.xml.XmlMapper
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import okhttp3.MediaType.Companion.toMediaType
internal object Xml {
val mediaType = "application/xml".toMediaType()
fun serialize(value: Any): String =
kotlinXmlMapper.writeValueAsString(value)
inline fun <reified T> deserialize(xml: String): T =
kotlinXmlMapper.readValue(xml, T::class.java)
private val kotlinXmlMapper: ObjectMapper =
XmlMapper(JacksonXmlModule().apply {
setDefaultUseWrapper(false)
}).registerKotlinModule()
.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
}
Loading…
Cancel
Save