이번 포스팅은 “GitLab과 Fastlane으로 iOS 앱 빌드 및 배포 자동화하기” 시리즈의 두 번째 글로, GitLab CI/CD 파이프라인을 구성하기 전에, Fastlane으로 Lint, Test, TestFlight 배포 등 각 단계별로 Lane을 정의하는 방법에 대해 알아보겠습니다.
이번 글에서는 Fastlane을 설치하고 기본 설정을 한 후, 필요한 환경 변수를 추가하고 각 단계별 Lane을 정의하는 내용을 다룹니다. 또한 match
방식으로 코드 사이닝 하는 방법도 설명합니다.
Fastlane 소개
Fastlane은 Android 및 iOS 배포를 단순화하는 것을 목표로 하는 오픈 소스 플랫폼입니다. Fastlane을 사용하면 스크린샷 생성, 코드사이닝, 빌드 번호 올리기, 애플리케이션 릴리스와 같은 개발 및 릴리스 워크플로의 모든 지루한 작업을 자동화할 수 있습니다.
Fastlane 설치
Fastlane은 여러 가 지 방법으로 설치할 수 있으나, Bundler와 Gemfile을 사용하여 설치하는 것이 좋습니다.
앱을 빌드하고 업로드하는 데 문제가 발생하지 않도록, 로케일(Locale) 관련 환경변수를 추가합니다.
Terminal 또는 iTerm을 열고, ~/.bashrc
, ~/.bash_profile
, ~/.profile
또는 ~/.zshrc
에 아래 환경 변수를 추가합니다.
export LC_ALL=ko_KR.UTF-8
export LANG=ko_KR.UTF-8
아래 명령을 실행하여 적용합니다.
source ~/.zshrc
Xcode 프로젝트 디렉토리로 이동하여 Gemfile
파일을 생성하고 아래 내용을 작성합니다.
source "https://rubygems.org"
gem "fastlane"
아래 명령을 실행하면 ./vendor/bundle
경로에 fastlane과 필요한 Ruby gem(라이브러리)이 설치되고, 버전 컨트롤을 위한 ./Gemfile.lock
이 생성됩니다.
bundle install --path vendor/bundle
fastlane 버전을 확인합니다.
$ fastlane --version
fastlane installation at path:
/Library/Ruby/Gems/2.6.0/gems/fastlane-2.206.2/bin/fastlane
-----------------------------
[✔] 🚀
fastlane 2.206.2
Fastlane 기본 설정
Lint, Test, TestFlight 배포 등 각 단계를 Lane으로 정의하기 전에, 구성 정보를 정의하는 Fastfile
파일과 Apple ID 또는 애플리케이션 Bundle Identifier와 같은 앱 관련 정보를 저장하는 Appfile
파일을 생성해야 합니다.
fastlane init
명령을 실행한 후, 빠르게 진행하게 위해 4
를 선택하고 Enter 키를 누릅니다.
$ fastlane init
[✔] 🚀
...
[✔] Looking for iOS and Android projects in current directory...
[14:32:32]: Created new folder './fastlane'.
[14:32:32]: Detected an iOS/macOS project in the current directory: 'MyFirstApp.xcodeproj'
[14:32:32]: -----------------------------
[14:32:32]: --- Welcome to fastlane 🚀 ---
[14:32:32]: -----------------------------
[14:32:32]: fastlane can help you with all kinds of automation for your mobile app
[14:32:32]: We recommend automating one task first, and then gradually automating more over time
[14:32:32]: What would you like to use fastlane for?
1. 📸 Automate screenshots
2. 👩✈️ Automate beta distribution to TestFlight
3. 🚀 Automate App Store distribution
4. 🛠 Manual setup - manually setup your project to automate your tasks
? 4
이후, 프롬프트가 나오면 계속해서 Enter 키를 누릅니다.
fastlane
폴더에 Appfile
및 Fastfile
파일이 생성됩니다.
fastlane/Appfile
파일에서 앱의 Bundle ID와 Apple ID으로 수정합니다. 여러 팀에 속해 있으면 Developer Portal 및 App Store Connect의 Team ID를 추가합니다.
app_identifier("net.infograb.MyFirstApp") # The bundle identifier of your app
apple_id("your_account@company.com") # Your Apple email address
itc_team_id("123456789") # App Store Connect Team ID
team_id("Q2CBPJ58CA") # Developer Portal Team ID
# For more information about the Appfile, see:
# https://docs.fastlane.tools/advanced/#appfile
Pod Install Lane
첫 번째로 구성할 Lane은 프로젝트에서 사용하는 의존성 라이브러리를 설치하기 위한 구성입니다.
Swift 스타일 및 코딩 컨벤션을 적용하는 도구인 SwiftLint를 CocoaPods를 통해 설치합니다.
CocoaPods는 Swift 및 Objective-C Cocoa 프로젝트에 많이 사용되는 의존성 매니저(dependency manager)입니다. 90,000개 이상 의 라이브러리가 있으며 300만 개 이상의 앱에서 사용됩니다.
-
pod init
명령을 실행하면, 프로젝트 루트 디렉토리에Podfile
파일이 생성됩니다. -
Podfile
파일에pod 'SwiftLint'
을 추가합니다.# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'
target 'MyFirstApp' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!
# Pods for MyFirstApp
pod 'SwiftLint'
target 'MyFirstAppTests' do
inherit! :search_paths
# Pods for testing
end
target 'MyFirstAppUITests' do
# Pods for testing
end
end -
fastlane/Fastfile
파일을 아래와 같이 수정합니다.# 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
desc "Runs `pod install`"
puts "Runs `pod install`"
lane :pod_install do
cocoapods(
clean_install: true,
podfile: "./Podfile"
)
end
end -
Gemfile
파일에gem "cocoapods"
을 추가합니다.source "https://rubygems.org"
gem "fastlane"
gem "cocoapods" -
아래 명령을 실행하여
cocoapods
를 설치합니다.bundle install --path vendor/bundle
-
bundle exec fastlane pod_install
명령으로 Lane을 실행합니다.$ bundle exec fastlane pod_install
[✔] 🚀
...
[14:37:18]: ------------------------------
[14:37:18]: --- Step: default_platform ---
[14:37:18]: ------------------------------
[14:37:18]: Runs `pod install`
[14:37:18]: Driving the lane 'ios pod_install' 🚀
[14:37:18]: -----------------------
[14:37:18]: --- Step: cocoapods ---
[14:37:18]: -----------------------
[14:37:19]: $ cd '.' && bundle exec pod install --clean-install
[14:37:19]: ▸ Analyzing dependencies
[14:37:19]: ▸ Downloading dependencies
[14:37:19]: ▸ Installing SwiftLint (0.47.1)
[14:37:19]: ▸ Generating Pods project
[14:37:19]: ▸ Integrating client project
[14:37:19]: ▸ Pod installation complete! There is 1 dependency from the Podfile and 1 total pod installed.
[14:37:19]: ▸ [!] Automatically assigning platform `iOS` with version `15.4` on target `HelloWorld` because no platform was specified. Please specify a platform for this target in your Podfile. See `https://guides.cocoapods.org/syntax/podfile.html#platform`.
+------+------------------+-------------+
| fastlane summary |
+------+------------------+-------------+
| Step | Action | Time (in s) |
+------+------------------+-------------+
| 1 | default_platform | 0 |
| 2 | cocoapods | 1 |
+------+------------------+-------------+
[14:37:19]: fastlane.tools finished successfully 🎉 -
Pods
디렉토리가 생성되고 의존성 라이브러리들이 설치됩니다.
Lint Lane
이전 단계에서 설치한 SwiftLint로 Swift 스타일 및 코딩 컨벤션을 체크하는 Lane을 구성합니다.
-
Xcode 프로젝트의 루트에
.swiftlint.yml
파일을 생성하고 아래 내용을 추가합니다.excluded: # paths to ignore during linting. Takes precedence over `included`.
- Pods
- vendor -
fastlane/Fastfile
파일에 아래 내용을 추가합니다....
desc "Run swift code validation using SwiftLint"
puts "Run swift code validation using SwiftLint"
lane :lint do
swiftlint(
mode: :lint,
ignore_exit_status: true,
raise_if_swiftlint_error: true,
executable: "./Pods/SwiftLint/swiftlint"
)
end
... -
bundle exec fastlane lint
명령으로 Lint Lane을 실행합니다.$ bundle exec fastlane lint
[✔] 🚀
...
[14:39:34]: ------------------------------
[14:39:34]: --- Step: default_platform ---
[14:39:34]: ------------------------------
[14:39:34]: Driving the lane 'ios lint' 🚀
[14:39:34]: -----------------------
[14:39:34]: --- Step: swiftlint ---
[14:39:34]: -----------------------
[14:39:34]: $ ./Pods/SwiftLint/swiftlint lint
[14:39:34]: ▸ Linting Swift files in current working directory
[14:39:34]: ▸ Linting 'MyFirstAppApp.swift' (1/5)
[14:39:34]: ▸ Linting 'MyFirstAppUITests.swift' (2/5)
[14:39:34]: ▸ Linting 'ContentView.swift' (3/5)
[14:39:34]: ▸ Linting 'MyFirstAppUITestsLaunchTests.swift' (4/5)
[14:39:34]: ▸ Linting 'MyFirstAppTests.swift' (5/5)
[14:39:34]: ▸ /Users/jason/Workspace/MyFirstApp/MyFirstAppUITests/MyFirstAppUITests.swift:18:1: warning: Line Length Violation: Line should be 120 characters or less: currently 182 characters (line_length)
[14:39:34]: ▸ /Users/jason/Workspace/MyFirstApp/MyFirstAppTests/MyFirstAppTests.swift:26:1: warning: Line Length Violation: Line should be 120 characters or less: currently 130 characters (line_length)
[14:39:34]: ▸ Done linting! Found 2 violations, 0 serious in 5 files.
+------+------------------+-------------+
| fastlane summary |
+------+------------------+-------------+
| Step | Action | Time (in s) |
+------+------------------+-------------+
| 1 | default_platform | 0 |
| 2 | swiftlint | 0 |
+------+------------------+-------------+
[14:39:34]: fastlane.tools finished successfully 🎉
Test Lane
단위 테스트 또는 UI 테스트를 수행하는 Test Lane을 구성합니다.
-
fastlane/Fastfile
파일에 아래 내용을 추가합니다....
desc "Runs all the tests"
puts "Runs all the tests"
lane :tests do
run_tests(
workspace: "MyFirstApp.xcworkspace",
scheme: "MyFirstApp",
devices: ["iPhone 13"],
skip_build: true
)
end
... -
bundle exec fastlane tests
명령으로 Test Lane을 실행합니다.$ bundle exec fastlane tests
[✔] 🚀
...
[14:43:25]: ------------------------------
[14:43:25]: --- Step: default_platform ---
[14:43:25]: ------------------------------
[14:43:25]: Runs `pod install`
[14:43:25]: Run swift code validation using SwiftLint
[14:43:25]: Runs all the tests
[14:43:25]: Driving the lane 'ios tests' 🚀
[14:43:25]: -----------------------
[14:43:25]: --- Step: run_tests ---
[14:43:25]: -----------------------
[14:43:26]: Resolving Swift Package Manager dependencies...
[14:43:26]: $ xcodebuild -resolvePackageDependencies -workspace MyFirstApp.xcworkspace -scheme MyFirstApp
...
[14:45:13]: ▸ Test Succeeded
+--------------------+-------------------+
| Test Results |
+--------------------+-------------------+
| Number of tests | 5 (and 3 retries) |
| Number of failures | 0 |
+--------------------+-------------------+
+------+------------------+-------------+
| fastlane summary |
+------+------------------+-------------+
| Step | Action | Time (in s) |
+------+------------------+-------------+
| 1 | default_platform | 0 |
| 2 | run_tests | 111 |
+------+------------------+-------------+
[14:45:16]: fastlane.tools finished successfully 🎉
Code signing
자동화된 설정으로 코드 사이닝 하는 방법은 match
를 사용하는 것과 cert
및 sigh
를 사용하는 방법이 있습니다. cert/sigh
방식은 기존 인증서를 사용하여 코드 사이닝 하는 것으로 GitLab CI/CD 파이프라인으로 자동화하려면 Runner에 인증서를 수동으로 설치해야 하는 번거로움이 있습니다.
여기서는 match
를 사용하여 코드 사이닝 하는 방법을 설명합니다. match
를 사용하면 개인 키와 인증서를 Git 저장소에 저장하여 머신 간에 동기화할 수 있습니다. 이를 통해 새로운 팀 구성원을 쉽게 온보딩 하고 새로운 Mac 시스템을 설정할 수 있습니다.
match
를 사용하려면 우선 Git 저장소가 필요하므로, GitLab 프로젝트를 생성한 후, 소스 코드를 Commit 하고 Push 합니다.
다음을 수행하여 GitLab 프로젝트를 생성합니다.
- 그룹 페이지에서 New project 버튼을 클릭합니다.
- Create new project 페이지에서 Create blank project를 클릭합니다.
- Create blank project 페이지에서 아래 항목을 입력 또는 선택하고 Create project 버튼을 클릭합니다.
- Project name : 프로젝트 이름 입력 (예:
iOS with Fastlane
) - Visibility Level : Private 선택
- Initialize repository with a README : 체 크 해제 (체크를 하게 되면
git push
할 때 번거로운 추가 작업을 해야 합니다.)
로컬 저장소를 생성(초기화)하고 설정한 후, 소스를 커밋하고 원격 저장소인 GitLab에 git push
하여 업로드합니다.
-
로컬 머신에 Git이 설치되어 있지 않으면, 설치하고 아래
git config
명령을 실행하여 이름과 이메일을 설정해야 합니다.git config --global user.name "<Your name>" # 본인의 이름으로 수정합니다.
git config --global user.email "<Your email>" # 본인의 이메일로 수정합니다. -
git init --initial-branch=main
명령을 실행하여 로컬 저장소를 생성(초기화)합니다. -
GitLab 프로젝트 페이지에서 Clone 버튼을 클릭한 후, Clone with HTTPS 주소를 복사하여 붙여 넣고 아래 명령을 완성하고 실행합니다.
git remote add origin https://<GitLab_Domain>/<Group_Namespace>/ios-with-fastlane.git
-
git config --list
명령을 실행하면 아래와 유사한 Git 구성 정보를 확인할 수 있습니다.credential.helper=osxkeychain
user.name=<Your name>
user.email=<Your email>
...
core.repositoryformatversion=0
core.filemode=true
core.bare=false
core.logallrefupdates=true
core.ignorecase=true
core.precomposeunicode=true
remote.origin.url=https://<GitLab_Domain>/<Group_Namespace>/ios-with-fastlane.git
remote.origin.fetch=+refs/heads/*:refs/remotes/origin/* -
아래 명령을 실행하여
.gitignore
파일을 생성합니다.$ curl -o .gitignore https://www.toptal.com/developers/gitignore/api/swift,cocoapods,fastlane
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 3290 100 3290 0 0 11826 0 --:--:-- --:--:-- --:--:-- 12095 -
.gitignore
파일에 아래 내용을 추가합니다.## Ruby Bundler
/.bundle/
/vendor/bundle -
git status
명령을 실행하여 커밋할 파일 목록 중에 버전 관리가 필요 없는 디렉토리 또는 파일이 있으면,.gitignore
파일에 추가합니다. -
다음과 같이 실행하여 커밋하고 원격 저장소에 Push 합니다.
git add .
git commit -m "Initial commit"
git push -u origin main -
GitLab 프로젝트를 새로고침 하면 소스가 업로드된 것을 확인할 수 있습니다.
다음을 수행하면 인증서와 프로파일이 생성되고 Git 저장소에 업로드됩니다.
-
bundle exec fastlane match init
명령을 실행한 후, 프롬프트에1
을 입력하고 Enter 키를 누릅니다.$ bundle exec fastlane match init
[✔] 🚀
...
[14:52:11]: fastlane match supports multiple storage modes, please select the one you want to use:
1. git
2. google_cloud
3. s3
? 1 -
프로젝트의 Git URL을 입력하고 Enter 키를 누릅니다.
-
fastlane/Matchfile
파일이 생성됩니다. -
fastlane/Matchfile
파일을 아래와 같이 수정합니다.git_url("https://<GitLab_Domain>/<Group_Namespace>/ios-with-fastlane.git")
git_branch("main")
storage_mode("git")
type("appstore") # The default type, can be: appstore, adhoc, enterprise or development
app_identifier("net.infograb.MyFirstApp")
username("your_apple_id@company.com") # Your Apple Developer Portal username
# For all available options run `fastlane match --help`
# Remove the # in the beginning of the line to enable the other options
# The docs are available on https://docs.fastlane.tools/actions/match -
bundle exec fastlane match appstore
명령을 실행합니다. (이전에 생성한 인증서와 프로파일이 있으면 삭제 후 실행합니다.) -
Passphrase
프롬프트가 나오면 입력하고 기록해 둡니다. (다음 단계에서MATCH_PASSWORD
변수의 값으로 사용됩니다.)[14:53:02]: Checking out branch main...
[14:53:02]: Enter the passphrase that should be used to encrypt/decrypt your certificates
[14:53:02]: This passphrase is specific per repository and will be stored in your local keychain
[14:53:02]: Make sure to remember the password, as you'll need it when you run match on a different machine
[14:53:02]: Passphrase for Match storage: ********
[14:53:02]: Type passphrase again: ********
[14:53:02]: 🔓 Successfully decrypted certificates repo -
진행 중 사용할 팀 번호를 확인하는 프롬프트가 나오면 팀 번호를 선택합니다.
...
Multiple teams found on the Developer Portal, please enter the number of the team you want to use:
1) 9876543210 "Your Compony Co., Ltd" (Company/Organization)
2) 1234567890 "Gildong Hong" (Individual)
... -
Apple Developer > Account > Certificates, IDs & Profiles에 접속한 후, Certificates 및 Profiles 페이지에서 인증서와 프로파일이 생성된 것을 확인할 수 있습니다.
-
로컬 머신에서 키체인 접근 > 로그인의 내 인증서 탭에 인증서가 추가된 것을 확인할 수 있습니다.
-
GitLab 프로젝트의 사이드 바에서 Repository > Commits을 선택하면,
[fastlane] Updated appstore and platform ios
커밋 내역을 확인할 수 있습니다.
TestFlight Lane
다음을 수행하여 Personal access token을 생성합니다.
-
GitLab에서 우측 상단에 있는 아바타를 클릭하고 Edit profile을 선택합니다.
-
User Settings 페이지의 좌측 사이드 바에서 Access Tokens를 클릭합니다.
-
Token name 필드에 토큰의 이름을 입력합니다. (예:
match_git_clone_auth_token
) -
Expiration date 필드에 만료 날짜를 선택합니다. (선택사항)
-
Select scopes에서
read_repository
체크박스를 체크하고 Create personal access token 버튼을 클릭합니다. -
Your new personal access token 필드에 생성된 액세스 토큰이 표시됩니다.
-
Copy personal access token 아이콘을 클릭하여 복사하고, 액 세스 토큰을 기록해 둡니다.
페이지를 나가거나 새로고침 하면 다시 액세스 할 수 없습니다.
Git 저장소에 저장된 인증서 및 프로비저닝 프로파일를 가져올 때 GitLab에 인증(Basic authentication)하기 위한 Base64 인코딩 된 키를 GitLab 계정과 이전 단계에서 생성한 Personal access token를 사용하여 생성합니다.
echo -n your_gitlab_username:your_personal_access_token | base64
아래는 실행한 예입니다.
$ echo -n jason:F7i-jNM8eRx8AVuTRxi7 | base64
amFzb246RjdpLWpOZVJ1VFJ4eE04OEFWaTc=
아래의 환경 변수를 추가합니다.
export TEMP_KEYCHAIN_NAME="fastlane"
export TEMP_KEYCHAIN_PASSWORD="abcd1234"
export MATCH_GIT_BASIC_AUTHORIZATION="amFzb246RjdpLWpOZVJ1VFJ4eE04OEFWaTc="
export MATCH_PASSWORD="1234abcd"
export API_ISSUER_ID="312de789-12d6-1234-5678-123ba54eb789"
export API_KEY_ID="AB9CDEFGH6"
export API_KEY_CONTENT="-----BEGIN PRIVATE KEY-----
MIGTAgEAMBM...
rn8ndgTU
-----END PRIVATE KEY-----"
Testflight에 앱을 업로드하는 Lane을 구성하고 실행합니다.
-
fastlane/Fastfile
파일에 환경 변수, 임시 키체인 삭제, 생성 함수와 TestFlight Lane을 추가합니다....
default_platform(:ios)
KEYCHAIN_NAME = ENV["TEMP_KEYCHAIN_NAME"]
KEYCHAIN_PASSWORD = ENV["TEMP_KEYCHAIN_PASSWORD"]
GIT_BASIC_AUTHORIZATION = ENV["MATCH_GIT_BASIC_AUTHORIZATION"]
ISSUER_ID = ENV["API_ISSUER_ID"]
KEY_ID = ENV["API_KEY_ID"]
KEY_CONTENT = ENV["API_KEY_CONTENT"]
def delete_temp_keychain(name)
delete_keychain(
name: name
) if File.exist? File.expand_path("~/Library/Keychains/#{name}-db")
end
def create_temp_keychain(name, password)
create_keychain(
name: name,
password: password,
unlock: false,
timeout: 0
)
end
def ensure_temp_keychain(name, password)
delete_temp_keychain(name)
create_temp_keychain(name, password)
end
platform :ios do
...
private_lane :code_signing do
match(
type:"appstore",
readonly: true,
git_basic_authorization: GIT_BASIC_AUTHORIZATION,
keychain_name: KEYCHAIN_NAME,
keychain_password: KEYCHAIN_PASSWORD
)
end
private_lane :get_api_key do
app_store_connect_api_key(
key_id: KEY_ID,
issuer_id: ISSUER_ID,
key_content: KEY_CONTENT,
duration: 1200,
in_house: false
)
end
private_lane :bump_build_number do |options|
api_key = options[:api_key]
version = get_version_number(
xcodeproj: "MyFirstApp.xcodeproj",
target: "MyFirstApp"
)
puts "Version : #{version}"
latest_build_number = latest_testflight_build_number(
api_key: api_key,
version: version,
initial_build_number: 0
)
puts "Latest build number : #{latest_build_number}"
increment_build_number(
build_number: latest_build_number + 1,
xcodeproj: "MyFirstApp.xcodeproj"
)
end
desc "Distribute app to TestFlight"
puts "Distribute app to TestFlight"
lane :distribute_testflight do
ensure_temp_keychain(KEYCHAIN_NAME, KEYCHAIN_PASSWORD)
code_signing
api_key = get_api_key
bump_build_number(api_key: api_key)
build_app(
workspace: "MyFirstApp.xcworkspace",
scheme: "MyFirstApp",
configuration: "Release",
export_method: "app-store",
output_directory: "build_output",
clean: true
)
upload_to_testflight(
api_key: api_key,
ipa: "./build_output/MyFirstApp.ipa",
skip_waiting_for_build_processing: true
)
delete_temp_keychain(KEYCHAIN_NAME)
end
... -
bundle exec fastlane distribute_testflight
명령으로 TestFlight Lane을 실행합니다. -
App Store Connect의 TestFlight에서 앱이 업로드된 것을 확인할 수 있습니다.
맺음말
지금까지 Fastlane에 대한 간단한 소개와 설치 및 구성하는 방법, match
를 이용하여 인증서 및 프로파일을 생성하여 코드 사이닝 하고, Lint, Test, TestFlight 배포 Lane을 정의하는 방법을 알아보았습니다. 다음 글은 GitLab CI/CD 파이프라인을 구성하여 Fastlane의 각 Lane을 실행하는 내용으로, GitLab과 Fastlane으로 iOS 앱 빌드 및 배포 자동화하기 시리즈를 마무리하겠습니다.