Skip to content

Commit bb07b8f

Browse files
authored
Add nmcpPublishCentralPortalDeployment task (#251)
* Add `nmcpPublishCentralPortalDeployment` task Part of #250. * Try to fix integration tests * Rename task
1 parent b652d81 commit bb07b8f

8 files changed

Lines changed: 219 additions & 123 deletions

File tree

nmcp-tasks/api/nmcp-tasks.api

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,16 @@ public final class nmcp/internal/task/NmcpCheckFilesEntryPoint$Companion {
1818
public final fun run (Ljava/util/List;Ljava/io/File;Z)V
1919
}
2020

21+
public final class nmcp/internal/task/NmcpPublishDeploymentEntryPoint {
22+
public static final field Companion Lnmcp/internal/task/NmcpPublishDeploymentEntryPoint$Companion;
23+
public fun <init> ()V
24+
public static final fun run (Ljava/util/function/BiConsumer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;)V
25+
}
26+
27+
public final class nmcp/internal/task/NmcpPublishDeploymentEntryPoint$Companion {
28+
public final fun run (Ljava/util/function/BiConsumer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;)V
29+
}
30+
2131
public final class nmcp/internal/task/NmcpPublishFileByFileToFileSystemEntryPoint {
2232
public static final field Companion Lnmcp/internal/task/NmcpPublishFileByFileToFileSystemEntryPoint$Companion;
2333
public fun <init> ()V
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package nmcp.internal.task
2+
3+
import gratatouille.tasks.GLogger
4+
import gratatouille.tasks.GTask
5+
import kotlin.time.Duration.Companion.seconds
6+
import nmcp.transport.Success
7+
import nmcp.transport.executeWithRetries
8+
import nmcp.transport.nmcpClient
9+
import okhttp3.Request
10+
import okhttp3.RequestBody
11+
12+
@GTask(pure = false)
13+
internal fun nmcpPublishDeployment(
14+
logger: GLogger,
15+
username: String?,
16+
password: String?,
17+
deploymentId: String?,
18+
baseUrl: String?,
19+
publishingTimeoutSeconds: Long?
20+
) {
21+
check(!deploymentId.isNullOrBlank()) {
22+
"Nmcp: deploymentId is missing"
23+
}
24+
25+
val token = toBearerToken(username, password)
26+
27+
@Suppress("NAME_SHADOWING")
28+
val baseUrl = baseUrl ?: "https://central.sonatype.com/"
29+
val url = baseUrl + "api/v1/publisher/deployment/$deploymentId"
30+
31+
logger.lifecycle("Publishing previously uploaded deployment bundle '$deploymentId'")
32+
val request = Request.Builder()
33+
.post(RequestBody.EMPTY)
34+
.addHeader("Authorization", "Bearer $token")
35+
.url(url)
36+
.build()
37+
val result = executeWithRetries(logger, nmcpClient, request)
38+
39+
if (result !is Success) {
40+
error("Cannot publish deployment '$deploymentId' to maven central: ($result)}")
41+
}
42+
43+
logger.lifecycle("Nmcp: deployment bundle '$deploymentId' moved to 'publishing' status.")
44+
45+
val timeout = publishingTimeoutSeconds?.seconds ?: 0.seconds
46+
if (timeout.isPositive()) {
47+
logger.lifecycle("Nmcp: waiting for publication...")
48+
waitForStatus(setOf(PUBLISHED), timeout, logger, deploymentId, baseUrl, token)
49+
logger.lifecycle("Nmcp: deployment is published.")
50+
} else {
51+
logger.lifecycle("Nmcp: deployment is publishing... Check the central portal UI to verify its status.")
52+
}
53+
}

nmcp-tasks/src/main/kotlin/nmcp/internal/task/nmcpPublishWithPublisherApi.kt

Lines changed: 4 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,16 @@ package nmcp.internal.task
33
import gratatouille.tasks.GInputFile
44
import gratatouille.tasks.GLogger
55
import gratatouille.tasks.GTask
6-
import java.net.SocketTimeoutException
7-
import kotlin.time.Duration
86
import kotlin.time.Duration.Companion.minutes
97
import kotlin.time.Duration.Companion.seconds
10-
import kotlin.time.TimeSource.Monotonic.markNow
11-
import kotlinx.serialization.json.Json
12-
import kotlinx.serialization.json.JsonObject
13-
import kotlinx.serialization.json.JsonPrimitive
148
import nmcp.transport.Success
159
import nmcp.transport.executeWithRetries
1610
import nmcp.transport.nmcpClient
1711
import okhttp3.MediaType.Companion.toMediaType
1812
import okhttp3.MultipartBody
1913
import okhttp3.Request
2014
import okhttp3.RequestBody.Companion.asRequestBody
21-
import okhttp3.RequestBody.Companion.toRequestBody
2215
import okio.Buffer
23-
import okio.ByteString
2416
import okio.use
2517

2618
@GTask(pure = false)
@@ -35,16 +27,7 @@ internal fun nmcpPublishWithPublisherApi(
3527
publishingTimeoutSeconds: Long?,
3628
inputFile: GInputFile,
3729
) {
38-
check(!username.isNullOrBlank()) {
39-
"Nmcp: username is missing"
40-
}
41-
check(!password.isNullOrBlank()) {
42-
"Nmcp: password is missing"
43-
}
44-
45-
val token = "$username:$password".let {
46-
Buffer().writeUtf8(it).readByteString().base64()
47-
}
30+
val token = toBearerToken(username, password)
4831

4932
val body = MultipartBody.Builder()
5033
.addFormDataPart(
@@ -79,13 +62,13 @@ internal fun nmcpPublishWithPublisherApi(
7962
val timeout1 = validationTimeoutSeconds?.seconds ?: 10.minutes
8063
if (timeout1.isPositive()) {
8164
logger.lifecycle("Nmcp: waiting for validation...")
82-
waitFor(setOf(VALIDATED, PUBLISHING, PUBLISHED), timeout1, logger, deploymentId, baseUrl, token)
65+
waitForStatus(setOf(VALIDATED, PUBLISHING, PUBLISHED), timeout1, logger, deploymentId, baseUrl, token)
8366

8467
val timeout2 = publishingTimeoutSeconds?.seconds ?: 0.seconds
8568
if (publishingType == "AUTOMATIC") {
8669
if (timeout2.isPositive()) {
8770
logger.lifecycle("Nmcp: deployment is validated, waiting for publication...")
88-
waitFor(setOf(PUBLISHED), timeout2, logger, deploymentId, baseUrl, token)
71+
waitForStatus(setOf(PUBLISHED), timeout2, logger, deploymentId, baseUrl, token)
8972
logger.lifecycle("Nmcp: deployment is published.")
9073
} else {
9174
logger.lifecycle("Nmcp: deployment is publishing... Check the central portal UI to verify its status.")
@@ -94,104 +77,9 @@ internal fun nmcpPublishWithPublisherApi(
9477
check(publishingTimeoutSeconds == null) {
9578
"Nmcp: 'publishingTimeout' has no effect if 'publishingType' is USER_MANAGED. Either set 'publishingType = AUTOMATIC' or remove 'publishingTimeout'"
9679
}
97-
logger.lifecycle("Nmcp: deployment has passed validation, publish it manually from the Central Portal UI.")
80+
logger.lifecycle("Nmcp: deployment has passed validation, publish it manually from the Central Portal UI or call './gradlew nmcpPublishDeployment -PnmcpDeploymentId=$deploymentId'.")
9881
}
9982
} else {
10083
logger.lifecycle("Nmcp: deployment is validating... Check the central portal UI to verify its status.")
10184
}
10285
}
103-
104-
private fun waitFor(
105-
target: Set<Status>,
106-
timeout: Duration,
107-
logger: GLogger,
108-
deploymentId: String,
109-
baseUrl: String,
110-
token: String,
111-
) {
112-
val pollingInterval = 5.seconds
113-
val mark = markNow()
114-
while (true) {
115-
check(mark.elapsedNow() < timeout) {
116-
"Nmcp: timeout while checking deployment '$deploymentId'. You might need to check the deployment status on the Central Portal UI (see $baseUrl), or you could increase the timeout."
117-
}
118-
119-
val status = verifyStatus(
120-
logger = logger,
121-
deploymentId = deploymentId,
122-
baseUrl = baseUrl,
123-
token = token,
124-
)
125-
if (status is FAILED) {
126-
error("Nmcp: deployment has failed:\n${status.error}")
127-
} else if (status in target) {
128-
return
129-
} else {
130-
logger.lifecycle("Nmcp: deployment status is '$status', will try again in ${pollingInterval.inWholeSeconds}s (${(timeout - mark.elapsedNow()).inWholeSeconds.seconds} left)...")
131-
// Wait for the next attempt to reduce the load on the Central Portal API
132-
Thread.sleep(pollingInterval.inWholeMilliseconds)
133-
continue
134-
}
135-
}
136-
}
137-
138-
private sealed interface Status
139-
140-
// A deployment is uploaded and waiting for processing by the validation service
141-
private data object PENDING : Status
142-
143-
// A deployment is being processed by the validation service
144-
private data object VALIDATING : Status
145-
146-
// A deployment has passed validation and is waiting on a user to manually publish via the Central Portal UI
147-
private data object VALIDATED : Status
148-
149-
// A deployment has been either automatically or manually published and is being uploaded to Maven Central
150-
private data object PUBLISHING : Status
151-
152-
// A deployment has successfully been uploaded to Maven Central
153-
private data object PUBLISHED : Status
154-
155-
// A deployment has encountered an error
156-
private class FAILED(val error: String) : Status
157-
158-
private fun verifyStatus(
159-
logger: GLogger,
160-
deploymentId: String,
161-
baseUrl: String,
162-
token: String,
163-
): Status {
164-
val request = Request.Builder()
165-
.post(ByteString.EMPTY.toRequestBody())
166-
.addHeader("Authorization", "Bearer $token")
167-
.url(baseUrl + "api/v1/publisher/status?id=$deploymentId")
168-
.build()
169-
val result = executeWithRetries(logger, nmcpClient, request)
170-
if (result !is Success) {
171-
error("Cannot verify deployment $deploymentId status ($result)")
172-
}
173-
174-
val str = result.body.use { it.readUtf8() }
175-
val element = Json.parseToJsonElement(str)
176-
check(element is JsonObject) {
177-
"Nmcp: unexpected status response for deployment $deploymentId: $str"
178-
}
179-
180-
val state = element["deploymentState"]
181-
check(state is JsonPrimitive && state.isString) {
182-
"Nmcp: unexpected deploymentState for deployment $deploymentId: $state"
183-
}
184-
185-
return when (state.content) {
186-
"PENDING" -> PENDING
187-
"VALIDATING" -> VALIDATING
188-
"VALIDATED" -> VALIDATED
189-
"PUBLISHING" -> PUBLISHING
190-
"PUBLISHED" -> PUBLISHED
191-
"FAILED" -> {
192-
FAILED(element["errors"].toString())
193-
}
194-
else -> error("Nmcp: unexpected deploymentState for deployment $deploymentId: $state")
195-
}
196-
197-
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package nmcp.internal.task
2+
3+
import gratatouille.tasks.GLogger
4+
import kotlin.time.Duration
5+
import kotlin.time.Duration.Companion.seconds
6+
import kotlin.time.TimeSource.Monotonic.markNow
7+
import kotlinx.serialization.json.Json
8+
import kotlinx.serialization.json.JsonObject
9+
import kotlinx.serialization.json.JsonPrimitive
10+
import nmcp.transport.Success
11+
import nmcp.transport.executeWithRetries
12+
import nmcp.transport.nmcpClient
13+
import okhttp3.Request
14+
import okhttp3.RequestBody.Companion.toRequestBody
15+
import okio.Buffer
16+
import okio.ByteString
17+
import okio.use
18+
19+
internal fun waitForStatus(
20+
target: Set<Status>,
21+
timeout: Duration,
22+
logger: GLogger,
23+
deploymentId: String,
24+
baseUrl: String,
25+
token: String,
26+
) {
27+
val pollingInterval = 5.seconds
28+
val mark = markNow()
29+
while (true) {
30+
check(mark.elapsedNow() < timeout) {
31+
"Nmcp: timeout while checking deployment '$deploymentId'. You might need to check the deployment status on the Central Portal UI (see $baseUrl), or you could increase the timeout."
32+
}
33+
34+
val status = verifyStatus(
35+
logger = logger,
36+
deploymentId = deploymentId,
37+
baseUrl = baseUrl,
38+
token = token,
39+
)
40+
if (status is FAILED) {
41+
error("Nmcp: deployment has failed:\n${status.error}")
42+
} else if (status in target) {
43+
return
44+
} else {
45+
logger.lifecycle("Nmcp: deployment status is '$status', will try again in ${pollingInterval.inWholeSeconds}s (${(timeout - mark.elapsedNow()).inWholeSeconds.seconds} left)...")
46+
// Wait for the next attempt to reduce the load on the Central Portal API
47+
Thread.sleep(pollingInterval.inWholeMilliseconds)
48+
continue
49+
}
50+
}
51+
}
52+
53+
internal sealed interface Status
54+
55+
// A deployment is uploaded and waiting for processing by the validation service
56+
internal data object PENDING : Status
57+
58+
// A deployment is being processed by the validation service
59+
internal data object VALIDATING : Status
60+
61+
// A deployment has passed validation and is waiting on a user to manually publish via the Central Portal UI
62+
internal data object VALIDATED : Status
63+
64+
// A deployment has been either automatically or manually published and is being uploaded to Maven Central
65+
internal data object PUBLISHING : Status
66+
67+
// A deployment has successfully been uploaded to Maven Central
68+
internal data object PUBLISHED : Status
69+
70+
// A deployment has encountered an error
71+
internal class FAILED(val error: String) : Status
72+
73+
internal fun verifyStatus(
74+
logger: GLogger,
75+
deploymentId: String,
76+
baseUrl: String,
77+
token: String,
78+
): Status {
79+
val request = Request.Builder()
80+
.post(ByteString.EMPTY.toRequestBody())
81+
.addHeader("Authorization", "Bearer $token")
82+
.url(baseUrl + "api/v1/publisher/status?id=$deploymentId")
83+
.build()
84+
val result = executeWithRetries(logger, nmcpClient, request)
85+
if (result !is Success) {
86+
error("Cannot verify deployment $deploymentId status ($result)")
87+
}
88+
89+
val str = result.body.use { it.readUtf8() }
90+
val element = Json.parseToJsonElement(str)
91+
check(element is JsonObject) {
92+
"Nmcp: unexpected status response for deployment $deploymentId: $str"
93+
}
94+
95+
val state = element["deploymentState"]
96+
check(state is JsonPrimitive && state.isString) {
97+
"Nmcp: unexpected deploymentState for deployment $deploymentId: $state"
98+
}
99+
100+
return when (state.content) {
101+
"PENDING" -> PENDING
102+
"VALIDATING" -> VALIDATING
103+
"VALIDATED" -> VALIDATED
104+
"PUBLISHING" -> PUBLISHING
105+
"PUBLISHED" -> PUBLISHED
106+
"FAILED" -> {
107+
FAILED(element["errors"].toString())
108+
}
109+
else -> error("Nmcp: unexpected deploymentState for deployment $deploymentId: $state")
110+
}
111+
112+
}
113+
114+
internal fun toBearerToken(username: String?, password: String?): String {
115+
check(!username.isNullOrBlank()) {
116+
"Nmcp: username is missing"
117+
}
118+
check(!password.isNullOrBlank()) {
119+
"Nmcp: password is missing"
120+
}
121+
122+
val token = "$username:$password".let {
123+
Buffer().writeUtf8(it).readByteString().base64()
124+
}
125+
return token
126+
}

nmcp-tasks/src/main/kotlin/nmcp/transport/transport.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ internal fun executeWithRetries(logger: GLogger, client: OkHttpClient, request:
195195
return result
196196
}
197197

198-
logger.lifecycle("Nmcp: put '${request.url}' failed (${result}), retrying... (attempt ${attempt + 1}/${attemptCount})")
198+
logger.lifecycle("Nmcp: ${request.method} '${request.url}' failed (${result}), retrying... (attempt ${attempt + 1}/${attemptCount})")
199199
Thread.sleep(2.0.pow(attempt.toDouble()).toLong() * 1_000)
200200
attempt++
201201
}
@@ -262,4 +262,3 @@ internal class FilesystemTransport(
262262
}
263263
}
264264
}
265-

0 commit comments

Comments
 (0)