project_dir = File.expand_path('..', Dir.pwd) require_relative File.join(project_dir, 'fastlane-config', 'android_config') require_relative File.join(project_dir, 'fastlane-config', 'ios_config') require_relative './config/config_helpers' default_platform(:android) platform :android do desc "Assemble debug APKs." lane :assembleDebugApks do |options| gradle( tasks: ["assembleDebug"], ) end desc "Assemble Release APK" lane :assembleReleaseApks do |options| signing_config = FastlaneConfig.get_android_signing_config(options) # Generate version generateVersion = generateVersion() buildAndSignApp( taskName: "assemble", buildType: "Release", **signing_config ) end desc "Bundle Release APK" lane :bundleReleaseApks do |options| signing_config = FastlaneConfig.get_android_signing_config(options) # Generate version generateVersion = generateVersion() buildAndSignApp( taskName: "bundle", buildType: "Release", **signing_config ) end desc "Publish Release Artifacts to Firebase App Distribution" lane :deployReleaseApkOnFirebase do |options| signing_config = FastlaneConfig.get_android_signing_config(options) firebase_config = FastlaneConfig.get_firebase_config(:android, :prod) build_paths = FastlaneConfig::AndroidConfig::BUILD_PATHS # Generate version generateVersion = generateVersion( platform: "firebase", **firebase_config ) # Generate Release Note releaseNotes = generateReleaseNote() buildAndSignApp( taskName: "assembleProd", buildType: "Release", **signing_config ) firebase_app_distribution( app: firebase_config[:appId], android_artifact_type: "APK", android_artifact_path: build_paths[:prod_apk_path], service_credentials_file: firebase_config[:serviceCredsFile], groups: firebase_config[:groups], release_notes: releaseNotes ) end desc "Publish Demo Artifacts to Firebase App Distribution" lane :deployDemoApkOnFirebase do |options| signing_config = FastlaneConfig.get_android_signing_config(options) firebase_config = FastlaneConfig.get_firebase_config(:android, :demo) build_paths = FastlaneConfig::AndroidConfig::BUILD_PATHS # Generate version generateVersion = generateVersion( platform: "firebase", **firebase_config ) # Generate Release Note releaseNotes = generateReleaseNote() buildAndSignApp( taskName: "assembleDemo", buildType: "Release", **signing_config ) firebase_app_distribution( app: firebase_config[:appId], android_artifact_type: "APK", android_artifact_path: build_paths[:demo_apk_path], service_credentials_file: firebase_config[:serviceCredsFile], groups: firebase_config[:groups], release_notes: releaseNotes ) end desc "Deploy internal tracks to Google Play" lane :deployInternal do |options| signing_config = FastlaneConfig.get_android_signing_config(options) build_paths = FastlaneConfig::AndroidConfig::BUILD_PATHS # Generate version generateVersion = generateVersion(platform: "playstore") # Generate Release Note releaseNotes = generateReleaseNote() # Write the generated release notes to default.txt buildConfigPath = "metadata/android/en-US/changelogs/default.txt" FileUtils.mkdir_p(File.dirname(buildConfigPath)) File.write(buildConfigPath, releaseNotes) buildAndSignApp( taskName: "bundleProd", buildType: "Release", **signing_config ) upload_to_play_store( track: 'internal', aab: build_paths[:prod_aab_path], skip_upload_metadata: true, skip_upload_images: true, skip_upload_screenshots: true, ) end desc "Promote internal tracks to beta on Google Play" lane :promoteToBeta do upload_to_play_store( track: 'internal', track_promote_to: 'beta', skip_upload_changelogs: true, skip_upload_metadata: true, skip_upload_images: true, skip_upload_screenshots: true, ) end desc "Promote beta tracks to production on Google Play" lane :promote_to_production do upload_to_play_store( track: 'beta', track_promote_to: 'production', skip_upload_changelogs: true, skip_upload_metadata: true, skip_upload_images: true, skip_upload_screenshots: true, ) end desc "Generate artifacts for the given [build] signed with the provided [keystore] and credentials." private_lane :buildAndSignApp do |options| # Get the project root directory project_dir = File.expand_path('..', Dir.pwd) # Construct the absolute path to the keystore keystore_path = File.join(project_dir, 'keystores', options[:storeFile]) # Check if keystore exists unless File.exist?(keystore_path) UI.error "Keystore file not found at: #{keystore_path}" UI.error "Please ensure the keystore file exists at the correct location" exit 1 # Exit with error code 1 end gradle( task: options[:taskName], build_type: options[:buildType], properties: { "android.injected.signing.store.file" => keystore_path, "android.injected.signing.store.password" => options[:storePassword], "android.injected.signing.key.alias" => options[:keyAlias], "android.injected.signing.key.password" => options[:keyPassword], }, print_command: false, ) end desc "Generate Version for different platforms" lane :generateVersion do |options| platform = (options[:platform] || 'git').downcase # Generate version file for all platforms gradle(tasks: ["versionFile"]) # Set version from file with fallback version = File.read("../version.txt").strip rescue "1.0.0" ENV['VERSION'] = version case platform when 'playstore' prod_codes = google_play_track_version_codes(track: 'production') beta_codes = google_play_track_version_codes(track: 'beta') latest_code = (prod_codes + beta_codes).max || 1 ENV['VERSION_CODE'] = (latest_code + 1).to_s when 'firebase' begin latest_release = firebase_app_distribution_get_latest_release( app: options[:appId], service_credentials_file: options[:serviceCredsFile] ) latest_build_version = latest_release ? latest_release[:buildVersion].to_i : 0 ENV['VERSION_CODE'] = (latest_build_version + 1).to_s rescue => e UI.error("Error generating Firebase version: #{e.message}") raise e end when 'git' # Calculate version code from git history commit_count = `git rev-list --count HEAD`.to_i ENV['VERSION_CODE'] = (commit_count << 1).to_s else UI.user_error!("Unsupported platform: #{platform}. Supported platforms are: playstore, firebase, git") end # Output the results UI.success("Generated version for #{platform}") UI.success("Set VERSION=#{ENV['VERSION']} VERSION_CODE=#{ENV['VERSION_CODE']}") version end desc "Generate release notes" lane :generateReleaseNote do |options| releaseNotes = changelog_from_git_commits( commits_count: 1, ) releaseNotes end desc "Generate full release notes from specified tag or latest release tag" lane :generateFullReleaseNote do |options| def get_latest_tag latest = `git describe --tags --abbrev=0`.strip return latest unless latest.empty? latest = `git tag --sort=-creatordate`.split("\n").first return latest unless latest.nil? || latest.empty? nil end from_tag = options[:fromTag] || get_latest_tag UI.message "Using tag: #{from_tag || 'No tags found. Getting all commits...'}" commits = if from_tag && !from_tag.empty? `git log #{from_tag}..HEAD --pretty=format:"%B"`.split("\n") else `git log --pretty=format:"%B"`.split("\n") end categories = process_commits(commits) format_release_notes(categories) end private_lane :process_commits do |commits| notes = { "breaking" => [], "feat" => [], "fix" => [], "perf" => [], "refactor" => [], "style" => [], "docs" => [], "test" => [], "build" => [], "ci" => [], "chore" => [], "other" => [] } commits.each do |commit| next if commit.empty? || commit.start_with?("Co-authored-by:", "Merge") if commit.include?("BREAKING CHANGE:") || commit.include?("!") notes["breaking"] << commit.sub(/^[^:]+:\s*/, "") elsif commit =~ /^(feat|fix|perf|refactor|style|docs|test|build|ci|chore)(\(.+?\))?:/ notes[$1] << commit.sub(/^[^:]+:\s*/, "") else notes["other"] << commit end end notes end private_lane :format_release_notes do |categories| sections = { "breaking" => "๐Ÿ’ฅ Breaking Changes", "feat" => "๐Ÿš€ New Features", "fix" => "๐Ÿ› Bug Fixes", "perf" => "โšก Performance Improvements", "refactor" => "โ™ป๏ธ Refactoring", "style" => "๐Ÿ’… Style Changes", "docs" => "๐Ÿ“š Documentation", "test" => "๐Ÿงช Tests", "build" => "๐Ÿ“ฆ Build System", "ci" => "๐Ÿ‘ท CI Changes", "chore" => "๐Ÿ”ง Maintenance", "other" => "๐Ÿ“ Other Changes" } notes = ["# Release Notes", "\nRelease date: #{Time.now.strftime('%d-%m-%Y')}"] sections.each do |type, title| next if categories[type].empty? notes << "\n## #{title}" categories[type].each { |commit| notes << "\n- #{commit}" } end UI.message "Generated Release Notes:" UI.message notes.join("\n") notes.join("\n") end end platform :ios do ############################# # Shared Private Lane Helpers ############################# private_lane :setup_ci_if_needed do unless ENV['CI'] UI.message("๐Ÿ–ฅ๏ธ Running locally, skipping CI-specific setup.") else setup_ci end end private_lane :load_api_key do |options| ios_config = FastlaneConfig::IosConfig::BUILD_CONFIG app_store_connect_api_key( key_id: options[:appstore_key_id] || ios_config[:key_id], issuer_id: options[:appstore_issuer_id] || ios_config[:issuer_id], key_filepath: options[:key_filepath] || ios_config[:key_filepath], duration: 1200 ) end private_lane :fetch_certificates_with_match do |options| ios_config = FastlaneConfig::IosConfig::BUILD_CONFIG match( type: options[:match_type] || ios_config[:match_type], app_identifier: options[:app_identifier] || ios_config[:app_identifier], readonly: true, git_url: options[:git_url] || ios_config[:git_url], git_branch: options[:git_branch] || ios_config[:git_branch], git_private_key: options[:git_private_key] || ios_config[:match_git_private_key], force_for_new_devices: true, api_key: Actions.lane_context[SharedValues::APP_STORE_CONNECT_API_KEY] ) end private_lane :build_ios_project do |options| ios_config = FastlaneConfig::IosConfig::BUILD_CONFIG app_identifier = options[:app_identifier] || ios_config[:app_identifier] provisioning_profile_name = options[:provisioning_profile_name] || ios_config[:provisioning_profile_name] cocoapods( podfile: ios_config[:podfile_path], clean_install: true, repo_update: true ) # Manual signing for your main app target update_code_signing_settings( use_automatic_signing: false, path: ios_config[:project_path], targets: [ios_config[:target]], team_id: ios_config[:team_id], code_sign_identity: ios_config[:code_sign_identity], profile_name: provisioning_profile_name, bundle_identifier: app_identifier ) build_ios_app( scheme: ios_config[:scheme], workspace: ios_config[:workspace_path], output_name: ios_config[:output_name], output_directory: ios_config[:output_directory], configuration: ios_config[:configuration] ) end private_lane :set_plist_values do ios_config = FastlaneConfig::IosConfig::BUILD_CONFIG update_plist( plist_path: ios_config[:plist_path], block: proc do |plist| plist["ITSAppUsesNonExemptEncryption"] = false plist["NSCameraUsageDescription"] = "We use the camera to scan QR codes for payments and to add beneficiaries. No images or video are stored." plist["NSPhotoLibraryUsageDescription"] = "Allow access to choose a photo or document you decide to upload (e.g., profile photo or ID)." end ) end ################### # Main Public lanes ################### desc "Build Ios application" lane :build_ios do |options| ios_config = FastlaneConfig::IosConfig::BUILD_CONFIG cocoapods( podfile: ios_config[:podfile_path], clean_install: true, repo_update: true ) build_ios_app( scheme: ios_config[:scheme], workspace: ios_config[:workspace_path], output_name: ios_config[:output_name], output_directory: ios_config[:output_directory], skip_codesigning: true, skip_archive: true ) end desc "Build Signed Ios application" lane :build_signed_ios do |options| setup_ci_if_needed load_api_key(options) fetch_certificates_with_match(options) build_ios_project(options) end desc "Increment build number from latest Firebase release" lane :increment_version do |options| firebase_config = FastlaneConfig.get_firebase_config(:ios) ios_config = FastlaneConfig::IosConfig::BUILD_CONFIG latest_release = firebase_app_distribution_get_latest_release( app: firebase_config[:appId], service_credentials_file: options[:serviceCredsFile] || firebase_config[:serviceCredsFile] ) if latest_release increment_build_number( xcodeproj: ios_config[:project_path], build_number: latest_release[:buildVersion].to_i + 1 ) else UI.important("โš ๏ธ No existing Firebase release found. Skipping build number increment.") end end desc "Generate release notes" lane :generateReleaseNote do branchName = `git rev-parse --abbrev-ref HEAD`.chomp() releaseNotes = changelog_from_git_commits( commits_count: 1, ) releaseNotes end desc "Upload iOS application to Firebase App Distribution" lane :deploy_on_firebase do |options| firebase_config = FastlaneConfig.get_firebase_config(:ios) increment_version(serviceCredsFile: firebase_config[:serviceCredsFile]) build_signed_ios( options.merge( match_type: "adhoc", provisioning_profile_name: "match AdHoc org.mifos.mobile" ) ) releaseNotes = generateReleaseNote() firebase_app_distribution( app: options[:firebase_app_id] || firebase_config[:appId], service_credentials_file: options[:serviceCredsFile] || firebase_config[:serviceCredsFile], release_notes: releaseNotes, groups: options[:groups] || firebase_config[:groups] ) end desc "Upload beta build to TestFlight" lane :beta do |options| ios_config = FastlaneConfig::IosConfig::BUILD_CONFIG setup_ci_if_needed load_api_key(options) fetch_certificates_with_match( options.merge(match_type: "appstore") ) increment_version_number( xcodeproj: ios_config[:project_path], version_number: ios_config[:version_number] ) latest_build_number = latest_testflight_build_number( app_identifier: options[:app_identifier] || ios_config[:app_identifier], api_key: Actions.lane_context[SharedValues::APP_STORE_CONNECT_API_KEY], version: ios_config[:version_number] ) increment_build_number( xcodeproj: ios_config[:project_path], build_number: latest_build_number + 1 ) set_plist_values build_ios_project( options.merge( provisioning_profile_name: "match AppStore org.mifos.mobile" ) ) pilot( api_key: Actions.lane_context[SharedValues::APP_STORE_CONNECT_API_KEY], skip_waiting_for_build_processing: true ) end desc "Upload iOS Application to AppStore" lane :release do |options| ios_config = FastlaneConfig::IosConfig::BUILD_CONFIG setup_ci_if_needed load_api_key(options) fetch_certificates_with_match( options.merge(match_type: "appstore") ) increment_version_number( xcodeproj: ios_config[:project_path], version_number: ios_config[:version_number] ) latest_build_number = latest_testflight_build_number( app_identifier: options[:app_identifier] || ios_config[:app_identifier], api_key: Actions.lane_context[SharedValues::APP_STORE_CONNECT_API_KEY], version: ios_config[:version_number] ) increment_build_number( xcodeproj: ios_config[:project_path], build_number: latest_build_number + 1 ) set_plist_values build_ios_project( options.merge( provisioning_profile_name: "match AppStore org.mifos.mobile" ) ) deliver( screenshots_path: ios_config[:screenshots_ios_path], metadata_path: options[:metadata_path] || ios_config[:metadata_path], submit_for_review: false, # Set to true if you want to auto-submit for review automatic_release: true, # Set to true if you want to auto-release once it approved api_key: Actions.lane_context[SharedValues::APP_STORE_CONNECT_API_KEY], skip_app_version_update: false, force: true, # Skips HTML report verification precheck_include_in_app_purchases: false, overwrite_screenshots: true, reject_if_possible: true, app_rating_config_path: ios_config[:app_rating_config_path], submission_information: { add_id_info_uses_idfa: false, add_id_info_limits_tracking: false, add_id_info_serves_ads: false, add_id_info_tracks_action: false, add_id_info_tracks_install: false, content_rights_has_rights: true, content_rights_contains_third_party_content: false, export_compliance_platform: 'ios', export_compliance_compliance_required: false, export_compliance_encryption_updated: false, export_compliance_app_type: nil, export_compliance_uses_encryption: false, export_compliance_is_exempt: true, export_compliance_contains_third_party_cryptography: false, export_compliance_contains_proprietary_cryptography: false, export_compliance_available_on_french_store: true } ) end end platform :mac do ############################# # Shared Private Lane Helpers ############################# private_lane :setup_ci_if_needed do if ENV['CI'] setup_ci else UI.message("๐Ÿ–ฅ๏ธ Running locally, skipping CI-specific setup.") end end private_lane :load_api_key_macos do |options| ios_config = FastlaneConfig::IosConfig::BUILD_CONFIG app_store_connect_api_key( key_id: options[:appstore_key_id] || ios_config[:key_id], issuer_id: options[:appstore_issuer_id] || ios_config[:issuer_id], key_filepath: options[:key_filepath] || ios_config[:key_filepath], duration: 1200 ) end private_lane :next_macos_build_number do |options| ios_config = FastlaneConfig::IosConfig::BUILD_CONFIG latest = latest_testflight_build_number( app_identifier: options[:app_identifier] || ios_config[:app_identifier], api_key: Actions.lane_context[SharedValues::APP_STORE_CONNECT_API_KEY], platform: "osx", version: ios_config[:version_number] ) (latest.to_i + 1).to_s end # Resolve the most-recent generated .pkg path private_lane :find_pkg_path do project_dir = File.expand_path('..', Dir.pwd) Dir[File.join(project_dir, 'cmp-desktop', 'build', 'release', '**', 'pkg', '*.pkg')] .max_by { |p| File.mtime(p) } || UI.user_error!('PKG not found!') end ################### # Public lanes ################### desc "Build & upload macOS (.pkg) to TestFlight" lane :desktop_testflight do |options| setup_ci_if_needed load_api_key_macos(options) new_build_number = next_macos_build_number(options) gradle( tasks: ["packageReleasePkg"], properties: { "buildNumber" => new_build_number, "macOsAppStoreRelease" => true } ) pkg_path = find_pkg_path UI.message("Found PKG at: #{pkg_path}") pilot( api_key: Actions.lane_context[SharedValues::APP_STORE_CONNECT_API_KEY], pkg: pkg_path, app_platform: 'osx', skip_waiting_for_build_processing: true, ) end desc "Build & submit macOS app to App Store (non-beta)" lane :desktop_release do |options| ios_config = FastlaneConfig::IosConfig::BUILD_CONFIG setup_ci_if_needed load_api_key_macos(options) new_build_number = next_macos_build_number(options) gradle( tasks: ["packageReleasePkg"], properties: { "buildNumber" => new_build_number, "macOsAppStoreRelease" => true } ) # Locate the produced PKG (adjust pattern if you rename) pkg_path = find_pkg_path UI.message("Found PKG at: #{pkg_path}") deliver( platform: 'osx', pkg: pkg_path, screenshots_path: ios_config[:screenshots_macos_path], metadata_path: options[:metadata_path] || ios_config[:metadata_path], submit_for_review: true, # Set to true if you want to auto-submit for review automatic_release: true, # Set to true if you want to auto-release once it approved api_key: Actions.lane_context[SharedValues::APP_STORE_CONNECT_API_KEY], skip_app_version_update: false, force: true, # Skips HTML report verification precheck_include_in_app_purchases: false, overwrite_screenshots: true, reject_if_possible: true, app_rating_config_path: ios_config[:app_rating_config_path], submission_information: { add_id_info_uses_idfa: false, add_id_info_limits_tracking: false, add_id_info_serves_ads: false, add_id_info_tracks_action: false, add_id_info_tracks_install: false, content_rights_has_rights: true, content_rights_contains_third_party_content: false, export_compliance_platform: 'osx', export_compliance_compliance_required: false, export_compliance_encryption_updated: false, export_compliance_app_type: nil, export_compliance_uses_encryption: false, export_compliance_is_exempt: true, export_compliance_contains_third_party_cryptography: false, export_compliance_contains_proprietary_cryptography: false, export_compliance_available_on_french_store: true } ) end end