Last active
February 21, 2020 02:10
-
-
Save johanquiroga/6b758f5ba278e708d15e5c3c3f50931f to your computer and use it in GitHub Desktop.
fastlane ios
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# This file contains the fastlane.tools configuration | |
# You can find the documentation at https://docs.fastlane.tools | |
# | |
# For a list of all available actions, check out | |
# | |
# https://docs.fastlane.tools/actions | |
# | |
# For a list of all available plugins, check out | |
# | |
# https://docs.fastlane.tools/plugins/available-plugins | |
# | |
# Uncomment the line if you want fastlane to automatically update itself | |
# update_fastlane | |
default_platform(:ios) | |
platform :ios do | |
workspace = '{{MyApp}}.xcworkspace' | |
build_number = nil | |
before_all do |options, params| | |
build_number = | |
# highest priority has build_number passes as parameter to the fastlane | |
params[:build_number] || | |
latest_testflight_build_number + 1 | |
end | |
desc "Generate new localized screenshots" | |
lane :screenshots do | |
# Run UI Tests and take screenshots | |
capture_screenshots | |
# Upload new screenshots to google play | |
deliver | |
end | |
desc "Verify next release" | |
lane :verify do |options| | |
# Git status has to be clean | |
ensure_git_status_clean | |
# if the build is run on local machine | |
if !is_ci? then | |
# it sends slack message about the deploy was triggered | |
# slack(message: "#{title} deploy was triggered on local machine") | |
UI.message("#{options[:title]} deploy was triggered on local machine") | |
# Clean project | |
xcclean(workspace: workspace, scheme: options[:scheme]) | |
# yarn lint && yarn jest && yarn flow | |
# check_code_quality | |
else | |
# We don't need to do this on CI because repo is fresh | |
# slack(message: "#{options[:title]} deploy was triggered on CircleCI") | |
UI.message("#{options[:title]} deploy was triggered on CircleCI") | |
end | |
# Check if there is any change since last version | |
is_releaseable = analyze_commits( | |
match: options[:tag_prefix], | |
releases: { | |
fix: 'patch', | |
feat: 'minor', | |
'BREAKING CHANGE': 'major' | |
} | |
) | |
unless is_releaseable | |
# slack(message: "Skip deploying #{options[:title]}. No changes since last one!") | |
UI.important("Skip deploying #{options[:title]}. No changes since last one!") | |
end | |
is_releaseable | |
end | |
desc "Build a new version of iOS App" | |
lane :build do |options| | |
next_version = lane_context[SharedValues::RELEASE_NEXT_VERSION] | |
# pod install | |
cocoapods(clean: true) | |
# check that .lock files were not changed | |
ensure_git_status_clean | |
# code signing | |
match(type: "appstore") | |
# Increment major version in XCode project | |
increment_version_number(version_number: next_version) | |
# Increment build number | |
increment_build_number(build_number: build_number) | |
# Build app | |
gym(workspace: workspace, scheme: options[:scheme], configuration: 'Release') | |
end | |
desc "Generates release notes for slack and create the next tag" | |
lane :post_deploy do |options| | |
flavor = options[:flavor] | |
title = options[:title] | |
next_version = lane_context[SharedValues::RELEASE_NEXT_VERSION] | |
clean_build_artifacts | |
# Get release notes since last version for slack | |
notes = conventional_changelog(title: title, format: 'slack') | |
commit_version_bump(message: 'chore: iOS version bump', xcodeproj: '{{MyApp}}.xcodeproj') | |
# Create tag to recognize future "last version" (the current version) | |
add_git_tag(tag: "ios/#{flavor}/#{next_version}/#{build_number}") | |
push_git_tags | |
# slack(message: notes) | |
UI.important(notes) | |
end | |
desc "Build a new alpha version (Internal testing)" | |
lane :alpha do |options| | |
flavor = 'alpha' | |
tag_prefix = 'ios/alpha*' | |
title = 'iOS Alpha' | |
scheme = '{{MyApp}}' | |
groups = 'Alpha Team' | |
# Verify next release | |
next unless verify(scheme: scheme, title: title, tag_prefix: tag_prefix) | |
# Build ios app | |
build(scheme: scheme) | |
# And deploy it to the test flight | |
upload_to_testflight(groups: groups, changelog: conventional_changelog(title: title, format: 'slack')) | |
# Notify slack and create the tag | |
post_deploy(flavor: flavor, title: title) | |
end | |
desc "Build a new production version" | |
lane :release do |options| | |
flavor = 'production' | |
tag_prefix = 'ios/production*' | |
title = 'iOS Release' | |
scheme = '{{MyApp}}.production' | |
# Verify next release | |
next unless verify(scheme: scheme, title: title, tag_prefix: tag_prefix) | |
# Build ios app | |
build(scheme: scheme) | |
# And deploy it to the test flight | |
upload_to_app_store( | |
release_notes: conventional_changelog(title: title, format: 'slack'), | |
submit_for_review: true, | |
automatic_release: true | |
) | |
# Notify slack and create the tag | |
post_deploy(flavor: flavor, title: title) | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Autogenerated by fastlane | |
# | |
# Ensure this file is checked in to source control! | |
gem 'fastlane-plugin-semantic_release' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Uncomment the lines below you want to change by removing the # in the beginning | |
# A list of devices you want to take the screenshots from | |
devices([ | |
"iPhone 8", | |
"iPhone 11 Pro Max", | |
# "iPhone 8 Plus", | |
# "iPhone SE", | |
# "iPhone X", | |
# "iPad Pro (12.9-inch)", | |
# "iPad Pro (9.7-inch)", | |
# "Apple TV 1080p" | |
]) | |
languages([ | |
"en-US", | |
# "de-DE", | |
# "it-IT", | |
# ["pt", "pt_BR"] # Portuguese with Brazilian locale | |
]) | |
workspace('{{MyApp}}.xcworkspace') | |
# The name of the scheme which contains the UI Tests | |
scheme("{{MyApp}}UITests") | |
configuration('Release') | |
# Where should the resulting screenshots be stored? | |
# output_directory("./screenshots") | |
# remove the '#' to clear all previously generated screenshots before creating new ones | |
clear_previous_screenshots(true) | |
# Arguments to pass to the app on launch. See https://docs.fastlane.tools/actions/snapshot/#launch-arguments | |
# launch_arguments(["-favColor red"]) | |
# For more information about all available options run | |
# fastlane action snapshot |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// SnapshotHelper.swift | |
// Example | |
// | |
// Created by Felix Krause on 10/8/15. | |
// | |
// ----------------------------------------------------- | |
// IMPORTANT: When modifying this file, make sure to | |
// increment the version number at the very | |
// bottom of the file to notify users about | |
// the new SnapshotHelper.swift | |
// ----------------------------------------------------- | |
import Foundation | |
import XCTest | |
var deviceLanguage = "" | |
var locale = "" | |
func setupSnapshot(_ app: XCUIApplication, waitForAnimations: Bool = true) { | |
Snapshot.setupSnapshot(app, waitForAnimations: waitForAnimations) | |
} | |
func snapshot(_ name: String, waitForLoadingIndicator: Bool) { | |
if waitForLoadingIndicator { | |
Snapshot.snapshot(name) | |
} else { | |
Snapshot.snapshot(name, timeWaitingForIdle: 0) | |
} | |
} | |
/// - Parameters: | |
/// - name: The name of the snapshot | |
/// - timeout: Amount of seconds to wait until the network loading indicator disappears. Pass `0` if you don't want to wait. | |
func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20) { | |
Snapshot.snapshot(name, timeWaitingForIdle: timeout) | |
} | |
enum SnapshotError: Error, CustomDebugStringConvertible { | |
case cannotDetectUser | |
case cannotFindHomeDirectory | |
case cannotFindSimulatorHomeDirectory | |
case cannotAccessSimulatorHomeDirectory(String) | |
case cannotRunOnPhysicalDevice | |
var debugDescription: String { | |
switch self { | |
case .cannotDetectUser: | |
return "Couldn't find Snapshot configuration files - can't detect current user " | |
case .cannotFindHomeDirectory: | |
return "Couldn't find Snapshot configuration files - can't detect `Users` dir" | |
case .cannotFindSimulatorHomeDirectory: | |
return "Couldn't find simulator home location. Please, check SIMULATOR_HOST_HOME env variable." | |
case .cannotAccessSimulatorHomeDirectory(let simulatorHostHome): | |
return "Can't prepare environment. Simulator home location is inaccessible. Does \(simulatorHostHome) exist?" | |
case .cannotRunOnPhysicalDevice: | |
return "Can't use Snapshot on a physical device." | |
} | |
} | |
} | |
@objcMembers | |
open class Snapshot: NSObject { | |
static var app: XCUIApplication? | |
static var waitForAnimations = true | |
static var cacheDirectory: URL? | |
static var screenshotsDirectory: URL? { | |
return cacheDirectory?.appendingPathComponent("screenshots", isDirectory: true) | |
} | |
open class func setupSnapshot(_ app: XCUIApplication, waitForAnimations: Bool = true) { | |
Snapshot.app = app | |
Snapshot.waitForAnimations = waitForAnimations | |
do { | |
let cacheDir = try pathPrefix() | |
Snapshot.cacheDirectory = cacheDir | |
setLanguage(app) | |
setLocale(app) | |
setLaunchArguments(app) | |
} catch let error { | |
NSLog(error.localizedDescription) | |
} | |
} | |
class func setLanguage(_ app: XCUIApplication) { | |
guard let cacheDirectory = self.cacheDirectory else { | |
NSLog("CacheDirectory is not set - probably running on a physical device?") | |
return | |
} | |
let path = cacheDirectory.appendingPathComponent("language.txt") | |
do { | |
let trimCharacterSet = CharacterSet.whitespacesAndNewlines | |
deviceLanguage = try String(contentsOf: path, encoding: .utf8).trimmingCharacters(in: trimCharacterSet) | |
app.launchArguments += ["-AppleLanguages", "(\(deviceLanguage))"] | |
} catch { | |
NSLog("Couldn't detect/set language...") | |
} | |
} | |
class func setLocale(_ app: XCUIApplication) { | |
guard let cacheDirectory = self.cacheDirectory else { | |
NSLog("CacheDirectory is not set - probably running on a physical device?") | |
return | |
} | |
let path = cacheDirectory.appendingPathComponent("locale.txt") | |
do { | |
let trimCharacterSet = CharacterSet.whitespacesAndNewlines | |
locale = try String(contentsOf: path, encoding: .utf8).trimmingCharacters(in: trimCharacterSet) | |
} catch { | |
NSLog("Couldn't detect/set locale...") | |
} | |
if locale.isEmpty && !deviceLanguage.isEmpty { | |
locale = Locale(identifier: deviceLanguage).identifier | |
} | |
if !locale.isEmpty { | |
app.launchArguments += ["-AppleLocale", "\"\(locale)\""] | |
} | |
} | |
class func setLaunchArguments(_ app: XCUIApplication) { | |
guard let cacheDirectory = self.cacheDirectory else { | |
NSLog("CacheDirectory is not set - probably running on a physical device?") | |
return | |
} | |
let path = cacheDirectory.appendingPathComponent("snapshot-launch_arguments.txt") | |
app.launchArguments += ["-FASTLANE_SNAPSHOT", "YES", "-ui_testing"] | |
do { | |
let launchArguments = try String(contentsOf: path, encoding: String.Encoding.utf8) | |
let regex = try NSRegularExpression(pattern: "(\\\".+?\\\"|\\S+)", options: []) | |
let matches = regex.matches(in: launchArguments, options: [], range: NSRange(location: 0, length: launchArguments.count)) | |
let results = matches.map { result -> String in | |
(launchArguments as NSString).substring(with: result.range) | |
} | |
app.launchArguments += results | |
} catch { | |
NSLog("Couldn't detect/set launch_arguments...") | |
} | |
} | |
open class func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20) { | |
if timeout > 0 { | |
waitForLoadingIndicatorToDisappear(within: timeout) | |
} | |
NSLog("snapshot: \(name)") // more information about this, check out https://docs.fastlane.tools/actions/snapshot/#how-does-it-work | |
if Snapshot.waitForAnimations { | |
sleep(1) // Waiting for the animation to be finished (kind of) | |
} | |
#if os(OSX) | |
guard let app = self.app else { | |
NSLog("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().") | |
return | |
} | |
app.typeKey(XCUIKeyboardKeySecondaryFn, modifierFlags: []) | |
#else | |
guard self.app != nil else { | |
NSLog("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().") | |
return | |
} | |
let screenshot = XCUIScreen.main.screenshot() | |
guard var simulator = ProcessInfo().environment["SIMULATOR_DEVICE_NAME"], let screenshotsDir = screenshotsDirectory else { return } | |
do { | |
// The simulator name contains "Clone X of " inside the screenshot file when running parallelized UI Tests on concurrent devices | |
let regex = try NSRegularExpression(pattern: "Clone [0-9]+ of ") | |
let range = NSRange(location: 0, length: simulator.count) | |
simulator = regex.stringByReplacingMatches(in: simulator, range: range, withTemplate: "") | |
let path = screenshotsDir.appendingPathComponent("\(simulator)-\(name).png") | |
try screenshot.pngRepresentation.write(to: path) | |
} catch let error { | |
NSLog("Problem writing screenshot: \(name) to \(screenshotsDir)/\(simulator)-\(name).png") | |
NSLog(error.localizedDescription) | |
} | |
#endif | |
} | |
class func waitForLoadingIndicatorToDisappear(within timeout: TimeInterval) { | |
#if os(tvOS) | |
return | |
#endif | |
guard let app = self.app else { | |
NSLog("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().") | |
return | |
} | |
let networkLoadingIndicator = app.otherElements.deviceStatusBars.networkLoadingIndicators.element | |
let networkLoadingIndicatorDisappeared = XCTNSPredicateExpectation(predicate: NSPredicate(format: "exists == false"), object: networkLoadingIndicator) | |
_ = XCTWaiter.wait(for: [networkLoadingIndicatorDisappeared], timeout: timeout) | |
} | |
class func pathPrefix() throws -> URL? { | |
let homeDir: URL | |
// on OSX config is stored in /Users/<username>/Library | |
// and on iOS/tvOS/WatchOS it's in simulator's home dir | |
#if os(OSX) | |
guard let user = ProcessInfo().environment["USER"] else { | |
throw SnapshotError.cannotDetectUser | |
} | |
guard let usersDir = FileManager.default.urls(for: .userDirectory, in: .localDomainMask).first else { | |
throw SnapshotError.cannotFindHomeDirectory | |
} | |
homeDir = usersDir.appendingPathComponent(user) | |
#else | |
#if arch(i386) || arch(x86_64) | |
guard let simulatorHostHome = ProcessInfo().environment["SIMULATOR_HOST_HOME"] else { | |
throw SnapshotError.cannotFindSimulatorHomeDirectory | |
} | |
guard let homeDirUrl = URL(string: simulatorHostHome) else { | |
throw SnapshotError.cannotAccessSimulatorHomeDirectory(simulatorHostHome) | |
} | |
homeDir = URL(fileURLWithPath: homeDirUrl.path) | |
#else | |
throw SnapshotError.cannotRunOnPhysicalDevice | |
#endif | |
#endif | |
return homeDir.appendingPathComponent("Library/Caches/tools.fastlane") | |
} | |
} | |
private extension XCUIElementAttributes { | |
var isNetworkLoadingIndicator: Bool { | |
if hasWhiteListedIdentifier { return false } | |
let hasOldLoadingIndicatorSize = frame.size == CGSize(width: 10, height: 20) | |
let hasNewLoadingIndicatorSize = frame.size.width.isBetween(46, and: 47) && frame.size.height.isBetween(2, and: 3) | |
return hasOldLoadingIndicatorSize || hasNewLoadingIndicatorSize | |
} | |
var hasWhiteListedIdentifier: Bool { | |
let whiteListedIdentifiers = ["GeofenceLocationTrackingOn", "StandardLocationTrackingOn"] | |
return whiteListedIdentifiers.contains(identifier) | |
} | |
func isStatusBar(_ deviceWidth: CGFloat) -> Bool { | |
if elementType == .statusBar { return true } | |
guard frame.origin == .zero else { return false } | |
let oldStatusBarSize = CGSize(width: deviceWidth, height: 20) | |
let newStatusBarSize = CGSize(width: deviceWidth, height: 44) | |
return [oldStatusBarSize, newStatusBarSize].contains(frame.size) | |
} | |
} | |
private extension XCUIElementQuery { | |
var networkLoadingIndicators: XCUIElementQuery { | |
let isNetworkLoadingIndicator = NSPredicate { (evaluatedObject, _) in | |
guard let element = evaluatedObject as? XCUIElementAttributes else { return false } | |
return element.isNetworkLoadingIndicator | |
} | |
return self.containing(isNetworkLoadingIndicator) | |
} | |
var deviceStatusBars: XCUIElementQuery { | |
guard let app = Snapshot.app else { | |
fatalError("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().") | |
} | |
let deviceWidth = app.windows.firstMatch.frame.width | |
let isStatusBar = NSPredicate { (evaluatedObject, _) in | |
guard let element = evaluatedObject as? XCUIElementAttributes else { return false } | |
return element.isStatusBar(deviceWidth) | |
} | |
return self.containing(isStatusBar) | |
} | |
} | |
private extension CGFloat { | |
func isBetween(_ numberA: CGFloat, and numberB: CGFloat) -> Bool { | |
return numberA...numberB ~= self | |
} | |
} | |
// Please don't remove the lines below | |
// They are used to detect outdated configuration files | |
// SnapshotHelperVersion [1.21] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment