Azimjon Abdurasulov

Bazel 11: CI/CD — GitHub Actions, remote cache, faqat o'zgarganini build/test qilish va release

📦 Bazel iOS — Mukammal Qo‘llanma & Amaliyot

Bazel 11: CI/CD — GitHub Actions, remote cache, faqat o'zgarganini build/test qilish va release

Holat: 2026-yil oktabr. Bu darsda Bazel'ning jamoaviy va CI qudratini to'liq ishga tushiramiz: GitHub Actions (macos-15 Apple Silicon runner), remote cache bilan bog'lash, Bazel query orqali selective testing (faqat PR'dagi o'zgarishga taalluqli testlarni topish va yugurtirish), test hisobotlari, sharding hamda App Store / TestFlight uchun release pipeline qurish. Kod misollari Bazel 9.3.0 (Bazelisk 1.29.0), rules_apple 5.3.0, rules_swift 4.2.1, Xcode 27.1 va GitHub Actions runner'larida tekshirilgan.

Bu darsdan keyin siz: har bir Pull Request'da butun loyihani 30 daqiqa kutmasdan, 1–2 daqiqada faqat o'zgargan modullarni test qiladigan, CI'dan developerlar uchun build cache isitadigan (warm qiladigan) va release'ni avtomatlashtiradigan to'liq CI/CD pipeline yarata olasiz.

  • Mobil CI nega qimmat va sekin ekanini hamda Bazel uni qanday qilib 80–90% ga arzonlashtirishini raqamlar bilan ko'rasiz.
  • GitHub Actions'da macOS runner'larni (macos-14, macos-15 M1/M2/M3 arxitektura) to'g'ri sozlashni va Bazelisk keshini boshqarishni o'rganasiz.
  • CI va lokal dasturchilar o'rtasida Remote Cache rolini xavfsiz taqsimlaysiz: CI yozadi (upload=true), dasturchilar faqat o'qiydi.
  • Selective testing mo''jizasini o'z qo'lingiz bilan qurasiz: git diff + bazel query 'tests(rdeps(...))' yordamida faqat o'zgarish tekkan testlarni yugurtirasiz.
  • Flaky testlarni --flaky_test_attempts va test sharding (--test_sharding_strategy, shard_count) bilan nazorat qilasiz.
  • TestFlight va App Store uchun .ipa tayyorlash, provisioning profile va imzolash bosqichini CI'da xavfsiz o'rnatasiz.

Mundarija

  1. Mobil CI og'rig'i: 30 daqiqa va qimmat runner'lar
  2. GitHub Actions runner arxitekturasi: macos-14 va macos-15
  3. Bazelisk va toolchain'ni CI'da keshlashtirish
  4. CI'da Remote Cache xavfsizligi: Read vs Write
  5. Selective Testing: faqat o'zgargan kodni test qilish
  6. To'liq GitHub Actions PR workflow namunasi
  7. Test natijalari, xatolar va JUnit XML hisobotlari
  8. Flaky testlar va Sharding bilan kurashish
  9. Release Workflow: TestFlight va App Store .ipa
  10. Real kompaniyalar tajribasi va narx tejash
  11. Amaliy mashqlar
  12. O'zingizni tekshiring
  13. Xulosa
  14. Manbalar

1. Mobil CI og'rig'i: 30 daqiqa va qimmat runner'lar {#mobil-ci-ogriq}

iOS dasturchilarining kundalik ishida CI (Continuous Integration) ko'pincha eng og'riqli nuqtadir. Backend olamida Docker konteynerlari 30 soniyada ko'tarilib, Linux'da bepul yoki arzon runner'larda soniyalar ichida testlar o'tadi. Mobil olamda esa manzara butunlay boshqacha:

  1. macOS runner narxi: GitHub Actions'da standart Linux runner daqiqasi bepul (yoki $0.008) bo'lsa, macOS runner'lar $0.08 dan $0.16 gacha — ya'ni 10 barobar qimmatroq.
  2. "Clean build" azobi: Xcode loyihalarida kesh buzilmasligi uchun jamoalar CI'da har safar xcodebuild clean yoki toza muhit ochishadi. Katta loyihada bu 20–40 daqiqa vaqt oladi.
  3. Navbatda turish: Jamoada 15 dasturchi bo'lsa va har bir PR 30 daqiqa yursa, macOS runner'lar band bo'lib qoladi, PR merge qilish uchun soatlab navbat kutiladi.

iOS CI/CD Pipeline: Bazel + GitHub Actions oqimi va xarajatlar taqqoslanishi

10-darsda ko'rganimizdek, Bazel uchun build — bu sof matematik funksiya. Agar siz faqat Core/Book.swift faylida bitta qatorni o'zgartirgan bo'lsangiz, nega CI butun ilovani, UI kutubxonalarni va aloqasi bo'lmagan 200 ta testni boshidan kompilyatsiya qilishi kerak?

Bazel CI'da ikkita asosiy kuch beradi:

  • Remote Cache hit: O'zgarmagan har bir action (Swift compile, asset compilation, link) tarmoq keshidan bir necha millisoniyada tortib olinadi.
  • Selective Testing (rdeps): Graf tahlili orqali faqat o'sha o'zgargan faylga bevosita yoki bilvosita bog'liq testlar ishga tushiriladi. Qolganlari hatto boshlanmaydi ham!

2. GitHub Actions runner arxitekturasi: macos-14 va macos-15 {#github-runners}

2026-yil holatiga ko'ra, GitHub Actions'da Intel (x86_64) macOS runner'lar butunlay eskirgan va olib tashlangan. Bugungi standart:

Runner nomiCPU ArxitekturasiApple ChipRAMXcode Standart
macos-14arm64 (Apple Silicon)M1 / M2 vCPU14 GBXcode 15 / 16
macos-15arm64 (Apple Silicon)M3 / M4 vCPU14–28 GBXcode 26 / 27

⚠️ Muhim qoida: Lokal dasturchi mashinasi (Apple Silicon arm64) va CI runner arxitekturasi bir xil bo'lishi shart (darwin_arm64). Aks holda 10-darsda ko'rganimizdek, platform farqi tufayli action key mos kelmaydi va remote cache miss beradi!

Runner'da Xcode versiyasini to'g'ri tanlash uchun develop bosqichida xcode-select ishlatiladi:

- name: Select Xcode Version
  run: |
    sudo xcode-select -s /Applications/Xcode_27.1.app/Contents/Developer
    xcodebuild -version

3. Bazelisk va toolchain'ni CI'da keshlashtirish {#bazelisk-ci}

Bazelisk — rasmiy Bazel launcher. U loyihadagi .bazelversion fayliga qarab kerakli Bazel binarini avtomatik yuklab oladi. Har bir CI ishga tushganda Bazel'ni internetdan qayta tortib o'tirmaslik uchun GitHub Actions cache action'dan foydalanamiz:

- name: Cache Bazelisk and tools
  uses: actions/cache@v4
  with:
    path: |
      ~/.cache/bazelisk
      ~/.cache/bazel
    key: ${{ runner.os }}-bazelisk-${{ hashFiles('.bazelversion') }}
    restore-keys: |
      ${{ runner.os }}-bazelisk-

Bazelisk'ning o'zi GitHub Actions runner'larida oldindan o'rnatilgan bo'ladi. Agar o'rnatilmagan bo'lsa:

brew install bazelisk

4. CI'da Remote Cache xavfsizligi: Read vs Write {#remote-cache-ci}

Eng xavfli xatolardan biri — har qanday PR yoki xohlagan shoxchaning natijasini asosiy Remote Cache'ga yozishga ruxsat berish (cache poisoning). Noto'g'ri yozilgan binar butun jamoaning lokal build'larini buzishi mumkin.

Shuning uchun qat'iy qoida joriy etiladi:

  1. Pull Request (PR) oqimida: Faqat o'qish (Read-only). --remote_upload_local_results=false
  2. Main / Master (Merged) oqimida: O'qish va yozish (Read & Write). --remote_upload_local_results=true

Buni .bazelrc faylida config profillari orqali hal qilamiz:

# .bazelrc

# Umumiy remote cache sozlamalari
build:remote_shared --remote_cache=grpcs://cache.buildbuddy.io
build:remote_shared --remote_download_outputs=toplevel

# CI faqat o'qish uchun (PR'lar)
build:ci_pr --config=remote_shared
build:ci_pr --remote_upload_local_results=false

# CI asosiy shoxcha uchun (keshni isitish)
build:ci_main --config=remote_shared
build:ci_main --remote_upload_local_results=true
build:ci_main --remote_header=x-buildbuddy-api-key=${BUILDBUDDY_API_KEY}

GitHub Actions ichida API kalitni faqat main shoxchasida yoki ruxsat berilgan maxfiy parametr (Secret) orqali uzatamiz:

- name: Bazel Test on PR
  run: |
    bazel test --config=ci_pr //...
  env:
    BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_READ_ONLY_KEY }}

5. Selective Testing: faqat o'zgargan kodni test qilish {#selective-testing}

Monolit yoki ko'p modulli loyihalarda har bir PR'da //... (butun repo) ni test qilish katta resurs talab qiladi. Hatto remote cache bo'lsa ham, test runner simulyatorni ochishi va xotirada tekshirishi kerak.

Bazel'ning eng katta ustunligi — dependency graphni mukammal bilishi. bazel query orqali "Qaysi testlar aynan manashu o'zgargan fayllarga bog'liq?" degan savolga 0.3 soniyada javob olamiz!

Selective Testing: rdeps orqali o'zgargan moduldan yuqoriga qarab ta'sirlangan testlarni filtrlash

Qanday ishlaydi?

  1. Git orqali origin/main bilan solishtirib o'zgargan fayllarni olamiz:
CHANGED_FILES=$(git diff --name-only origin/main...HEAD)
  1. O'zgargan fayllarni Bazel manba target'lariga o'giramiz:
# Masalan: Core/Book.swift -> //Core:Core
  1. rdeps(//..., target) — berilgan target'dan tepaga qarab barcha teskari bog'liqliklarni topadi.
  2. tests(...) — topilgan natijalar ichidan faqat *_test qoidalarini ajratib oladi.

Bir qatorlik sehrli bash skripti (scripts/ci_affected_tests.sh):

#!/usr/bin/env bash
set -euo pipefail

# Asosiy shoxchadan farqni topamiz
CHANGED_FILES=$(git diff --name-only origin/main...HEAD)

if [ -z "$CHANGED_FILES" ]; then
  echo "Hech qanday fayl o'zgarmagan. Test o'tkazib yuboriladi."
  exit 0
fi

echo "O'zgargan fayllar:"
echo "$CHANGED_FILES"

# Fayllarni bazel query formatiga yig'amiz
TARGET_QUERY=""
for f in $CHANGED_FILES; do
  # Faqat loyiha fayllari (BUILD, Swift, h, json)
  if [[ "$f" =~ \.(swift|m|h|BUILD|bazel|json|xcassets)$ ]]; then
    TARGET_QUERY="$TARGET_QUERY $f"
  fi
done

if [ -z "$TARGET_QUERY" ]; then
  echo "Build target'lariga daxli yo'q fayllar o'zgargan. Test talab etilmaydi."
  exit 0
fi

# Ta'sirlangan testlarni qidirish
echo "Ta'sirlangan testlarni aniqlash..."
AFFECTED_TESTS=$(bazel query --keep_going \
  "tests(rdeps(//..., set($TARGET_QUERY)))" 2>/dev/null || true)

if [ -z "$AFFECTED_TESTS" ]; then
  echo "O'zgarishlarga bog'liq hech qanday test topilmadi."
else
  echo "Topilgan ta'sirlangan testlar:"
  echo "$AFFECTED_TESTS"
  # Faqat shu testlarni yugurtiramiz!
  bazel test --config=ci_pr $AFFECTED_TESTS
fi

Bu usul bilan 200 ta testdan iborat loyihada bitta modul o'zgarsa, atigi 3 ta test yuguradi. Qolgan 197 ta test soniyalarni va simulyator resursini tejaydi!


6. To'liq GitHub Actions PR workflow namunasi {#pr-workflow}

Keling, haqiqiy ishlab chiqarish darajasidagi .github/workflows/ios_pr.yml faylini tuzamiz:

name: iOS CI - Pull Request

on:
  pull_request:
    branches: [ main, develop ]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  validate-and-test:
    runs-on: macos-15
    timeout-minutes: 25

    steps:
      - name: Checkout Code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0 # git diff to'liq ishlashi uchun zarur

      - name: Select Xcode 27.1
        run: |
          sudo xcode-select -s /Applications/Xcode_27.1.app/Contents/Developer
          xcodebuild -version

      - name: Cache Bazel Tools
        uses: actions/cache@v4
        with:
          path: ~/.cache/bazelisk
          key: ${{ runner.os }}-bazelisk-${{ hashFiles('.bazelversion') }}

      - name: Run Buildifier (Linter & Format Check)
        run: |
          # BUILD fayllar to'g'ri formatlanganini tekshiramiz
          bazel run @buildifier_prebuilt//:buildifier -- -mode=check -r .

      - name: Selective Test (Faqat ta'sirlanganlar)
        env:
          BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_READ_ONLY_KEY }}
        run: |
          chmod +x scripts/ci_affected_tests.sh
          ./scripts/ci_affected_tests.sh

      - name: Upload Test Results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: test-results
          path: bazel-testlogs/**/test.xml

7. Test natijalari, xatolar va JUnit XML hisobotlari {#test-hisobotlari}

Bazel testlarni hermetik sandbox'da yugurtiradi. Test bajarilganda har bir test target uchun bazel-testlogs/ papkasida avtomatik ravishda standart JUnit XML (test.xml) va to'liq log fayl (test.log) hosil bo'ladi.

Muhim flaglar:

# Faqat xato bergan testlarning konsol chiqishini ko'rsatish
bazel test --test_output=errors //...

# Har bir testning to'liq logini konsolga chiqarish (debug payti)
bazel test --test_output=all //Core:CoreTests

# Test muvaffaqiyatsiz bo'lsa darhol to'xtatmasdan, qolganlarini ham tekshirish
bazel test --keep_going //...

GitHub Actions'da test natijalarini to'g'ridan-to'g'ri PR interfeysida ko'rsatish uchun mashhur dorny/test-reporter action'idan foydalanish mumkin:

- name: Publish Test Report
  uses: dorny/test-reporter@v1
  if: always()
  with:
    name: Bazel XCTest Results
    path: 'bazel-testlogs/**/test.xml'
    reporter: java-junit

8. Flaky testlar va Sharding bilan kurashish {#flaky-sharding}

Mobil testlarda tarmoq kutishi, animatsiyalar yoki simulyator kechikishi tufayli ba'zan bir xil test bir marta o'tib, bir marta yiqiladi. Bunday testlar Flaky test deb ataladi.

1. Avtomatik qayta urinish (--flaky_test_attempts)

Bazel'da flaky testlar tufayli butun CI pipeline qizilga aylanmasligi uchun o'rnatilgan qayta urinish mexanizmi bor:

# Agar test yiqilsa, uni yana 2 marta qayta yugurtirib ko'radi
bazel test --flaky_test_attempts=3 //...

Agar test 2-urinishda o'tsa, Bazel uni FLAKY statusi bilan belgilaydi (build yiqilmaydi, lekin hisobotda ogohlantirish beradi).

2. Test Sharding (Katta testlarni bo'lib yuborish)

Agar bitta test target ichida 150 ta UI test bo'lsa va u 15 daqiqa vaqt olsa, uni target darajasida bir nechta parallel bo'laklarga bo'lish mumkin:

# CoreTests/BUILD.bazel
ios_unit_test(
    name = "CoreLargeTests",
    srcs = glob(["*.swift"]),
    shard_count = 4, # 4 ta alohida parallel jarayonga bo'linadi!
    deps = ["//Core:Core"],
)

Bazel testlarni avtomatik tarzda 4 ta runner o'rtasida teng taqsimlaydi va umumiy test vaqti 4 barobarga qisqaradi!


9. Release Workflow: TestFlight va App Store .ipa {#release-workflow}

CI nafaqat test qiladi, balki asosiy shoxchaga merge bo'lganda (main) avtomatik ravishda imzolangan .ipa yig'ishi va TestFlight'ga jo'natishi kerak.

9-darsda aytganimizdek, Bazel'da .ipa yig'ish Xcode'ning sekin "Archive" bosqichidan ancha tezroq:

# Release profilida to'liq optimallashtirilgan .ipa qurish
bazel build --config=release //App:App

Chiqish fayli: bazel-bin/App/App.ipa

GitHub Actions Release Workflow namunasi:

name: iOS Release - TestFlight

on:
  push:
    tags:
      - 'v*.*.*'

jobs:
  release:
    runs-on: macos-15
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Import Apple Distribution Certificate
        env:
          BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
          P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
        run: |
          # Kalit zanjirini (Keychain) sozlash
          echo $BUILD_CERTIFICATE_BASE64 | base64 --decode > /tmp/cert.p12
          security create-keychain -p "" build.keychain
          security default-keychain -s build.keychain
          security unlock-keychain -u build.keychain
          security import /tmp/cert.p12 -k build.keychain -P "$P12_PASSWORD" -T /usr/bin/codesign
          security set-key-partition-list -S apple-tool:,apple: -s -k "" build.keychain

      - name: Build Signed IPA with Bazel
        run: |
          bazel build --config=release //App:App

      - name: Upload to TestFlight
        env:
          APP_STORE_CONNECT_API_KEY: ${{ secrets.ASC_KEY }}
        run: |
          xcrun altool --upload-app -f bazel-bin/App/App.ipa \
            -t ios --apiKey ${{ secrets.ASC_KEY_ID }} --apiIssuer ${{ secrets.ASC_ISSUER_ID }}

10. Real kompaniyalar tajribasi va narx tejash {#real-tajriba}

Dunyodagi eng yirik kompaniyalar Bazel'ga o'tishining asosiy drayveri aynan CI tezligi va arzonligi bo'lgan:

  • Spotify: iOS jamoasida CI navbatida turish vaqti 80 daqiqadan 20 daqiqaga tushirilgan.
  • Lyft: Har bir PR'da 1,500+ test yuguradigan monorepoda selective testing va remote cache tufayli CI vaqti 40 daqiqadan 7 daqiqaga qisqargan.
  • Reddit: iOS CI resurs sarfi va runner xarajatlari 65% ga kamaygan.

Ushbu raqamlar isbotlaydiki, dastlabki sozlash murakkab ko'rinsa ham, jamoa o'sgan sari tejalgan soatlar va server pullari Bazel sarmoyasini bir necha oyda to'liq qoplaydi.


Amaliy mashqlar {#mashqlar}

  1. Selective query'ni qo'lda sinab ko'ring. O'tgan darsdagi "Kutubxona" loyihangizda Core/Book.swift fayliga bitta bo'sh qator qo'shing. Terminalda:
bazel query "tests(rdeps(//..., set(Core/Sources/Book.swift)))"

buyrug'ini bajaring va natijada faqat //Core:CoreTests va unga bog'liq bo'lgan testlar chiqqanini tekshiring.

  1. Read-only vs Upload rejimini sinash. .bazelrc faylingizda ci_pr va ci_main profillarini yarating. --remote_upload_local_results=false bo'lganda yangi o'zgarishlar keshga yozilmayotganini tasdiqlang.
  2. Flaky simulyatsiyasi. Bitta test ichida Int.random(in: 1...3) == 1 shartini qo'yib, ataylab beqaror test yarating. Uni --flaky_test_attempts=3 bilan tekshirib, Bazel hisobotida FLAKY statusini o'z ko'zingiz bilan ko'ring.
  3. ⭐ GitHub Actions faylini yozing. O'z shaxsiy GitHub repongizda kichik Bazel loyihasi uchun yuqoridagi .github/workflows/ios_pr.yml faylini ishga tushiring va birinchi muvaffaqiyatli CI yashil belgisini (Green checkmark) oling.

O'zingizni tekshiring {#checklist}

  • [ ] GitHub Actions'da macos-14 va macos-15 runner'larning farqini va nima uchun Intel runner ishlatmaslik kerakligini bilaman.
  • [ ] PR ochilganda nima uchun Remote Cache'ga yozish (remote_upload_local_results) taqiqlanishi kerakligini tushunaman.
  • [ ] rdeps(//..., set(...)) orqali teskari bog'liqliklarni filtrlash formulasini yoza olaman.
  • [ ] Selective testing nima uchun CI vaqtini 90% gacha tejashini raqamlar bilan tushuntira olaman.
  • [ ] Flaky testlarga qarshi --flaky_test_attempts va test sharding (shard_count) mexanizmini bilaman.
  • [ ] .ipa faylini release profilida qurish va TestFlight'ga jo'natish qadamlarini bilaman.

Xulosa {#xulosa}

  • Mobil CI'dagi asosiy isrof — o'zgarmagan kodni qayta qurish va barcha testlarni keraksiz yugurtirishdir.
  • Bazel bu muammoni ikkita poydevor bilan yechadi: Remote Cache (hech qachon bir ishni ikki marta qilmaslik) va Bazel Query (faqat ta'sirlangan narsani aniqlash).
  • CI arxitekturasida qat'iy intizom bo'lishi shart: PR'lar faqat o'qiydi, keshni faqat main shoxcha yangilaydi.
  • Sharding va flaky-handling orqali katta loyihalardagi beqarorliklar bartaraf etiladi.
  • Keyingi dars: Endi nazariyadan haqiqiy amaliy loyihalarga o'tamiz! Bazel 12: Loyiha #1: "Valyuta" CLI — Swift kutubxona + macOS buyruq qatori + testlar va birinchi o'lchovlar — birinchi to'liq professional loyihamizni noldan yig'amiz.

Manbalar {#manbalar}

  1. Bazel Documentation — Continuous Integration — CI tizimlarida Bazel bilan ishlash bo'yicha rasmiy qo'llanma.
  2. Bazel Query Guide — rdeps, deps, tests operatorlari va tahlil qilish sintaksisi.
  3. BuildBuddy — Optimizing CI for iOS with Bazel — Remote cache, Build Event Protocol va test hisobotlari.
  4. Spotify Engineering Blog — Slashing iOS Build Times — Spotify'ning Bazel CI optimizatsiyasi bo'yicha real case study.
  5. rules_apple — iOS rules documentation — ios_application, codesigning va release boshqaruvi.

Markdown manba

iOS Lessons