Browse Source
- 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
12 changed files with 142 additions and 465 deletions
@ -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() |
||||
} |
||||
} |
||||
@ -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() |
||||
} |
||||
@ -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 |
||||
) |
||||
} |
||||
@ -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) |
||||
} |
||||
} |
||||
@ -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) |
||||
} |
||||
@ -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()}'") |
||||
} |
||||
} |
||||
} |
||||
@ -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…
Reference in new issue