# Customise this file, documentation can be found here: # https://github.com/KrauseFx/fastlane/tree/master/docs $:.unshift File.dirname(__FILE__) default_platform :ios configurations_to_test_on_pull = [ {device: 'iPhone 14 Pro', os: '16.3'}, {device: 'iPad Pro (12.9-inch) (5th generation)', os: '15.7.1'} ] def get_devices_by_scheme_from_options(options) configs = options[:configurations] || [{device: 'iPhone 14 Pro', os: '16.3'}] devices_by_scheme = {} configs.each do |options| scheme = options[:scheme] || 'Wikipedia' device = options[:device] || 'iPhone 14 Pro' os = options[:os] || '16.3' sim = "#{device} (#{os})" devices = devices_by_scheme[scheme] || [] devices << sim devices_by_scheme[scheme] = devices end return devices_by_scheme end def get_version_number_from_options(options) scheme = options[:scheme] || 'Wikipedia' config = options[:configuration] || 'Release' return get_version_number(target: scheme, configuration: config) end def clean_out_reference_images UI.header "Step: Clear previous reference images" UI.command "FileUtils.rm_rf(Dir.glob('../WikipediaUnitTests/ReferenceImages_64/*'))" FileUtils.rm_rf(Dir.glob('../WikipediaUnitTests/ReferenceImages_64/*')) end platform :ios do before_all do # Set project for commit_version_bump, which seems to get confused by projects in other folders ENV['FL_BUILD_NUMBER_PROJECT'] = "Wikipedia.xcodeproj" ensure_git_status_clean if ENV['FL_ENSURE_CLEAN'] end desc "Checks out the sha specified in the environment variables or the main branch" lane :checkout do sha = ENV['SHA'] if sha != nil puts sha commit = last_git_commit sh "git checkout #{sha}" end end desc "Runs linting (and eventually static analysis)" lane :analyze do read_xcversion xcodebuild( project: "Wikipedia.xcodeproj", scheme: "Wikipedia", configuration: "Debug", sdk: 'iphonesimulator', destination: 'platform=iOS Simulator,OS=15.1,name=iPhone 13 Pro', analyze: true ) end desc "Runs tests on select platforms for verifying pull requests" lane :verify_pull_request do |options| verify({configurations: configurations_to_test_on_pull}) end desc "Reads Xcode version from the .xcversion file and sets it using xcversion()" lane :read_xcversion do |options| version = `cat ../.xcversion` if ENV['GITHUB_ACTIONS'] # GitHub actions keeps beta versions around that confuse xcversion() # Use the absolute path to select the right version instead # Remove trailing 0's to match GitHub's naming conventions version_name = version.split('.').select { |n| n != '0' }.join('.') `sudo xcode-select -s /Applications/Xcode_#{version_name}.app` else xcversion(version: version) end end desc "Runs unit tests, generates reports." lane :verify do |options| slack_message = nil if ENV.key?('CIRCLECI') url = ENV['CIRCLE_BUILD_URL'] number = ENV['CIRCLE_BUILD_NUM'] job = ENV['CIRCLE_JOB'] slack_message = "<#{url}|CircleCI #{job} ##{number}>" end devices_by_scheme = get_devices_by_scheme_from_options(options) devices_by_scheme.each do |scheme, devices| opts = { devices: devices, scheme: scheme, project: 'Wikipedia.xcodeproj', configuration: 'Test', disable_concurrent_testing: true, output_files: "#{scheme}.junit", output_types: 'junit', slack_message: slack_message } scan(opts) end end desc "Records visual tests." lane :record_visual_tests do |options| read_xcversion clean_out_reference_images options[:configurations] ||= configurations_to_test devices_by_scheme = get_devices_by_scheme_from_options(options) devices_by_scheme.each do |scheme, devices| opts = { devices: devices, scheme: scheme, disable_concurrent_testing: true, project: 'Wikipedia.xcodeproj', buildlog_path: './build', configuration: 'Test', xcargs: "GCC_PREPROCESSOR_DEFINITIONS='\$(value) WMF_VISUAL_TEST_RECORD_MODE=1'", fail_build: false, output_types: '' } scan(opts) end end desc "Set the build number" lane :set_build_number do |options| build = options[:build] || 0 increment_build_number({ build_number: build }) end desc "Set version number" lane :set_version_number do |options| increment_version_number( version_number: options[:version_number] ) end desc "Increment the app version patch" lane :bump_patch do increment_version_number( bump_type: "patch" ) end desc "Increment the app version minor" lane :bump_minor do increment_version_number( bump_type: "minor" ) end desc "Increment the app version major" lane :bump_major do increment_version_number( bump_type: "major" ) end desc "Change version number and create PR with changes" lane :change_version do |options| ensure_git_status_clean(show_diff: true) size = options[:size] branch = "fastlane_version_bump" case size when "patch" bump_patch() when "minor" bump_minor() when "major" bump_major() end commit_version_bump(message: "Version bump") push_to_git_remote( remote: "origin", remote_branch: branch, force: true, set_upstream: true ) create_pull_request( api_token: ENV['GITHUB_TOKEN'], repo: "wikimedia/wikipedia-ios", title: "Version bump", head: branch, base: "main", body: "Please merge this in." ) end desc "Add a build tag for the current build number and push to repo. While this tags a build, tag_release sets a release tag." lane :tag do |options| prefix = options[:prefix] || "betas" build_number = options[:build_number] || options[:build] || get_build_number tag_name = "#{prefix}/#{build_number}" message = options[:message] || "" add_git_tag(tag: tag_name, message: message) sh "git push origin --tags" end desc "Add a release tag for the latest beta and push to repo. For tagging non-releases, use `tag`." lane :tag_release do |options| specified_build = options[:build_tag] if specified_build.nil? || specified_build.empty? latest_beta = get_latest_build_number() sh "git checkout betas/#{latest_beta}" else sh "git checkout #{specified_build}" end version = options[:version] || get_version_number_from_options(options) tag_name = "releases/#{version}" add_git_tag(tag: tag_name) sh "git push origin --tags && git checkout main" set_github_release( repository_name: "wikimedia/wikipedia-ios", api_token: ENV['GITHUB_TOKEN'], name: version, tag_name: tag_name, description: "release to iOS app store" ) end desc "Build the app for distribution" lane :build do |options| project_dir = ".." build_dir = "../build" product_name = options[:product_name] || "Wikipedia" project_name = options[:project] || product_name scheme_name = options[:scheme] || product_name version = options[:version] || '' number = options[:number] || '' read_xcversion sh "xcodebuild -project \"#{project_dir}/#{project_name}.xcodeproj\" -scheme \"#{scheme_name}\" -archivePath \"#{build_dir}/#{product_name}.xcarchive\" archive" sh "xcodebuild -exportArchive -exportOptionsPlist ExportOptions.plist -archivePath \"#{build_dir}/#{product_name}.xcarchive\" -exportPath \"#{build_dir}\"" end desc "Pushes both the production and staging app to TestFlight and tags the release. Only releases to internal testers. (This is very similar to `push_production`, although this command also tags the build in git.)" lane :deploy do |options| tag_prefix = "betas" last_build = get_latest_build_number(prefix: tag_prefix) build = options[:build] || last_build + 1 last_public_release_tag = get_latest_tag_with_prefix(prefix: "releases/")[0..-2] last_public_release_number = last_public_release_tag.gsub("releases/", "") full_merges_since_last_build = get_recent_commits(start: "#{tag_prefix}/#{last_build}", is_brief: false) full_merges_since_last_release = get_recent_commits(start: last_public_release_tag, is_brief: false) full_changelog = "\n__New since last build (#{last_build})__\n#{full_merges_since_last_build.empty? ? 'None' : full_merges_since_last_build}\n\n" + "__New since last public release (#{last_public_release_number})__\n#{full_merges_since_last_release.empty? ? 'None' : full_merges_since_last_release}" brief_merges_since_last_build = get_recent_commits(start: "betas/#{last_build}", is_brief: true) brief_merges_since_last_release = get_recent_commits(start: last_public_release_tag, is_brief: true) brief_changelog = "__New since last build (#{last_build})__\n#{brief_merges_since_last_build.empty? ? 'None' : brief_merges_since_last_build}\n\n" + "__New since last public release (#{last_public_release_number})__\n#{brief_merges_since_last_release.empty? ? 'None' : brief_merges_since_last_release}" push_production(build: build, changelog: brief_changelog) tag(prefix: tag_prefix, build_number: build, message: full_changelog) push_staging(build: build) end desc "Updates version, builds, and pushes the production build to TestFlight. Only releases to internal testers." lane :push_production do |options| push( product_name: "Wikipedia", app_identifier: "org.wikimedia.wikipedia", build: options[:build], changelog: options[:changelog] ) end desc "Updates version, builds, and pushes the staging build to TestFlight. Only releases to internal testers." lane :push_staging do |options| push( product_name: "Staging", app_identifier: "org.wikimedia.wikipedia.tfbeta", build: options[:build] ) end desc "Updates version, builds, and pushes experimental build to TestFlight. Only releases to internal testers." lane :push_experimental do |options| tag_prefix = "alphas" build = options[:build] || get_latest_build_number(prefix: tag_prefix) + 1 push( product_name: "Experimental", app_identifier: "org.wikimedia.wikipedia.tfalpha", build: build ) tag(prefix: tag_prefix, build_number: build) end lane :get_latest_tag_with_prefix do |options| prefix = options[:prefix] || "betas/" `git tag -l #{prefix}* --sort=-creatordate | head -n 1` end lane :get_latest_build_number do |options| prefix = options[:prefix] || "betas" prefix = "#{prefix}/" get_latest_tag_with_prefix(prefix: prefix)[prefix.length..-1].to_i end lane :get_recent_commits do |options| start = options[:start] || "HEAD" is_brief = options[:is_brief] || false format = is_brief ? '• %b (%s)' : '• %h - %b (%s)' small_sed = "s/(Merge pull.*\\(#[0-9]*\\).*)/(\\1)/" sed_removal = is_brief ? small_sed : "s/Merge pull request //g" if is_brief list_without_localizations = `git log #{start}..HEAD --merges --first-parent origin/main --grep="Merge branch 'main' into" --grep="Localisation updates from" --invert-grep --pretty=format:"#{format}" | sed '#{sed_removal}'` localization_numbers = `git log #{start}..HEAD --merges --first-parent origin/main --grep="Localisation updates from" --pretty=format:"%s" | sed "s/Merge pull.*\\(#[0-9]*\\).*/\\1/" | xargs` localizations_list = "• Localisation updates from https://translatewiki.net. Pull requests: " + localization_numbers localization_numbers.empty? ? list_without_localizations : "#{list_without_localizations}\n#{localizations_list}" else `git log #{start}..HEAD --merges --first-parent origin/main --grep="Merge branch 'main' into" --invert-grep --pretty=format:"#{format}" | sed '#{sed_removal}'` end end desc "updates version, builds, and pushes to TestFlight" lane :push do |options| build_dir = "build" product_name = options[:product_name] || "Wikipedia" app_identifier = options[:app_identifier] || "org.wikimedia.wikipedia" ipa_path = "#{build_dir}/#{product_name}.ipa" build_number = options[:build] || get_latest_build_number(prefix: "betas") + 1 version = get_version_number_from_options(options) apple_key = `cat ~/AuthKey.p8` issuer_id = `cat ~/issuerID.txt` key_id = `cat ~/keyID.txt` api_key = app_store_connect_api_key( key_id: key_id, issuer_id: issuer_id, key_content: apple_key ) increment_build_number( build_number: build_number ) # the changelog was overwriting whatever custom test notes we added # changelog_from_git_commits( # pretty: '- (%ae) %s', # Optional, lets you provide a custom format to apply to each commit when generating the changelog text # tag_match_pattern: "#{tag_prefix}/*", # Optional, lets you search for a tag name that matches a glob(7) pattern # include_merges: false # Optional, lets you filter out merge commits # ) build( product_name: product_name, project: "Wikipedia", number: build_number, version: version ) pilot( api_key: api_key, ipa: ipa_path, skip_waiting_for_build_processing: true, skip_submission: true, distribute_external: false, app_identifier: app_identifier, beta_app_feedback_email: "mobile-ios-wikipedia@wikimedia.org", changelog: options[:changelog] ) end desc "Upload app store metadata" lane :upload_app_store_metadata do deliver(skip_binary_upload: true, skip_screenshots: true) end desc "Download dSYMs from iTunes Connect" lane :dsyms do |options| app_identifier = options[:app_identifier] version = options[:version] build_number = options[:build_number] || latest_testflight_build_number(app_identifier: app_identifier) ipa_path = options[:ipa_path] output_directory = options[:output_directory] || "build" raise "Missing parameters" unless app_identifier && version && build_number && ipa_path && output_directory download_dsyms( build_number: build_number.to_s, version: version.to_s, app_identifier: app_identifier, output_directory: output_directory ) # Download dSYM files from iTC end lane :dsyms_alpha do |options| app_identifier = "org.wikimedia.wikipedia.tfalpha" ipa_path = "build/Wikipedia Alpha.ipa" version = options[:version] || get_version_number_from_options(options) dsyms( app_identifier: app_identifier, version: version, build_number: options[:build_number], ipa_path: ipa_path ) end lane :dsyms_beta do |options| app_identifier = "org.wikimedia.wikipedia" ipa_path = "build/Wikipedia.ipa" version = options[:version] || get_version_number_from_options(options) dsyms( app_identifier: app_identifier, version: version, build_number: options[:build_number] || options[:build], ipa_path: ipa_path ) end lane :dsyms_beta_app do |options| app_identifier = "org.wikimedia.wikipedia.tfbeta" ipa_path = "build/Wikipedia.ipa" version = options[:version] || get_version_number_from_options(options) dsyms( app_identifier: app_identifier, version: version, build_number: options[:build_number] || options[:build], ipa_path: ipa_path ) end end