diff --git a/build.gradle.kts b/build.gradle.kts index 2f23a67979..3e06cf5df6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -79,3 +79,35 @@ tasks.named("dokkaHtmlMultiModu outputDirectory.set(file("docs/${project.property("VERSION_NAME")}")) includes.from("README.md") } + +tasks.register("listPublications") { + description = "Lists all Maven publications with their coordinates (groupId:artifactId:version)" + group = "publishing" + + // Ensure all subprojects are evaluated before running + dependsOn(subprojects.map { "${it.path}:tasks" }) + + doLast { + val groupId = project.findProperty("GROUP") as String? ?: "com.revenuecat.purchases" + val version = project.findProperty("VERSION_NAME") as String? ?: project.version.toString() + val pomArtifactId = project.findProperty("POM_ARTIFACT_ID") as String? + + subprojects.forEach { subproject -> + val publishing = subproject.extensions.findByType() + + if (publishing != null) { + publishing.publications.forEach { publication -> + if (publication is MavenPublication) { + // Use POM_ARTIFACT_ID property if set, otherwise use publication's artifactId + val artifactId = pomArtifactId ?: publication.artifactId + val pubGroupId = publication.groupId ?: groupId + val pubVersion = publication.version ?: version + + // Output in format: groupId:artifactId:version + println("$pubGroupId:$artifactId:$pubVersion") + } + } + } + } + } +} diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 7d57f9fdea..a00ffd1c26 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -105,6 +105,24 @@ platform :android do ) end + desc "Check if version already exists in Maven Central" + lane :validate_version_not_in_maven_central do |options| + version = options[:version] || current_version_number + UI.message("Checking if version #{version} already exists in Maven Central...") + + group_id = "com.revenuecat.purchases" + + artifact_ids = discover_published_artifacts + + if artifact_ids.empty? + UI.user_error!("No artifacts discovered. There should be artifacts deployed") + else + UI.message("Found #{artifact_ids.length} artifacts to check: #{artifact_ids.join(', ')}") + check_version_not_in_maven_central(version, group_id, artifact_ids) + UI.success("Version #{version} does not exist in Maven Central. Proceeding with deployment.") + end + end + desc "Upload and close a release" lane :deploy do |options| version = current_version_number @@ -116,6 +134,10 @@ platform :android do "mavenCentralPassword" => ENV['MAVEN_CENTRAL_PORTAL_PASSWORD'], "RELEASE_SIGNING_ENABLED" => true } + + # Check if version already exists in Maven Central before deploying + validate_version_not_in_maven_central(version: version) + UI.verbose("Deploying #{version}") gradle( tasks: [ @@ -143,6 +165,93 @@ platform :android do github_release(version: version) unless is_snapshot_version?(version) end + def discover_published_artifacts + artifacts = Set.new + + UI.message("Discovering all publications...") + + # Run gradle command from project root to discover all publications + Dir.chdir("..") do + cmd = ["./gradlew", "listPublications", "--no-daemon", "--quiet"] + output = sh(cmd.join(" "), log: false) + + # Parse output to extract artifact IDs + output.split("\n").each do |line| + line.strip! + # Format is: groupId:artifactId:version + # Only accept lines that match the format and have artifact IDs starting with "purchases-" + if line.match?(/^[^:]+:[^:]+:[^:]+$/) + parts = line.split(":") + artifact_id = parts[1] if parts.length == 3 + # Only add artifact IDs that start with "purchases-" to filter out project names and other noise + if artifact_id && artifact_id.start_with?("purchases-") + artifacts.add(artifact_id) + end + end + end + end + + # Add the custom artifact IDs that are published via deploy_specific_package + # These use POM_ARTIFACT_ID to override the artifact ID, so they might not appear + # in the default publications list, but they will be published with these IDs + artifacts.add("purchases-custom-entitlement-computation") + artifacts.add("purchases-debug-view") + + artifacts.to_a + end + + def check_version_not_in_maven_central(version, group_id, artifact_ids) + require 'rest-client' + require 'json' + require 'uri' + + auth_token = ENV["FETCH_PUBLICATIONS_USER_TOKEN_MAVEN_CENTRAL"] + if auth_token.nil? || auth_token.empty? + UI.user_error!("FETCH_PUBLICATIONS_USER_TOKEN_MAVEN_CENTRAL environment variable is not set. Please provide a valid token to check Maven Central publications.") + end + + base_url = "https://central.sonatype.com/api/v1/publisher/published" + existing_artifacts = [] + + artifact_ids.each do |artifact_id| + # Build query parameters for the API endpoint with proper URI encoding + # The API uses 'name' parameter which corresponds to the artifact_id + api_url = "#{base_url}?namespace=#{URI.encode_www_form_component(group_id)}&name=#{URI.encode_www_form_component(artifact_id)}&version=#{URI.encode_www_form_component(version)}" + + UI.verbose("Checking Sonatype API for publication status: #{group_id}:#{artifact_id}:#{version}") + + begin + response = RestClient.get( + api_url, + { + 'Authorization' => "Bearer #{auth_token}", + 'accept' => 'application/json' + } + ) + + # Parse JSON response + response_data = JSON.parse(response.body) + + # Check if published field is true + if response_data["published"] == true + existing_artifacts << artifact_id + UI.important("Artifact #{group_id}:#{artifact_id}:#{version} already exists in Maven Central") + end + rescue => e + UI.user_error!("Failed to check #{group_id}:#{artifact_id}:#{version}: #{e.message}") + end + end + + unless existing_artifacts.empty? + error_message = "Version #{version} already exists in Maven Central for the following artifacts:\n" + existing_artifacts.each do |artifact_id| + error_message += " - #{group_id}:#{artifact_id}:#{version}\n" + end + error_message += "\nDeployment cancelled to prevent duplicate releases." + UI.user_error!(error_message) + end + end + def deploy_specific_package(variant, artifact_id, module_to_deploy, gradle_properties) gradle_properties["ANDROID_VARIANT_TO_PUBLISH"] = variant gradle_properties["POM_ARTIFACT_ID"] = artifact_id diff --git a/fastlane/README.md b/fastlane/README.md index c04466ed47..ad552da20e 100644 --- a/fastlane/README.md +++ b/fastlane/README.md @@ -63,6 +63,14 @@ Automatically bumps version, replaces version numbers, updates changelog and cre Creates github release +### android validate_version_not_in_maven_central + +```sh +[bundle exec] fastlane android validate_version_not_in_maven_central +``` + +Check if version already exists in Maven Central + ### android deploy ```sh @@ -233,14 +241,6 @@ Builds a Purchase Tester APK and prompts for: -### android send_slack_load_shedder_integration_test - -```sh -[bundle exec] fastlane android send_slack_purchases_integration_test -``` - - - ### android update_paywall_preview_resources_submodule ```sh