⭐ Setup Fastlane + TestFlight + AppCenter for Android/iOS & Flutter
Visual Studio App Center is scheduled for retirement on March 31, 2025. Learn more about support timelines and recommended alternatives
Please before following this documentation make sure you already have confirgurations described here : ⭐ Setup Mobile Development Environment
For example in this url : https://appcenter.ms/orgs/ToTo/applications
- owner_name : ToTo
- app_name: Your AppName
- app_platform: iOS (Obj-c/swift) / Android (Java)
- api_token: Go to https://appcenter.ms/orgs/YOUR_ORG/apps/YOUR_APP_NAME/settings/apitokens & generate one !
You are ready !
- Go to Identifiers
- Select : AppIds
- Select Type : App
- Fill Description : AppName
- Fill Bundle ID : (ex. com.example.toto)
- Choose App Capabilities (can be edited after)
- Then click Register
- Go to Certificates
- Choose : iOS Distribution (App Store and Ad Hoc)
- Optionnal : Generate a CSR
- Choose the generated CSR
- Click Validate
- Click download to download certificat
- Double click on the certificate to install it on your machine
- Go to Profiles
- Choose : AppStore
- Select AppId : (ex. com.example.toto)
- Choose distribution certificate
- Fill Provisionning Profile Name : (ex. toto-prod-pp)
curl -L https://get.rvm.io | bash -s stable
echo $"\nsource /Users/$(whoami)/.rvm/scripts/rvm\n" >> /Users/$(whoami)/.zshrc # or .bashrc
rvm install ruby-3.2.2
rvm use ruby-3.2.2
rvm --default use 3.2.2
gem install bundler
gem install fastlane -NV
then run :
bundle update
bundle install
bundle exec fastlane init
This will create folder fastlane with :
- Fastfile
- Appfile
default_platform(:ios)
platform :ios do
desc "Build & Deploy to TestFlight"
lane :public do |options|
## Update pubspec.yaml
## Work 50% of the time !!!
# Update Info plist Version
# increment_version_number(
# version_number: options[:version] # Set a specific version number
# )
## Work 50% of the time !!!
# auto increment buildNumber
# build_number = number_of_commits(all: true)
# increment_build_number(build_number: build_number)
# run this : chmod +x ./scripts/upgrade_buildNumber.sh
sh("../scripts/upgrade_buildNumber.sh", options[:version])
# Update Info plist Bundle ID
update_app_identifier(
xcodeproj: "Test.xcodeproj", # Optional path to xcodeproj, will use the first .xcodeproj if not set
plist_path: "./Test/Info.plist", # Path to info plist file, relative to xcodeproj
app_identifier: ENV["APP_ID"] # The App Identifier
)
# Update Info plist AppName
update_info_plist( # Change the Display Name of your app
plist_path: "./Test/Info.plist",
display_name: ENV["APP_NAME"]
)
# download and use certificate
match(type: "appstore", readonly: is_ci)
# Use gym to archive your app
gym(
silent: true,
output_directory: "./fastlane/builds",
scheme: ENV["SCHEME"]
)
# Use pilot to upload your app to testflight
pilot(
app_identifier: ENV["APP_ID"],
distribute_external: false,
)
end
desc "Build & Zip for Private Store"
lane :private do |options|
## some cleanup
sh "rm -rf builds/**.zip"
## Update pubspec.yaml
## Work 50% of the time !!!
# Update Info plist Version
# increment_version_number(
# version_number: options[:version] # Set a specific version number
# )
## Work 50% of the time !!!
# auto increment buildNumber
# build_number = number_of_commits(all: true)
# increment_build_number(build_number: build_number)
# run this : chmod +x ./scripts/upgrade_buildNumber.sh
sh("../scripts/upgrade_buildNumber.sh", options[:version])
# Update Info plist Bundle ID
update_app_identifier(
xcodeproj: "Test.xcodeproj", # Optional path to xcodeproj, will use the first .xcodeproj if not set
plist_path: "./Test/Info.plist", # Path to info plist file, relative to xcodeproj
app_identifier: ENV["APP_ID"] # The App Identifier
)
# Update Info plist AppName
update_info_plist( # Change the Display Name of your app
plist_path: "./Test/Info.plist",
display_name: ENV["APP_NAME"]
)
# Build Archive
xcodebuild(
archive: true,
archive_path: "./fastlane/builds/Test.xcarchive",
scheme: ENV["SCHEME"],
workspace: "Test.xcworkspace",
build_settings: {
"CODE_SIGNING_REQUIRED" => "NO",
"CODE_SIGN_IDENTITY" => "",
"CODE_SIGN_ENTITLEMENTS" => "",
"CODE_SIGNING_ALLOWED" => "NO"
}
)
# Clean Archive
sh "rm -rf builds/Test.xcarchive/dSYMs/"
sh "rm -rf builds/Test.xcarchive/SwiftSupport/"
# Zip Archive
zip(
path: "./fastlane/builds/Test.xcarchive",
output_path: "./fastlane/builds/test-"+ options[:version] +".xcarchive.zip"
)
end
desc "AppCenter Upload"
lane :appcenter do |options|
appcenter_upload(
api_token: "", # found in settings of user
owner_name: "", # found in the url : https://appcenter.ms/orgs/<owner_name>/applications
owner_type: "organization", # Default is user - set to organization for appcenter organizations
app_name: options[:app_name], # your app name
file: "./fastlane/app-release.ipa",
notify_testers: true # Set to false if you don't want to notify testers of your new release (default: `false`)
)
end
end
Geet team_id
This data can be found on Apple Account
Get itc_team_id
$ irb
irb> require "spaceship"
irb> Spaceship::Tunes.login("iTunesConnect_username", "iTunesConnect_password")
irb> Spaceship::Tunes.select_team
Don't forget to replace :
- iTunesConnect_username
- iTunesConnect_password
The result contains the itc_team_id
Get the FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD
- Connect to AppleId
- Go to Security -> Generate Password for App -> set AppName -> copy the generated code "....-....-....-...."
- This code is the FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD
#!/bin/bash
# Info Plist path
PATH_TO_INFOPLIST="YOUR_PATH"
INFOPLIST=$PATH_TO_INFOPLIST"/Info.plist"
# Type a script or drag a script file from your workspace to insert its path.
buildNumber=$(git rev-list HEAD | wc -l | tr -d ' ')
# Updrage BuildNumber with git build Numbe
oldversion=`/usr/libexec/PlistBuddy -c "Print :CFBundleVersion" "$INFOPLIST"`
## Works 100% : Change BuildNumber
if [ "$buildNumber" != "$oldversion" ] ; then
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $buildNumber" "$INFOPLIST"
fi
## Change Version
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $1" "$INFOPLIST"
# The bundle identifier of your app
app_identifier "com.exemple.toto"
# Apple Developer Account
apple_dev_portal_id "[email protected]"
# App Store Connect Account
itunes_connect_id "[email protected]"
# Developer Portal Team ID
## is found in the url : https://developer.apple.com/account/#/membership/<team_id>
team_id ""
# App Store Connect Team ID
## Check Step "Get itc_team_id"
itc_team_id ""
# Env for Pilot
ENV["FASTLANE_USER"] = "[email protected]"
ENV["FASTLANE_ITC_TEAM_ID"] = "<itc_team_id>"
## To setup 2 factor Auth for delivery
# Check Step "Get the FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD"
ENV["FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD"] = ""
# Specify the Trusted phone number to automatize sms verification step
## https://github.com/fastlane/fastlane/blob/master/spaceship/docs/Authentication.md#auto-select-sms-via-spaceship_2fa_sms_default_phone_number
### Go here and add your phone number as trusted phone, then fill the field : https://appleid.apple.com/
ENV["SPACESHIP_2FA_SMS_DEFAULT_PHONE_NUMBER"] = ""
# To Replace : .git storage for the certificate shared with hole team
git_url("https://toto/titi/tata.git")
storage_mode("git")
# The default type, can be: appstore, adhoc, enterprise or development
type("development")
# Your Apple Developer Portal username
username("[email protected]")
gem 'fastlane-plugin-appcenter'
source "https://rubygems.org"
gem "fastlane"
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
eval_gemfile(plugins_path) if File.exist?(plugins_path)
- (ex. .env.dev for dev)
SCHEME="test-dev"
APP_ID="fr.exemple.test"
APP_NAME="Test DEV"
bundle update
bundle exec fastlane ios public version:"1.0.0" --env "dev" # ex. for dev
fastlane init
# choose 2 for testflight setup
This will create folder fastlane with :
- Fastfile
- Appfile
default_platform(:android)
platform :android do
desc "Deploy a new version to the Google Play"
lane :deploy do |options|
flutter_build
## To use this u need to generate : api.json !
## Check the Appfile
upload_to_play_store(track: 'beta')
end
desc "Deploy a new version to AppCenter"
lane :appcenter_bad do |options|
flutter_build
appcenter
end
desc "Build with fastlane with auto upgrade VersionCode"
lane :flutter_build do
# Return the number of commits in current git branch
build_number = number_of_commits()
Dir.chdir ".." do
sh("flutter", "packages", "get")
sh("flutter", "clean")
# sh("flutter", "build", "apk", "--build-number=#{build_number}")
# sh("flutter", "build", "appbundle", "--build-number=#{build_number}")
## TODO : Build for different BuildTypes !
sh("flutter", "build", "apk", "--release")
end
end
desc "AppCenter Upload"
lane :appcenter do |options|
appcenter_upload(
api_token: "", # set api Token from appcenter
owner_name: "", # Set App owner name
owner_type: "organization", # Default is user - set to organization for appcenter organizations
app_name: "", # your app name
file: "../build/app/outputs/flutter-apk/app-release.apk",
notify_testers: true,
app_platform: 'Java',
destinations: "", # Distribution group
destination_type: "group"
)
end
end
json_key_file("") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one
package_name("com.krausefx.app") # e.g.
gem 'fastlane-plugin-appcenter'
source "https://rubygems.org"
gem "fastlane"
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
eval_gemfile(plugins_path) if File.exist?(plugins_path)
bundle update
bundle exec fastlane android appcenter_bad
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3
.DS_Store
.Trashes
*.swp
*.lock
*~.nib
buildArchive/
DerivedData/
build/
*.pbxuser
*.mode1v3
*.mode2v3
*.perspectivev3
!default.pbxuser
!default.mode1v3
!default.mode2v3
!default.perspectivev3
*.hmap
*.ipa
*.dSYM.zip
*.dSYM
timeline.xctimeline
playground.xcworkspace
*.xccheckout
xcuserdata/
*.moved-aside
.build/
Pods/
Carthage/Checkouts
Carthage/Build
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots/**
fastlane/screenshots/**/*.png
fastlane/test_output
*.mobileprovision
*.cer
fastlane/*.cer
fastlane/*.mobileprovision
fastlane/builds/**
.history/**
*.p12
*.certSigningRequest
*.pdf
Mocks/**
**/android/.gradle
**/android/captures/
**/android/local.properties
**/android/**/GeneratedPluginRegistrant.java
vendor/
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Miscellaneous
*.class
*.lock
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# Visual Studio Code related
.classpath
.project
.settings/
# .vscode/
# Flutter repo-specific
/bin/cache/
/bin/mingit/
/dev/benchmarks/mega_gallery/
/dev/bots/.recipe_deps
/dev/bots/android_tools/
/dev/docs/doc/
/dev/docs/flutter.docs.zip
/dev/docs/lib/
/dev/docs/pubspec.yaml
/dev/integration_tests/**/xcuserdata
/dev/integration_tests/**/Pods
/packages/flutter/coverage/
version
# packages file containing multi-root paths
.packages.generated
# Flutter/Dart/Pub related
**/doc/api/
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
build/
flutter_*.png
linked_*.ds
unlinked.ds
unlinked_spec.ds
# Android related
**/android/**/gradle-wrapper.jar
**/android/.gradle
**/android/captures/
**/android/gradlew
**/android/gradlew.bat
**/android/local.properties
**/android/**/GeneratedPluginRegistrant.java
**/android/key.properties
*.jks
# iOS/XCode related
**/ios/**/*.mode1v3
**/ios/**/*.mode2v3
**/ios/**/*.moved-aside
**/ios/**/*.pbxuser
**/ios/**/*.perspectivev3
**/ios/**/*sync/
**/ios/**/.sconsign.dblite
**/ios/**/.tags*
**/ios/**/.vagrant/
**/ios/**/DerivedData/
**/ios/**/Icon?
**/ios/**/Pods/
**/ios/**/.symlinks/
**/ios/**/profile
**/ios/**/xcuserdata
**/ios/.generated/
**/ios/Flutter/App.framework
**/ios/Flutter/Flutter.framework
**/ios/Flutter/Flutter.podspec
**/ios/Flutter/Generated.xcconfig
**/ios/Flutter/app.flx
**/ios/Flutter/app.zip
**/ios/Flutter/flutter_assets/
**/ios/Flutter/flutter_export_environment.sh
**/ios/ServiceDefinitions.json
**/ios/Runner/GeneratedPluginRegistrant.*
# macOS
**/macos/Flutter/GeneratedPluginRegistrant.swift
**/macos/Flutter/Flutter-Debug.xcconfig
**/macos/Flutter/Flutter-Release.xcconfig
**/macos/Flutter/Flutter-Profile.xcconfig
# Coverage
coverage/
# Symbols
app.*.symbols
# Exceptions to above rules.
!**/ios/**/default.mode1v3
!**/ios/**/default.mode2v3
!**/ios/**/default.pbxuser
!**/ios/**/default.perspectivev3
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
!/dev/ci/**/Gemfile.lock
doc/*
.idea/*
#.env*
I will keep this updated in time !