diff --git a/ci/build-helpers/cli/build.gradle.kts b/ci/build-helpers/cli/build.gradle.kts index c9e76d0f79..7240d1d8fe 100644 --- a/ci/build-helpers/cli/build.gradle.kts +++ b/ci/build-helpers/cli/build.gradle.kts @@ -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( diff --git a/ci/build-helpers/publishing/build.gradle.kts b/ci/build-helpers/publishing/build.gradle.kts index aeef16f91a..d30558c283 100644 --- a/ci/build-helpers/publishing/build.gradle.kts +++ b/ci/build-helpers/publishing/build.gradle.kts @@ -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") diff --git a/ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/DownloadFromSpaceTask.kt b/ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/DownloadFromSpaceTask.kt index 004e6e882f..af9dd6bb50 100644 --- a/ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/DownloadFromSpaceTask.kt +++ b/ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/DownloadFromSpaceTask.kt @@ -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 = diff --git a/ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/MavenCentralProperties.kt b/ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/MavenCentralProperties.kt index c464eeaaa4..d6dd612eb1 100644 --- a/ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/MavenCentralProperties.kt +++ b/ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/MavenCentralProperties.kt @@ -13,11 +13,8 @@ class MavenCentralProperties(private val myProject: Project) { val coordinates: Provider = propertyProvider("maven.central.coordinates") - val stage: Provider = - propertyProvider("maven.central.stage") - - val description: Provider = - propertyProvider("maven.central.description") + val deployName: Provider = + propertyProvider("maven.central.deployName") val user: Provider = propertyProvider("maven.central.user", envVar = "MAVEN_CENTRAL_USER") @@ -25,8 +22,8 @@ class MavenCentralProperties(private val myProject: Project) { val password: Provider = propertyProvider("maven.central.password", envVar = "MAVEN_CENTRAL_PASSWORD") - val autoCommitOnSuccess: Provider = - propertyProvider("maven.central.staging.close.after.upload", defaultValue = "false") + val publishAfterUploading: Provider = + propertyProvider("maven.central.publishAfterUploading", defaultValue = "false") .map { it.toBoolean() } val signArtifacts: Boolean diff --git a/ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/UploadToSonatypeTask.kt b/ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/UploadToSonatypeTask.kt index 7602814d89..b0cee1deb8 100644 --- a/ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/UploadToSonatypeTask.kt +++ b/ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/UploadToSonatypeTask.kt @@ -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 @get:Internal abstract val user: Property @@ -25,73 +45,134 @@ abstract class UploadToSonatypeTask : DefaultTask() { abstract val password: Property @get:Internal - abstract val stagingProfileName: Property - - @get:Internal - abstract val stagingDescription: Property + abstract val deployName: Property @get:Internal - abstract val autoCommitOnSuccess: Property + abstract val publishAfterUploading: Property @get:Internal abstract val modulesToUpload: ListProperty @TaskAction fun run() { - SonatypeRestApiClient( - sonatypeServer = sonatypeServer.get(), - user = user.get(), - password = password.get(), - logger = logger - ).use { client -> run(client) } + 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) + } + } } - 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() + private fun createDeploymentBundle(modules: List): InputProvider { + val zipFile = project.buildDir.resolve("publishing/compose-deploy.zip") + zipFile.parentFile.mkdirs() - validate(stagingProfile, modules) + ZipOutputStream(FileOutputStream(zipFile)).use { zipOut -> + val sourcesToDestinations = modules.map { it.localDir to it.mavenDirectory() } - val stagingRepo = sonatype.createStagingRepo( - stagingProfile, stagingDescription.get() - ) - try { - for (module in modules) { - sonatype.upload(stagingRepo, module) - } - if (autoCommitOnSuccess.get()) { - sonatype.closeStagingRepo(stagingRepo) + 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) + } + zipOut.closeEntry() + } + } } - } catch (e: Exception) { - throw e } - } - private fun validate(stagingProfile: StagingProfile, modules: List) { - val validationIssues = arrayListOf>() - for (module in modules) { - val status = ModuleValidator(stagingProfile, module).validate() - if (status is ModuleValidator.Status.Error) { - validationIssues.add(module to status) - } + logger.info("Zip bundle is created at $zipFile") + + 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") } -} \ No newline at end of file + + private fun ModuleToUpload.mavenDirectory() = + groupId.replace(".", "/") + "/" + artifactId + "/" + version +} diff --git a/ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/AbstractRestApiClient.kt b/ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/AbstractRestApiClient.kt deleted file mode 100644 index 5376e8f74d..0000000000 --- a/ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/AbstractRestApiClient.kt +++ /dev/null @@ -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 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() - } -} diff --git a/ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/Json.kt b/ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/Json.kt deleted file mode 100644 index 39efbdc0b9..0000000000 --- a/ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/Json.kt +++ /dev/null @@ -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() -} \ No newline at end of file diff --git a/ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/ModuleValidator.kt b/ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/ModuleValidator.kt deleted file mode 100644 index 812a7435d6..0000000000 --- a/ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/ModuleValidator.kt +++ /dev/null @@ -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() - private var status: Status? = null - - sealed class Status { - object OK : Status() - class Error(val errors: List) : 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(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? = null, - var developers: List? = 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 - ) -} diff --git a/ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/RequestError.kt b/ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/RequestError.kt deleted file mode 100644 index c5433cd58c..0000000000 --- a/ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/RequestError.kt +++ /dev/null @@ -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) - } -} \ No newline at end of file diff --git a/ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/SonatypeApi.kt b/ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/SonatypeApi.kt deleted file mode 100644 index 59a0ffa4c9..0000000000 --- a/ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/SonatypeApi.kt +++ /dev/null @@ -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 -) - -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) -} diff --git a/ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/SonatypeRestApiClient.kt b/ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/SonatypeRestApiClient.kt deleted file mode 100644 index 029bb3606a..0000000000 --- a/ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/SonatypeRestApiClient.kt +++ /dev/null @@ -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 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(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()}'") - } - } -} diff --git a/ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/Xml.kt b/ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/Xml.kt deleted file mode 100644 index 388224f854..0000000000 --- a/ci/build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/Xml.kt +++ /dev/null @@ -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 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) -}