Skip to content

Instantly share code, notes, and snippets.

@johanquiroga
Last active February 21, 2020 02:10
Show Gist options
  • Save johanquiroga/6b758f5ba278e708d15e5c3c3f50931f to your computer and use it in GitHub Desktop.
Save johanquiroga/6b758f5ba278e708d15e5c3c3f50931f to your computer and use it in GitHub Desktop.
fastlane ios
# 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
# Autogenerated by fastlane
#
# Ensure this file is checked in to source control!
gem 'fastlane-plugin-semantic_release'
# 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
//
// 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