Panagiotis Triantafyllou

migrate to maven central

...@@ -11,13 +11,16 @@ buildscript { ...@@ -11,13 +11,16 @@ buildscript {
11 classpath 'com.android.tools.build:gradle:8.8.0' 11 classpath 'com.android.tools.build:gradle:8.8.0'
12 classpath 'com.google.gms:google-services:4.3.10' 12 classpath 'com.google.gms:google-services:4.3.10'
13 classpath 'com.huawei.agconnect:agcp:1.9.1.300' 13 classpath 'com.huawei.agconnect:agcp:1.9.1.300'
14 - classpath 'io.github.gradle-nexus:publish-plugin:1.1.0'
15 14
16 // NOTE: Do not place your application dependencies here; they belong 15 // NOTE: Do not place your application dependencies here; they belong
17 // in the individual module build.gradle files 16 // in the individual module build.gradle files
18 } 17 }
19 } 18 }
20 19
20 +plugins {
21 + id 'maven-publish'
22 +}
23 +
21 allprojects { 24 allprojects {
22 repositories { 25 repositories {
23 mavenCentral() 26 mavenCentral()
...@@ -27,5 +30,4 @@ allprojects { ...@@ -27,5 +30,4 @@ allprojects {
27 } 30 }
28 } 31 }
29 32
30 -apply plugin: 'io.github.gradle-nexus.publish-plugin'
31 apply from: "${rootDir}/scripts/publish-root.gradle" 33 apply from: "${rootDir}/scripts/publish-root.gradle"
......
...@@ -40,7 +40,7 @@ afterEvaluate { ...@@ -40,7 +40,7 @@ afterEvaluate {
40 // Sources are now handled by the android block's singleVariant 40 // Sources are now handled by the android block's singleVariant
41 // artifact javadocJar 41 // artifact javadocJar
42 42
43 - // Mostly self-explanatory metadata 43 + // POM metadata for Maven Central
44 pom { 44 pom {
45 name = PUBLISH_ARTIFACT_ID 45 name = PUBLISH_ARTIFACT_ID
46 description = 'Warply Android SDK Maven Plugin' 46 description = 'Warply Android SDK Maven Plugin'
...@@ -60,8 +60,7 @@ afterEvaluate { ...@@ -60,8 +60,7 @@ afterEvaluate {
60 // Add all other devs here... 60 // Add all other devs here...
61 } 61 }
62 62
63 - // Version control info - if you're using GitHub, follow the 63 + // Version control info
64 - // format as seen here
65 scm { 64 scm {
66 connection = 'scm:git:git.warp.ly/open-source/warply_android_sdk_maven_plugin.git' 65 connection = 'scm:git:git.warp.ly/open-source/warply_android_sdk_maven_plugin.git'
67 developerConnection = 'scm:git:ssh://git.warp.ly/open-source/warply_android_sdk_maven_plugin.git' 66 developerConnection = 'scm:git:ssh://git.warp.ly/open-source/warply_android_sdk_maven_plugin.git'
...@@ -81,3 +80,365 @@ signing { ...@@ -81,3 +80,365 @@ signing {
81 ) 80 )
82 sign publishing.publications 81 sign publishing.publications
83 } 82 }
83 +
84 +// Configuration for Central Publishing (similar to Maven plugin configuration)
85 +ext.centralPublishing = [
86 + autoPublish: false, // Manual publishing for safety
87 + // waitUntil: "published", // Commented out - don't wait for publishing
88 + deploymentName: "Warply Android SDK ${PUBLISH_VERSION}",
89 + centralBaseUrl: "https://central.sonatype.com"
90 +]
91 +
92 +// Custom task that implements the same functionality as org.sonatype.central:central-publishing-maven-plugin:0.8.0
93 +// This uses the Central Portal API directly to achieve the same result
94 +task publishToCentralPortal {
95 + dependsOn 'publishReleasePublicationToMavenLocal'
96 +
97 + description = 'Publishes to Maven Central Portal using the same API as central-publishing-maven-plugin:0.8.0'
98 + group = 'publishing'
99 +
100 + doLast {
101 + def username = rootProject.ext["centralPortalUsername"]
102 + def password = rootProject.ext["centralPortalPassword"]
103 + def config = project.ext.centralPublishing
104 +
105 + if (!username || !password) {
106 + throw new GradleException("Central Portal credentials not configured. Please set centralPortalUsername and centralPortalPassword in local.properties or environment variables.")
107 + }
108 +
109 + println "=== Central Portal Publishing ==="
110 + println "Deployment: ${config.deploymentName}"
111 + println "Auto-publish: ${config.autoPublish}"
112 + println "Portal URL: ${config.centralBaseUrl}"
113 + println ""
114 +
115 + // Step 1: Create deployment bundle
116 + println "Step 1: Creating deployment bundle..."
117 + def bundleFile = createDeploymentBundle()
118 + println "✓ Bundle created: ${bundleFile.name} (${bundleFile.length()} bytes)"
119 +
120 + // Step 2: Upload bundle to Central Portal
121 + println "\nStep 2: Uploading to Central Portal..."
122 + def deploymentId = uploadBundle(bundleFile, username, password, config)
123 + println "✓ Upload successful. Deployment ID: ${deploymentId}"
124 +
125 + // Step 3: Wait for validation
126 + println "\nStep 3: Waiting for validation..."
127 + def validationResult = waitForValidation(deploymentId, username, password, config)
128 +
129 + if (validationResult.success) {
130 + def state = validationResult.state
131 + println "✓ Validation successful! State: ${state}"
132 +
133 + if (config.autoPublish && state == "VALIDATED") {
134 + println "\nStep 4: Auto-publishing..."
135 + def publishResult = publishDeployment(deploymentId, username, password, config)
136 + if (publishResult.success) {
137 + println "✓ Published successfully to Maven Central!"
138 + } else {
139 + throw new GradleException("Auto-publishing failed: ${publishResult.error}")
140 + }
141 + } else if (state == "PUBLISHED") {
142 + println "✓ Already published to Maven Central!"
143 + def response = validationResult.response
144 + def purls = response.purls ?: []
145 + if (purls) {
146 + println " Published artifacts:"
147 + purls.each { purl -> println " - ${purl}" }
148 + }
149 + } else {
150 + println "\n✓ Deployment uploaded and validated successfully!"
151 + println "📋 Manual action required:"
152 + println " Visit: ${config.centralBaseUrl}/publishing/deployments"
153 + println " Find deployment: ${config.deploymentName}"
154 + println " Click 'Publish' to complete the process"
155 + }
156 + } else {
157 + throw new GradleException("Validation failed: ${validationResult.error}")
158 + }
159 +
160 + println "\n=== Publishing Complete ==="
161 + }
162 +}
163 +
164 +def createDeploymentBundle() {
165 + def bundleDir = file("${buildDir}/central-publishing")
166 + def stagingDir = file("${bundleDir}/staging")
167 +
168 + // Clean and create directories
169 + bundleDir.deleteDir()
170 + stagingDir.mkdirs()
171 +
172 + // Create Maven repository structure
173 + def groupPath = PUBLISH_GROUP_ID.replace('.', '/')
174 + def artifactDir = file("${stagingDir}/${groupPath}/${PUBLISH_ARTIFACT_ID}/${PUBLISH_VERSION}")
175 + artifactDir.mkdirs()
176 +
177 + // Copy artifacts to staging area
178 + def artifacts = [:]
179 +
180 + // AAR file
181 + def aarFile = file("${buildDir}/outputs/aar/warply_android_sdk-release.aar")
182 + if (aarFile.exists()) {
183 + def targetAar = file("${artifactDir}/${PUBLISH_ARTIFACT_ID}-${PUBLISH_VERSION}.aar")
184 + copy {
185 + from aarFile
186 + into artifactDir
187 + rename { targetAar.name }
188 + }
189 + artifacts['aar'] = targetAar
190 +
191 + // Copy AAR signature if exists
192 + def aarSigFile = file("${aarFile.path}.asc")
193 + if (aarSigFile.exists()) {
194 + def targetAarSig = file("${targetAar.path}.asc")
195 + copy {
196 + from aarSigFile
197 + into artifactDir
198 + rename { targetAarSig.name }
199 + }
200 + artifacts['aar-sig'] = targetAarSig
201 + }
202 + }
203 +
204 + // Sources JAR
205 + def sourcesFile = file("${buildDir}/libs/warply_android_sdk-${PUBLISH_VERSION}-sources.jar")
206 + if (sourcesFile.exists()) {
207 + def targetSources = file("${artifactDir}/${PUBLISH_ARTIFACT_ID}-${PUBLISH_VERSION}-sources.jar")
208 + copy {
209 + from sourcesFile
210 + into artifactDir
211 + rename { targetSources.name }
212 + }
213 + artifacts['sources'] = targetSources
214 +
215 + // Copy sources signature if exists
216 + def sourcesSigFile = file("${sourcesFile.path}.asc")
217 + if (sourcesSigFile.exists()) {
218 + def targetSourcesSig = file("${targetSources.path}.asc")
219 + copy {
220 + from sourcesSigFile
221 + into artifactDir
222 + rename { targetSourcesSig.name }
223 + }
224 + artifacts['sources-sig'] = targetSourcesSig
225 + }
226 + }
227 +
228 + // POM file
229 + def pomFile = file("${buildDir}/publications/release/pom-default.xml")
230 + if (pomFile.exists()) {
231 + def targetPom = file("${artifactDir}/${PUBLISH_ARTIFACT_ID}-${PUBLISH_VERSION}.pom")
232 + copy {
233 + from pomFile
234 + into artifactDir
235 + rename { targetPom.name }
236 + }
237 + artifacts['pom'] = targetPom
238 +
239 + // Copy POM signature if exists
240 + def pomSigFile = file("${pomFile.path}.asc")
241 + if (pomSigFile.exists()) {
242 + def targetPomSig = file("${targetPom.path}.asc")
243 + copy {
244 + from pomSigFile
245 + into artifactDir
246 + rename { targetPomSig.name }
247 + }
248 + artifacts['pom-sig'] = targetPomSig
249 + }
250 + }
251 +
252 + // Generate checksums for all files
253 + artifacts.each { type, artifactFile ->
254 + if (artifactFile.exists()) {
255 + generateChecksums(artifactFile)
256 + }
257 + }
258 +
259 + // Create bundle ZIP
260 + def bundleFile = file("${bundleDir}/central-bundle.zip")
261 + ant.zip(destfile: bundleFile) {
262 + fileset(dir: stagingDir)
263 + }
264 +
265 + return bundleFile
266 +}
267 +
268 +def generateChecksums(File file) {
269 + ['md5', 'sha1', 'sha256', 'sha512'].each { algorithm ->
270 + def checksum = file.withInputStream { stream ->
271 + java.security.MessageDigest.getInstance(algorithm.toUpperCase()).digest(stream.bytes).encodeHex().toString()
272 + }
273 + new File("${file.path}.${algorithm}").text = checksum
274 + }
275 +}
276 +
277 +def uploadBundle(File bundleFile, String username, String password, Map config) {
278 + def url = "${config.centralBaseUrl}/api/v1/publisher/upload"
279 + def credentials = "${username}:${password}".bytes.encodeBase64().toString()
280 +
281 + // Add query parameters
282 + def publishingType = config.autoPublish ? "AUTOMATIC" : "USER_MANAGED"
283 + def urlWithParams = "${url}?publishingType=${publishingType}&name=${URLEncoder.encode(config.deploymentName, 'UTF-8')}"
284 +
285 + println " Uploading to: ${urlWithParams}"
286 + println " Publishing type: ${publishingType}"
287 +
288 + def connection = new URL(urlWithParams).openConnection() as HttpURLConnection
289 + connection.setRequestMethod("POST")
290 + connection.setRequestProperty("Authorization", "Bearer ${credentials}")
291 + connection.setDoOutput(true)
292 + connection.setDoInput(true)
293 +
294 + // Create multipart/form-data boundary
295 + def boundary = "----WebKitFormBoundary" + System.currentTimeMillis()
296 + connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=${boundary}")
297 +
298 + // Write multipart data
299 + connection.outputStream.withWriter("UTF-8") { writer ->
300 + writer.write("--${boundary}\r\n")
301 + writer.write("Content-Disposition: form-data; name=\"bundle\"; filename=\"${bundleFile.name}\"\r\n")
302 + writer.write("Content-Type: application/octet-stream\r\n")
303 + writer.write("\r\n")
304 + writer.flush()
305 +
306 + // Write file content
307 + bundleFile.withInputStream { input ->
308 + connection.outputStream << input
309 + }
310 +
311 + writer.write("\r\n--${boundary}--\r\n")
312 + writer.flush()
313 + }
314 +
315 + // Get response
316 + def responseCode = connection.responseCode
317 + if (responseCode == 201) {
318 + def deploymentId = connection.inputStream.text.trim()
319 + println " ✓ Upload successful (HTTP ${responseCode})"
320 + return deploymentId
321 + } else {
322 + def errorMessage = connection.errorStream?.text ?: "Unknown error"
323 + throw new GradleException("Upload failed (HTTP ${responseCode}): ${errorMessage}")
324 + }
325 +}
326 +
327 +def waitForValidation(String deploymentId, String username, String password, Map config) {
328 + def credentials = "${username}:${password}".bytes.encodeBase64().toString()
329 + def maxAttempts = 60 // 5 minutes with 5-second intervals
330 + def attempt = 0
331 +
332 + while (attempt < maxAttempts) {
333 + attempt++
334 +
335 + def url = "${config.centralBaseUrl}/api/v1/publisher/status?id=${deploymentId}"
336 + def connection = new URL(url).openConnection() as HttpURLConnection
337 + connection.setRequestMethod("POST")
338 + connection.setRequestProperty("Authorization", "Bearer ${credentials}")
339 + connection.setRequestProperty("Content-Type", "application/json")
340 +
341 + def responseCode = connection.responseCode
342 + if (responseCode == 200) {
343 + def response = new groovy.json.JsonSlurper().parseText(connection.inputStream.text)
344 + def state = response.deploymentState
345 +
346 + println " Status check ${attempt}: ${state}"
347 +
348 + switch (state) {
349 + case "PENDING":
350 + case "VALIDATING":
351 + // Continue waiting
352 + Thread.sleep(5000)
353 + break
354 + case "VALIDATED":
355 + return [success: true, state: state, response: response]
356 + case "PUBLISHED":
357 + return [success: true, state: state, response: response]
358 + case "FAILED":
359 + def errors = response.errors ?: ["Unknown validation error"]
360 + return [success: false, error: "Validation failed: ${errors.join(', ')}", response: response]
361 + default:
362 + return [success: false, error: "Unknown deployment state: ${state}", response: response]
363 + }
364 + } else {
365 + def errorMessage = connection.errorStream?.text ?: "Unknown error"
366 + throw new GradleException("Status check failed (HTTP ${responseCode}): ${errorMessage}")
367 + }
368 + }
369 +
370 + return [success: false, error: "Timeout waiting for validation (${maxAttempts * 5} seconds)"]
371 +}
372 +
373 +def publishDeployment(String deploymentId, String username, String password, Map config) {
374 + def credentials = "${username}:${password}".bytes.encodeBase64().toString()
375 + def url = "${config.centralBaseUrl}/api/v1/publisher/deployment/${deploymentId}"
376 +
377 + println " Calling publish API..."
378 +
379 + def connection = new URL(url).openConnection() as HttpURLConnection
380 + connection.setRequestMethod("POST")
381 + connection.setRequestProperty("Authorization", "Bearer ${credentials}")
382 +
383 + def responseCode = connection.responseCode
384 + if (responseCode == 204) {
385 + println " ✓ Publish request successful (HTTP ${responseCode})"
386 +
387 + // Wait for publishing to complete
388 + println " Waiting for publishing to complete..."
389 + def result = waitForPublishing(deploymentId, username, password, config)
390 + return result
391 + } else {
392 + def errorMessage = connection.errorStream?.text ?: "Unknown error"
393 + throw new GradleException("Publish failed (HTTP ${responseCode}): ${errorMessage}")
394 + }
395 +}
396 +
397 +def waitForPublishing(String deploymentId, String username, String password, Map config) {
398 + def credentials = "${username}:${password}".bytes.encodeBase64().toString()
399 + def maxAttempts = 120 // 10 minutes with 5-second intervals
400 + def attempt = 0
401 +
402 + while (attempt < maxAttempts) {
403 + attempt++
404 +
405 + def url = "${config.centralBaseUrl}/api/v1/publisher/status?id=${deploymentId}"
406 + def connection = new URL(url).openConnection() as HttpURLConnection
407 + connection.setRequestMethod("POST")
408 + connection.setRequestProperty("Authorization", "Bearer ${credentials}")
409 + connection.setRequestProperty("Content-Type", "application/json")
410 +
411 + def responseCode = connection.responseCode
412 + if (responseCode == 200) {
413 + def response = new groovy.json.JsonSlurper().parseText(connection.inputStream.text)
414 + def state = response.deploymentState
415 +
416 + println " Publishing status ${attempt}: ${state}"
417 +
418 + switch (state) {
419 + case "PUBLISHING":
420 + // Continue waiting
421 + Thread.sleep(5000)
422 + break
423 + case "PUBLISHED":
424 + def purls = response.purls ?: []
425 + println " ✓ Successfully published to Maven Central!"
426 + if (purls) {
427 + println " Published artifacts:"
428 + purls.each { purl -> println " - ${purl}" }
429 + }
430 + return [success: true, state: state, response: response]
431 + case "FAILED":
432 + def errors = response.errors ?: ["Unknown publishing error"]
433 + return [success: false, error: "Publishing failed: ${errors.join(', ')}", response: response]
434 + default:
435 + return [success: false, error: "Unexpected state during publishing: ${state}", response: response]
436 + }
437 + } else {
438 + def errorMessage = connection.errorStream?.text ?: "Unknown error"
439 + throw new GradleException("Publishing status check failed (HTTP ${responseCode}): ${errorMessage}")
440 + }
441 + }
442 +
443 + return [success: false, error: "Timeout waiting for publishing (${maxAttempts * 5} seconds)"]
444 +}
......
1 // Create variables with empty default values 1 // Create variables with empty default values
2 2
3 +// Central Portal credentials
4 +ext["centralPortalUsername"] = ''
5 +ext["centralPortalPassword"] = ''
6 +
3 // keyId is the last 8 characters of the GPG key 7 // keyId is the last 8 characters of the GPG key
4 ext["signing.keyId"] = '' 8 ext["signing.keyId"] = ''
5 // password is the passphrase of the GPG key 9 // password is the passphrase of the GPG key
6 ext["signing.password"] = '' 10 ext["signing.password"] = ''
7 // key is the base64 private GPG key 11 // key is the base64 private GPG key
8 ext["signing.key"] = '' 12 ext["signing.key"] = ''
9 -// osshrUsername and ossrhPassword are the account details for MavenCentral
10 -// which we’ve chosen at the Jira registration step (Sonatype site))
11 -ext["ossrhUsername"] = ''
12 -ext["ossrhPassword"] = ''
13 -ext["sonatypeStagingProfileId"] = ''
14 13
15 File secretPropsFile = project.rootProject.file('local.properties') 14 File secretPropsFile = project.rootProject.file('local.properties')
16 if (secretPropsFile.exists()) { 15 if (secretPropsFile.exists()) {
...@@ -19,24 +18,11 @@ if (secretPropsFile.exists()) { ...@@ -19,24 +18,11 @@ if (secretPropsFile.exists()) {
19 new FileInputStream(secretPropsFile).withCloseable { is -> p.load(is) } 18 new FileInputStream(secretPropsFile).withCloseable { is -> p.load(is) }
20 p.each { name, value -> ext[name] = value } 19 p.each { name, value -> ext[name] = value }
21 } else { 20 } else {
22 - // Use system environment variables 21 + // Use system environment variables for signing
23 - ext["ossrhUsername"] = System.getenv('OSSRH_USERNAME')
24 - ext["ossrhPassword"] = System.getenv('OSSRH_PASSWORD')
25 - ext["sonatypeStagingProfileId"] = System.getenv('SONATYPE_STAGING_PROFILE_ID')
26 ext["signing.keyId"] = System.getenv('SIGNING_KEY_ID') 22 ext["signing.keyId"] = System.getenv('SIGNING_KEY_ID')
27 ext["signing.password"] = System.getenv('SIGNING_PASSWORD') 23 ext["signing.password"] = System.getenv('SIGNING_PASSWORD')
28 ext["signing.key"] = System.getenv('SIGNING_KEY') 24 ext["signing.key"] = System.getenv('SIGNING_KEY')
29 -} 25 + // Central Portal credentials can also come from environment
30 - 26 + ext["centralPortalUsername"] = System.getenv('CENTRAL_PORTAL_USERNAME')
31 -// Set up Sonatype repository 27 + ext["centralPortalPassword"] = System.getenv('CENTRAL_PORTAL_PASSWORD')
32 -nexusPublishing {
33 - repositories {
34 - sonatype {
35 - stagingProfileId = sonatypeStagingProfileId
36 - username = ossrhUsername
37 - password = ossrhPassword
38 - nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/"))
39 - snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/"))
40 - }
41 - }
42 } 28 }
......
...@@ -5,7 +5,7 @@ android.buildFeatures.buildConfig = true ...@@ -5,7 +5,7 @@ android.buildFeatures.buildConfig = true
5 5
6 ext { 6 ext {
7 PUBLISH_GROUP_ID = 'ly.warp' 7 PUBLISH_GROUP_ID = 'ly.warp'
8 - PUBLISH_VERSION = '4.5.5.4m6' 8 + PUBLISH_VERSION = '4.5.5.4m7'
9 PUBLISH_ARTIFACT_ID = 'warply-android-sdk' 9 PUBLISH_ARTIFACT_ID = 'warply-android-sdk'
10 } 10 }
11 11
......