Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions build.gradle.kts
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(This comment is not related to this file at all 😄)

Another idea I had was to separate the upload step from the publish step, (plugin docs), and only publish at the end, once we verify that all packages have been uploaded. (And if not, we could e.g. drop whatever we did upload to try again.)

We can publish (and drop) uploaded packages using the API.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm indeed I think it's another valid approach... But I guess there is still a chance of the process being stopped midway in this approach when publishing right? Not sure if having all the artifacts uploaded would be enough for the plugin to fail on the next attempt though...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is still a chance of the process being stopped midway in this approach when publishing

Hmm good point, but this should be much faster (seconds vs minutes), so the chance is very low I'd say.

Not sure if having all the artifacts uploaded would be enough for the plugin to fail on the next attempt though...

If we (are able to) check uploaded artifacts, we could make the release job fail if it finds any.

Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,41 @@ tasks.named<org.jetbrains.dokka.gradle.DokkaMultiModuleTask>("dokkaHtmlMultiModu
outputDirectory.set(file("docs/${project.property("VERSION_NAME")}"))
includes.from("README.md")
}

tasks.register("listPublications") {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly not sure if it's worth the extra complexity here. The idea is to be able to know what we publish to maven dynamically so, if we add more modules we publish in the future, we don't need to update any hardcoded list.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is nice!

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 ->
// Check if maven publish plugin is applied
val hasMavenPublish = subproject.plugins.hasPlugin("com.vanniktech.maven.publish")

if (hasMavenPublish) {
// Get the publishing extension
val publishing = subproject.extensions.findByType<org.gradle.api.publish.PublishingExtension>()

if (publishing != null) {
publishing.publications.forEach { publication ->
if (publication is org.gradle.api.publish.maven.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")
}
}
}
}
}
}
}
110 changes: 110 additions & 0 deletions fastlane/Fastfile
Original file line number Diff line number Diff line change
Expand Up @@ -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|
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we going to move this to the Fastlane plugin? That way we can reuse for PHC and KMP.

Copy link
Copy Markdown
Contributor Author

@tonidero tonidero Nov 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, I think it will be needed there as well. Will try to move it there instead

Edit: Waiting until it's ready to have all the reviews in one place. Once it's ready, I will move to the plugin.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I extracted part of the logic here to the plugin: RevenueCat/fastlane-plugin-revenuecat_internal#101. We would still need to keep some logic to get the artifacts to check... but this felt more like a gradle task, and separate from the plugin action... I guess we could assume the gradle command would be available in all SDKs using it but preferred to keep it separate for now. lmk what you think there! (still need to adapt this PR to use that lane from the plugin)

version = options[:version] || current_version_number
UI.message("Checking if version #{version} already exists in Maven Central...")

group_id = "com.revenuecat.purchases"
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm assuming the group id, which should be fine for this repo.


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
Expand All @@ -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: [
Expand Down Expand Up @@ -143,6 +165,94 @@ 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)
base_url = "https://repo1.maven.org/maven2"
existing_artifacts = []

artifact_ids.each do |artifact_id|
# Convert group ID dots to slashes for URL path
group_path = group_id.gsub(".", "/")
url = "#{base_url}/#{group_path}/#{artifact_id}/#{version}/"

UI.verbose("Checking Maven Central: #{url}")

# Use curl to check if the version directory exists (HTTP HEAD request)
# Don't use -f flag so we can check the HTTP status code
# Use -L to follow redirects
begin
# Capture both stdout (HTTP code) and stderr separately
result = sh("curl -s -S -o /dev/null -w '%{http_code}' -L -I '#{url}' 2>&1", log: false).strip

# Extract the HTTP status code (last 3 digits at the end of the output)
match_result = result.match(/(\d{3})\s*$/)
if match_result.nil?
UI.important("Could not parse HTTP status code for #{url}. Response: #{result}")
next
end

http_code = match_result[1]

if http_code == "200"
existing_artifacts << artifact_id
UI.important("Artifact #{group_id}:#{artifact_id}:#{version} already exists in Maven Central")
elsif http_code != "404"
# If we get an unexpected status code, warn but don't fail
UI.user_error!("Unexpected HTTP status #{http_code} when checking #{url}")
end
rescue => e
UI.user_error!("Failed to check #{url}: #{e.message}")
# Continue checking other artifacts even if one fails
next
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|
group_path = group_id.gsub(".", "/")
maven_url = "#{base_url}/#{group_path}/#{artifact_id}/#{version}/"
error_message += " - #{group_id}:#{artifact_id}:#{version}\n #{maven_url}\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
Expand Down
8 changes: 8 additions & 0 deletions fastlane/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down