Product Management
Đăng nhập
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Bài 48 — Mobile CI/CD Setup

Đã đọc 0/14 mục · còn khoảng 10 phút
Cách chúng tôi ghi nhận

Một mục được tính là đã đọc khi nó nằm giữa màn hình đủ lâu so với độ dài của nó (bảng và sơ đồ tính một nửa), bạn vẫn đang cuộn hoặc bấm phím, và bạn đã thấy tới cuối mục. Lướt nhanh hoặc để tab mở không được tính. Chúng tôi chỉ lưu thời gian đọc từng mục — không lưu gì khác.

Mở đầu — vì sao bài này quan trọng

Hãy tưởng tượng đội của bạn có 200 test case Appium chạy ngon trên máy bạn. Nhưng mỗi lần dev push code lên, ai đó phải nhớ mở terminal, npm run test, ngồi chờ 40 phút, rồi báo kết quả trong nhóm chat. Được vài hôm là chẳng ai chạy nữa — vì phiền, vì quên, vì "chắc không sao đâu". Ba tuần sau, một bản build lên TestFlight bị crash ngay màn login, và câu hỏi đau đớn nhất được đặt ra: "Sao test không bắt được?" Câu trả lời: test có bắt được, nhưng chẳng ai chạy nó.

Đó chính xác là bài toán mà CI/CD (Continuous Integration / Continuous Delivery) sinh ra để giải. CI là "cứ có code mới là tự động build + chạy test", CD là "cứ pass hết thì tự động đóng gói và phân phối bản cài". Với mobile QA, CI/CD không phải thứ "hay ho để có" — nó là ranh giới giữa một bộ test được tin cậy và một bộ test chết dần trên máy cá nhân.

Nhưng CI/CD cho mobile khó hơn web đáng kể vì một lý do rất vật lý: bạn cần thiết bị thật hoặc máy ảo, và với iOS bạn bắt buộc phải chạy trên macOS. Bài này sẽ đưa bạn từ con số 0 tới một pipeline mobile hoàn chỉnh, hiểu vì sao macOS runner đắt, cách chọn công cụ, và những cái bẫy khiến pipeline "xanh giả" (báo pass nhưng thật ra không test gì).

Khái niệm cốt lõi

CI/CD trong ngữ cảnh mobile là gì

Một pipeline mobile QA điển hình gồm các giai đoạn (stage) nối tiếp nhau. Khi có sự kiện kích hoạt (trigger) — thường là push hoặc pull request — hệ thống CI sẽ tuần tự:

  • Checkout — kéo code mới nhất về máy runner (máy do CI cấp để chạy job).
  • Setup môi trường — cài đúng phiên bản Node, JDK, Ruby, Android SDK, Xcode... Đây là stage hay hỏng nhất với mobile.
  • Build app — biên dịch ra file .apk/.aab (Android) hoặc .app/.ipa (iOS).
  • Khởi động thiết bị — bật emulator/simulator hoặc kết nối device farm.
  • Chạy test — unit → UI/Appium/Espresso/XCUITest.
  • Thu artifact — lưu report, screenshot, video, log để điều tra sau.
  • Distribute (CD) — nếu pass, đẩy bản build lên TestFlight, Firebase App Distribution hoặc Google Play Internal Track.

Runner — trái tim và cũng là ví tiền của pipeline

Runner là máy tính thực thi job. Với mobile, đây là điểm mấu chốt về chi phí:

  • Android build và test được trên Linux runner — rẻ, khởi động nhanh, thường free hoặc rất rẻ.
  • iOS bắt buộc chạy trên macOS runner — vì cần Xcode, chỉ chạy trên phần cứng Apple. macOS runner đắt hơn Linux 8–10 lần và khan hiếm hơn nhiều.
Chính "thuế Apple" này định hình gần như mọi quyết định kiến trúc CI/CD mobile. Bạn sẽ luôn cân nhắc: chạy iOS test khi nào, ở đâu, và làm sao để không đốt tiền vào những phút macOS vô ích.

So sánh các công cụ CI/CD phổ biến cho mobile

Công cụĐiểm mạnhĐiểm yếu
GitHub ActionsMiễn phí cho repo public, tích hợp sẵn với GitHub, marketplace action phong phú, có macOS runner sẵnmacOS runner tính phí theo phút với hệ số nhân 10x; free tier hết nhanh khi làm iOS
GitLab CICấu hình .gitlab-ci.yml mạnh, self-host runner dễ, tốt cho team có hạ tầng riêngmacOS runner phải tự dựng hoặc mua add-on; setup ban đầu tốn công
BitriseSinh ra dành riêng cho mobile: có sẵn step build/sign/distribute, quản lý code signing tốtĐắt khi scale; ít linh hoạt hơn khi cần custom sâu
CircleCImacOS executor ổn định, cache mạnh, song song hóa tốtCấu hình phức tạp; chi phí macOS vẫn cao
CodemagicChuyên mobile, đặc biệt mượt với Flutter; cấu hình bằng UI hoặc YAMLHệ sinh thái nhỏ hơn; phụ thuộc một nhà cung cấp
Lời khuyên thực dụng: nếu team đã dùng GitHub, hãy bắt đầu với GitHub Actions — bạn học được nhiều nhất và ít lệ thuộc vendor. Khi phần code signing iOS trở nên đau đầu, cân nhắc Bitrise hoặc Codemagic.

Trigger — chạy cái gì, khi nào

Không phải test nào cũng chạy ở mọi lúc. Chiến lược trigger tốt tiết kiệm rất nhiều tiền:

  • Trên mỗi pull request: chạy unit test + smoke test (5–15 phút). Nhanh, chặn bug sớm.
  • Khi merge vào main/develop: chạy full UI regression trên emulator.
  • Theo lịch (nightly, 2h sáng): chạy bộ test dài, đa thiết bị trên device farm — lúc runner rảnh và không ai chờ.
  • Khi tạo tag release: build production, ký (sign) và distribute lên beta.

Tình huống thực tế

Ví dụ 1 — Startup fintech "Tikop-clone" và hóa đơn macOS 3.000 USD

Một startup fintech ở TP.HCM (gọi là "SaveNow", 12 người) dựng CI/CD trên GitHub Actions. Ban đầu mọi thứ ổn với Android. Khi thêm iOS, họ cấu hình để mọi pull request đều chạy cả full UI test iOS trên macos-latest. Mỗi lần chạy mất ~35 phút, và mỗi phút macOS bị tính hệ số nhân 10 so với Linux.

Với ~40 PR mỗi tuần, họ đốt hết 3.000 phút miễn phí của gói Team chỉ trong 9 ngày, rồi phát sinh hóa đơn overage khoảng 3.000 USD/tháng — gần bằng lương một QA. Điều trớ trêu: 90% PR chỉ sửa backend hoặc text, chẳng liên quan gì tới iOS UI.

Bài học: họ tái cấu trúc trigger. PR chỉ chạy unit test + iOS smoke test (login + màn chính, ~8 phút). Full iOS regression dời sang nightly. Thêm paths filter để PR không đụng tới code mobile thì bỏ qua hoàn toàn job iOS. Hóa đơn tụt xuống dưới 400 USD/tháng mà độ an toàn gần như không đổi. macOS runner là tài nguyên đắt đỏ — hãy tiêu nó như tiêu tiền túi, không phải tiền chùa.

Ví dụ 2 — Sàn TMĐT và pipeline "xanh giả" suốt 6 tuần

Một team QA ở một sàn thương mại điện tử Đông Nam Á có pipeline Espresso "luôn xanh". Ai cũng yên tâm. Cho tới khi một release lọt bug nghiêm trọng ở luồng thanh toán mà test đáng lẽ phải bắt.

Điều tra ra: emulator trên CI runner thỉnh thoảng khởi động chậm, và bước chạy test có dòng || true ở cuối (di sản từ lúc ai đó "tạm cho pass để merge gấp"). Kết quả: khi emulator không kịp boot, test crash, nhưng || true nuốt luôn lỗi và job vẫn trả về exit code 0 → xanh. Suốt 6 tuần, một phần ba số lần chạy thực chất không test gì cả.

Bài học: pipeline xanh không đồng nghĩa với "đã test". Phải kiểm chứng số lượng test thực sự chạy (ví dụ assert "đã chạy đúng 214 test"), gỡ mọi || true và continue-on-error khỏi bước test, và thêm bước chờ emulator sẵn sàng (adb wait-for-device + boot completed) trước khi chạy. Một pipeline nói dối còn nguy hiểm hơn không có pipeline.

Ví dụ 3 — Team Flutter chọn Codemagic để thoát địa ngục code signing

Một team 5 người làm app giao đồ ăn bằng Flutter loay hoay 3 tuần với GitHub Actions cho iOS. Vấn đề không phải test — mà là code signing: certificate, provisioning profile, App Store Connect API key... Mỗi lần cập nhật cert là pipeline đỏ, và không ai trong team đủ hiểu Xcode signing để sửa dứt điểm.

Họ chuyển sang Codemagic. Việc quản lý signing được UI hóa: upload API key một lần, chọn automatic signing, xong. Thời gian dựng pipeline iOS từ "3 tuần chưa xong" rút xuống còn 1 buổi chiều. Đổi lại họ chấp nhận lệ thuộc một vendor và chi phí cố định hàng tháng.

Bài học: chọn công cụ theo điểm đau lớn nhất của team. Nếu code signing iOS là nút thắt, một nền tảng mobile-first như Bitrise/Codemagic đáng giá từng đồng. Đừng cố tỏ ra "hardcore" với YAML thuần khi nó ngốn cả tuần mà không tạo giá trị.

Hướng dẫn từng bước

Ta sẽ dựng một pipeline GitHub Actions tối thiểu nhưng đúng đắn cho một app Android, chạy Espresso trên emulator. Đây là nền tảng bạn mở rộng dần.

Bước 1 — Tạo file workflow. Trong repo, tạo .github/workflows/android-ci.yml.

Bước 2 — Định nghĩa trigger thông minh.

on:
  pull_request:
    paths:
      - 'app/**'
      - '.github/workflows/**'
  push:
    branches: [ main, develop ]

paths đảm bảo PR không đụng code app thì không kích hoạt job — tiết kiệm runner.

Bước 3 — Setup môi trường build.

jobs:
  android-ui-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          distribution: 'temurin'
          java-version: '17'
      - name: Gradle cache
        uses: gradle/actions/setup-gradle@v3

Bước cache Gradle cực kỳ quan trọng: nó có thể cắt vài phút mỗi lần chạy.

Bước 4 — Chạy emulator và test. Dùng action đã đóng gói sẵn phần khó của emulator:

      - name: Run Espresso on emulator
        uses: reactivecircus/android-emulator-runner@v2
        with:
          api-level: 30
          arch: x86_64
          disable-animations: true
          script: ./gradlew connectedCheck

disable-animations: true là bắt buộc — animation là nguyên nhân số 1 gây flaky trên emulator CI.

Bước 5 — Thu thập artifact dù pass hay fail.

      - name: Upload test report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: espresso-report
          path: app/build/reports/androidTests/

if: always() đảm bảo bạn lấy được report cả khi test đỏ — vì đó chính là lúc bạn cần điều tra nhất.

Bước 6 — Thêm bước phân phối (CD) cho nhánh main. Sau khi test pass, dùng một job riêng để đẩy .apk lên Firebase App Distribution hoặc Google Play Internal Track. Tách job giúp bạn không distribute khi test đỏ.

Bước 7 — Với iOS, thêm một workflow riêng runs-on: macos-14, dùng xcodebuild để build và chạy XCUITest trên simulator, và quản lý code signing qua App Store Connect API key lưu trong GitHub Secrets. Nhớ áp paths filter và giới hạn trigger để không đốt macOS runner.

Lỗi thường gặp & mẹo

Lỗi: nuốt exit code. Mọi || true, set +e hay continue-on-error: true ở bước chạy test đều biến pipeline thành kẻ nói dối. Chỉ cho phép fail-soft ở bước thu artifact, tuyệt đối không ở bước test.

Lỗi: "works on my machine". Runner luôn là môi trường sạch, không có sẵn thứ gì. Phải khai báo tường minh mọi phiên bản Node/JDK/Xcode. Đừng bao giờ giả định tool có sẵn.

Lỗi: bỏ qua flaky test. Trên CI, emulator/simulator chạy trên máy ảo tài nguyên hạn chế, test dễ flaky hơn máy bạn nhiều. Đừng "xử lý" bằng cách retry vô tội vạ để giấu vấn đề — hãy tắt animation, thêm wait đúng chỗ (liên quan bài Waits & Sync), và tăng timeout hợp lý.

Lỗi: không cache. Không cache Gradle/CocoaPods/SPM khiến mỗi lần chạy tải lại dependency, đội thời gian và tiền (đặc biệt trên macOS). Cache là khoản đầu tư hoàn vốn ngay lần chạy thứ hai.

Lỗi: bí mật lộ ra log. Certificate, API key, keystore password phải nằm trong Secrets của CI, không hardcode trong YAML. Với iOS, dùng App Store Connect API key thay cho username/password.

Mẹo — chiến lược tầng test: smoke test nhanh chặn PR, regression đầy đủ chạy nightly, đa thiết bị đẩy lên device farm theo lịch. Đừng chạy mọi thứ ở mọi lúc.

Mẹo — đặt timeout cho job. Thêm timeout-minutes để một job treo không ngốn runner (và tiền) vô hạn.

Mẹo — làm pipeline "biết nói". Gắn thông báo Slack/Telegram khi build đỏ. Feedback nhanh là linh hồn của "Continuous".

Bài tập thực hành

  • Dựng pipeline Android tối thiểu: fork một app Android mẫu có sẵn vài test Espresso, tạo .github/workflows/android-ci.yml chạy được trên PR. Cố ý làm một test fail để xác nhận pipeline chuyển sang đỏ — chứng minh nó không "xanh giả".
  • Tối ưu chi phí trigger: giả sử bạn có iOS UI test 35 phút. Viết ra chiến lược trigger (PR chạy gì, merge chạy gì, nightly chạy gì) sao cho giảm được ít nhất 70% số phút macOS mà vẫn giữ smoke test iOS trên mỗi PR. Trình bày bằng bảng.
  • Săn "xanh giả": cho một file YAML có ./gradlew test || true và không có bước chờ emulator boot. Chỉ ra chính xác hai chỗ khiến pipeline có thể báo pass sai, và viết lại cho đúng.
  • So sánh công cụ: chọn giữa GitHub Actions và Codemagic cho một team Flutter 4 người đang vật lộn với code signing iOS. Viết một đoạn 150 từ bảo vệ lựa chọn của bạn dựa trên điểm đau thực tế, không chỉ dựa trên giá.

Tóm tắt

CI/CD biến bộ test mobile của bạn từ "thứ ai đó thỉnh thoảng chạy trên máy cá nhân" thành một cơ chế tự động, đáng tin, chạy trên mỗi thay đổi code. Với mobile, đặc thù lớn nhất là runner: Android chạy rẻ trên Linux, còn iOS bắt buộc dùng macOS runner đắt gấp gần 10 lần — điều này chi phối mọi quyết định về trigger và kiến trúc. Hãy chọn công cụ theo điểm đau của team (GitHub Actions để học và làm chủ, Bitrise/Codemagic khi code signing iOS thành nút thắt), thiết kế trigger theo tầng (smoke chặn PR, regression nightly, đa thiết bị theo lịch), và tuyệt đối cảnh giác với pipeline "xanh giả" — luôn kiểm chứng test thực sự đã chạy, gỡ mọi cách nuốt exit code, và thu artifact cả khi đỏ. Một pipeline tốt cho bạn feedback nhanh và niềm tin; một pipeline nói dối còn tệ hơn không có gì.

Học xong bài này rồi? Tạo tài khoản miễn phí để lưu lại — lần sau vào là biết ngay đang dở ở đâu, và học hết khóa thì có chứng chỉ. Lưu tiến độ của tôi