Chúng ta đang ở trong trạng thái mà các công ty có thể phát hành phần mềm và giải pháp chỉ trong vòng vài phút và họ đang làm như vậy bằng cách tuân theo bộ nguyên tắc hoạt động Tích hợp liên tục (CI) và phân phối liên tục (CD).

Đường dẫn CI/CD giúp phân phối tự động phần mềm của bạn thường xuyên hơn, đáng tin cậy và an toàn hơn. Nó tập trung vào chất lượng code cao hơn và đó là lý do tại sao nó quan trọng đối với các team hoặc mobile developer.

Trong hướng dẫn này, bạn sẽ tìm hiểu cách triển khai ứng dụng Flutter của mình theo nguyên tắc CI/CD với GitHub Actions làm công cụ.

Hướng dẫn này yêu cầu bạn phải có một tài khoản dịch vụ (Service Accounts) của Google, tài khoản này sẽ được sử dụng trong Github Actions để xuất bản bản dựng Android lên Play Store. Vì vậy, để tạo dự án trong GCP, hãy tạo tài khoản dịch vụ và chọn dự án đã tạo của bạn.

Lưu ý để thực hiện được giả định này bạn cần phải có một số kiến ​​thức trước đó về Flutter. Nếu bạn chưa quen với Flutter, vui lòng tham khảo tài liệu chính thức để tìm hiểu về nó.

Thực hiện theo các bước thiết lập ban đầu sau đây:

     1. Thiết lập dự án Flutter mới bằng IDE yêu thích của bạn hoặc sử dụng Command Line Tool trong Flutter

     2. Khởi tạo Git trong dự án mới trên máy của bạn và tạo kho lưu trữ mới được liên kết với tài khoản GitHub của bạn.

     3. Tạo thư mục cấu hình trong thư mục gốc của dự án .github của bạn và một thư mục mới có tên workflows. Workflow ở đây sẽ chứa tất cả quy trình công việc CI/CD của bạn dưới dạng tệp .yml.

Sử dụng Flutter action cơ bản để xây dựng bản phát hành Android

Bây giờ, bạn sẽ tạo một workflow cơ bản của Android để giúp bạn hiểu cách GitHub Actions hoạt động trong việc xây dựng ứng dụng Flutter của bạn.

Tạo một tệp .yml, android-release.yml, bên trong workflows với đoạn code sau:

name: Android Release

# 1
on:
  # 2
  push:
    branches: [ "master" ]
  pull_request:
    branches: [ "master" ]

  # 3
  workflow_dispatch:

# 4
jobs:
  # 5
  build:
    # 6
    runs-on: ubuntu-latest

    # 7
    steps:
      # 8
      - uses: actions/checkout@v3
      # 9
      - uses: actions/setup-java@v3
        with:
          distribution: 'zulu'
          java-version: "12.x"
      # 10   
      - uses: subosito/flutter-action@v2
        with:
          # 11
          flutter-version: "3.0.0"
          channel: 'stable'
      # 12
      - name: Get dependencies
        run: flutter pub get

      # Runs a set of commands using the runners shell
      - name: Start release build
        run: flutter build appbundle

     1. Kiểm soát thời điểm workflow sẽ chạy.

     2. Kích hoạt workflow trên các events của push hoặc pull request nhánh "master"; bạn có thể tùy chỉnh nó theo yêu cầu của bạn.

     3. Cho phép bạn chạy workflow này theo cách thủ công từ tab Actions đến kho lưu trữ GitHub của bạn (một quy trình chạy Workflow được tạo thành từ một hoặc nhiều job chạy tuần tự hoặc song song) 

     4. Chứa một job duy nhất được gọi là build

     5. Chứa loại runner mà job sẽ chạy trên đó

     6. Sử dụng các bước để thể hiện một chuỗi các nhiệm vụ sẽ được thực hiện như một phần của job.

     7. Chuẩn bị sẵn kho lưu trữ của bạn bên dưới $GITHUB_WORKSPACE để job của bạn có thể truy cập kho lưu trữ đó

     8. Thiết lập Java để job của bạn có thể sử dụng nó cho xây dựng ứng dụng Flutter

     9. Thiết lập Flutter bằng subosito Flutter workflow

     10. Điều chỉnh theo phiên bản Flutter mà bạn đang làm việc

     11. Chạy một lệnh duy nhất bằng cách sử dụng trình bao của runner

Workflow cơ bản này có một tí vấn đề đó là bất cứ khi nào bạn đẩy các thay đổi trong nhánh master, workflow này sẽ kích hoạt và bắt đầu thiết lập Java SDK và Flutter SDK mỗi lần như thế. Vì vậy, nó sẽ dẫn đến độ trễ trong việc xây dựng ứng dụng của bạn vì bạn phải thiết lập dịch vụ mọi lúc.

Làm sao để khiến workflow của bạn chạy nhanh hơn?

Bạn có thể làm cho quy trình làm việc Flutter của mình nhanh hơn bằng cách lưu vào bộ nhớ cache SDK Java và Flutter để trong lần chạy tiếp theo, nó sẽ không tìm nạp SDK trực tiếp trước khi kiểm tra sự tồn tại của SDK.

Trong tệp main.yml của bạn, hãy thực hiện các thay đổi sau:

      - uses: actions/setup-java@v3
        with:
          distribution: 'zulu'
          java-version: "12.x"
          cache: 'gradle' // 1
         
      - uses: subosito/flutter-action@v2
        with:
          flutter-version: "3.0.0"
          channel: 'stable'
          cache: true // 2

Bạn đã cập nhật thiết lập SDK bằng cách cung cấp cho Gradle được lưu vào bộ đệm đối với Java SDK (1) và bật bộ nhớ đệm cho Flutter SDK (2).

Lần tới khi bạn chạy job post, hãy lưu các thay đổi ở trên và quan sát thời gian đã sử dụng; bạn sẽ thấy sự khác biệt về thời gian so với flow cơ bản.

Tạo số cho version

Đối với bất kỳ bản phát hành mới nào, bạn phải có số phiên bản phát hành, vì vậy trước khi xây dựng, bạn cần tạo số phiên bản bằng job bên dưới:

# 1
version:
    name: Create version number
    # The type of runner that the job will run on
    runs-on: ubuntu-latest
    # Steps represent a sequence of tasks that will be executed as part of the job
    steps:
      - uses: actions/checkout@v3
      # 2
      - name: Install GitVersion
        uses: gittools/actions/gitversion/setup@v0.9.7
        with:
          versionSpec: "5.x"
      - name: Use GitVersion
        id: gitversion
        uses: gittools/actions/gitversion/execute@v0.9.7
      # 3
      - name: Create version.txt with nuGetVersion
        run: echo ${{ steps.gitversion.outputs.nuGetVersion  }} > version.txt
      # 4
      - name: Upload version.txt
        uses: actions/upload-artifact@v2
        with:
          name: gitversion
          path: version.txt

 

Trong đoạn code trên, chúng ta đã làm như sau:

     1. Đã tạo một phiên bản job mới sẽ được thực hiện trước việc xây dựng job.

     2. Đã cài đặt GitVersion, một công cụ được sử dụng để lập phiên bản bằng cách xem lịch sử Git của bạn

     3. Đăng bằng GitVersion, đặt version vào tệp version.text

     4. Đăng tải tệp version.text như một artifact cho hệ thống action với tên gitversion được sử dụng sau này trong việc build job.

Sign the app

Để xuất bản ứng dụng lên Play Store, bạn cần cung cấp cho ứng dụng của mình chữ ký điện tử bằng cách sử dụng KeyStore. Theo dõi Flutter Doc chính thức này về cách thực hiện điều đó tùy thuộc vào máy của bạn:

keytool -genkey -v -keystore %userprofile%\upload-keystore.jks -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 -alias upload

Điều này sẽ lưu trữ một tệp có phần mở rộng .jks trong thư mục chính của bạn hoặc bất kỳ đường dẫn nào bạn đã cung cấp.

Lưu ý đảm bảo bạn đã thêm mật khẩu Store, mật khẩu khóa và key alias vào kho lưu trữ GitHub bí mật của bạn (từ kho lưu trữ GitHub > Secrets > Actions).

Gặp sự cố khi chạy Keytool?

Nếu bạn gặp vấn đề “’keytool’ is not recognized as an internal or external command” thì hãy thêm path của JDK bin để sử dụng Biến môi trường (Environment variables) hoặc cài đặt JDK và lặp lại việc bổ sung path cho các Biến môi trường (Environment variables).

Tiếp theo, tạo một tệp mới key.properties trong thư mục Android của ứng dụng của bạn và cung cấp tham chiếu đến keystore được tạo trước đó.

storePassword=<password from previous step>
keyPassword=<password from previous step>
keyAlias=upload
storeFile=<location of the key store file, such as /Users/<user name>/your-keystore-file.jks>

Để sử dụng key này khi xây dựng ứng dụng của bạn ở chế độ phát hành, hãy cập nhật bản Android-level build.gradle của bạn như file bên dưới:

     1. Xác định biến keyProperties để tham chiếu đến file key.properties từ mục filesystem

   def keystoreProperties = new Properties()
   def keystorePropertiesFile = rootProject.file('key.properties')
   if (keystorePropertiesFile.exists()) {
       keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
   }

     2. Cập nhật buildTypes và thêm signingConfigs như bên dưới:

   signingConfigs {
       release {
           keyAlias keystoreProperties['keyAlias']
           keyPassword keystoreProperties['keyPassword']
           storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
           storePassword keystoreProperties['storePassword']
       }
   }
   buildTypes {
       release {
           signingConfig signingConfigs.release
       }
   }

Sau này, bản dựng mới của bạn sẽ được tạo trong chế độ phát hành bằng key.

Lưu ý: Không cho phép keystore và file key.properties để ở chế độ riêng tư.

Triển khai ứng dụng

Bây giờ, bạn cần sử dụng bundle và gửi nó đến Play Store. Trước đó, phải đảm bảo rằng bạn đã có tài khoản dịch vụ của Google. Nếu bạn đã có tài khoản, hãy sao chép key cho tài khoản đó và lưu trữ bí mật dưới dạng tệp PLAYSTORE_ACCOUNT_KEY.

Tiếp theo, trong Google Play Console > Users & Permissions , hãy mời người dùng và thêm email của người dùng tài khoản dịch vụ tại đây.

Nếu bạn không thấy ứng dụng của mình trong App permissions, hãy đảm bảo rằng API nhà phát triển Google Play trong GCP được bật cho dự án của bạn.

Tiếp theo, hãy cập nhật quyền của người dùng để họ có quyền phát hành ứng dụng như vai trò quản trị viên.

Bây giờ, hãy thêm một new job deploy trong flow android-release của bạn.

deploy:
    name: Deploy Android Build
    # 1
    needs: build
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v1
      # 2
    - name: Get Android Build from artifacts
      uses: actions/download-artifact@v2
      with:
        name: android-release
      # 3
    - name: Release Build to internal track
      uses: r0adkll/upload-google-play@v1
      with:
        serviceAccountJsonPlainText: ${{ secrets.PLAYSTORE_ACCOUNT_KEY }}
        packageName: <YOUR_PACKAGE_NAME>
        releaseFiles: app-release.aab
        track: alpha
        status: completed

Bạn đã thực hiện:

  1. Đã thêm một dependency để chạy job này một cách tuần tự
  2. Đã tải xuống bản dựng Android từ nhân tạo bằng cách sử dụng tên android-release
  3. Sử dụng workflow upload-google.play@v1 với PLAYSTORE_ACCOUNT_KEY bí mật, tên gói ứng dụng của bạn, bản nhạc mà bạn muốn tải bản dựng lên và trạng thái của bản dựng.

Sau đó, đẩy các thay đổi của bạn lên GitHub và xem quy trình triển khai ứng dụng của bạn lên Play Store.

Dưới đây là workflow hoàn chỉnh:

name: Android Release

on:
  push:
    branches: [ "master" ]
  pull_request:
    branches: [ "master" ]

  workflow_dispatch:

jobs:
  version:
    name: Create version number
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Install GitVersion
        uses: gittools/actions/gitversion/setup@v0.9.7
        with:
          versionSpec: "5.x"
      - name: Use GitVersion
        id: gitversion
        uses: gittools/actions/gitversion/execute@v0.9.7
      - name: Create version.txt with nuGetVersion
        run: echo ${{ steps.gitversion.outputs.nuGetVersion  }} > version.txt
      - name: Upload version.txt
        uses: actions/upload-artifact@v2
        with:
          name: gitversion
          path: version.txt

  build:
    name: Create Android Build
    needs: version
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Get version.txt
        uses: actions/download-artifact@v2
        with:
          name: gitversion
      - name: Create new file without newline char from version.txt
        run: tr -d '\n' < version.txt > version1.txt
      - name: Read version
        id: version
        uses: juliangruber/read-file-action@v1
        with:
          path: version1.txt
      - name: Update version in YAML
        run: sed -i 's/99.99.99+99/${{ steps.version.outputs.content }}+${{ github.run_number }}/g' pubspec.yaml
      - name: Download Android keystore
        id: android_keystore
        uses: timheuer/base64-to-file@v1.0.3
        with:
          fileName: upload-keystore.jks
          encodedString: ${{ secrets.KEYSTORE_BASE64 }}
      - name: Create key.properties
        run: |
          echo "storeFile=${{ steps.android_keystore.outputs.filePath }}" > android/key.properties
          echo "storePassword=${{ secrets.STORE_PASSWORD }}" >> android/key.properties
          echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/key.properties
          echo "keyAlias=${{ secrets.KEY_ALIAS }}" >> android/key.properties
      - uses: actions/setup-java@v3
        with:
          distribution: 'zulu'
          java-version: "12.x"
          cache: gradle
      - uses: subosito/flutter-action@v2
        with:
          flutter-version: "3.0.0"
          channel: 'stable'
          cache: true
      
      - name: Get dependencies
        run: flutter pub get

      - name: Start Android Release Build
        run: flutter build appbundle

      - name: Upload Android Release
        uses: actions/upload-artifact@v2
        with:
          name: android-release
          path: build/app/outputs/bundle/release/app-release.aab

  deploy:
    name: Deploy Android Build
    needs: build
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v1
    - name: Get Android Build from artifacts
      uses: actions/download-artifact@v2
      with:
        name: android-release
    - name: Release Build to internal track
      uses: r0adkll/upload-google-play@v1
      with:
        serviceAccountJsonPlainText: ${{ secrets.PLAYSTORE_ACCOUNT_KEY }}
        packageName: <YOUR_PACKAGE_NAME>
        releaseFiles: app-release.aab
        track: alpha
        status: completed

Lưu ý:

Bạn có thể kết hợp tất cả các job này trong một job duy nhất để việc chia sẻ tệp giữa các job sẽ không yêu cầu xuất bản artifacts.

Có một vấn đề là đôi khi ứng dụng không được xuất bản trong lần chạy đầu tiên. Vì vậy, hãy tải lên APK hoặc gói ứng dụng được tạo từ quy trình này và triển khai cho người dùng nội bộ. Sau đó, workflow này sẽ có thể phát hành ứng dụng mà không gặp sự cố nào.

Nếu bạn vẫn gặp sự cố trong quá trình triển khai, hãy đảm bảo tất cả cấu hình và quyền đều chính xác hoặc truy cập trang này để được trợ giúp.

Xuất bản flutter web lên Github

Bây giờ hãy tạo một workflow mới web-release.yml và dán đoạn mã sau:

 name: Web Release

on:
  push:
    branches:  [ "master" ]

  pull_request:
    branches: [ "master" ]

  workflow_dispatch:

jobs:
  build:
    name: Create Web Build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-java@v3
        with:
          distribution: 'zulu'
          java-version: "12.x"
          cache: gradle
      - uses: subosito/flutter-action@v2
        with:
          flutter-version: "3.0.0"
          channel: 'stable'
          cache: true
      
      - name: Get dependencies
        run: flutter pub get

      - name: Start Web Release Build
        run: flutter build web --release
     
      - name: Upload Web Build Files
        uses: actions/upload-artifact@v2
        with:
          name: web-release
          path: ./build/web

  deploy:
    name: Deploy Web Build
    needs: build
    runs-on: ubuntu-latest

    steps:
    - name: Download Web Release
      uses: actions/download-artifact@v2
      with:
        name: web-release

    - name: Deploy to gh-pages
      uses: peaceiris/actions-gh-pages@v3
      with:
        github_token: ${{ secrets.GITHUB_TOKEN }}
        publish_dir: ./

Workflow làm việc ở trên khá giống với workflow của Android, nhưng ở đây bạn sử dụng lệnh Flutter web build và sau đó sử dụng workflow peaceiris/ actions-gh-pages@v3 để triển khai bản dựng web lên Trang GitHub.

Lưu ý:

  • GITHUB TOKEN không phải là mã thông báo truy cập cá nhân. Nó được tạo tự động để xác thực trong quy trình làm việc của bạn.
  • Đảm bảo nhánh trong phần GitHub Pages section mục Settings được đặt thành gh-pages.

KẾT LUẬN

Trong hướng dẫn này, bạn đã tìm hiểu về cách thiết lập quy trình làm việc GitHub Actions để triển khai ứng dụng Flutter của mình trên Web và Android. Đối với bước tiếp theo, bạn có thể sao chép và sửa đổi workflow để phát hành trực tiếp ứng dụng lên cửa hàng ứng dụng hoặc tìm hiểu về các lựa chọn thay thế khác của GitHub Actions như CircleCI , GitLab CI, Jenkins, v.v.

VietnamWorks inTECH