+5

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, apiIssuerfile 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 image.png

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 ... image.png
  • Nhập mail của bạn (mail đã đăng ký AppleID), tên chữ ký, và chọn Saved to disk image.png
  • Sau khi tạo xong nó sẽ ra 1 file như này CertificateSigningRequest.certSigningRequest image.png

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 image.png
  • 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 image.png

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 image.png

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. image.png

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 image.png
  • 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 image.png
  • Bạn sẽ thấy được file .p12, Đặt tên thành BUILD_CERTIFICATE cho mình, cài đặt password cho file đó -> nhấn Save image.png

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 image.png
  • Đợ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 image.png
  • 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 image.png
  • 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 image.png image.png image.png

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://docs.github.com/en/actions/deployment/deploying-xcode-applications/installing-an-apple-certificate-on-macos-runners-for-xcode-development

https://stackoverflow.com/questions/74869907/trying-to-set-certificate-and-provisioning-profile-in-github-actions-for-xcodebu

https://github.com/Apple-Actions/upload-testflight-build/issues/27


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.