fastlane_version '2.71.0' fastlane_require 'aws-sdk-s3' fastlane_require 'erb' fastlane_require 'json' fastlane_require 'pathname' skip_docs configured = false is_build_pr = false # Executes before anything else use to setup the script before_all do ENV['SPACESHIP_SKIP_2FA_UPGRADE'] = '1' is_build_pr = ENV['BUILD_PR'] == 'true' if is_build_pr && ENV['CI'] == 'true' ENV['BRANCH_TO_BUILD'] = ENV['GITHUB_REF_NAME'] end if ENV['CI'] != 'true' # Raises an error is git is not clean if ENV['COMMIT_CHANGES_TO_GIT'] == 'true' ensure_git_status_clean end # Block to ensure we are on the right branch branch = ENV['BRANCH_TO_BUILD'] || ENV['GIT_BRANCH'] begin ensure_git_branch( branch: branch ) rescue sh "git checkout #{branch}" UI.success("Using branch \"#{branch}\"") end # If we are going to commit changes to git create a separate branch # so no changes are done in the branch that is being built if ENV['COMMIT_CHANGES_TO_GIT'] == 'true' local_branch = ENV['GIT_LOCAL_BRANCH'] || 'build' sh "git checkout -b #{local_branch}" UI.success("Creating branch \"#{local_branch}\"") end end end after_all do |lane| if ENV['CI'] != 'true' if ENV['RESET_GIT_BRANCH'] == 'true' branch = ENV['BRANCH_TO_BUILD'] || 'main' package_id = ENV['MAIN_APP_IDENTIFIER'] || 'com.mattermost.rnbeta' beta_dir = '../android/app/src/main/java/com/mattermost/rnbeta' release_dir = "../android/app/src/main/java/#{package_id.gsub '.', '/'}" if beta_dir != release_dir sh "rm -rf #{release_dir}" end reset_git_repo( force: true, skip_clean: true ) sh "git checkout #{branch}" if ENV['COMMIT_CHANGES_TO_GIT'] == 'true' local_branch = ENV['GIT_LOCAL_BRANCH'] || 'build' sh "git branch -D #{local_branch}" UI.success("Deleted working branch \"#{local_branch}\"") if lane.to_s == 'build_pr' sh 'git checkout main' ## Remove the branch for the PR sh "git branch -D #{branch}" UI.success("Deleted PR branch \"#{branch}\"") end end end end end desc 'Increment version number for both Android and iOS' lane :set_app_version do version_number = ENV['VERSION_NUMBER'] unless version_number.nil? || version_number.empty? package = load_config_json('../package.json') package['version'] = version_number save_json_as_file('../package.json', package) lock = load_config_json('../package-lock.json') lock['version'] = version_number save_json_as_file('../package-lock.json', lock) android_set_version_name( gradle_file: './android/app/build.gradle', version_name: version_number ) increment_version_number( xcodeproj: './ios/Mattermost.xcodeproj', version_number: version_number ) if ENV['COMMIT_CHANGES_TO_GIT'] == 'true' msg = ENV['INCREMENT_VERSION_NUMBER_MESSAGE'] || 'Bump app version number to' commit_message = "#{msg} #{version_number.to_s}" build_folder_path = Dir[File.expand_path('..')].first repo_path = (sh "git -C #{build_folder_path} rev-parse --show-toplevel").strip git_dirty_files = (sh "git -C #{repo_path} diff --name-only HEAD").split(" ").join(' ') unless git_dirty_files.empty? sh "git -C #{repo_path} commit -m \"#{commit_message}\" #{git_dirty_files}" UI.success("Successfully committed \"#{git_dirty_files}\" 💾.") end end end end desc 'Increments the build number for both Android and iOS' lane :set_app_build_number do ## set the build number for both platforms # use the one for iOS if no env variable set if ENV['INCREMENT_BUILD_NUMBER'] === 'true' build_number = ENV['BUILD_NUMBER'] || (get_build_number(xcodeproj: './ios/Mattermost.xcodeproj').to_i + 1) increment_build_number( xcodeproj: './ios/Mattermost.xcodeproj', build_number: build_number ) android_set_version_code( gradle_file: './android/app/build.gradle', version_code: build_number ) if ENV['COMMIT_CHANGES_TO_GIT'] == 'true' msg = ENV['INCREMENT_BUILD_NUMBER_MESSAGE'] || 'Bump app build number to' commit_message = "#{msg} #{build_number.to_s}" build_folder_path = Dir[File.expand_path('..')].first repo_path = (sh "git -C #{build_folder_path} rev-parse --show-toplevel").strip git_dirty_files = (sh "git -C #{repo_path} diff --name-only HEAD").split(" ").join(' ') unless git_dirty_files.empty? sh "git -C #{repo_path} commit -m \"#{commit_message}\" #{git_dirty_files}" UI.success("Successfully committed \"#{git_dirty_files}\" 💾.") end end end end desc 'Increments version and build number for both Android and iOS' lane :set_app_version_build do set_app_version set_app_build_number end desc 'Configure the app before building' lane :configure do json = load_config_json('../dist/assets/config.json') # Set the Segment API Key unless ENV['RUDDER_API_KEY'].nil? || ENV['RUDDER_API_KEY'].empty? json['RudderApiKey'] = ENV['RUDDER_API_KEY'] end # Configure Sentry if enabled if ENV['SENTRY_ENABLED'] == 'true' json['SentryEnabled'] = true json['SentryOrg'] = ENV['SENTRY_ORG'] json['SentryProjectIos'] = ENV['SENTRY_PROJECT_IOS'] json['SentryProjectAndroid'] = ENV['SENTRY_PROJECT_ANDROID'] json['SentryDsnIos'] = ENV['SENTRY_DSN_IOS'] json['SentryDsnAndroid'] = ENV['SENTRY_DSN_ANDROID'] end # Configure show app review if ENV['SHOW_REVIEW'] == 'true' json['ShowReview'] = true end # Configure Show Onboarding if ENV['SHOW_ONBOARDING'] == 'true' json['ShowOnboarding'] = true end # Collect network request information with different request group (notifications, login, cold start, etc) if ENV['COLLECT_NETWORK_METRICS'] == 'true' json['CollectNetworkMetrics'] = true end # Configure the auth schemes auth_scheme = ENV['APP_AUTH_SCHEME'].to_s.empty? ? 'mmauth' : ENV['APP_AUTH_SCHEME'] auth_scheme_dev = ENV['APP_AUTH_SCHEME_BETA'].to_s.empty? ? 'mmauthbeta' : ENV['APP_AUTH_SCHEME_BETA'] json["AuthUrlScheme"] = auth_scheme.end_with?('://') ? auth_scheme : "#{auth_scheme}://" json["AuthUrlSchemeDev"] = auth_scheme_dev.end_with?('://') ? auth_scheme_dev : "#{auth_scheme_dev}://" # Save the config.json file save_json_as_file('../dist/assets/config.json', json) configured = true end desc 'Set Sentry upload properties' lane :link_sentry do |options| if ENV['SENTRY_ENABLED'] == 'true' os_type = options[:os_type].downcase if os_type == 'ios' project_dir = 'ios/' sentry_project = ENV['SENTRY_PROJECT_IOS'] elsif os_type == 'android' project_dir = 'android/' sentry_project = ENV['SENTRY_PROJECT_ANDROID'] end url = ENV['SENTRY_URL'] || 'https://sentry.io' File.open("../#{project_dir}sentry.properties", 'w+') do |f| UI.message('Creating sentry.properties from environment') f.write( "defaults.url=#{url}\n"\ "defaults.org=#{ENV['SENTRY_ORG']}\n"\ "defaults.project=#{sentry_project}\n"\ "auth.token=#{ENV['SENTRY_AUTH_TOKEN']}\n" ) end else UI.message('Not creating sentry.properties because Sentry is disabled') end end # Uploads built files to S3 # It's currently uploading to two different buckets depending on the # enable_s3_new_bucket option. # Default, uploads to the legacy bucket using static credentials (AWS_BUCKET_NAME). # To upload to the new bucket AWS_NEW_BUCKET_NAME (using IAM/OIDC), pass :enable_s3_new_bucket => true in options. # Example: upload_file_to_s3({:os_type => "ios", :enable_s3_new_bucket => true}) desc 'Upload file to s3' lane :upload_file_to_s3 do |options| os_type = options[:os_type].downcase extensions = os_type == "android" ? ["*.apk", "*.aab"] : ["*.ipa"] build_folder_path = Dir[File.expand_path('..')].first files = [] enable_s3_new_bucket = options[:enable_s3_new_bucket] == true unless options[:file].nil? || options[:file].empty? files.push("#{build_folder_path}/#{options[:file]}") else extensions.each do |extension| files.push(*Dir.glob("#{build_folder_path}/#{extension}").select { |f| File.file? f}) end end unless ENV['AWS_BUCKET_NAME'].nil? || ENV['AWS_BUCKET_NAME'].empty? || ENV['AWS_REGION'].nil? || ENV['AWS_REGION'].empty? || files.length == 0 version_number = '' build_number = '' pr_file = '' plist_file = '' if is_build_pr pr_file = File.basename(files.first) plist_file = "#{File.basename(files.first, '.*')}.plist" else version_number = os_type == "android" ? android_get_version_name(gradle_file: './android/app/build.gradle') : get_version_number(xcodeproj: './ios/Mattermost.xcodeproj', target: 'Mattermost') build_number = os_type == "android" ? android_get_version_code(gradle_file: './android/app/build.gradle') : get_build_number(xcodeproj: './ios/Mattermost.xcodeproj') end s3_region = ENV['AWS_REGION'] s3_bucket_name = ENV['AWS_BUCKET_NAME'] s3_new_bucket_name = ENV['AWS_NEW_BUCKET_NAME'] bucket_name = enable_s3_new_bucket ? s3_new_bucket_name : s3_bucket_name if is_build_pr # Get the current git commit SHA (short version) commit_sha = sh("git rev-parse --short HEAD").strip s3_folder = "#{ENV['AWS_FOLDER_NAME']}/#{ENV['BRANCH_TO_BUILD']}/#{commit_sha}" else s3_folder = "#{ENV['AWS_FOLDER_NAME']}/#{version_number}/#{build_number}" end if enable_s3_new_bucket # Client for new bucket assumes role via GitHub Actions OIDC s3_client = Aws::S3::Client.new(region: s3_region) UI.message("Set S3 Client to upload to new S3 bucket") else s3_client = Aws::S3::Client.new( region: s3_region, access_key_id: ENV['AWS_ACCESS_KEY_ID'], secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'] ) UI.message("Set S3 Client to upload to existing S3 bucket: #{s3_bucket_name}") end transfer_manager = Aws::S3::TransferManager.new(client: s3_client) mutex = Mutex.new threads = [] file_number = 0 total_files = files.length links = [] total_files.times do |i| threads[i] = Thread.new { until files.empty? mutex.synchronize do file_number += 1 Thread.current["file_number"] = file_number end file = files.pop rescue nil next unless file filename = File.basename(file) puts "[#{Thread.current["file_number"]}/#{total_files}] #{filename} uploading..." s3_key = "#{s3_folder}/#{filename}" transfer_manager.upload_file(file, bucket: bucket_name, key: s3_key) # Keep s3_bucket_name to support both buckets. links.push("https://#{s3_bucket_name}/#{s3_key}") end } end threads.each { |t| t.join } if is_build_pr if os_type == 'android' install_url = "https://#{s3_bucket_name}/#{s3_folder}/#{pr_file}" elsif os_type == 'ios' current_build_number = get_build_number(xcodeproj: './ios/Mattermost.xcodeproj') plist_template = File.read('plist.erb') plist_body = ERB.new(plist_template).result(binding) s3_client.put_object( bucket: bucket_name, key: "#{s3_folder}/#{plist_file}", body: plist_body, content_type: 'application/xml' ) install_url = "itms-services://?action=download-manifest&url=https://#{s3_bucket_name}/#{s3_folder}/#{plist_file}" end if enable_s3_new_bucket == false qa_build_message({ :os_type => os_type, :install_url => install_url }) end end if options[:file]&.start_with?('Mattermost-simulator-') && options[:file]&.end_with?('.app.zip') && enable_s3_new_bucket == false pretext = '#### New iOS build for VM/Simulator' msg = "Download link: #{links.first}" send_message_to_mattermost({ :version_number => version_number, :build_number => build_number, :pretext => pretext, :title => '', :thumb_url => 'https://support.apple.com/library/content/dam/edam/applecare/images/en_US/iOS/move-to-ios-icon.png', :msg => msg, :default_payloads => [:git_branch, :last_git_commit_hash], :success => true, }) end UI.message("S3 public paths:\n#{links.join("\n")}") end end desc 'Create GitHub release' lane :github do tag = ENV['GITHUB_REF_NAME'] || ENV['TAG'] if tag version = android_get_version_name(gradle_file: './android/app/build.gradle') build = android_get_version_code(gradle_file: './android/app/build.gradle') changelog = File.read("metadata/changelog") android = [ " * [Mattermost arm64-v8a](https://releases.mattermost.com/mattermost-mobile/#{version}/#{build}/Mattermost-arm64-v8a.apk)", " * [Mattermost armeabi-v7a](https://releases.mattermost.com/mattermost-mobile/#{version}/#{build}/Mattermost-armeabi-v7a.apk)", " * [Mattermost x86](https://releases.mattermost.com/mattermost-mobile/#{version}/#{build}/Mattermost-x86.apk)", " * [Mattermost x86_64](https://releases.mattermost.com/mattermost-mobile/#{version}/#{build}/Mattermost-x86_64.apk)", ] changelog.concat("* Android\n#{android.join("\n")}\n") changelog.concat("* iOS\n * [Mattermost](https://releases.mattermost.com/mattermost-mobile/#{version}/#{build}/Mattermost.ipa)") github_release = set_github_release( repository_name: "mattermost/mattermost-mobile", api_token: ENV["GITHUB_TOKEN"], name: "Mobile Version #{version}", tag_name: tag, description: changelog, upload_assets: ["./Mattermost-unsigned.ipa", "./Mattermost-unsigned.apk"], is_draft: true ) end end platform :ios do before_all do if ENV['CI'] == 'true' setup_ci end end desc 'Get iOS match profiles' lane :certs do if !ENV['IOS_API_KEY_ID'].nil? && !ENV['IOS_API_KEY_ID'].empty? api_key = get_apple_api_key() match( api_key: api_key, type: ENV['MATCH_TYPE'] || 'adhoc', ) end end desc 'Build iOS app' lane :build do unless configured configure end update_settings replace_assets link_sentry({:os_type => "ios"}) build_ios upload_file_to_s3({:os_type => "ios"}) end desc 'Build an unsigned ipa' lane :unsigned do UI.success('Building unsigned iOS app') ENV['APP_NAME'] = 'Mattermost' ENV['REPLACE_ASSETS'] = 'true' ENV['BUILD_FOR_RELEASE'] = 'true' ENV['APP_SCHEME'] = 'mattermost' update_settings replace_assets build_ios_unsigned_or_simulator( more_xc_args: "-sdk iphoneos CODE_SIGNING_ALLOWED=NO", rel_build_dir: "Release-iphoneos", output_file: "Mattermost-unsigned.ipa", ) end lane :simulator do |options| skip_upload_to_s3_bucket = options[:skip_upload_to_s3_bucket] == true UI.success('Building iOS app for simulator') # Detect the host architecture host_arch = `uname -m`.strip # Use arm64 for Apple Silicon (arm64), x86_64 for Intel if host_arch == "arm64" build_arch = "arm64" output_file = "Mattermost-simulator-arm64.app.zip" else build_arch = "x86_64" output_file = "Mattermost-simulator-x86_64.app.zip" end UI.message("Building for #{build_arch} architecture on #{host_arch} host") build_ios_unsigned_or_simulator( more_xc_args: "-sdk iphonesimulator -arch #{build_arch}", rel_build_dir: "Release-iphonesimulator", output_file: output_file, ) unless skip_upload_to_s3_bucket upload_file_to_s3({ :os_type => "ios", :file => output_file }) end end desc 'Upload artifacts to S3' lane :upload_artifacts_to_s3 do |options| output_file = options[:output_file] UI.message("Uploading to new S3 bucket..." + (output_file ? " file: #{output_file}" : "")) begin if output_file && !output_file.empty? upload_file_to_s3({ :os_type => "ios", :file => output_file, :enable_s3_new_bucket => true }) else upload_file_to_s3({ :os_type => "ios", :enable_s3_new_bucket => true }) end rescue SyntaxError, StandardError UI.error("Failed to upload to new S3 bucket") end end lane :update_settings do # Configure Main app settings app_name = ENV['APP_NAME'] || 'Mattermost Beta' app_bundle_id = ENV['MAIN_APP_IDENTIFIER'] || 'com.mattermost.rnbeta' app_group_id = ENV['IOS_APP_GROUP'] || 'group.com.mattermost.rnbeta' icloud_container = ENV['IOS_ICLOUD_CONTAINER'] || 'iCloud.com.mattermost.rnbeta' app_scheme = ENV['APP_SCHEME'] || 'mattermost' app_auth_scheme = get_auth_scheme keychain_group = "$(AppIdentifierPrefix)#{ENV["IOS_KEYCHAIN_GROUP"] || app_bundle_id}" update_info_plist( xcodeproj: './ios/Mattermost.xcodeproj', plist_path: 'Mattermost/Info.plist', block: proc do |plist| plist["CFBundleDisplayName"] = app_name plist["CFBundleIdentifier"] = app_bundle_id plist["AppGroupIdentifier"] = app_group_id end ) find_replace_string( path_to_file: './ios/Mattermost.xcodeproj/project.pbxproj', old_string: 'com.mattermost.rnbeta', new_string: app_bundle_id ) update_icloud_container_identifiers( entitlements_file: './ios/Mattermost/Mattermost.entitlements', icloud_container_identifiers: [icloud_container] ) update_app_group_identifiers( entitlements_file: './ios/Mattermost/Mattermost.entitlements', app_group_identifiers: [app_group_id] ) update_url_schemes( path: './ios/Mattermost/Info.plist', url_schemes: [app_scheme, app_auth_scheme] ) update_keychain_access_groups( entitlements_file: './ios/Mattermost/Mattermost.entitlements', identifiers: [keychain_group], ) # Configure Share extension settings extension_bundle_id = ENV['EXTENSION_APP_IDENTIFIER'] || 'com.mattermost.rnbeta.MattermostShare' update_info_plist( xcodeproj: './ios/Mattermost.xcodeproj', plist_path: 'MattermostShare/Info.plist', block: proc do |plist| plist["CFBundleDisplayName"] = app_name plist["CFBundleIdentifier"] = extension_bundle_id plist["AppGroupIdentifier"] = app_group_id end ) find_replace_string( path_to_file: './ios/Mattermost.xcodeproj/project.pbxproj', old_string: 'com.mattermost.rnbeta.MattermostShare', new_string: extension_bundle_id ) update_app_group_identifiers( entitlements_file: './ios/MattermostShare/MattermostShare.entitlements', app_group_identifiers: [app_group_id] ) # Configure Notification Service settings notification_bundle_id = ENV['NOTIFICATION_SERVICE_IDENTIFIER'] || 'com.mattermost.rnbeta.NotificationService' update_info_plist( xcodeproj: './ios/Mattermost.xcodeproj', plist_path: 'NotificationService/Info.plist', block: proc do |plist| plist["CFBundleIdentifier"] = notification_bundle_id plist["AppGroupIdentifier"] = app_group_id end ) update_app_identifier( xcodeproj: './ios/Mattermost.xcodeproj', plist_path: 'NotificationService/Info.plist', app_identifier: notification_bundle_id ) find_replace_string( path_to_file: './ios/Mattermost.xcodeproj/project.pbxproj', old_string: 'com.mattermost.rnbeta.NotificationService', new_string: notification_bundle_id ) update_app_group_identifiers( entitlements_file: './ios/NotificationService/NotificationService.entitlements', app_group_identifiers: [app_group_id] ) # Update the project Team ID if provided unless ENV['FASTLANE_TEAM_ID'].nil? || ENV['FASTLANE_TEAM_ID'].empty? update_project_team( path: './ios/Mattermost.xcodeproj', teamid: ENV['FASTLANE_TEAM_ID'] ) end ######################## # Configure Intune intune_enabled = ENV['INTUNE_ENABLED'] == 'true' || ENV['INTUNE_ENABLED'] == '1' if intune_enabled intune_client_id = ENV['INTUNE_CLIENT_ID'] intune_redirect_uri = "msauth.#{app_bundle_id}://auth" update_info_plist( xcodeproj: './ios/Mattermost.xcodeproj', plist_path: 'Mattermost/Info.plist', block: proc do |plist| urlScheme = plist["CFBundleURLTypes"].find{|scheme| scheme["CFBundleURLName"] == "com.mattermost"} urlScheme[:CFBundleURLSchemes] = [ app_scheme, app_auth_scheme, "msauth.#{app_bundle_id}", "#{app_scheme}-intunemam", "#{app_auth_scheme}-intunemam", "msauth.com.microsoft.intunemam", "msauth.com.microsoft.intunemam-intunemam" ] plist["IntuneMAMSettings"] = { "AppGroupIdentifiers" => [ENV['IOS_APP_GROUP'] || 'group.com.mattermost.rnbeta'], "MAMTelemetryDisabled" => true, "MultiIdentity" => true, "ADALLogOverrideDisabled" => true, "HooksAlwaysEnabled" => true, "ADALClientId" => intune_client_id, "ADALRedirectUri" => intune_redirect_uri, "MaxFileProtectionLevel" => "NSFileProtectionCompleteUntilFirstUserAuthentication" } plist["LSApplicationQueriesSchemes"] = [ "scmx", "smsec", "microsoft-edge-http", "microsoft-edge-http-intunemam", "lookoutwork-ase", "mvisionmobile", "skycure", "companyportal", "lacoonsecurity", "intunemam-mtd", "msauthv2", "wandera", "betteractiveshield", "msauthv3", "microsoft-edge-https", "microsoft-edge-https-intunemam" ] end ) update_keychain_access_groups( entitlements_file: './ios/Mattermost/Mattermost.entitlements', identifiers: [ keychain_group, "$(AppIdentifierPrefix)com.microsoft.intune.mam", "$(AppIdentifierPrefix)com.microsoft.adalcache", "$(AppIdentifierPrefix)#{app_bundle_id}.intunemam" ], ) end # Sync the provisioning profiles using match if ENV['SYNC_PROVISIONING_PROFILES'] == 'true' if !ENV['MATCH_PASSWORD'].nil? && !ENV['MATCH_PASSWORD'].empty? match(type: ENV['MATCH_TYPE'] || 'adhoc') end if !ENV['IOS_API_KEY_ID'].nil? && !ENV['IOS_API_KEY_ID'].empty? api_key = get_apple_api_key() match( api_key: api_key, type: ENV['MATCH_TYPE'] || 'adhoc' ) end end end lane :replace_assets do if ENV['REPLACE_ASSETS'] == 'true' sh 'cp -R ../dist/assets/release/icons/ios/* ../ios/Mattermost/Images.xcassets/AppIcon.appiconset/' sh 'cp -R ../dist/assets/release/splash_screen/ios/LaunchScreen.storyboard ../ios/SplashScreenResource/LaunchScreen.storyboard' sh 'cp -R ../dist/assets/release/splash_screen/ios/SplashBackground.imageset ../ios/Mattermost/Images.xcassets/' sh 'cp -R ../dist/assets/release/splash_screen/ios/SplashIcon.imageset ../ios/Mattermost/Images.xcassets/' end app_name = ENV['APP_NAME'] || 'Mattermost Beta' localization_base_path = '../dist/assets/i18n/' Dir.glob('*.json', base: localization_base_path) do |item| localization_file = File.read(localization_base_path + item) json = JSON.parse(localization_file) plist_strings = '' localized_strings = '' json.each do |key, value| unless value.nil? || value.empty? if key.match?('mobile.ios.plist') plist_strings.concat("#{key.split('.').last} = \"#{value}\";\n") else value.gsub!('\\', '\\\\\\\\') # How to replace backslash with double backslash https://stackoverflow.com/a/6209532 value.gsub!('"', '\"') localized_strings.concat("\"#{key}\" = \"#{value}\";\n") end end end plist_strings.gsub!('{applicationName}', "#{app_name}") localized_strings.gsub!('{applicationName}', "#{app_name}") language = item.split('.').first project_localization_path = "../ios/Mattermost/i18n/#{language}.lproj" FileUtils.mkdir_p project_localization_path File.write("#{project_localization_path}/InfoPlist.strings", plist_strings) File.write("#{project_localization_path}/Localizable.strings", localized_strings) end end lane :deploy do |options| ipa_path = options[:file] unless ipa_path.nil? submit_to_testflight(ipa_path) end end error do |lane, exception| version = get_version_number(xcodeproj: './ios/Mattermost.xcodeproj', target: 'Mattermost') build_number = get_build_number(xcodeproj: './ios/Mattermost.xcodeproj') send_message_to_mattermost({ :version_number => version, :build_number => build_number, :pretext => '', :title => 'Unsuccessful Build', :thumb_url => 'https://support.apple.com/library/content/dam/edam/applecare/images/en_US/iOS/move-to-ios-icon.png', :msg => exception.message, :default_payloads => [:lane], :success => false }) end def setup_code_signing update_code_signing_settings( use_automatic_signing: false, path: './ios/Mattermost.xcodeproj' ) ENV['MATCH_APP_IDENTIFIER'].split(',').each do |id| target = 'Mattermost' if id.include? 'NotificationService' target = 'NotificationService' elsif id.include? 'MattermostShare' target = 'MattermostShare' end profile = "sigh_#{id}_#{ENV['MATCH_TYPE']}" config_mode = ENV['BUILD_FOR_RELEASE'] == 'true' ? 'Release' : 'Debug' update_project_provisioning( xcodeproj: './ios/Mattermost.xcodeproj', profile: ENV["#{profile}_profile-path"], # optional if you use sigh target_filter: ".*#{target}$", # matches name or type of a target build_configuration: config_mode, code_signing_identity: 'iPhone Distribution' # optionally specify the codesigning identity ) end end def build_ios app_name = ENV['APP_NAME'] || 'Mattermost Beta' app_name_sub = app_name.gsub(" ", "_") config_mode = ENV['BUILD_FOR_RELEASE'] == 'true' ? 'Release' : 'Debug' method = ENV['IOS_BUILD_EXPORT_METHOD'].nil? || ENV['IOS_BUILD_EXPORT_METHOD'].empty? ? 'ad-hoc' : ENV['IOS_BUILD_EXPORT_METHOD'] # Need to add xcargs to only notification and share extensions xcargs = ENV['SENTRY_ENABLED'] == 'true' ? "SENTRY_DSN_IOS='#{ENV['SENTRY_DSN_IOS']}' SENTRY_ENABLED='#{ENV['SENTRY_ENABLED']}'" : '' setup_code_signing gym( clean: true, scheme: 'Mattermost', configuration: config_mode, workspace: './ios/Mattermost.xcworkspace', export_method: method, skip_profile_detection: true, output_name: "#{app_name_sub}.ipa", export_xcargs: "-allowProvisioningUpdates", export_options: { signingStyle: 'manual', iCloudContainerEnvironment: 'Production' }, xcargs: xcargs ) end def build_ios_unsigned_or_simulator(more_xc_args:, rel_build_dir:, output_file:) root_dir = Dir[File.expand_path('..')].first ios_build_dir = "#{root_dir}/ios/Build/Products" ENV['APP_NAME'] = 'Mattermost' ENV['REPLACE_ASSETS'] = 'true' ENV['BUILD_FOR_RELEASE'] = 'true' ENV['APP_SCHEME'] = 'mattermost' update_settings replace_assets sh "rm -rf #{ios_build_dir}/" sh "cd #{root_dir}/ios && xcodebuild -workspace Mattermost.xcworkspace -scheme Mattermost -configuration Release -parallelizeTargets CODE_SIGN_IDENTITY='' CODE_SIGNING_REQUIRED=NO SYMROOT='#{ios_build_dir}' #{more_xc_args} " sh "cd #{ios_build_dir}/#{rel_build_dir} && zip -r #{output_file} Mattermost.app" sh "mv #{ios_build_dir}/#{rel_build_dir}/#{output_file} #{root_dir}" end end platform :android do desc 'Build Android app' lane :build do unless configured configure end update_identifiers replace_assets pinned_certificates link_sentry({:os_type => "android"}) build_android move_android_to_root upload_file_to_s3({:os_type => "android"}) end desc 'Build an unsigned apk' lane :unsigned do UI.success('Building unsigned Android app') ENV['APP_NAME'] = 'Mattermost' ENV['REPLACE_ASSETS'] = 'true' ENV['BUILD_FOR_RELEASE'] = 'true' ENV['APP_SCHEME'] = 'mattermost' update_identifiers replace_assets gradle( task: 'assemble', build_type: 'Unsigned', project_dir: 'android/' ) sh "mv #{lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH]} #{Pathname.new(File.expand_path(File.dirname(__FILE__))).parent.to_s}/Mattermost-unsigned.apk" end desc 'Upload artifacts to S3' lane :upload_artifacts_to_s3 do UI.message("Uploading to new S3 bucket...") begin upload_file_to_s3({:os_type => "android", :enable_s3_new_bucket => true}) rescue SyntaxError, StandardError UI.error("Failed to upload to new S3 bucket") end end lane :update_identifiers do # Set the name for the app app_name = ENV['APP_NAME'] || 'Mattermost Beta' sh "echo '#{app_name}' > ../fastlane/metadata/android/en-US/title.txt" android_change_string_app_name( newName: app_name, stringsFile: './android/app/src/main/res/values/strings.xml' ) package_id = ENV['MAIN_APP_IDENTIFIER'] || 'com.mattermost.rnbeta' android_change_package_identifier(newIdentifier: package_id, manifest: './android/app/src/main/AndroidManifest.xml') android_update_application_id(app_folder_name: 'android/app', application_id: package_id) android_update_namespace(app_folder_name: 'android/app', namespace: package_id) app_scheme = ENV['APP_SCHEME'] || 'mattermost' find_replace_string( path_to_file: "./android/app/src/main/AndroidManifest.xml", old_string: 'scheme="mattermost"', new_string: "scheme=\'#{app_scheme}\'" ) app_auth_scheme = get_auth_scheme find_replace_string( path_to_file: "./android/app/src/main/AndroidManifest.xml", old_string: 'scheme="mmauthbeta"', new_string: "scheme=\"#{app_auth_scheme}\"", ) helpers_dir = './android/app/src/main/java/com/mattermost/helpers/' beta_dir = './android/app/src/main/java/com/mattermost/rnbeta/' release_dir = "./android/app/src/main/java/#{package_id.gsub '.', '/'}/" if release_dir != beta_dir unless Dir.exist?(".#{release_dir}") FileUtils.mkdir_p ".#{release_dir}" end sh "mv .#{beta_dir}* .#{release_dir}" Dir.glob(".#{release_dir}*.java") do |item| find_replace_string( path_to_file: item[1..-1], old_string: 'package com.mattermost.rnbeta;', new_string: "package #{package_id};" ) end Dir.glob(".#{release_dir}*.kt") do |item| find_replace_string( path_to_file: item[1..-1], old_string: 'package com.mattermost.rnbeta', new_string: "package #{package_id}" ) end Dir.glob('../android/app/src/main/java/com/mattermost/share/*.java') do |item| find_replace_string( path_to_file: item[1..-1], old_string: 'import com.mattermost.rnbeta.MainApplication;', new_string: "import #{package_id}.MainApplication;" ) end Dir.glob('../android/app/src/main/java/com/mattermost/helpers/*.java') do |item| find_replace_string( path_to_file: item[1..-1], old_string: 'import com.mattermost.rnbeta.*;', new_string: "import #{package_id}.*;" ) end Dir.glob('../android/app/src/main/java/com/mattermost/helpers/*.kt') do |item| find_replace_string( path_to_file: item[1..-1], old_string: 'import com.mattermost.rnbeta.*', new_string: "import #{package_id}.*" ) end end end lane :replace_assets do if ENV['REPLACE_ASSETS'] == 'true' sh 'cp -R ../dist/assets/release/icons/android/* ../android/app/src/main/res/' sh 'cp -R ../dist/assets/release/splash_screen/android/* ../android/app/src/main/res/' end sh 'mkdir -p ../android/app/src/main/res/raw/' sh 'cp -R ../assets/sounds/* ../android/app/src/main/res/raw/' end lane :pinned_certificates do sh 'cp -R ../assets/certs/ ../android/app/src/main/assets/certs' end lane :deploy do |options| file_path = options[:file] unless file_path.nil? submit_to_google_play(file_path) end end error do |lane, exception| build_number = android_get_version_code( gradle_file: './android/app/build.gradle' ) version_number = android_get_version_name( gradle_file: './android/app/build.gradle' ) send_message_to_mattermost({ :version_number => version_number, :build_number => build_number, :pretext => '', :title => 'Unsuccessful Build', :thumb_url => 'https://lh3.ggpht.com/XL0CrI8skkxnboGct-duyg-bZ_MxJDTrjczyjdU8OP2PM1dmj7SP4jL1K8JQeMIB3AM=w300', :msg => exception.message, :default_payloads => [:lane], :success => false, }) end def build_android config_mode = ENV['BUILD_FOR_RELEASE'] == 'true' ? 'Release' : 'Debug' build_task = ENV['ANDROID_BUILD_TASK'].nil? || ENV['ANDROID_BUILD_TASK'].empty? ? 'assemble' : ENV['ANDROID_BUILD_TASK'] build_task = build_task.split(",") if build_task.include?("assemble") gradle( task: 'app:assemble', build_type: config_mode, project_dir: 'android/', properties: { 'separateApk' => ENV["SEPARATE_APKS"] || false, 'universalApk' => ENV["UNIVERSAL_APK"] || false, } ) end if build_task.include?("bundle") gradle( task: 'app:bundle', build_type: config_mode, project_dir: 'android/', ) end end def move_android_to_root app_name = ENV['APP_NAME'] || 'Mattermost Beta' app_name_sub = app_name.gsub(" ", "_") build_task = ENV['ANDROID_BUILD_TASK'].nil? || ENV['ANDROID_BUILD_TASK'].empty? ? 'assemble' : ENV['ANDROID_BUILD_TASK'] build_task = build_task.split(",") if build_task.include?("assemble") apks = lane_context[SharedValues::GRADLE_ALL_APK_OUTPUT_PATHS] json_path = lane_context[SharedValues::GRADLE_OUTPUT_JSON_OUTPUT_PATH] if json_path.nil? || json_path.empty? dir_name = File.dirname(apks.first) json_path = Dir.glob(File.join(dir_name, '*.json')).first end UI.message("JSON PATH APK #{json_path}") outputJson = load_config_json(json_path) paths = [] apks.each do |apk| filename = File.basename(apk) output = outputJson["elements"].find { |o| o["outputFile"] == filename } unless output.nil? filterName = output["filters"].empty? ? '' : output["filters"].first["value"] name = "#{app_name_sub}" unless filterName.nil? || filterName.empty? name += "-#{filterName}" end new_apk_path = "#{Pathname.new(File.expand_path(File.dirname(__FILE__))).parent.to_s}/#{name}.apk" paths.push(new_apk_path) sh "mv #{apk} #{new_apk_path}" UI.message("APK PATH \"#{new_apk_path}\", #{filterName}") end end lane_context[SharedValues::GRADLE_ALL_APK_OUTPUT_PATHS] = paths end if build_task.include?("bundle") aab = lane_context[SharedValues::GRADLE_AAB_OUTPUT_PATH] new_aab_path = "#{Pathname.new(File.expand_path(File.dirname(__FILE__))).parent.to_s}/#{app_name_sub}.aab" sh "mv #{aab} #{new_aab_path}" UI.message("AAB PATH \"#{new_aab_path}\"") end end end def get_auth_scheme if ENV['BETA_BUILD'] == 'true' ENV['APP_AUTH_SCHEME_BETA'].to_s.empty? ? 'mmauthbeta' : ENV['APP_AUTH_SCHEME_BETA'] else ENV['APP_AUTH_SCHEME'].to_s.empty? ? 'mmauth' : ENV['APP_AUTH_SCHEME'] end end def load_config_json(json_path) config_file = File.read(json_path) JSON.parse(config_file) end def save_json_as_file(json_path, json) File.open(json_path, 'w') do |f| f.write(JSON.pretty_generate(json)) f.write("\n") end end def send_message_to_mattermost(options) unless ENV['MATTERMOST_WEBHOOK_URL'].nil? || ENV['MATTERMOST_WEBHOOK_URL'].empty? mattermost( pretext: options[:pretext], message: options[:msg], default_payloads: options[:default_payloads], username: 'Fastlane', payload: {}, attachment_properties: { title: options[:title], thumb_url: options[:thumb_url], fields: [{ title: 'Version', value: options[:version_number], short: true }, { title: 'Build Number', value: options[:build_number], short: true }] }, success: options[:success] ) end end def get_apple_api_key api_key = '' unless ENV['IOS_API_KEY_ID'].nil? || ENV['IOS_API_KEY_ID'].empty? || ENV['IOS_API_ISSUER_ID'].nil? || ENV['IOS_API_ISSUER_ID'].empty? || ENV['IOS_API_KEY'].nil? || ENV['IOS_API_KEY'].empty? api_key_path = "#{ENV['IOS_API_KEY_ID']}.p8" File.open("../#{api_key_path}", 'w') do |f| key_string = ENV['IOS_API_KEY'] p8_array = key_string.split('\n') p8_array.each_with_index do |value, index| f.write(value) f.write("\n") unless index == p8_array.length - 1 end end api_key = app_store_connect_api_key( key_id: ENV['IOS_API_KEY_ID'], issuer_id: ENV['IOS_API_ISSUER_ID'], key_filepath: "./#{api_key_path}", in_house: ENV['IOS_IN_HOUSE'] == 'true', # optional but may be required if using match/sigh ) File.delete("../#{api_key_path}") end return api_key end def submit_to_testflight(ipa_path) ipa = (Dir.glob(ipa_path).select { |f| File.file? f}).first if !ipa.nil? UI.success("ipa file #{ipa}") api_key = get_apple_api_key() unless api_key.empty? pilot( ipa: ipa, api_key: api_key ) else UI.success("Uploading with Username / Password") pilot( ipa: ipa ) end else UI.user_error! "ipa file does not exist #{ipa}" return end filename = File.basename(ipa) version_number = get_version_number(xcodeproj: './ios/Mattermost.xcodeproj', target: 'Mattermost') build_number = get_build_number(xcodeproj: './ios/Mattermost.xcodeproj') s3_folder = "#{ENV['AWS_FOLDER_NAME']}/#{version_number}/#{build_number}" public_link = "https://#{ENV['AWS_BUCKET_NAME']}/#{s3_folder}/#{filename}" # Send a build message to Mattermost pretext = '#### New iOS released ready to be published' msg = "Release has been cut and is on TestFlight ready to be published.\nDownload link: #{public_link}" if ENV['BETA_BUILD'] == 'true' pretext = '#### New iOS beta published to TestFlight' msg = "Sign up as a beta tester [here](https://testflight.apple.com/join/Q7Rx7K9P).\nDownload link: #{public_link}" end send_message_to_mattermost({ :version_number => version_number, :build_number => build_number, :pretext => pretext, :title => '', :thumb_url => 'https://support.apple.com/library/content/dam/edam/applecare/images/en_US/iOS/move-to-ios-icon.png', :msg => msg, :default_payloads => [], :success => true, }) end def submit_to_google_play(file_path) filenames = [] if(file_path.include?("aab")) aab_filename = Dir.glob(file_path).select { |f| File.file? f}.first unless ENV['SUPPLY_JSON_KEY'].nil? || ENV['SUPPLY_JSON_KEY'].empty? supply( package_name: ENV['MAIN_APP_IDENTIFIER'] || ENV['SUPPLY_PACKAGE_NAME'], track: ENV['SUPPLY_TRACK'] || 'alpha', aab: aab_filename, skip_upload_apk: true ) end filenames.push(aab_filename) else apks = Dir.glob(file_path).select { |f| File.file? f} filenames = apks.map {|f| File.basename(f)} if(apks.length > 0) unless ENV['SUPPLY_JSON_KEY'].nil? || ENV['SUPPLY_JSON_KEY'].empty? supply( package_name: ENV['MAIN_APP_IDENTIFIER'] || ENV['SUPPLY_PACKAGE_NAME'], track: ENV['SUPPLY_TRACK'] || 'alpha', apk_paths: apks, skip_upload_aab: true ) end else UI.user_error! "There are no apks in #{apk_path}" return end end version_number = android_get_version_name(gradle_file: './android/app/build.gradle') build_number = android_get_version_code(gradle_file: './android/app/build.gradle') s3_folder = "#{ENV['AWS_FOLDER_NAME']}/#{version_number}/#{build_number}" public_path = "https://#{ENV['AWS_BUCKET_NAME']}/#{s3_folder}" links = (filenames.map { |f| "* #{public_path}/#{f}"}).join("\n") # Send a build message to Mattermost pretext = '#### New Android released ready to be published' msg = "Release has been cut and is on the Beta track ready to be published.\nDownload links:\n#{links}" if ENV['BETA_BUILD'] == 'true' pretext = '#### New Android beta published to Google Play' msg = "Sign up as a beta tester [here](https://play.google.com/apps/testing/com.mattermost.rnbeta).\nDownload links:\n#{links}" end send_message_to_mattermost({ :version_number => version_number, :build_number => build_number, :pretext => pretext, :title => '', :thumb_url => 'https://lh3.ggpht.com/XL0CrI8skkxnboGct-duyg-bZ_MxJDTrjczyjdU8OP2PM1dmj7SP4jL1K8JQeMIB3AM=w300', :msg => msg, :default_payloads => [], :success => true, }) end def qa_build_message(options) abbreviated_commit_hash = last_git_commit[:abbreviated_commit_hash] os_type = options[:os_type] install_url = options[:install_url] unless ENV['MATTERMOST_WEBHOOK_URL'].nil? || ENV['MATTERMOST_WEBHOOK_URL'].empty? os_type = options[:os_type] if os_type == 'android' msg = "QA build [#{abbreviated_commit_hash}](https://github.com/mattermost/mattermost-mobile/commit/#{abbreviated_commit_hash}) — [Android APK Link](#{install_url})" mattermost(message: msg, username: 'Fastlane', icon_url: 'https://lh3.ggpht.com/XL0CrI8skkxnboGct-duyg-bZ_MxJDTrjczyjdU8OP2PM1dmj7SP4jL1K8JQeMIB3AM=w300') elsif os_type == 'ios' msg = "QA build [#{abbreviated_commit_hash}](https://github.com/mattermost/mattermost-mobile/commit/#{abbreviated_commit_hash}) — [iOS pList Link](#{install_url})" mattermost(message: msg, username: 'Fastlane', icon_url: 'https://support.apple.com/library/content/dam/edam/applecare/images/en_US/iOS/move-to-ios-icon.png') end else UI.success("PR Built for #{os_type}: #{install_url}") end end