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.
2. Chuẩn bị
- Account Apple Developer để có thể đăng nhập được App Store Connect
- Project Flutter, Github
Bạn chưa có tài khoản ? Đă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
6. Tài liệu tham khảo
https://github.com/Apple-Actions/upload-testflight-build/issues/27