Flutter CI/CD to TestFlight with Github Actions
1. Mở đầu
Nếu bạn tìm thấy bài viết này thì khả năng cao bạn cũng đã biết sơ về các khái niệm như Github Actions, CI/CD, TestFlight, Flutter. Ở đây mình sẽ không giải thích thêm mấy từ khóa này.
Bài này thiên về hướng dẫn, note những thao tác cần thiết và không phải về lý thuyết nên mình sẽ không giải thích vì rõ nó là gì, vì sao cần nó, tại sao có nó ví dụ như các file, từ khóa .certSigningRequest, .p8, .p12, .mobileprovision, .plist, Certificate, Provisioning Profile,...bla bla
Khi bạn làm việc với các service khác, luôn luôn phải "đăng ký" để sử dụng. Apple cũng như vậy, và tùy vào mỗi bên sẽ yêu cầu các thông tin khác nhau.
2. Chuẩn bị
- Account Apple Developer để có thể đăng nhập được App Store Connect
- Project Flutter, Github
- Macbook/MiniMac/MacOS
Bạn chưa có tài khoản Apple ? Đăng ký chứ còn gì nữa ~~ 🫠
- Tài khoản cá nhân phí hằng năm là 99$
- Tài khoản doanh nghiệp phí hằng năm là 299$
2.1 App Store Connect API Key
Để có thể đẩy file .ipa lên testflight bằng câu lệnh khi CI/CD, Bạn cần phải có apiKey, apiIssuer và file private key .p8
Truy cập vào link này và tạo key, tải file về sẽ có 1 file .p8 https://appstoreconnect.apple.com/access/integrations
Tên của file .p8 sẽ có dạng AuthKey_$apiKey.p8
2.2 Apple Certificate và Provisioning profile
Bước này rất quan trọng, không có nó không Sign remote được
2.2.1 Tạo CertificateSigningRequest
Nó giống như việc bạn đi xin việc, nhà tuyển dụng cần biết bạn là ai, ở đâu, mail nào, sử dụng thiết bị nào... na ná thế.
Tại máy tính của bạn làm theo thao tác sau:
- Mở
keychain Access.app
trong máy Mac của bạn - Phía trên bên góc trái Keychain Access -> Certificate Assistant -> Request a Cert ...
- Nhập mail của bạn (mail đã đăng ký AppleID), tên chữ ký, và chọn Saved to disk
- Sau khi tạo xong nó sẽ ra 1 file như này
CertificateSigningRequest.certSigningRequest
2.2.2 Tạo Certificate
Truy cập vào trang tạo Cert tại Apple Developer
- nhấn nút + to tổ bố kế bên chữ Certificates
- Nhấn tiếp tục và Chọn Apple Distribution
- Nhấn tiếp tục và Upload file
.certSigningRequest
vừa tạo từ keychain - Nhấn tiếp tục và và kết thúc, ở đây bạn ko cần download file
.cer
về
2.2.3 Tạo Profiles
Bấm qua mục Profiles
- Nhấn tạo
- mục Distribution chọn App Store Connect
- App ID bạn chọn đến App hiện tại của bạn ( cái app đã tạo từ tab Identifiers )
- Chọn Certificate đã tạo từ bước 2.2.2
- Provisioning Profile Name -> Đặt tên cho cái Profile của bạn -> Ấn Generate
- Download file về và bạn sẽ được 1 file .mobileprovision
Video Tham khảo, nó ko chính xác.
2.2.4 Testing Profiles
Bạn phải kiểm tra file .mobileprovision vừa tạo có Sign được không, nếu không được do bạn có vấn đề !
Cách để kiểm tra như sau:
- Trong project Flutter của bạn Mở Xcode folder IOS
- Bên trong Xcode tab Signing Bỏ chọn
Automatically manage signing
- Mục IOS -> Provisioning profile -> Nhấn để import file
.mobileprovision
bạn vừa tạo ở bước 2.2.3 - Nếu không thấy xuất hiện vấn đề gì như hình thì file đã OK, Sign thành công
Nếu bạn thấy có bất kì lỗi đỏ to đùng nào được hiển thị ra, yếu tố tâm linh nhất đó là reset lại máy, vấn đề sẽ được giải quyết ! (mình đã bị lỗi No signing certificate "iOS Distribution" found, thử reset máy và thành công ) Nếu không giải quyết được thì google lỗi đó
3. Cài đặt
Câu lệnh thần thánh bạn cần biết để convert file sang base64 Sau khi gõ, nội dung base64 sẽ được lưu trong clipboard của bạn (chỉ việc Ctrl+V để paste ra)
base64 -i path/to/file.txt | pbcopy
Truy cập vào Reponsitory -> Settings -> Secrets and variables để tạo mới 1 secret
3.1 Tạo Chữ ký khi build cho bản android (có thể bỏ qua)
Bạn không biết .jks, key.properties của android là gì ? Bạn có thể xem cách tạo từ bài viết gốc flutter tại đây
Khi bạn đã có tạo được 2 trên, chúng sẽ có đường dẫn như sau trong source flutter:
android/app/upload-keystore.jks
android/key.properties
3.1.1
Tạo 1 secrect và đặt tên là UPLOAD_KEYSTORE_BASE64
Tiếp đó mở terminal command line lên và gõ
base64 -i android/app/upload-keystore.jks | pbcopy
Sau khi gõ xong Ctrl+V vào ô value Secrect, tiếp đó ấn Add là xong.
3.1.2
tạo lần lượt các github secret mang tên STOREPASSWORD
, KEYPASSWORD
tương ứng với giá trị trong file android/key.properties
của bạn
3.2 CERTIFICATE .p12 base64
Bạn có thể lấy từ keychain Access.app
cũng được, nhưng ở đây mình sẽ hướng dẫn lấy từ XCode.
- Xcode -> Settings -> Accounts
- Chọn Team của bạn (đã được Sign thành công ở bước 2.2.4) -> Click vào
Manage Certificates...
- Ở mục Apple Distribution -> chuột phải vào certificate còn sáng -> nhấn Export
- Bạn sẽ thấy được file
.p12
, Đặt tên thànhBUILD_CERTIFICATE
cho mình, cài đặt password cho file đó -> nhấn Save
Chuyển file BUILD_CERTIFICATE.p12
sang base64 thông qua lệnh
base64 -i BUILD_CERTIFICATE.p12 | pbcopy
- Tạo github secret
BUILD_CERTIFICATE_BASE64
từ file tương tự như 3.1.1 - Tạo github secret
P12_PASSWORD
password vừa nhập khi tạo file .p12
3.3 Upload Profile .mobileprovision base64
- Tạo github secret
BUILD_PROVISION_PROFILE_BASE64
Lấy từ Provisioning Profile .mobileprovision ở bước 2.2.3
Ở đây mình đặt nó tên là PROVISIONING_PROFILE.mobileprovision nên dùng lệnh:
base64 -i PROVISIONING_PROFILE.mobileprovision | pbcopy
- Tạo github secret
KEYCHAIN_PASSWORD
với pass tùy ý đặt
3.4 ExportOptions.plist
Lưu ý phải Sign được profile trước đó.
Để có được file này chính xác làm theo các bước sau:
- XCode -> phía trên tab có chữ Product -> chọn Archive
- Đợi 1 lúc build xong sẽ tự nhảy sang màn hình Organizer -> nhấn vào bản vừa build -> chọn Distribute App
- Chọn mục Custom -> App Store Connect -> Next chọn Export
- Đợi nó xoay xoay 1 chút, nó sẽ hiện cho bạn màn hình chọn, cứ giữ nguyên mạc định và bấm Next
- Chọn đến Profile của bạn -> bấm Next -> Cuối cùng là Export
- Sau khi Export thành công, sẽ có 1 folder vừa tạo ra, đi vào trong Copy file
ExportOptions.plist
đó vào folder ios tại project flutter
File ExportOptions.plist sẽ có dạng như sau:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>destination</key>
<string>export</string>
<key>manageAppVersionAndBuildNumber</key>
<true/>
<key>method</key>
<string>app-store</string>
<key>provisioningProfiles</key>
<dict>
<key>xxxxxxx</key>
<string>xxxxxxx</string>
</dict>
<key>signingCertificate</key>
<string>Apple Distribution</string>
<key>signingStyle</key>
<string>manual</string>
<key>stripSwiftSymbols</key>
<true/>
<key>teamID</key>
<string>xxxxxxx</string>
<key>uploadSymbols</key>
<true/>
</dict>
</plist>
Nếu trong file export đó method ghi là app-store-connect
bạn có thể sửa lại thành app-store
để tránh lỗi method của flutter
Chi tiết thì cứ để nguyên trong lúc CI/CD, nó báo lỗi bạn sẽ biết được từ khóa mình đề cập...
3.5 App Store Connect Private API key .p8 base64
- Tạo github secret
APPSTORE_KEY_P8_BASE64
lấy ở bước 2.1 Chuyển file .p8 thành base64.
Nhớ copy ráp đúng cái tên $apiKey để convert
base64 -i AuthKey_$apiKey.p8 | pbcopy
- Tạo github secret
APPSTORE_APIKEY
lấy ở bước 2.1, với apiKey tương ứng - Tạo github secret
APPSTORE_APIISSUER
với apiIssuer
3.6 Github Repository Token
Token dùng để tự tạo 1 bản Release sau khi build apk, ipa và upload thành công lên testflight.
- Truy cập vào Github -> Developer Setting -> Personal Access Token hoặc nhấn Tại đây cho lẹ
- Chọn Generate New Token (Classic)
- Chỉ cần chọn quyền Repo là được
- Quay lại repository của project, tạo github secret
REPOSITORY_TOKEN
với token vừa tạo
3.7 Full workflows
Bên trong source code của bạn, hãy tạo 1 github workflow với đường dẫn như sau .github/workflows/.main.yaml
name: Build & Release
on:
# sự kiện khi push lên nhánh master hoặc tag mới bắt đầu chữ 'v' sẽ chạy job
push:
# branches:
# - master
tags:
- "v*"
jobs:
build:
name: Build
# chạy trên hệ điều hành macos
runs-on: macos-latest
steps:
# Bắt đầu clone repository về máy
- name: Clone repository
uses: actions/checkout@v4
# Setup Java để chạy
- name: Set up Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
# Setup Flutter
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
# version flutter sẽ được lấy từ chính file pubspec.yaml của bạn
flutter-version-file: pubspec.yaml
architecture: x64
# Xem lại log version Flutter và XCodeBuild
- name: Check Flutter Version
run: flutter --version
- name: Check XCodeBuild Version
run: xcodebuild -version
# Cài đặt các dependencies
- name: Install dependencies
run: flutter pub get
# Download keystore upload-keystore.jks được tạo từ keytool lúc release Google Play
- name: Download Android Keystore
run: |
echo ${{ secrets.UPLOAD_KEYSTORE_BASE64 }} | base64 --decode > android/app/upload-keystore.jks
# Tạo key.properties lúc release Google Play
- name: Create key.properties
run: |
# Download keystore first (ensure success before creating key.properties)
if [[ $? -eq 0 ]]; then
echo "storePassword=${{ secrets.STOREPASSWORD }}" >> android/key.properties
echo "keyPassword=${{ secrets.KEYPASSWORD }}" >> android/key.properties
echo "keyAlias=upload" >> android/key.properties
echo "storeFile=upload-keystore.jks" >> android/key.properties
else
echo "Error: Downloading keystore failed. Skipping key.properties creation."
exit 1
fi
# Tạo chứng chỉ và provisioning profile cho iOS để XCodeBuild có thể build
- name: Install the Apple certificate and provisioning profile
env:
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} # lấy từ keychain access -> export -> export as p12
P12_PASSWORD: ${{ secrets.P12_PASSWORD }} # mật khẩu khi export p12
BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.BUILD_PROVISION_PROFILE_BASE64 }} # lấy từ xcode -> export -> export as provisioning profile
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} # mật khẩu keychain
run: |
# create variables
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
# import certificate and provisioning profile from secrets
echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode -o $PP_PATH
# create temporary keychain
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
# import certificate to keychain
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
# apply provisioning profile
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
# Bắt đầu build các file cho Android
# khi split-per-abi
# - app-armeabi-v7a-release.apk: đại đa số các máy thường dùng bản này (file nhẹ)
# - app-arm64-v8a.apk: dành cho máy mới (Samsung đời mới chẵn hạn, file vừa)
# - app-x86_64-release.apk: đồ cổ, intel (file vừa)
- name: Build Android
run: |
# Bản gom lại cho tất cả các máy, máy nào cũng cài được
flutter build apk --release
# Bản chia nhỏ theo từng loại máy, máy nào cần cài thì cài
flutter build apk --release --split-per-abi
# Bản cần để upload lên Google Play
flutter build appbundle
# Pod install cho iOS
- name: Pod install
run: cd ios && pod install --repo-update && cd ..
# Bắt đầu build file ipa cho iOS
# ExportOptions.plist được tạo từ XCode -> Product -> Archive -> Export -> Development -> Next -> Next -> Save
- name: Build iOS
run: |
flutter build ipa --release --export-options-plist=ios/ExportOptions.plist
# Upload các file đã build lên GitHub artifacts
- name: Collect the file and upload as artifact
uses: actions/upload-artifact@v4.3.3
with:
# Đặt tên là app-release
name: app-release
path: |
build/app/outputs/flutter-apk/*.apk
build/app/outputs/bundle/release/*.aab
build/ios/ipa/*.ipa
# Này rất cần thiết, Xóa keychain và provisioning profile sau khi build xong
- name: Clean up keychain and provisioning profile
if: ${{ always() }}
run: |
security delete-keychain $RUNNER_TEMP/app-signing.keychain-db
rm ~/Library/MobileDevice/Provisioning\ Profiles/build_pp.mobileprovision
# Release job, upload the ipa to App Distribution
release:
name: Release IPA
# Cần job [build] trước đó
needs: [build]
runs-on: macos-latest
steps:
# Download các file đã build và được upload lên artifact từ job [build] trước đó
- name: Get app-release from artifacts
uses: actions/download-artifact@v4.1.7
with:
# Lấy từ app-release
name: app-release
# Lưu vào thư mục build
path: build
merge-multiple: true
# Cài đặt private key .p8 vào máy để xcode có thể nhận diện được
- name: Install private API key P8
env:
APPSTORE_KEY_P8_BASE64: ${{ secrets.APPSTORE_KEY_P8_BASE64 }}
APPSTORE_APIKEY: ${{ secrets.APPSTORE_APIKEY }}
run: |
mkdir -p ~/private_keys
echo -n "$APPSTORE_KEY_P8_BASE64" | base64 --decode > ~/private_keys/AuthKey_$APPSTORE_APIKEY.p8
# Log ra cấu trúc của các file hiện tại
- name: Display structure of downloaded files
run: ls -R
- name: Upload to AppStore
env:
APPSTORE_APIKEY: ${{ secrets.APPSTORE_APIKEY }} # lấy từ appstore connect -> users and access -> keys -> create key
APPSTORE_APIISSUER: ${{ secrets.APPSTORE_APIISSUER }} # lấy từ appstore connect -> users and access -> keys -> create key
# khi chạy lệnh này cần phải có file AuthKey_$APPSTORE_APIKEY.p8 đã tải vô trong private_keys trước đó
run: |
xcrun altool --upload-app --type ios -f build/ios/ipa/*.ipa --apiKey $APPSTORE_APIKEY --apiIssuer $APPSTORE_APIISSUER
# Đóng gói file đã build và upload lên GitHub Releases
- name: Push to Releases file
uses: ncipollo/release-action@v1.14.0
with:
name: ${{ github.ref_name }} # tên release
artifacts: |
build/app/outputs/flutter-apk/*.apk
build/app/outputs/bundle/release/*.aab
build/ios/ipa/*.ipa
tag: ${{ github.ref_name }} # tag release
token: "${{ secrets.REPOSITORY_TOKEN }}" # token repo, tạo từ setting -> developer settings -> personal access token
4. Kết quả
Sau cả tỉ lần mình thử nghiệm thì cuối cùng cũng chạy được
5. Tổng kết
Hi vọng qua bài viết này bạn sẽ nắm được đại khái các cách làm:
- Tạo Cert, Profile cho Xcode để có thể Sign được và đem sang máy khác
- Biết cách lấy Key của App Store Connect API
- Hiểu thêm về github secret, github token
- Tự động tạo ra được github artifact, github release, tự upload file ipa lên testflight
Tại sao lại sử dụng github secret cho rườm rà thế ?
Như bạn đã biết, 1 vài tác hại xấu khi public repo và bị lộ key, tốn tiền tỉ cho các phí dịch vụ cho những sai lầm không đáng đó thì ta có Secret giảm tiểu được những điều đó, tránh những mong muốn không nên xảy ra.
6. Tài liệu tham khảo
https://github.com/Apple-Actions/upload-testflight-build/issues/27
All rights reserved