Panagiotis Triantafyllou

migrate to maven central

......@@ -11,13 +11,16 @@ buildscript {
classpath 'com.android.tools.build:gradle:8.8.0'
classpath 'com.google.gms:google-services:4.3.10'
classpath 'com.huawei.agconnect:agcp:1.9.1.300'
classpath 'io.github.gradle-nexus:publish-plugin:1.1.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
plugins {
id 'maven-publish'
}
allprojects {
repositories {
mavenCentral()
......@@ -27,5 +30,4 @@ allprojects {
}
}
apply plugin: 'io.github.gradle-nexus.publish-plugin'
apply from: "${rootDir}/scripts/publish-root.gradle"
\ No newline at end of file
apply from: "${rootDir}/scripts/publish-root.gradle"
......
......@@ -40,7 +40,7 @@ afterEvaluate {
// Sources are now handled by the android block's singleVariant
// artifact javadocJar
// Mostly self-explanatory metadata
// POM metadata for Maven Central
pom {
name = PUBLISH_ARTIFACT_ID
description = 'Warply Android SDK Maven Plugin'
......@@ -60,8 +60,7 @@ afterEvaluate {
// Add all other devs here...
}
// Version control info - if you're using GitHub, follow the
// format as seen here
// Version control info
scm {
connection = 'scm:git:git.warp.ly/open-source/warply_android_sdk_maven_plugin.git'
developerConnection = 'scm:git:ssh://git.warp.ly/open-source/warply_android_sdk_maven_plugin.git'
......@@ -81,3 +80,365 @@ signing {
)
sign publishing.publications
}
// Configuration for Central Publishing (similar to Maven plugin configuration)
ext.centralPublishing = [
autoPublish: false, // Manual publishing for safety
// waitUntil: "published", // Commented out - don't wait for publishing
deploymentName: "Warply Android SDK ${PUBLISH_VERSION}",
centralBaseUrl: "https://central.sonatype.com"
]
// Custom task that implements the same functionality as org.sonatype.central:central-publishing-maven-plugin:0.8.0
// This uses the Central Portal API directly to achieve the same result
task publishToCentralPortal {
dependsOn 'publishReleasePublicationToMavenLocal'
description = 'Publishes to Maven Central Portal using the same API as central-publishing-maven-plugin:0.8.0'
group = 'publishing'
doLast {
def username = rootProject.ext["centralPortalUsername"]
def password = rootProject.ext["centralPortalPassword"]
def config = project.ext.centralPublishing
if (!username || !password) {
throw new GradleException("Central Portal credentials not configured. Please set centralPortalUsername and centralPortalPassword in local.properties or environment variables.")
}
println "=== Central Portal Publishing ==="
println "Deployment: ${config.deploymentName}"
println "Auto-publish: ${config.autoPublish}"
println "Portal URL: ${config.centralBaseUrl}"
println ""
// Step 1: Create deployment bundle
println "Step 1: Creating deployment bundle..."
def bundleFile = createDeploymentBundle()
println "✓ Bundle created: ${bundleFile.name} (${bundleFile.length()} bytes)"
// Step 2: Upload bundle to Central Portal
println "\nStep 2: Uploading to Central Portal..."
def deploymentId = uploadBundle(bundleFile, username, password, config)
println "✓ Upload successful. Deployment ID: ${deploymentId}"
// Step 3: Wait for validation
println "\nStep 3: Waiting for validation..."
def validationResult = waitForValidation(deploymentId, username, password, config)
if (validationResult.success) {
def state = validationResult.state
println "✓ Validation successful! State: ${state}"
if (config.autoPublish && state == "VALIDATED") {
println "\nStep 4: Auto-publishing..."
def publishResult = publishDeployment(deploymentId, username, password, config)
if (publishResult.success) {
println "✓ Published successfully to Maven Central!"
} else {
throw new GradleException("Auto-publishing failed: ${publishResult.error}")
}
} else if (state == "PUBLISHED") {
println "✓ Already published to Maven Central!"
def response = validationResult.response
def purls = response.purls ?: []
if (purls) {
println " Published artifacts:"
purls.each { purl -> println " - ${purl}" }
}
} else {
println "\n✓ Deployment uploaded and validated successfully!"
println "📋 Manual action required:"
println " Visit: ${config.centralBaseUrl}/publishing/deployments"
println " Find deployment: ${config.deploymentName}"
println " Click 'Publish' to complete the process"
}
} else {
throw new GradleException("Validation failed: ${validationResult.error}")
}
println "\n=== Publishing Complete ==="
}
}
def createDeploymentBundle() {
def bundleDir = file("${buildDir}/central-publishing")
def stagingDir = file("${bundleDir}/staging")
// Clean and create directories
bundleDir.deleteDir()
stagingDir.mkdirs()
// Create Maven repository structure
def groupPath = PUBLISH_GROUP_ID.replace('.', '/')
def artifactDir = file("${stagingDir}/${groupPath}/${PUBLISH_ARTIFACT_ID}/${PUBLISH_VERSION}")
artifactDir.mkdirs()
// Copy artifacts to staging area
def artifacts = [:]
// AAR file
def aarFile = file("${buildDir}/outputs/aar/warply_android_sdk-release.aar")
if (aarFile.exists()) {
def targetAar = file("${artifactDir}/${PUBLISH_ARTIFACT_ID}-${PUBLISH_VERSION}.aar")
copy {
from aarFile
into artifactDir
rename { targetAar.name }
}
artifacts['aar'] = targetAar
// Copy AAR signature if exists
def aarSigFile = file("${aarFile.path}.asc")
if (aarSigFile.exists()) {
def targetAarSig = file("${targetAar.path}.asc")
copy {
from aarSigFile
into artifactDir
rename { targetAarSig.name }
}
artifacts['aar-sig'] = targetAarSig
}
}
// Sources JAR
def sourcesFile = file("${buildDir}/libs/warply_android_sdk-${PUBLISH_VERSION}-sources.jar")
if (sourcesFile.exists()) {
def targetSources = file("${artifactDir}/${PUBLISH_ARTIFACT_ID}-${PUBLISH_VERSION}-sources.jar")
copy {
from sourcesFile
into artifactDir
rename { targetSources.name }
}
artifacts['sources'] = targetSources
// Copy sources signature if exists
def sourcesSigFile = file("${sourcesFile.path}.asc")
if (sourcesSigFile.exists()) {
def targetSourcesSig = file("${targetSources.path}.asc")
copy {
from sourcesSigFile
into artifactDir
rename { targetSourcesSig.name }
}
artifacts['sources-sig'] = targetSourcesSig
}
}
// POM file
def pomFile = file("${buildDir}/publications/release/pom-default.xml")
if (pomFile.exists()) {
def targetPom = file("${artifactDir}/${PUBLISH_ARTIFACT_ID}-${PUBLISH_VERSION}.pom")
copy {
from pomFile
into artifactDir
rename { targetPom.name }
}
artifacts['pom'] = targetPom
// Copy POM signature if exists
def pomSigFile = file("${pomFile.path}.asc")
if (pomSigFile.exists()) {
def targetPomSig = file("${targetPom.path}.asc")
copy {
from pomSigFile
into artifactDir
rename { targetPomSig.name }
}
artifacts['pom-sig'] = targetPomSig
}
}
// Generate checksums for all files
artifacts.each { type, artifactFile ->
if (artifactFile.exists()) {
generateChecksums(artifactFile)
}
}
// Create bundle ZIP
def bundleFile = file("${bundleDir}/central-bundle.zip")
ant.zip(destfile: bundleFile) {
fileset(dir: stagingDir)
}
return bundleFile
}
def generateChecksums(File file) {
['md5', 'sha1', 'sha256', 'sha512'].each { algorithm ->
def checksum = file.withInputStream { stream ->
java.security.MessageDigest.getInstance(algorithm.toUpperCase()).digest(stream.bytes).encodeHex().toString()
}
new File("${file.path}.${algorithm}").text = checksum
}
}
def uploadBundle(File bundleFile, String username, String password, Map config) {
def url = "${config.centralBaseUrl}/api/v1/publisher/upload"
def credentials = "${username}:${password}".bytes.encodeBase64().toString()
// Add query parameters
def publishingType = config.autoPublish ? "AUTOMATIC" : "USER_MANAGED"
def urlWithParams = "${url}?publishingType=${publishingType}&name=${URLEncoder.encode(config.deploymentName, 'UTF-8')}"
println " Uploading to: ${urlWithParams}"
println " Publishing type: ${publishingType}"
def connection = new URL(urlWithParams).openConnection() as HttpURLConnection
connection.setRequestMethod("POST")
connection.setRequestProperty("Authorization", "Bearer ${credentials}")
connection.setDoOutput(true)
connection.setDoInput(true)
// Create multipart/form-data boundary
def boundary = "----WebKitFormBoundary" + System.currentTimeMillis()
connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=${boundary}")
// Write multipart data
connection.outputStream.withWriter("UTF-8") { writer ->
writer.write("--${boundary}\r\n")
writer.write("Content-Disposition: form-data; name=\"bundle\"; filename=\"${bundleFile.name}\"\r\n")
writer.write("Content-Type: application/octet-stream\r\n")
writer.write("\r\n")
writer.flush()
// Write file content
bundleFile.withInputStream { input ->
connection.outputStream << input
}
writer.write("\r\n--${boundary}--\r\n")
writer.flush()
}
// Get response
def responseCode = connection.responseCode
if (responseCode == 201) {
def deploymentId = connection.inputStream.text.trim()
println " ✓ Upload successful (HTTP ${responseCode})"
return deploymentId
} else {
def errorMessage = connection.errorStream?.text ?: "Unknown error"
throw new GradleException("Upload failed (HTTP ${responseCode}): ${errorMessage}")
}
}
def waitForValidation(String deploymentId, String username, String password, Map config) {
def credentials = "${username}:${password}".bytes.encodeBase64().toString()
def maxAttempts = 60 // 5 minutes with 5-second intervals
def attempt = 0
while (attempt < maxAttempts) {
attempt++
def url = "${config.centralBaseUrl}/api/v1/publisher/status?id=${deploymentId}"
def connection = new URL(url).openConnection() as HttpURLConnection
connection.setRequestMethod("POST")
connection.setRequestProperty("Authorization", "Bearer ${credentials}")
connection.setRequestProperty("Content-Type", "application/json")
def responseCode = connection.responseCode
if (responseCode == 200) {
def response = new groovy.json.JsonSlurper().parseText(connection.inputStream.text)
def state = response.deploymentState
println " Status check ${attempt}: ${state}"
switch (state) {
case "PENDING":
case "VALIDATING":
// Continue waiting
Thread.sleep(5000)
break
case "VALIDATED":
return [success: true, state: state, response: response]
case "PUBLISHED":
return [success: true, state: state, response: response]
case "FAILED":
def errors = response.errors ?: ["Unknown validation error"]
return [success: false, error: "Validation failed: ${errors.join(', ')}", response: response]
default:
return [success: false, error: "Unknown deployment state: ${state}", response: response]
}
} else {
def errorMessage = connection.errorStream?.text ?: "Unknown error"
throw new GradleException("Status check failed (HTTP ${responseCode}): ${errorMessage}")
}
}
return [success: false, error: "Timeout waiting for validation (${maxAttempts * 5} seconds)"]
}
def publishDeployment(String deploymentId, String username, String password, Map config) {
def credentials = "${username}:${password}".bytes.encodeBase64().toString()
def url = "${config.centralBaseUrl}/api/v1/publisher/deployment/${deploymentId}"
println " Calling publish API..."
def connection = new URL(url).openConnection() as HttpURLConnection
connection.setRequestMethod("POST")
connection.setRequestProperty("Authorization", "Bearer ${credentials}")
def responseCode = connection.responseCode
if (responseCode == 204) {
println " ✓ Publish request successful (HTTP ${responseCode})"
// Wait for publishing to complete
println " Waiting for publishing to complete..."
def result = waitForPublishing(deploymentId, username, password, config)
return result
} else {
def errorMessage = connection.errorStream?.text ?: "Unknown error"
throw new GradleException("Publish failed (HTTP ${responseCode}): ${errorMessage}")
}
}
def waitForPublishing(String deploymentId, String username, String password, Map config) {
def credentials = "${username}:${password}".bytes.encodeBase64().toString()
def maxAttempts = 120 // 10 minutes with 5-second intervals
def attempt = 0
while (attempt < maxAttempts) {
attempt++
def url = "${config.centralBaseUrl}/api/v1/publisher/status?id=${deploymentId}"
def connection = new URL(url).openConnection() as HttpURLConnection
connection.setRequestMethod("POST")
connection.setRequestProperty("Authorization", "Bearer ${credentials}")
connection.setRequestProperty("Content-Type", "application/json")
def responseCode = connection.responseCode
if (responseCode == 200) {
def response = new groovy.json.JsonSlurper().parseText(connection.inputStream.text)
def state = response.deploymentState
println " Publishing status ${attempt}: ${state}"
switch (state) {
case "PUBLISHING":
// Continue waiting
Thread.sleep(5000)
break
case "PUBLISHED":
def purls = response.purls ?: []
println " ✓ Successfully published to Maven Central!"
if (purls) {
println " Published artifacts:"
purls.each { purl -> println " - ${purl}" }
}
return [success: true, state: state, response: response]
case "FAILED":
def errors = response.errors ?: ["Unknown publishing error"]
return [success: false, error: "Publishing failed: ${errors.join(', ')}", response: response]
default:
return [success: false, error: "Unexpected state during publishing: ${state}", response: response]
}
} else {
def errorMessage = connection.errorStream?.text ?: "Unknown error"
throw new GradleException("Publishing status check failed (HTTP ${responseCode}): ${errorMessage}")
}
}
return [success: false, error: "Timeout waiting for publishing (${maxAttempts * 5} seconds)"]
}
......
// Create variables with empty default values
// Central Portal credentials
ext["centralPortalUsername"] = ''
ext["centralPortalPassword"] = ''
// keyId is the last 8 characters of the GPG key
ext["signing.keyId"] = ''
// password is the passphrase of the GPG key
ext["signing.password"] = ''
// key is the base64 private GPG key
ext["signing.key"] = ''
// osshrUsername and ossrhPassword are the account details for MavenCentral
// which we’ve chosen at the Jira registration step (Sonatype site))
ext["ossrhUsername"] = ''
ext["ossrhPassword"] = ''
ext["sonatypeStagingProfileId"] = ''
File secretPropsFile = project.rootProject.file('local.properties')
if (secretPropsFile.exists()) {
......@@ -19,24 +18,11 @@ if (secretPropsFile.exists()) {
new FileInputStream(secretPropsFile).withCloseable { is -> p.load(is) }
p.each { name, value -> ext[name] = value }
} else {
// Use system environment variables
ext["ossrhUsername"] = System.getenv('OSSRH_USERNAME')
ext["ossrhPassword"] = System.getenv('OSSRH_PASSWORD')
ext["sonatypeStagingProfileId"] = System.getenv('SONATYPE_STAGING_PROFILE_ID')
// Use system environment variables for signing
ext["signing.keyId"] = System.getenv('SIGNING_KEY_ID')
ext["signing.password"] = System.getenv('SIGNING_PASSWORD')
ext["signing.key"] = System.getenv('SIGNING_KEY')
// Central Portal credentials can also come from environment
ext["centralPortalUsername"] = System.getenv('CENTRAL_PORTAL_USERNAME')
ext["centralPortalPassword"] = System.getenv('CENTRAL_PORTAL_PASSWORD')
}
// Set up Sonatype repository
nexusPublishing {
repositories {
sonatype {
stagingProfileId = sonatypeStagingProfileId
username = ossrhUsername
password = ossrhPassword
nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/"))
snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/"))
}
}
}
\ No newline at end of file
......
......@@ -5,7 +5,7 @@ android.buildFeatures.buildConfig = true
ext {
PUBLISH_GROUP_ID = 'ly.warp'
PUBLISH_VERSION = '4.5.5.4m6'
PUBLISH_VERSION = '4.5.5.4m7'
PUBLISH_ARTIFACT_ID = 'warply-android-sdk'
}
......