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 |lane, options| if lane.to_s == 'build_pr' pr = options[:pr] UI.success("Building #{pr}") ENV['BRANCH_TO_BUILD'] = pr is_build_pr = true end # 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 after_all do |lane| if lane.to_s == 'build' submit_to_store end if ENV['RESET_GIT_BRANCH'] == 'true' branch = ENV['BRANCH_TO_BUILD'] || 'master' 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 master' ## Remove the branch for the PR sh "git branch -D #{branch}" UI.success("Deleted PR branch \"#{branch}\"") 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_config_json('../package.json', package) 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 'Configure the app before building' lane :configure do json = load_config_json('../dist/assets/config.json') # Set the Segment API Key unless ENV['SEGMENT_API_KEY'].nil? || ENV['SEGMENT_API_KEY'].empty? json['SegmentApiKey'] = ENV['SEGMENT_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 # Save the config.json file save_config_json('../dist/assets/config.json', json) configured = true end desc 'Build the app for Android and iOS' lane :build do |options| configure set_app_version set_app_build_number # Build the android app self.runner.current_platform = :android build # Build the ios app self.runner.current_platform = :ios build end desc 'Build the app for Android and iOS unsigned' lane :unsigned do configure # Build the android app self.runner.current_platform = :android unsigned # Build the ios app self.runner.current_platform = :ios unsigned end desc 'Build the app using a PR so QA can test' lane :build_pr do configure # Build the android app self.runner.current_platform = :android build # Build the ios app self.runner.current_platform = :ios build end platform :ios do desc 'Build iOS app' lane :build do unless configured configure end update_identifiers replace_assets build_ios upload_file_to_s3({:os_type => "iOS"}) end desc 'Build an unsigned ipa' lane :unsigned do unless configured configure end update_identifiers replace_assets end lane :update_identifiers do # Set the name for the app app_name = ENV['APP_NAME'] || 'Mattermost Beta' update_info_plist( xcodeproj: './ios/Mattermost.xcodeproj', plist_path: 'Mattermost/Info.plist', display_name: app_name ) # Set the notification service extension bundle identifier notification_bundle_id = ENV['NOTIFICATION_SERVICE_IDENTIFIER'] || 'com.mattermost.rnbeta.NotificationService' 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 ) # Set the share extension bundle identifier extension_bundle_id = ENV['EXTENSION_APP_IDENTIFIER'] || 'com.mattermost.rnbeta.MattermostShare' update_app_identifier( xcodeproj: './ios/Mattermost.xcodeproj', plist_path: 'MattermostShare/Info.plist', app_identifier: extension_bundle_id ) find_replace_string( path_to_file: './ios/Mattermost.xcodeproj/project.pbxproj', old_string: 'com.mattermost.rnbeta.MattermostShare', new_string: extension_bundle_id ) # Set the app bundle id app_bundle_id = ENV['MAIN_APP_IDENTIFIER'] || 'com.mattermost.rnbeta' update_app_identifier( xcodeproj: './ios/Mattermost.xcodeproj', plist_path: 'Mattermost/Info.plist', app_identifier: app_bundle_id ) find_replace_string( path_to_file: './ios/Mattermost.xcodeproj/project.pbxproj', old_string: 'com.mattermost.rnbeta', new_string: app_bundle_id ) # If set update the development team unless ENV['FASTLANE_TEAM_ID'].nil? || ENV['FASTLANE_TEAM_ID'].empty? update_project_team( path: './ios/Mattermost.xcodeproj', teamid: ENV['FASTLANE_TEAM_ID'] ) end # Set the ICloud container icloud_container = ENV['IOS_ICLOUD_CONTAINER'] || 'iCloud.com.mattermost.rnbeta' update_icloud_container_identifiers( entitlements_file: './ios/Mattermost/Mattermost.entitlements', icloud_container_identifiers: [icloud_container] ) # Set the app group id to share data between the app and the extension app_group_id = ENV['IOS_APP_GROUP'] || 'group.com.mattermost.rnbeta' update_app_group_identifiers( entitlements_file: './ios/Mattermost/Mattermost.entitlements', app_group_identifiers: [app_group_id] ) find_replace_string( path_to_file: './ios/UploadAttachments/UploadAttachments/MMMConstants.m', old_string: 'group.com.mattermost.rnbeta', new_string: app_group_id ) update_app_group_identifiers( entitlements_file: './ios/MattermostShare/MattermostShare.entitlements', app_group_identifiers: [app_group_id] ) update_app_group_identifiers( entitlements_file: './ios/NotificationService/NotificationService.entitlements', app_group_identifiers: [app_group_id] ) # Sync the provisioning profiles using match if ENV['SYNC_PROVISIONING_PROFILES'] == 'true' match(type: ENV['MATCH_TYPE'] || 'adhoc') 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/* ../ios/SplashScreenResource/' 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 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'] 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_options: { signingStyle: 'manual', iCloudContainerEnvironment: 'Production' } ) end end platform :android do desc 'Build Android app' lane :build do unless configured configure end configure_telemetry_android update_identifiers replace_assets link_sentry_android build_android move_apk_to_root upload_file_to_s3({:os_type => "Android"}) end desc 'Build an unsigned apk' lane :unsigned do unless configured configure end update_identifiers replace_assets gradle( task: 'assemble', build_type: 'Unsigned', project_dir: 'android/' ) end desc 'Update config and module/listener for Android telemetry' lane :configure_telemetry_android do if ENV['TELEMETRY_ENABLED'] == 'true' && ENV['TELEMETRY_URL'] != '' && ENV['TELEMETRY_API_KEY'] != '' # Update telemetry config json = load_config_json('../dist/assets/config.json') json['TelemetryEnabled'] = true json['TelemetryUrl'] = ENV['TELEMETRY_URL'] json['TelemetryApiKey'] = ENV['TELEMETRY_API_KEY'] save_config_json('../dist/assets/config.json', json) beta_dir = './android/app/src/main/java/com/mattermost/rnbeta/' # Listen to telemetry react markers find_replace_string( path_to_file: "#{beta_dir}MainApplication.java", old_string: '// addReactMarkerListener();', new_string: 'addReactMarkerListener();' ) 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) beta_dir = './android/app/src/main/java/com/mattermost/rnbeta/' release_dir = "./android/app/src/main/java/#{package_id.gsub '.', '/'}/" if ENV['BUILD_FOR_RELEASE'] == 'true' find_replace_string( path_to_file: "#{beta_dir}MainApplication.java", old_string: 'return BuildConfig.DEBUG;', new_string: 'return false;' ) end if release_dir != beta_dir unless Dir.exist?(".#{release_dir}") FileUtils.mkdir_p ".#{release_dir}" end sh "mv .#{beta_dir}* .#{release_dir}" find_replace_string( path_to_file: './android/app/BUCK', old_string: 'package com.mattermost.rnbeta;', new_string: "package #{package_id};" ) 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('../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 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 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' gradle( task: 'app:assemble', build_type: config_mode, project_dir: 'android/' ) end def move_apk_to_root app_name = ENV['APP_NAME'] || 'Mattermost Beta' app_name_sub = app_name.gsub(" ", "_") new_apk_path = "#{Pathname.new(File.expand_path(File.dirname(__FILE__))).parent.to_s}/#{app_name_sub}.apk" apk_path = lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH] unless apk_path.nil? sh "mv #{apk_path} #{new_apk_path}" lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH] = new_apk_path end end def link_sentry_android if ENV['SENTRY_ENABLED'] == 'true' url = 'https://sentry.io' File.open('../android/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=#{ENV['SENTRY_PROJECT_ANDROID']}\n"\ "auth.token=#{ENV['SENTRY_AUTH_TOKEN']}\n" ) end else UI.message('Not creating sentry.properties because Sentry is disabled') end end end def load_config_json(json_path) config_file = File.read(json_path) JSON.parse(config_file) end def save_config_json(json_path, json) File.open(json_path, 'w') do |f| f.write(JSON.pretty_generate(json)) 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', icon_url: 'https://mattermost-plugin-media.s3.amazonaws.com/fastlane.png', 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 }, { title: 'Built by', value: 'Jenkins', short: true }] }, success: options[:success] ) end end def submit_to_store apk_path = lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH] ipa_path = lane_context[SharedValues::IPA_OUTPUT_PATH] app_name = ENV['APP_NAME'] || 'Mattermost Beta' app_name_sub = app_name.gsub(" ", "_") s3_region = ENV['AWS_REGION'] s3_bucket = ENV['AWS_BUCKET_NAME'] # Submit to Google Play if required if !apk_path.nil? && ENV['SUBMIT_ANDROID_TO_GOOGLE_PLAY'] == 'true' UI.success("apk file #{apk_path}") supply( track: ENV['SUPPLY_TRACK'] || 'alpha', apk: apk_path ) 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_link = "https://#{s3_bucket}/#{s3_folder}/#{app_name_sub}.apk" # 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 link: #{public_link}" 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 link: #{public_link}" 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 # Submit to TestFlight if required if !ipa_path.nil? && ENV['SUBMIT_IOS_TO_TESTFLIGHT'] == 'true' UI.success("ipa file #{ipa_path}") pilot 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://#{s3_bucket}/#{s3_folder}/#{app_name_sub}.ipa" # 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 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 desc 'Upload file to s3' lane :upload_file_to_s3 do |options| os_type = options[:os_type] file = options[:file] file_plist = "" if file.nil? || file.empty? app_name = ENV['APP_NAME'] || 'Mattermost Beta' filename = app_name.gsub(" ", "_") if os_type == 'Android' file = "#{filename}.apk" elsif os_type == 'iOS' file = "#{filename}.ipa" file_plist = "#{filename}.plist" end end build_folder_path = Dir[File.expand_path('..')].first file_path = "#{build_folder_path}/#{file}" unless ENV['AWS_BUCKET_NAME'].nil? || ENV['AWS_BUCKET_NAME'].empty? || ENV['AWS_REGION'].nil? || ENV['AWS_REGION'].empty? || file_path.nil? s3_region = ENV['AWS_REGION'] s3_bucket = ENV['AWS_BUCKET_NAME'] s3_folder = '' if is_build_pr s3_folder = "#{ENV['AWS_FOLDER_NAME']}/#{ENV['BRANCH_TO_BUILD']}" else if os_type == 'Android' version_number = android_get_version_name(gradle_file: './android/app/build.gradle') build_number = android_get_version_code(gradle_file: './android/app/build.gradle') elsif os_type == 'iOS' version_number = get_version_number(xcodeproj: './ios/Mattermost.xcodeproj', target: 'Mattermost') build_number = get_build_number(xcodeproj: './ios/Mattermost.xcodeproj') end s3_folder = "#{ENV['AWS_FOLDER_NAME']}/#{version_number}/#{build_number}" end s3 = Aws::S3::Resource.new(region: s3_region) file_obj = s3.bucket(s3_bucket).object("#{s3_folder}/#{file}") file_obj.upload_file("#{file_path}") if is_build_pr if os_type == 'Android' install_url = "https://#{s3_bucket}/#{s3_folder}/#{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) plist_obj = s3.bucket(s3_bucket).object("#{s3_folder}/#{file_plist}") plist_obj.put(body: plist_body) install_url = "itms-services://?action=download-manifest&url=https://#{s3_bucket}/#{s3_folder}/#{file_plist}" end qa_build_message({ :os_type => os_type, :install_url => install_url }) end UI.success("S3 bucket @#{s3_bucket}, object @#{s3_folder}/#{file}") UI.success("S3 public path: https://#{s3_bucket}/#{s3_folder}/#{file}") end end