Files
purchases-ios/fastlane/Fastfile
T
Pol Piella Abadia 3bf9fa9576 Uses the remote version of purchases-ios for the SwiftUI sample app (#4841)
* Uses the remote version of `purchases-ios` for the sample app

* Removes framework reference to fix build failure

* Removes workspace

* Resets `Package.resolved` to fix compiler errors

* Re-adds missing dependency

* Updates `README.md`
2025-03-03 16:28:49 +00:00

1117 lines
36 KiB
Ruby

# This file contains the fastlane.tools configuration
# You can find the documentation at https://docs.fastlane.tools
#
# For a list of all available actions, check out
#
# https://docs.fastlane.tools/actions
#
# For a list of all available plugins, check out
#
# https://docs.fastlane.tools/plugins/available-plugins
#
default_platform(:ios)
def current_version_number
File.read("#{File.dirname(__FILE__)}/../.version").strip
end
files_with_version_number = {
'./RevenueCat.podspec' => ['s.version = "{x}"'],
'./RevenueCatUI.podspec' => ['s.version = "{x}"'],
'./Sources/Misc/SystemInfo.swift' => ['return "{x}"'],
'./.version' => ['{x}'],
'./scripts/docs/v4_api_migration_guide.html' => ['purchases-ios-docs/{x}/documentation/revenuecat/v4_api_migration_guide'],
'./scripts/docs/v5_api_migration_guide.html' => ['purchases-ios-docs/{x}/documentation/revenuecat/v5_api_migration_guide']
}
PLIST_VERSION_PATTERNS = ["<key>CFBundleShortVersionString</key>\n\t<string>{x}</string>",]
files_with_version_number_without_prerelease_modifiers = {
'./Sources/Info.plist' => PLIST_VERSION_PATTERNS,
'./Tests/BackendIntegrationTestApp/Info.plist' => PLIST_VERSION_PATTERNS,
'./Tests/BackendIntegrationTests/Info.plist' => PLIST_VERSION_PATTERNS,
'./Tests/UnitTests/Info.plist' => PLIST_VERSION_PATTERNS,
'./Tests/UnitTestsHostApp/Info.plist' => PLIST_VERSION_PATTERNS
}
files_to_update_on_latest_stable_releases = {
'./scripts/docs/index.html' => ['purchases-ios-docs/{x}/documentation/revenuecat'],
}
repo_name = 'purchases-ios'
snapshots_repo_name = 'purchases-ios-snapshots'
changelog_latest_path = './CHANGELOG.latest.md'
changelog_path = './CHANGELOG.md'
platform :ios do
before_all do
setup_circle_ci
if ENV['APP_STORE_CONNECT_API_KEY_ISSUER_ID']
app_store_connect_api_key # This will create a shared client that all the tools know how to use and will look for
end
end
desc "Bump version, update swift header, edit changelog, and create pull request"
lane :bump do |options|
bump_version_update_changelog_create_pr(
current_version: current_version_number,
changelog_latest_path: changelog_latest_path,
changelog_path: changelog_path,
files_to_update: files_with_version_number,
files_to_update_without_prerelease_modifiers: files_with_version_number_without_prerelease_modifiers,
files_to_update_on_latest_stable_releases: files_to_update_on_latest_stable_releases,
repo_name: repo_name,
github_rate_limit: options[:github_rate_limit],
editor: options[:editor],
next_version: options[:next_version],
automatic_release: options[:automatic_release]
)
compile_autogenerated_header_commit_and_push(options)
end
desc "Automatically bumps version, update swift header, edit changelog, and create pull request"
lane :automatic_bump do |options|
next_version, type_of_bump = determine_next_version_using_labels(
repo_name: repo_name,
github_rate_limit: options[:github_rate_limit]
)
options[:next_version] = next_version
options[:automatic_release] = true
if type_of_bump == :skip
UI.message('Skipping automatic bump since the next version doesn\'t include public facing changes')
next
end
if type_of_bump == :major
UI.message('Skipping automatic bump since the next version is a major release')
next
end
bump(options)
end
desc "Make github release"
lane :github_release do |options|
create_github_release(
version: options[:version],
repo_name: repo_name,
github_api_token: ENV["GITHUB_TOKEN"],
changelog_latest_path: changelog_latest_path,
upload_assets: ['RevenueCat.xcframework.zip', 'RevenueCatUI.xcframework.zip']
)
end
desc "Creates PR changing version to next minor adding a -SNAPSHOT suffix"
lane :prepare_next_version do |options|
create_next_snapshot_version(
current_version: current_version_number,
repo_name: repo_name,
github_pr_token: ENV["GITHUB_PULL_REQUEST_API_TOKEN"],
files_to_update: files_with_version_number,
files_to_update_without_prerelease_modifiers: files_with_version_number_without_prerelease_modifiers
)
end
desc "Setup development environment"
lane :setup_dev do |options|
begin
sh("brew install swiftlint")
rescue => exception
UI.error("❌ Please install homebrew and then re-run this lane: /bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"")
raise exception
end
pre_commit_link = "../.git/hooks/pre-commit"
if File.exist?(pre_commit_link) || File.symlink?(pre_commit_link)
UI.message("pre-commit script already linked ✅")
else
UI.message("Linking pre-commit script 🔗")
Dir.chdir ".." do
sh("ln -s -f ../../scripts/pre-commit.sh .git/hooks/pre-commit")
end
end
end
desc "Build the Paywalls Test app for Emerge Snapshots"
lane :build_paywalls_tester_for_emerge do |options|
scan(
workspace: "Tests/TestingApps/PaywallsTester/PaywallsTester.xcworkspace",
scheme: "PaywallsTester - Live Config",
build_for_testing: true,
)
emerge(repo_name: "RevenueCat/purchases-ios")
end
desc "Runs all the iOS tests"
lane :test_ios do |options|
generate_snapshots = ENV["CIRCLECI_TESTS_GENERATE_SNAPSHOTS"] == "true"
# For platforms that fail to run StoreKit tests.
# See https://developer.apple.com/forums/thread/724068 and `FB12223404`.
skip_sk_tests = options[:skip_sk_tests]
if generate_snapshots
if skip_sk_tests
test_plan = "CI-RevenueCat-Snapshots"
else
test_plan = "CI-Snapshots"
end
elsif skip_sk_tests
test_plan = "CI-RevenueCat"
else
test_plan = "CI-AllTests"
end
scan_with_flaky_test_retries(
step_name: "scan - iPhone",
device: ENV['SCAN_DEVICE'] || "iPhone 12 (15.2)",
ensure_devices_found: true,
scheme: "RevenueCat",
prelaunch_simulator: true,
output_types: 'junit',
result_bundle: true,
testplan: test_plan,
configuration: 'Debug',
output_directory: "fastlane/test_output/xctest/ios",
number_of_retries: generate_snapshots ? 0 : 5, # Number of retries in an xcodebuild run
fail_build: !generate_snapshots,
number_of_flaky_retries: generate_snapshots ? 0 : 5 # Number of retries in an xcodebuild run
)
end
desc "Runs all the tvOS tests"
lane :test_tvos do |options|
scan_with_flaky_test_retries(
step_name: "scan - Apple TV",
device: ENV['SCAN_DEVICE'] || "Apple TV",
ensure_devices_found: true,
scheme: "RevenueCat",
prelaunch_simulator: true,
output_types: 'junit',
number_of_retries: 5, # Number of retries in an xcodebuild run
result_bundle: true,
testplan: "CI-AllTests",
configuration: 'Debug',
output_directory: "fastlane/test_output/xctest/tvos",
number_of_flaky_retries: 5 # Number of xcodebuild runs to retry
)
end
desc "Runs all the watchOS tests"
lane :test_watchos do |options|
generate_snapshots = ENV["CIRCLECI_TESTS_GENERATE_SNAPSHOTS"] == "true"
if generate_snapshots
test_plan = "CI-RevenueCat-Snapshots"
else
test_plan = "CI-RevenueCat"
end
scan_with_flaky_test_retries(
step_name: "scan - watchOS",
device: ENV['SCAN_DEVICE'] || "Apple Watch Series 10 (46mm)",
ensure_devices_found: true,
scheme: "RevenueCat",
prelaunch_simulator: true,
output_types: 'junit',
result_bundle: true,
# Host app isn't available on watchOS
testplan: test_plan,
configuration: 'Debug',
output_directory: "fastlane/test_output/xctest/watchos",
number_of_retries: generate_snapshots ? 0 : 5, # Number of retries in an xcodebuild run
fail_build: !generate_snapshots,
number_of_flaky_retries: generate_snapshots ? 0 : 5 # Number of retries in an xcodebuild run
)
end
desc "Runs all RevenueCatUI tests"
lane :test_revenuecatui do |options|
generate_snapshots = ENV["CIRCLECI_TESTS_GENERATE_REVENUECAT_UI_SNAPSHOTS"] == "true"
platform = ENV['PLATFORM'] || 'iOS Simulator'
destination = ENV['DEVICE'] || "iPhone 14,OS=16.4"
sdk = ENV['BUILD_SDK'] || 'iphonesimulator'
fetch_snapshots
begin
# See https://forums.swift.org/t/i-made-a-little-spm-package-that-uses-uikit-how-can-i-run-tests-from-command-line/50399/2
xcodebuild(
workspace: '.',
scheme: 'RevenueCatUI',
destination: "platform=" + platform + ",name=" + destination,
sdk: sdk,
output_style: :basic,
result_bundle_path: 'fastlane/test_output/revenuecatui.xcresult',
report_formats: [:junit],
report_path: 'fastlane/test_output/revenuecatui/tests.xml',
test: true,
xcargs: generate_snapshots ? "-testPlan CI-Snapshots" : "-testPlan CI-RevenueCatUI"
)
rescue => e
# Equivalent to `fail_build: !generate_snapshots`
raise e if !generate_snapshots
end
end
desc "Release checks"
lane :release_checks do |options|
version_number = current_version_number
check_no_git_tag_exists(version_number)
check_pods
export_xcframeworks
check_no_github_release_exists(version_number)
end
desc "build tvOS, watchOS, macOS"
lane :build_tv_watch_mac do |options|
load_spm_dependencies
build_release(platform: 'watchOS')
build_release(platform: 'tvOS')
build_release(platform: 'macOS')
end
desc "macOS build"
lane :build_mac do |options|
load_spm_dependencies
build_release(platform: 'macOS')
end
desc "visionOS build"
lane :build_visionos do |options|
build_release(
platform: 'visionOS',
scheme: 'RevenueCat'
)
build_release(
platform: 'visionOS',
scheme: 'RevenueCatUI'
)
end
desc "archive"
lane :archive do
match(type: "appstore", readonly: true)
gym(export_method: "app-store")
end
desc "archive all platforms"
lane :archive_all_platforms do
platforms = {
'ios' => 'generic/platform=ios',
'tvos' => 'generic/platform=tvos',
'catalyst' => 'generic/platform=macOS,variant=Mac Catalyst'
}
platforms.each do |platform, destination|
match(type: "appstore", platform: platform, readonly: true)
gym(export_method: "app-store", destination: destination)
end
end
desc "run API Tests"
lane :run_api_tests do
# note: it's called "run" tests but these tests only need to build to run
xcodebuild(
workspace: 'RevenueCat.xcworkspace',
scheme: 'All API Tests',
destination: 'generic/platform=iOS Simulator',
configuration: 'release',
)
end
desc "replace API KEY for installation and integration tests"
lane :replace_api_key_integration_tests do
replace_text_in_files(
previous_text: "REVENUECAT_API_KEY",
new_text: ENV["REVENUECAT_API_KEY"],
paths_of_files_to_update: [
'./Tests/BackendIntegrationTests/Constants.swift',
'./Tests/InstallationTests/CommonFiles/RCInstallationRunner.m'
]
)
replace_text_in_files(
previous_text: "REVENUECAT_LOAD_SHEDDER_API_KEY",
new_text: ENV["REVENUECAT_LOAD_SHEDDER_API_KEY"],
paths_of_files_to_update: [
'./Tests/BackendIntegrationTests/Constants.swift',
'./Tests/v3LoadShedderIntegration/v3LoadShedderIntegrationTests/V3LoadShedderIntegrationTests.swift'
]
)
replace_text_in_files(
previous_text: "REVENUECAT_CUSTOM_ENTITLEMENT_COMPUTATION_API_KEY",
new_text: ENV["REVENUECAT_CUSTOM_ENTITLEMENT_COMPUTATION_API_KEY"],
paths_of_files_to_update: [
'./Tests/BackendIntegrationTests/Constants.swift',
'./Tests/InstallationTests/SPMCustomEntitlementComputationInstallation/InstallationTests.swift'
]
)
replace_text_in_files(
previous_text: "REVENUECAT_PROXY_URL",
new_text: ENV["REVENUECAT_PROXY_URL"].to_s,
paths_of_files_to_update: [
'./Tests/BackendIntegrationTests/Constants.swift',
'./Tests/InstallationTests/CommonFiles/RCInstallationRunner.m',
'./Tests/InstallationTests/SPMCustomEntitlementComputationInstallation/InstallationTests.swift'
],
allow_empty: true
)
end
desc "Create XCFramework, and create GitHub release"
lane :release do |options|
version_number = current_version_number
export_xcframeworks
github_release(version: version_number)
end
desc "Release to CocoaPods"
lane :push_revenuecat_pod do |options|
pod_push(path: "RevenueCat.podspec", synchronous: true)
end
desc "Release to CocoaPods"
lane :push_revenuecatui_pod do |options|
pod_push(path: "RevenueCatUI.podspec", synchronous: true)
end
desc "Tag current branch with current version number"
lane :tag_current_branch do |options|
version_number = current_version_number
check_no_git_tag_exists(version_number)
add_git_tag(tag: version_number)
push_git_tags(tag: version_number)
end
desc "Trigger bump"
lane :trigger_bump do
trigger_action_in_circle_ci(action: 'bump', repo_name: repo_name)
end
desc "Checks whether the RevenueCat-Swift.h is up to date"
private_lane :check_autogenerated_header_up_to_date do |options|
ensure_git_status_clean
compile_autogenerated_header
begin
ensure_git_status_clean
rescue => e
UI.error("RevenueCat-Swift.h autogenerated file changed. Make sure it's up-to-date before releasing.")
end
end
private_lane :compile_autogenerated_header_commit_and_push do |options|
ensure_git_status_clean
compile_autogenerated_header
begin
ensure_git_status_clean
rescue => e
commit_current_changes(commit_message: "Update RevenueCat-Swift.h for version #{current_version_number}")
push_to_git_remote
end
end
desc "Trigger CircleCI job to generate snapshots for RevenueCat"
lane :generate_snapshots_RC do
generate_snapshots(
pipeline: "generate_snapshots"
)
end
desc "Trigger CircleCI job to generate snapshots for RevenueCatUI"
lane :generate_snapshots_RCUI do
generate_snapshots(
pipeline: "generate_revenuecatui_snapshots"
)
end
private_lane :build_release do |options|
platform = options[:platform]
scheme = options[:scheme] || 'RevenueCat'
xcodebuild(
workspace: 'RevenueCat.xcworkspace',
scheme: scheme,
destination: "generic/platform=#{platform}",
configuration: 'release',
xcargs: "CODE_SIGN_IDENTITY='' CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=YES"
)
end
private_lane :generate_snapshots do |options|
require 'rest-client'
# Prompt branch
default_branch = git_branch
branch = UI.input("Branch (defaults to #{default_branch}): ")
branch = default_branch if branch == ""
# Get CircleCI token
circle_token = ENV["CIRCLE_TOKEN"]
UI.user_error! "Please set the CIRCLE_TOKEN environment variable" unless circle_token
# Make request
headers = {"Circle-Token": circle_token, "Content-Type": "application/json", "Accept": "application/json"}
data = {parameters: {options[:pipeline] => true}, branch: branch}
url = "https://circleci.com/api/v2/project/github/RevenueCat/#{repo_name}/pipeline"
resp = RestClient.post url, data.to_json, headers
# Print workflow url
number = JSON.parse(resp.body)["number"]
workflow_url = "https://app.circleci.com/pipelines/github/RevenueCat/#{repo_name}/#{number}"
UI.important "Workflow: #{workflow_url}"
end
desc "Creates a new PR after new snapshot files were generated"
lane :create_snapshot_pr do |options|
push_snapshot_pr(
version: options[:version],
repo: "revenuecat/#{repo_name}",
base_branch: ENV["CIRCLE_BRANCH"],
files_to_add: "../*/__Snapshots__/*"
)
end
desc "Creates a new PR on purchases-ios-snapshots after new snapshot files were generated"
lane :create_snapshots_repo_pr do |options|
Dir.chdir("../Tests/#{snapshots_repo_name}") { |dir|
push_snapshot_pr(
version: options[:version],
repo: "revenuecat/#{snapshots_repo_name}",
base_branch: "main",
files_to_add: "."
)
}
end
private_lane :push_snapshot_pr do |options|
version = options[:version]
base_branch = options[:base_branch]
build_number = ENV["CIRCLE_BUILD_NUM"]
branch_name = "generated_snapshots/#{base_branch}-#{build_number}-#{version}"
sh("git", "checkout", "-b", branch_name)
sh("git", "add", options[:files_to_add])
file_count = sh("git diff --cached --numstat | wc -l").strip.to_i
if file_count == 0
UI.important("No files to be committed")
else
sh("git", "commit", "-m", "[skip ci] Generating new test snapshots")
sh("git", "push", "origin", branch_name)
circle_user = ENV["CIRCLE_USERNAME"]
branch_link = "https://github.com/RevenueCat/purchases-ios/tree/#{base_branch}"
body = "Requested by @#{circle_user} for [#{base_branch}](#{branch_link})"
create_pull_request(
repo: options[:repo],
title: "Generating new test snapshots for `#{base_branch}` - #{version}",
body: body,
base: base_branch,
api_token: ENV["GITHUB_TOKEN"],
head: branch_name,
labels: ["test"]
)
end
end
desc "Creates RevenueCat-Swift.h for a new release"
lane :compile_autogenerated_header do |options|
# See https://github.com/RevenueCat/purchases-ios/blob/main/Documentation.docc/V4_API_Migration_guide.md#objc--spm
require 'fileutils'
arch = "x86_64"
Dir.mktmpdir {|dir|
xcodebuild(
project: 'RevenueCat.xcodeproj',
scheme: 'RevenueCat',
derivedDataPath: dir,
configuration: 'release',
arch: arch,
build_settings: {
'ONLY_ACTIVE_ARCH': 'YES'
}
)
FileUtils.cp(
"#{dir}/Build/Intermediates.noindex/RevenueCat.build/Release/RevenueCat.build/Objects-normal/#{arch}/RevenueCat-Swift.h",
File.absolute_path("../Tests/InstallationTests/CommonFiles/RevenueCat-Swift.h")
)
}
end
lane :export_xcframeworks do |options|
create_and_sign_xcframework(
scheme: 'RevenueCat',
product_name: 'RevenueCat',
platforms: [
'iOS',
'macOS',
'maccatalyst',
'tvOS',
'watchOS',
'visionOS'
])
create_and_sign_xcframework(
scheme: 'RevenueCatUIDev',
product_name: 'RevenueCatUI',
platforms: [
'iOS',
'maccatalyst',
'watchOS',
])
end
private_lane :create_and_sign_xcframework do |options|
scheme = options[:scheme]
product_name = options[:product_name]
platforms = options[:platforms]
output_directory = "build/xcframeworks/#{product_name}"
create_xcframework(
destinations: platforms,
scheme: scheme,
product_name: product_name,
xcframework_output_directory: output_directory,
# Bitcode is no longer required since Xcode 14.
enable_bitcode: false,
include_debug_symbols: true,
# Set SKIP_INSTALL and BUILD_LIBRARY_FOR_DISTRIBUTION.
# See https://developer.apple.com/documentation/xcode/creating-a-multi-platform-binary-framework-bundle#Set-up-the-project
override_xcargs: true
)
# sh runs from the Fastfile's location, but other commands run from the project root.
output_directory_for_sh = "../#{output_directory}"
# Sign XCFramework for Xcode 15
sign_xcframework(
file: "#{output_directory_for_sh}/#{product_name}.xcframework"
)
xcframeworks_zip_path_for_sh = "../#{product_name}.xcframework.zip"
sh("ditto", "-c", "-k", "--sequesterRsrc", "--keepParent",
output_directory_for_sh,
xcframeworks_zip_path_for_sh)
end
desc "Run BackendIntegrationTests"
lane :backend_integration_tests do |options|
fail_build = options[:fail_build]
fetch_snapshots
replace_api_key_integration_tests
scan_with_flaky_test_retries(
scheme: "BackendIntegrationTests",
ensure_devices_found: true,
derived_data_path: "scan_derived_data",
output_types: 'junit',
output_style: 'raw',
result_bundle: true,
testplan: options[:test_plan],
configuration: 'Debug',
output_directory: "fastlane/test_output/xctest/ios",
number_of_flaky_retries: 5 # Number of retries in an xcodebuild run
)
end
desc "Run LoadShedder tests"
lane :v3_loadshedder_integration_tests do |options|
replace_api_key_integration_tests
scan(
project: "./Tests/v3LoadShedderIntegration/v3LoadShedderIntegration.xcodeproj",
scheme: "v3LoadShedderIntegration",
ensure_devices_found: true,
derived_data_path: "scan_derived_data",
output_types: 'junit',
output_style: 'raw',
result_bundle: true,
configuration: 'Debug',
output_directory: "fastlane/test_output/xctest/ios"
)
end
desc "Update swift package commit"
lane :update_swift_package_commit do
project_file_locations = [
'./Tests/InstallationTests/SPMInstallation/SPMInstallation.xcodeproj/project.pbxproj',
'./Tests/InstallationTests/SPMCustomEntitlementComputationInstallation/SPMInstallation.xcodeproj/project.pbxproj',
'./Tests/InstallationTests/ReceiptParserInstallation/ReceiptParserInstallation.xcodeproj/project.pbxproj',
'./Examples/MagicWeather/MagicWeather.xcodeproj/project.pbxproj',
'./Examples/MagicWeatherSwiftUI/Magic Weather SwiftUI.xcodeproj/project.pbxproj'
]
old_kind_line = "kind = branch;"
new_kind_line = "kind = revision;"
commit_hash = last_git_commit[:commit_hash]
old_branch_line = "branch = main;"
new_revision_line = "revision = #{commit_hash};"
replace_text_in_files(
previous_text: old_kind_line,
new_text: new_kind_line,
paths_of_files_to_update: project_file_locations
)
replace_text_in_files(
previous_text: old_branch_line,
new_text: new_revision_line,
paths_of_files_to_update: project_file_locations
)
end
private_lane :sign_xcframework do |options|
match(
readonly: true,
git_url: ENV['CERTS_REPO_URL'],
type: 'appstore',
skip_provisioning_profiles: true,
# This isn't important, we just need to fetch the certificate
app_identifier: "com.revenuecat.CarthageIntegration"
)
sh(
"codesign",
"--timestamp",
"-v",
"--sign", "Apple Distribution: RevenueCat, Inc. (8SXR2327BM)",
options[:file]
)
end
private_lane :build_symbols_for_docs do
ios_version = ENV['DOCS_IOS_VERSION'] || "16.1"
sh("swift",
"build",
"--target", "RevenueCat",
# Build for iOS instead of the default macOS. This ensures that iOS-only symbols are included in the docs.
"-Xswiftc", "-sdk", "-Xswiftc", sh("xcrun", "--sdk", "iphonesimulator", "--show-sdk-path").strip!,
"-Xswiftc", "-target", "-Xswiftc", "arm64-apple-ios#{ios_version}-simulator",
"-Xswiftc", "-emit-symbol-graph",
"-Xswiftc", "-emit-symbol-graph-dir",
"-Xswiftc", ".build")
end
desc "Preview docs"
lane :preview_docs do
ios_version = ENV['DOCS_IOS_VERSION'] || "16.1"
ENV["INCLUDE_DOCC_PLUGIN"] = "true"
Dir.chdir("..") do
build_symbols_for_docs
sh("swift",
"package",
"--disable-sandbox",
"preview-documentation",
"--target",
"RevenueCat",
"--platform",
"name=iOS,version=#{ios_version}",
"--transform-for-static-hosting",
"--enable-inherited-docs",
"--additional-symbol-graph-dir", ".build")
end
end
desc "Generate docs without publishing"
lane :build_docs do
build_docs_shared
end
desc "Generate & publish docs"
lane :build_and_publish_docs do
docs_generation_folder = build_docs_shared
version_number = current_version_number
docs_repo_base_url = ENV["DOCS_REPO_BASE_URL"]
docs_repo_name = ENV["DOCS_REPO_NAME"]
UI.user_error!("Missing environment variable: DOCS_REPO_BASE_URL") unless docs_repo_base_url
UI.user_error!("Missing environment variable: DOCS_REPO_NAME") unless docs_repo_name
docs_repo_url = File.join(docs_repo_base_url, docs_repo_name)
docs_files = Dir.glob("scripts/docs/*.html").map { |file| File.realpath(file) }
# clone docs repo
Dir.mktmpdir do |docs_repo_clone_dir|
Dir.chdir(docs_repo_clone_dir) do
sh("git", "clone", docs_repo_url)
Dir.chdir(docs_repo_name) do
# copy docs generated in the previous step into the docs folder
# and push the changes
docs_destination_folder = "docs/#{version_number}"
FileUtils.cp_r docs_generation_folder + "/.", docs_destination_folder
docs_files.each do |file|
FileUtils.cp file, "docs/"
end
# using sh instead of fastlane commands because fastlane would run
# from the repo root
sh("git", "add", docs_destination_folder)
sh("git", "add", "docs/*.html")
sh("git", "commit", "-m", "Update documentation for #{version_number}")
sh("git", "push")
end
end
end
end
desc "Builds the docs"
private_lane :build_docs_shared do
ENV["INCLUDE_DOCC_PLUGIN"] = "true"
version_number = current_version_number
docs_repo_name = ENV["DOCS_REPO_NAME"]
ios_version = ENV['DOCS_IOS_VERSION'] || "16.1"
UI.user_error!("Missing environment variable: DOCS_REPO_NAME") unless docs_repo_name
hosting_base_path = File.join(docs_repo_name, version_number)
docs_generation_folder = Dir.mktmpdir
# swift package must be run from the dir that contains the Package.swift
# output is generated in docs_generation_folder
Dir.chdir("..") do
build_symbols_for_docs
sh("swift",
"package",
"--disable-sandbox",
"--allow-writing-to-directory",
docs_generation_folder,
"generate-documentation",
"--target",
"RevenueCat",
"--disable-indexing", # Produces a DocC archive that is best-suited for hosting online but incompatible with Xcode.
"--platform",
"name=iOS,version=#{ios_version}",
"--output-path",
docs_generation_folder,
"--hosting-base-path",
hosting_base_path,
"--transform-for-static-hosting",
"--enable-inherited-docs",
"--additional-symbol-graph-dir", ".build")
end
docs_generation_folder # returning path from lane
end
desc "Build and deploy PurchaseTesterSwiftUI"
lane :deploy_purchase_tester do |options|
dry_run = options[:dry_run] || false
project_root = File.expand_path("#{File.dirname(__FILE__)}/..")
latest_changelog = File.read("#{project_root}/#{changelog_latest_path}")
changelog = "Release #{current_version_number}:\n\n#{latest_changelog}"
purchase_tester_root = "#{project_root}/Tests/TestingApps/PurchaseTesterSwiftUI"
xcodeproj_path = "#{purchase_tester_root}/PurchaseTester.xcodeproj"
# The following commands use configuration that lives in the PurchaseTesterSwiftUI directory,
# like the Matchfile. So we move to that project's directory to run them.
Dir.chdir("#{purchase_tester_root}/fastlane") do
match(readonly: true, platform: "ios")
increment_build_number(
build_number: latest_testflight_build_number(platform: 'ios') + 1,
xcodeproj: xcodeproj_path,
)
end
build_ios_app(
workspace: 'RevenueCat.xcworkspace',
scheme: "PurchaseTester",
destination: "generic/platform=ios",
xcodebuild_formatter: '',
export_method: 'app-store',
export_options: {
manageAppVersionAndBuildNumber: true,
provisioningProfiles: {
"com.revenuecat.sampleapp" => "match AppStore com.revenuecat.sampleapp",
"com.revenuecat.sampleapp.watchkitapp" => "match AppStore com.revenuecat.sampleapp.watchkitapp"
}
}
)
if not dry_run
upload_to_testflight(
changelog: changelog,
app_identifier: "com.revenuecat.sampleapp",
)
else
UI.message("Dry run mode enabled. Skipping upload to TestFlight")
end
# The following commands use configuration that lives in the PurchaseTesterSwiftUI directory,
# like the Matchfile. So we move to that project's directory to run them.
Dir.chdir("#{purchase_tester_root}/fastlane") do
match(readonly: true, platform: "macos", additional_cert_types: "mac_installer_distribution")
increment_build_number(
build_number: latest_testflight_build_number(platform: 'osx') + 1,
xcodeproj: xcodeproj_path,
)
end
build_mac_app(
workspace: 'RevenueCat.xcworkspace',
scheme: "PurchaseTester",
destination: 'generic/platform=macOS',
xcodebuild_formatter: '',
export_method: 'app-store',
export_options: {
manageAppVersionAndBuildNumber: true,
provisioningProfiles: {
"com.revenuecat.sampleapp" => "match AppStore com.revenuecat.sampleapp macos",
}
}
)
if not dry_run
upload_to_testflight(
changelog: changelog,
app_platform: 'osx',
app_identifier: "com.revenuecat.sampleapp",
)
else
UI.message("Dry run mode enabled. Skipping upload to TestFlight")
end
end
desc "Clones or updates snapshots repo"
lane :fetch_snapshots do
Dir.chdir("../Tests") do
if !File.directory?(snapshots_repo_name)
UI.important("Cloning snapshots repo")
sh "git", "clone", "git@github.com:RevenueCat/#{snapshots_repo_name}.git"
end
commit = current_snapshots_commit
UI.message("Updating snapshots repo to #{commit}")
Dir.chdir(snapshots_repo_name) do
sh "git", "fetch", "origin"
sh "git", "checkout", commit
end
end
end
desc "Updates purchases-ios-snapshots-commit to point to latest commit on main"
lane :update_snapshots_repo do
new_hash = ""
Dir.chdir("../Tests/#{snapshots_repo_name}") do
sh "git", "checkout", "main"
sh "git", "pull", "--rebase", "origin"
new_hash = sh("git", "rev-parse", "HEAD")
end
UI.important("Updating snapshot commit to #{new_hash}")
update_snapshots_commit(new_hash)
end
lane :deploy_to_spm do
source_repo = ENV["CIRCLE_REPOSITORY_URL"] || "https://github.com/revenuecat/#{repo_name}"
destination_repo = source_repo.sub(/\.git\z/, "-spm.git")
git_clone_and_push(
source_repo: source_repo,
destination_repo: destination_repo
)
end
desc "Create or delete sandbox testers"
lane :sandbox_testers do
Spaceship::ConnectAPI.login(use_portal: false)
UI.deprecated("Email pattern will be used when creating and deleting multiple testers")
UI.deprecated("fastlane will increment a number before '@'")
UI.deprecated("Example: testparty.v4@revenuecat.com will make: ")
UI.deprecated(" testparty.v4.1@revenuecat.com")
UI.deprecated(" testparty.v4.2@revenuecat.com")
UI.deprecated(" testparty.v4.3@revenuecat.com")
email_pattern = UI.input("Sanbox Tester Email pattern:")
options = ["Create", "Delete"]
option = UI.select("What do you want to do?", options)
if option == options[0]
sandbox_testers_create(email_pattern: email_pattern)
elsif option == options[1]
sandbox_testers_delete(email_pattern: email_pattern)
end
end
desc "Enable customer center development by cherry-picking a specific commit"
lane :enable_customer_center do |options|
commit_hash = options[:commit_hash] || "517ad0bf5"
UI.message("Applying changes from commit to enable customer center: #{commit_hash}")
begin
sh("git", "cherry-pick", "--no-commit", commit_hash)
UI.success("Successfully applied changes for customer center development from commit #{commit_hash}")
UI.important("Changes are now in your working directory. Review and stage/commit as needed.")
rescue => ex
UI.error("Failed to enable customer center. Error while applying changes from commit #{commit_hash}")
UI.error(ex.message)
if UI.confirm("Do you want to abort the customer center enablement?")
sh("git", "cherry-pick", "--abort")
UI.message("Customer center enablement aborted")
end
end
end
private_lane :sandbox_testers_delete do |options|
email_pattern = options[:email_pattern]
email_prefix = email_pattern.split('@').first
testers = Spaceship::ConnectAPI::SandboxTester.all.select do |tester|
tester.email.start_with?(email_prefix)
end
UI.important("Found testers:")
testers.each do |tester|
UI.message(tester.email)
end
if UI.confirm("Do you want to delete these sandbox testers?")
testers.each do |tester|
UI.message("Deleting #{tester.email}...")
tester.delete!
end
end
end
private_lane :sandbox_testers_create do |options|
email_pattern = options[:email_pattern]
number = UI.input("Number of testers:").to_i
UI.user_error!("Number needs to be more than 0") if number <= 0
UI.user_error!("Number needs to be less than 51") if number > 50
password = UI.input("Sandbox Tester Password (ex: 'SuperSecret01'):")
question = UI.input("Sandbox Tester Secret Question (ex: 'Secret'):")
answer = UI.input("Sandbox Tester Secret Answer (ex: 'Answer'):")
emails = (1..number).map do |i|
email_pattern.gsub('@', ".#{i.to_s}@")
end
emails.each do |email|
data = {
first_name: "Tester",
last_name: "FastlaneCreated",
email: email,
password: password,
confirm_password: password,
secret_question: question,
secret_answer: answer,
birth_date: "1990-01-01",
app_store_territory: "USA"
}
UI.message("Creating #{email}...")
Spaceship::ConnectAPI::SandboxTester.create(**data)
end
end
end
platform :mac do
desc "Runs tests on macOS"
lane :test_macos do |options|
generate_snapshots = ENV["CIRCLECI_TESTS_GENERATE_SNAPSHOTS"] == "true"
if generate_snapshots
test_plan = "CI-RevenueCat-Snapshots"
else
test_plan = "CI-RevenueCat"
end
begin
xcodebuild(
workspace: 'RevenueCat.xcworkspace',
scheme: 'RevenueCat',
destination: "platform=macOS,arch=arm64",
output_style: :basic,
result_bundle_path: 'fastlane/test_output/xctest/macos/revenuecat.xcresult',
report_formats: [:junit],
report_path: 'fastlane/test_output/xctest/macos/revenuecat/tests.xml',
test: true,
xcargs: "-testPlan #{test_plan} CODE_SIGN_IDENTITY='' CODE_SIGNING_REQUIRED=NO"
)
rescue => e
# Equivalent to `fail_build: !generate_snapshots`
raise e if !generate_snapshots
end
end
end
def check_no_git_tag_exists(version_number)
if git_tag_exists(tag: version_number, remote: true, remote_name: 'origin')
raise "git tag with version #{version_number} already exists!"
else
UI.message("✅ git tag with version #{version_number} doesn't exist")
end
end
def check_no_github_release_exists(version_number)
found_release_number = get_github_release(url: "revenuecat/purchases-ios", version: version_number)
raise "Release with version #{version_number} already exists!" unless found_release_number.nil?
end
lane :check_pods do
pod_lib_lint(
verbose: true,
podspec: 'RevenueCat.podspec',
platforms: 'ios,osx,tvos,watchos,visionos',
fail_fast: true
)
pod_lib_lint(
verbose: true,
podspec: 'RevenueCatUI.podspec',
include_podspecs: 'RevenueCat.podspec',
platforms: 'ios,osx,tvos,watchos,visionos',
fail_fast: true
)
end
lane :load_spm_dependencies do
Dir.chdir("#{File.dirname(__FILE__)}/../") do
sh("xcodebuild", "-list")
end
end
# Snapshots
def snapshots_commit_file
"#{File.dirname(__FILE__)}/../Tests/purchases-ios-snapshots-commit"
end
def current_snapshots_commit
File.read(snapshots_commit_file).strip
end
def update_snapshots_commit(hash)
File.write(snapshots_commit_file, hash)
end