Skip to content

Instantly share code, notes, and snippets.

@ThePredators
Last active January 15, 2025 17:11
Show Gist options
  • Save ThePredators/a1d105b0794198cade4ef2b0e0836541 to your computer and use it in GitHub Desktop.
Save ThePredators/a1d105b0794198cade4ef2b0e0836541 to your computer and use it in GitHub Desktop.

⭐ 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

⚠️ The following configuration has been tested on Intel, M1 & M2 Ships ⚠️

AppCenter data for Fastlane

For example in this url : https://appcenter.ms/orgs/ToTo/applications

You are ready !

iOS

Generate necessary stuff in Apple Developer Portal

Generate AppId

  • 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

Generate Distribution Certificat

  • 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

Generate Provisionning Profile

  • Go to Profiles
  • Choose : AppStore
  • Select AppId : (ex. com.example.toto)
  • Choose distribution certificate
  • Fill Provisionning Profile Name : (ex. toto-prod-pp)

Install Ruby

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

Install Bundler

gem install bundler

Install FastLane

gem install fastlane -NV

then run :

bundle update
bundle install
bundle exec fastlane init

This will create folder fastlane with :

  • Fastfile
  • Appfile

⚠️ Don't forget to add "Vendor" Folder to your ".gitignore" ⚠️

Setup file Fastfile : (in fastlane dir)

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

Get Apple Informations :

Geet team_id

This data can be found on Apple Account

Get itc_team_id

⚠️ copy/paste on irb doesn't work all time ! You may get error message ! So please take time to write this piece of code yourself ! ⚠️

$ 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

Add script : upgrade_buildNumber (in iOS rootDir scripts folder)

#!/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"

Setup file : Appfile (in fastlane dir)

# 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"] = ""

Setup file : Matchfile (in fastlane dir)

# 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]")

Setup file : Pluginfile (in fastlane dir)

gem 'fastlane-plugin-appcenter'

Setup file : Gemfile (in ios rootDir)

source "https://rubygems.org"

gem "fastlane"

plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
eval_gemfile(plugins_path) if File.exist?(plugins_path)

Add file .env.environement : (in fastlane dir)

  • (ex. .env.dev for dev)
SCHEME="test-dev"
APP_ID="fr.exemple.test"
APP_NAME="Test DEV"

How to Build & Deliver to Tesflight ?

bundle update
bundle exec fastlane ios public version:"1.0.0" --env "dev" # ex. for dev

Android

Install Fastlane

fastlane init
# choose 2 for testflight setup

This will create folder fastlane with :

  • Fastfile
  • Appfile

Setup file : Fastfile (in fastlane dir)

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

Setup file : Appfile (in fastlane dir)

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. 

Setup file : Pluginfile (in fastlane dir)

gem 'fastlane-plugin-appcenter'

Setup file : Gemfile (in android rootDir)

source "https://rubygems.org"

gem "fastlane"

plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
eval_gemfile(plugins_path) if File.exist?(plugins_path)

How to Build & Deliver to AppCenter ?

bundle update
bundle exec fastlane android appcenter_bad

.gitignore

iOS .gitignore

*.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/

Android .gitignore

gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java

Flutter RootProj .gitignore

# 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*

More Infos :

@ThePredators
Copy link
Author

I will keep this updated in time !

@ThePredators
Copy link
Author

Big thanks to Microsoft for AppCenter but its sad the AppCenter will be done in March 2025 !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment