Skip to content

Commit 5f43a2a

Browse files
authored
Migrate to new Maven Central API (#5344)
- new API requires creating a zip - API code is copied and adapted from [compose-hot-reload](https://github.com/JetBrains/compose-hot-reload/blob/799b90b76ffc6d93207c25626be53ecb0cb86a63/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
1 parent 0a68f00 commit 5f43a2a

12 files changed

Lines changed: 142 additions & 465 deletions

File tree

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,10 @@ val reuploadArtifactsToMavenCentral by tasks.registering(UploadToSonatypeTask::c
4949

5050
modulesToUpload.set(project.provider { readComposeModules(modulesFile, preparedArtifactsRoot) })
5151

52-
sonatypeServer.set("https://oss.sonatype.org")
5352
user.set(mavenCentral.user)
5453
password.set(mavenCentral.password)
55-
autoCommitOnSuccess.set(mavenCentral.autoCommitOnSuccess)
56-
stagingProfileName.set(mavenCentral.stage)
57-
stagingDescription.set(mavenCentral.description)
54+
deployName.set(mavenCentral.deployName)
55+
publishAfterUploading.set(mavenCentral.publishAfterUploading)
5856
}
5957

6058
fun readComposeModules(

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ dependencies {
2626
val jacksonVersion = "2.12.5"
2727
implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-xml:$jacksonVersion")
2828
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion")
29-
implementation("io.ktor:ktor-client-okhttp:2.3.13")
29+
implementation("io.ktor:ktor-client-core:3.1.3")
30+
implementation("io.ktor:ktor-client-cio:3.1.3")
31+
implementation("io.ktor:ktor-client-okhttp:3.1.3")
3032
implementation("org.apache.tika:tika-parsers:1.24.1")
3133
implementation("org.jsoup:jsoup:1.14.3")
3234
implementation("org.jetbrains:space-sdk-jvm:2024.3-185883")

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ abstract class DownloadFromSpaceMavenRepoTask : DefaultTask() {
3030
}
3131

3232
private fun downloadArtifactsFromComposeDev(module: ModuleToUpload) {
33+
logger.info("Downloading ${module.coordinate}...")
3334
val groupUrl = module.groupId.replace(".", "/")
3435

3536
val filesListingDocument =

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

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,17 @@ class MavenCentralProperties(private val myProject: Project) {
1313
val coordinates: Provider<String> =
1414
propertyProvider("maven.central.coordinates")
1515

16-
val stage: Provider<String> =
17-
propertyProvider("maven.central.stage")
18-
19-
val description: Provider<String> =
20-
propertyProvider("maven.central.description")
16+
val deployName: Provider<String> =
17+
propertyProvider("maven.central.deployName")
2118

2219
val user: Provider<String> =
2320
propertyProvider("maven.central.user", envVar = "MAVEN_CENTRAL_USER")
2421

2522
val password: Provider<String> =
2623
propertyProvider("maven.central.password", envVar = "MAVEN_CENTRAL_PASSWORD")
2724

28-
val autoCommitOnSuccess: Provider<Boolean> =
29-
propertyProvider("maven.central.staging.close.after.upload", defaultValue = "false")
25+
val publishAfterUploading: Provider<Boolean> =
26+
propertyProvider("maven.central.publishAfterUploading", defaultValue = "false")
3027
.map { it.toBoolean() }
3128

3229
val signArtifacts: Boolean

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

Lines changed: 132 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,38 @@
55

66
package org.jetbrains.compose.internal.publishing
77

8+
import io.ktor.client.*
9+
import io.ktor.client.engine.cio.*
10+
import io.ktor.client.plugins.*
11+
import io.ktor.client.plugins.HttpTimeout
12+
import io.ktor.client.request.accept
13+
import io.ktor.client.request.bearerAuth
14+
import io.ktor.client.request.forms.*
15+
import io.ktor.client.request.parameter
16+
import io.ktor.client.request.post
17+
import io.ktor.client.request.setBody
18+
import io.ktor.client.request.url
19+
import io.ktor.client.statement.bodyAsText
20+
import io.ktor.http.*
21+
import io.ktor.http.headers
22+
import io.ktor.utils.io.streams.asInput
23+
import kotlinx.coroutines.delay
24+
import kotlinx.coroutines.runBlocking
825
import org.gradle.api.DefaultTask
926
import org.gradle.api.provider.ListProperty
1027
import org.gradle.api.provider.Property
1128
import org.gradle.api.tasks.Internal
1229
import org.gradle.api.tasks.TaskAction
1330
import org.jetbrains.compose.internal.publishing.utils.*
31+
import java.io.FileInputStream
32+
import java.io.FileOutputStream
33+
import java.util.*
34+
import java.util.zip.ZipEntry
35+
import java.util.zip.ZipOutputStream
1436

1537
@Suppress("unused") // public api
1638
abstract class UploadToSonatypeTask : DefaultTask() {
1739
// the task must always re-run anyway, so all inputs can be declared Internal
18-
@get:Internal
19-
abstract val sonatypeServer: Property<String>
2040

2141
@get:Internal
2242
abstract val user: Property<String>
@@ -25,73 +45,134 @@ abstract class UploadToSonatypeTask : DefaultTask() {
2545
abstract val password: Property<String>
2646

2747
@get:Internal
28-
abstract val stagingProfileName: Property<String>
29-
30-
@get:Internal
31-
abstract val stagingDescription: Property<String>
48+
abstract val deployName: Property<String>
3249

3350
@get:Internal
34-
abstract val autoCommitOnSuccess: Property<Boolean>
51+
abstract val publishAfterUploading: Property<Boolean>
3552

3653
@get:Internal
3754
abstract val modulesToUpload: ListProperty<ModuleToUpload>
3855

3956
@TaskAction
4057
fun run() {
41-
SonatypeRestApiClient(
42-
sonatypeServer = sonatypeServer.get(),
43-
user = user.get(),
44-
password = password.get(),
45-
logger = logger
46-
).use { client -> run(client) }
58+
val deploymentBundle = createDeploymentBundle(modulesToUpload.get())
59+
runBlocking {
60+
HttpClient(CIO) {
61+
install(HttpTimeout) {
62+
requestTimeoutMillis = 5 * 60 * 1000 // 5 minutes
63+
connectTimeoutMillis = 60 * 1000 // 1 minute
64+
socketTimeoutMillis = 5 * 60 * 1000 // 5 minutes
65+
}
66+
install(HttpRequestRetry) {
67+
retryOnExceptionOrServerErrors(maxRetries = 5)
68+
exponentialDelay()
69+
}
70+
}.use { client ->
71+
client.publish(deploymentBundle)
72+
}
73+
}
4774
}
4875

49-
private fun run(sonatype: SonatypeApi) {
50-
val stagingProfiles = sonatype.stagingProfiles()
51-
val stagingProfileName = stagingProfileName.get()
52-
val stagingProfile = stagingProfiles.data.firstOrNull { it.name == stagingProfileName }
53-
?: error(
54-
"Cannot find staging profile '$stagingProfileName' among existing staging profiles: " +
55-
stagingProfiles.data.joinToString { "'${it.name}'" }
56-
)
57-
val modules = modulesToUpload.get()
76+
private fun createDeploymentBundle(modules: List<ModuleToUpload>): InputProvider {
77+
val zipFile = project.buildDir.resolve("publishing/compose-deploy.zip")
78+
zipFile.parentFile.mkdirs()
5879

59-
validate(stagingProfile, modules)
80+
ZipOutputStream(FileOutputStream(zipFile)).use { zipOut ->
81+
val sourcesToDestinations = modules.map { it.localDir to it.mavenDirectory() }
6082

61-
val stagingRepo = sonatype.createStagingRepo(
62-
stagingProfile, stagingDescription.get()
63-
)
64-
try {
65-
for (module in modules) {
66-
sonatype.upload(stagingRepo, module)
67-
}
68-
if (autoCommitOnSuccess.get()) {
69-
sonatype.closeStagingRepo(stagingRepo)
83+
for ((sourceDir, destDir) in sourcesToDestinations) {
84+
val files = sourceDir.listFiles() ?: continue
85+
86+
for (file in files) {
87+
if (file.isFile) {
88+
val entryPath = "$destDir/${file.name}"
89+
val entry = ZipEntry(entryPath)
90+
zipOut.putNextEntry(entry)
91+
file.inputStream().use { input ->
92+
input.copyTo(zipOut)
93+
}
94+
zipOut.closeEntry()
95+
}
96+
}
7097
}
71-
} catch (e: Exception) {
72-
throw e
7398
}
74-
}
7599

76-
private fun validate(stagingProfile: StagingProfile, modules: List<ModuleToUpload>) {
77-
val validationIssues = arrayListOf<Pair<ModuleToUpload, ModuleValidator.Status.Error>>()
78-
for (module in modules) {
79-
val status = ModuleValidator(stagingProfile, module).validate()
80-
if (status is ModuleValidator.Status.Error) {
81-
validationIssues.add(module to status)
82-
}
100+
logger.info("Zip bundle is created at $zipFile")
101+
102+
return InputProvider(zipFile.length()) {
103+
FileInputStream(zipFile).asInput()
83104
}
84-
if (validationIssues.isNotEmpty()) {
85-
val message = buildString {
86-
appendLine("Some modules violate Maven Central requirements:")
87-
for ((module, status) in validationIssues) {
88-
appendLine("* ${module.coordinate} (files: ${module.localDir})")
89-
for (error in status.errors) {
90-
appendLine(" * $error")
105+
}
106+
107+
// By the doc https://central.sonatype.org/publish/publish-portal-api/
108+
private suspend fun HttpClient.publish(deploymentBundle: InputProvider) {
109+
val publishAfterUploading = publishAfterUploading.get()
110+
111+
val bearerToken = Base64.getEncoder().encode(
112+
"${user.get()}:${password.get()}".toByteArray()
113+
).toString(Charsets.UTF_8)
114+
115+
logger.info("Start uploading ${deployName.get()}")
116+
117+
val response = submitForm {
118+
url("https://central.sonatype.com/api/v1/publisher/upload")
119+
parameter("name", deployName.get())
120+
parameter("publishingType", if (publishAfterUploading) "AUTOMATIC" else "USER_MANAGED")
121+
bearerAuth(bearerToken)
122+
setBody(
123+
MultiPartFormDataContent(
124+
formData {
125+
append("bundle", deploymentBundle, headers {
126+
append(HttpHeaders.ContentType, ContentType.Application.OctetStream.contentType)
127+
append(HttpHeaders.ContentDisposition, "filename=\"bundle.zip\"")
128+
})
91129
}
130+
)
131+
)
132+
var lastUploadLogTime = 0L
133+
onUpload { bytesSentTotal, contentLength ->
134+
val currentTime = System.currentTimeMillis()
135+
if (currentTime - lastUploadLogTime >= 5000) { // 5 seconds debounce
136+
logger.info("Sent $bytesSentTotal bytes from $contentLength")
137+
lastUploadLogTime = currentTime
92138
}
93139
}
94-
error(message)
95140
}
141+
142+
if (response.status != HttpStatusCode.Created) {
143+
error("Deployment failed (${response.status}):\n ${response.bodyAsText()}")
144+
}
145+
146+
val deploymentId = response.bodyAsText().trim()
147+
logger.info("Successfully uploaded ${deploymentId.take(4)}")
148+
149+
val endStatus = if (publishAfterUploading) "PUBLISHED" else "VALIDATED"
150+
151+
while (true) {
152+
logger.info("Checking the status of the deployment...")
153+
154+
val statusResponse = post {
155+
bearerAuth(bearerToken)
156+
accept(ContentType.Application.Json)
157+
url("https://central.sonatype.com/api/v1/publisher/status")
158+
parameter("id", deploymentId)
159+
}
160+
161+
if (statusResponse.status != HttpStatusCode.OK) {
162+
error("Deployment failed (${statusResponse.status}):\n ${statusResponse.bodyAsText()}")
163+
}
164+
165+
if (statusResponse.bodyAsText().contains(endStatus)) break
166+
if (statusResponse.bodyAsText().contains("FAILED")) {
167+
error("Deployment failed (${statusResponse.status}):\n ${statusResponse.bodyAsText()}")
168+
}
169+
170+
delay(5000)
171+
}
172+
173+
logger.info("Successfully published")
96174
}
97-
}
175+
176+
private fun ModuleToUpload.mavenDirectory() =
177+
groupId.replace(".", "/") + "/" + artifactId + "/" + version
178+
}

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

Lines changed: 0 additions & 77 deletions
This file was deleted.

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

Lines changed: 0 additions & 12 deletions
This file was deleted.

0 commit comments

Comments
 (0)