CI/CD والنشر على متاجر التطبيقات

أتمتة عمليات البناء باستخدام GitHub Actions

16 دقيقة الدرس 6 من 12

أتمتة عمليات البناء باستخدام GitHub Actions

بناء تطبيق Flutter يدوياً وتوقيعه ورفعه قبل كل إصدار عملية مرهقة وعرضة للأخطاء. تحل GitHub Actions هذه المشكلة بتشغيل خط أنابيب البناء بالكامل تلقائياً في كل مرة تدفع فيها إلى فرع أو تنشئ وسماً (tag). في هذا الدرس ستكتب ملف YAML لسير عمل جاهز للإنتاج يثبّت Flutter ويشغّل مجموعة الاختبارات ويبني AAB أندرويد موقّعاً وIPA لنظام iOS، ويرفع كلاهما كاداءات قابلة للتنزيل — كل ذلك دون تخزين بيانات اعتماد حساسة في مستودعك.

كيف تعمل GitHub Actions

سير العمل هو ملف YAML مخزّن تحت .github/workflows/. تقرأه GitHub وتشغّل كل مهمة (job) على مشغّل مستضاف في السحابة (Ubuntu أو macOS أو Windows). تحتوي المهام على خطوات (steps): أوامر shell أو actions جاهزة مسحوبة من Marketplace. لعمليات CI/CD في Flutter تحتاج عادةً إلى مهمتين منفصلتين — إحداهما على ubuntu-latest للأندرويد وأخرى على macos-latest لنظام iOS — لأن سلسلة أدوات iOS تعمل على macOS فقط.

ملاحظة: كل مهمة تبدأ من مشغّل نظيف. لا شيء من تشغيل سابق يبقى إلا إذا رفعت اداءً صراحةً أو استخدمت إجراء تخزين مؤقت (cache action). احرص دائماً على استعادة ذاكرة pub المؤقتة وذاكرة Gradle لتجنب إعادة تنزيل مئات الميغابايتات في كل تشغيل.

تخزين الأسرار بأمان

مخزن مفاتيح الأندرويد وشهادات توقيع iOS يجب أن لا تُدرج أبداً في المستودع. توفر GitHub مخزن Secrets مشفراً (Settings → Secrets and variables → Actions). الملفات الثنائية كالمخازن تُشفَّر بترميز base-64 قبل التخزين وتُفك شفرتها داخل سير العمل:

  • KEYSTORE_BASE64 — ملف .jks / .keystore مشفَّر بـ base-64
  • KEY_ALIAS — الاسم المستعار المستخدم عند إنشاء مخزن المفاتيح
  • KEY_PASSWORD — كلمة مرور المفتاح
  • STORE_PASSWORD — كلمة مرور مخزن المفاتيح
  • IOS_P12_BASE64 — شهادة التوزيع (.p12) مشفَّرة بـ base-64
  • IOS_P12_PASSWORD — كلمة مرور ملف .p12
  • PROVISIONING_PROFILE_BASE64 — ملف .mobileprovision مشفَّر بـ base-64
تحذير: لا تطبع الأسرار أبداً في السجل. تحجب GitHub قيم الأسرار المعروفة، لكن إذا فككت ترميز base-64 لسر وطبعته قد تكشف أجزاءً منه. اكتب الملفات المفككة إلى القرص فوراً واحذفها في نهاية المهمة.

سير العمل الكامل: AAB أندرويد موقَّع

يُشغَّل سير العمل التالي عند كل دفع إلى فرع main وعند وسوم الإصدار (v*). تثبّت مهمة الأندرويد Flutter وتخزّن حزم pub مؤقتاً وتشغّل الاختبارات وتفك ترميز مخزن المفاتيح وتبني AAB نسخة إصدار وترفعه كادائة.

.github/workflows/flutter_ci.yml — مهمة الأندرويد

name: Flutter CI/CD

on:
  push:
    branches: [main]
    tags: ['v*']
  pull_request:
    branches: [main]

jobs:
  # ── Android ─────────────────────────────────────────────
  build_android:
    name: Build Android AAB
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Java 17
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: '17'

      - name: Cache pub packages
        uses: actions/cache@v4
        with:
          path: ~/.pub-cache
          key: pub-${{ runner.os }}-${{ hashFiles('**/pubspec.lock') }}
          restore-keys: pub-${{ runner.os }}-

      - name: Install Flutter
        uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.22.0'
          channel: stable
          cache: true

      - name: Install dependencies
        run: flutter pub get

      - name: Run tests
        run: flutter test --coverage

      - name: Decode keystore
        run: |
          echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode \
            > android/app/keystore.jks

      - name: Build release AAB
        run: flutter build appbundle --release
        env:
          KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
          KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
          STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }}

      - name: Upload AAB artifact
        uses: actions/upload-artifact@v4
        with:
          name: android-release-aab
          path: build/app/outputs/bundle/release/app-release.aab
          retention-days: 14

سير العمل الكامل: IPA لنظام iOS موقَّع

تعمل مهمة iOS على macos-latest. تستورد شهادة التوزيع إلى keychain مؤقت وتثبّت ملف provisioning profile وتبني التطبيق باستخدام flutter build ipa وترفع ملف IPA الناتج.

.github/workflows/flutter_ci.yml — مهمة iOS (أضفها إلى نفس الملف)

  # ── iOS ─────────────────────────────────────────────────
  build_ios:
    name: Build iOS IPA
    runs-on: macos-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Install Flutter
        uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.22.0'
          channel: stable
          cache: true

      - name: Install dependencies
        run: flutter pub get

      - name: Import signing certificate
        env:
          P12_BASE64: ${{ secrets.IOS_P12_BASE64 }}
          P12_PASSWORD: ${{ secrets.IOS_P12_PASSWORD }}
        run: |
          KEYCHAIN_PATH=$RUNNER_TEMP/build.keychain
          security create-keychain -p "" "$KEYCHAIN_PATH"
          security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
          security unlock-keychain -p "" "$KEYCHAIN_PATH"
          echo "$P12_BASE64" | base64 --decode > $RUNNER_TEMP/cert.p12
          security import "$RUNNER_TEMP/cert.p12" \
            -k "$KEYCHAIN_PATH" -P "$P12_PASSWORD" \
            -A -t cert -f pkcs12
          security list-keychain -d user -s "$KEYCHAIN_PATH"

      - name: Install provisioning profile
        env:
          PP_BASE64: ${{ secrets.PROVISIONING_PROFILE_BASE64 }}
        run: |
          PP_PATH=$RUNNER_TEMP/profile.mobileprovision
          echo "$PP_BASE64" | base64 --decode > "$PP_PATH"
          mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
          cp "$PP_PATH" \
            ~/Library/MobileDevice/Provisioning\ Profiles/

      - name: Build iOS IPA
        run: flutter build ipa --release --no-codesign

      - name: Upload IPA artifact
        uses: actions/upload-artifact@v4
        with:
          name: ios-release-ipa
          path: build/ios/ipa/*.ipa
          retention-days: 14

ضبط android/app/build.gradle لتوقيع CI

يُدخل سير العمل بيانات اعتماد التوقيع عبر متغيرات البيئة. يجب أن يقرأها ملف android/app/build.gradle حتى يلتقطها Gradle في وقت البناء:

android/app/build.gradle — كتلة signingConfigs

android {
    signingConfigs {
        release {
            keyAlias     System.getenv("KEY_ALIAS")     ?: "mykey"
            keyPassword  System.getenv("KEY_PASSWORD")  ?: ""
            storeFile    file("keystore.jks")
            storePassword System.getenv("STORE_PASSWORD") ?: ""
        }
    }
    buildTypes {
        release {
            signingConfig signingConfigs.release
            minifyEnabled true
            proguardFiles getDefaultProguardFile(
                'proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

استراتيجيات التخزين المؤقت لتسريع التشغيلات

يمكن أن تستغرق عمليات البناء الباردة 8–15 دقيقة. يختصر التخزين المؤقت الفعّال هذا إلى 2–4 دقائق في التشغيلات اللاحقة:

  • ذاكرة pub المؤقتة — مفتاحها على pubspec.lock؛ توفر ~30 ثانية لكل تشغيل
  • ذاكرة Flutter SDK المؤقتة — يحتوي إجراء subosito/flutter-action على خيار cache: true مدمج
  • ذاكرة Gradle المؤقتة — أضف خطوة actions/cache منفصلة تستهدف ~/.gradle/caches و~/.gradle/wrapper
  • ذاكرة CocoaPods المؤقتة — على macOS، خزّن ios/Pods مؤقتاً مع مفتاح على ios/Podfile.lock
نصيحة: ثبّت إصدار Flutter صراحةً (مثل flutter-version: '3.22.0') بدلاً من الاعتماد على channel: stable وحده. هذا يجعل عمليات البناء قابلة للتكرار: إصدار Flutter الجديد لن يكسر خط أنابيبك صامتاً حتى ترفع الإصدار بشكل متعمد.

الخلاصة

يمنحك سير عمل GitHub Actions المنظَّم جيداً عمليات بناء Flutter آلية وقابلة للتكرار عند كل دفع. النقاط الجوهرية هي: خزّن جميع بيانات الاعتماد كأسرار في المستودع وفك ترميزها في وقت التشغيل؛ شغّل الاختبارات قبل البناء حتى تُكتشف الفشوط مبكراً؛ استخدم التخزين المؤقت لإبقاء أوقات التشغيل أقل من خمس دقائق؛ وارفع الادائيات حتى يتمكن فريق ضمان الجودة وأصحاب المصلحة من تنزيل الثنائيات مباشرةً من تبويب Actions دون الحاجة إلى بيئة محلية.

النقطة الرئيسية: ملف سير العمل الخاص بك هو كود — ضعه في إدارة الإصدارات وراجعه في طلبات السحب (PRs) وتعامل مع أسرار التوقيع كبنية تحتية من الدرجة الأولى. ملف .github/workflows/flutter_ci.yml واحد يمكنه استبدال ساعات من العمل اليدوي للإصدار بخط أنابيب آلي وقابل للتدقيق بالكامل.