شارك المقالة
في عالم تطوير التطبيقات الحديثة، أصبحت الأتمتة جزءًا مهمًا جدًا من أي فريق تطوير، خصوصًا عندما يكون الفريق يعمل على تطبيقات يتم تحديثها بشكل مستمر. بدل ما كل مرة تعمل فيها push على GitHub تقوم ببناء التطبيق يدويًا وترفع ملف APK للفريق، تقدر تخلي العملية كلها تتم تلقائيًا باستخدام GitHub Actions. في المقال ده هنتعلم خطوة بخطوة إزاي نعمل CI/CD لتطبيق Flutter بحيث يتم بناء ملف APK تلقائيًا عند كل Push على فرع معين، وبعدها يتم رفع الملف مباشرة إلى قناة Slack باستخدام Slack API.

المشكلة التي نريد حلها

لو الفريق بتاعك بيعتمد على Slack كأداة أساسية للتواصل ومتابعة سير العمل، فغالبًا هتحتاج تبعت نسخة APK جديدة للفريق بعد كل تحديث مهم. المشكلة هنا إن العملية اليدوية بتكون متكررة ومملة:
  • تعمل Push على GitHub
  • تبني التطبيق يدويًا
  • تدور على ملف APK
  • ترفعه على Slack
  • تبلغ الفريق إن فيه نسخة جديدة
مع الوقت، الخطوات دي بتستهلك وقت، وبتزود احتمالية الأخطاء البشرية، خصوصًا لو التحديثات كتير أو الفريق كبير.

الحل باستخدام GitHub Actions

الحل هو إنشاء Workflow داخل GitHub Actions يقوم بكل الخطوات تلقائيًا. يعني بمجرد ما تعمل push على فرع معين مثل dev، GitHub Actions هيبدأ يشغل خطوات البناء، وبعد ما يخلص هيتم رفع ملف APK مباشرة إلى Slack.

لماذا نستخدم CI/CD مع Flutter؟

استخدام CI/CD في تطبيقات Flutter بيوفر مميزات مهمة جدًا لأي فريق تطوير.
  • تسريع دورة التطوير
  • تقليل الأخطاء البشرية
  • تنفيذ نفس خطوات البناء في بيئة نظيفة كل مرة
  • إرسال النسخ الجديدة للفريق تلقائيًا
  • سهولة التوسع لاحقًا ورفع التطبيق إلى Google Play أو TestFlight

المتطلبات الأساسية قبل البدء

قبل ما نبدأ، لازم يكون عندك شوية حاجات جاهزة:
  • مشروع Flutter يعمل بشكل سليم
  • مستودع GitHub يحتوي على الكود
  • حساب Slack Workspace
  • Slack Bot Token
  • Channel ID للقناة التي سيتم رفع APK عليها
  • إضافة Secrets داخل GitHub Repository

التأكد من بناء تطبيق Flutter محليًا

قبل ما تستخدم GitHub Actions، لازم تتأكد إن التطبيق بيتبني عندك محليًا بدون مشاكل.
flutter build apk --release
لو الأمر اشتغل بنجاح، هتلاقي ملف APK في المسار التالي:
build/app/outputs/apk/release/app-release.apk

إعداد GitHub Secrets

عشان نحافظ على البيانات الحساسة زي Slack Token، مينفعش نكتبها مباشرة داخل ملف Workflow. بدل كده، هنضيفها داخل GitHub Secrets. افتح GitHub Repository ثم اذهب إلى:
Settings → Secrets and variables → Actions → New repository secret
بعد كده أضف القيم التالية:
  • SLACK_BOT_TOKEN: توكن البوت الخاص بـ Slack
  • SLACK_CHANNEL_ID: رقم القناة التي سيتم رفع الملف إليها

الصلاحيات المطلوبة من Slack Bot

البوت الخاص بك يحتاج إلى بعض الصلاحيات حتى يتمكن من الدخول إلى القناة ورفع الملفات.
  • files:write لرفع الملفات
  • channels:read لقراءة بيانات القنوات
  • channels:join للانضمام إلى القنوات العامة
لو القناة Private، غالبًا ستحتاج إلى دعوة البوت يدويًا داخل القناة.

إنشاء ملف GitHub Actions Workflow

داخل مشروع Flutter، أنشئ المسار التالي:
.github/workflows/build.yaml
بعدها أضف الكود التالي داخل الملف.

كود GitHub Actions كامل لبناء APK ورفعه إلى Slack

name: Flutter

on:
  push:
    branches: [ "dev" ]

  pull_request:
    branches: [ "dev" ]

env:
  FLUTTER_VERSION: '3.32.0'
  JAVA_VERSION: '17.x'
  BUILD_TYPE: 'release'

jobs:
  build_android:
    name: Build & Deploy Android
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Java
        uses: actions/setup-java@v3
        with:
          java-version: ${{ env.JAVA_VERSION }}
          distribution: 'adopt'

      - name: Setup Flutter
        uses: subosito/flutter-action@v2.21.0
        with:
          channel: 'stable'
          flutter-version: ${{ env.FLUTTER_VERSION }}

      - name: Check Flutter Version
        run: flutter --version

      - name: Install dependencies
        run: flutter pub get

      - name: Build APK
        run: flutter build apk --release

      - name: Join Slack channel public
        env:
          SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
          SLACK_CHANNEL_ID: ${{ secrets.SLACK_CHANNEL_ID }}
        run: |
          set -e

          RESP=$(curl -sS -X POST \
            -H "Authorization: Bearer $SLACK_BOT_TOKEN" \
            -H "Content-type: application/json; charset=utf-8" \
            --data "{\"channel\":\"$SLACK_CHANNEL_ID\"}" \
            https://slack.com/api/conversations.join)

          echo "$RESP"

          command -v jq >/dev/null 2>&1 && echo "$RESP" | jq -e '(.ok == true) or (.error == "already_in_channel")' > /dev/null

      - name: Upload APK to Slack v2 API
        env:
          SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
          SLACK_CHANNEL_ID: ${{ secrets.SLACK_CHANNEL_ID }}
          APK_PATH: build/app/outputs/apk/release/app-release.apk
        run: |
          set -e

          [ -f "$APK_PATH" ] || { echo "APK not found at $APK_PATH"; ls -R build/app/outputs || true; exit 1; }

          BASENAME="$(basename "$APK_PATH")"
          SIZE=$(stat -c%s "$APK_PATH" 2>/dev/null || wc -c < "$APK_PATH")
          COMMENT=$(printf "🚀 New Android build\n• Commit: %s" "$GITHUB_SHA")

          GET_RESP=$(curl -sS -X POST https://slack.com/api/files.getUploadURLExternal \
            -H "Authorization: Bearer $SLACK_BOT_TOKEN" \
            -H "Content-type: application/x-www-form-urlencoded" \
            --data "filename=${BASENAME}&length=${SIZE}")

          echo "$GET_RESP"

          UPLOAD_URL=$(echo "$GET_RESP" | jq -r '.upload_url')
          FILE_ID=$(echo "$GET_RESP" | jq -r '.file_id')

          [ -n "$UPLOAD_URL" ] && [ "$UPLOAD_URL" != "null" ] || { echo "Failed to get upload_url"; exit 1; }

          curl -sS -X POST -F "file=@${APK_PATH}" "$UPLOAD_URL" >/dev/null

          BODY=$(jq -n --arg fid "$FILE_ID" --arg title "$BASENAME" --arg ch "$SLACK_CHANNEL_ID" --arg c "$COMMENT" \
            '{files:[{id:$fid,title:$title}], channel_id:$ch, initial_comment:$c}')

          curl -sS -X POST https://slack.com/api/files.completeUploadExternal \
            -H "Authorization: Bearer $SLACK_BOT_TOKEN" \
            -H "Content-type: application/json" \
            -d "$BODY" | tee /tmp/slack_complete.json

          jq -e '.ok == true' /tmp/slack_complete.json > /dev/null

شرح ملف Workflow خطوة بخطوة

خلينا نشرح أهم الأجزاء الموجودة داخل ملف build.yaml.

تحديد اسم Workflow

name: Flutter
ده اسم الـ Workflow اللي هيظهر داخل تبويب Actions في GitHub.

تشغيل Workflow عند Push أو Pull Request

on:
  push:
    branches: [ "dev" ]

  pull_request:
    branches: [ "dev" ]
هنا بنقول لـ GitHub Actions إن الـ Workflow يشتغل لما يحصل Push أو Pull Request على فرع dev. لو عايز تشغله على فرع main، غير dev إلى main.

تحديد نسخة Flutter وJava

env:
  FLUTTER_VERSION: '3.32.0'
  JAVA_VERSION: '17.x'
  BUILD_TYPE: 'release'
هنا بنحدد المتغيرات العامة اللي هنستخدمها داخل Workflow.
  • FLUTTER_VERSION: نسخة Flutter المطلوبة
  • JAVA_VERSION: نسخة Java المستخدمة في البناء
  • BUILD_TYPE: نوع البناء، وفي المثال Release

اختيار نظام التشغيل

runs-on: ubuntu-latest
هنا بنحدد إن الـ Job هيشتغل على آخر نسخة متاحة من Ubuntu داخل GitHub Actions.

استنساخ الكود من المستودع

- uses: actions/checkout@v4
الخطوة دي بتجيب كود المشروع من GitHub عشان الـ Runner يقدر يشتغل عليه.

إعداد Java

- name: Setup Java
  uses: actions/setup-java@v3
  with:
    java-version: ${{ env.JAVA_VERSION }}
    distribution: 'adopt'
Flutter Android Build يحتاج Java وGradle، لذلك بنثبت نسخة Java المناسبة قبل عملية البناء.

إعداد Flutter

- name: Setup Flutter
  uses: subosito/flutter-action@v2.21.0
  with:
    channel: 'stable'
    flutter-version: ${{ env.FLUTTER_VERSION }}
الخطوة دي بتثبت Flutter داخل بيئة GitHub Actions باستخدام النسخة المحددة في المتغيرات.

تثبيت Dependencies

- name: Install dependencies
  run: flutter pub get
الأمر flutter pub get بيقوم بتحميل الحزم الموجودة داخل ملف pubspec.yaml.

بناء ملف APK

- name: Build APK
  run: flutter build apk --release
الخطوة دي هي المسؤولة عن بناء التطبيق وإنتاج ملف APK بصيغة Release. المسار المتوقع للملف بعد البناء:
build/app/outputs/apk/release/app-release.apk

الانضمام إلى قناة Slack

قبل رفع الملف، لازم نتأكد إن البوت موجود داخل قناة Slack.
- name: Join Slack channel public
  env:
    SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
    SLACK_CHANNEL_ID: ${{ secrets.SLACK_CHANNEL_ID }}
  run: |
    set -e

    RESP=$(curl -sS -X POST \
      -H "Authorization: Bearer $SLACK_BOT_TOKEN" \
      -H "Content-type: application/json; charset=utf-8" \
      --data "{\"channel\":\"$SLACK_CHANNEL_ID\"}" \
      https://slack.com/api/conversations.join)

    echo "$RESP"

    command -v jq >/dev/null 2>&1 && echo "$RESP" | jq -e '(.ok == true) or (.error == "already_in_channel")' > /dev/null
هنا بنستخدم Slack API وتحديدًا endpoint:
https://slack.com/api/conversations.join
لو البوت موجود بالفعل في القناة، مش هيحصل مشكلة لأن الكود بيسمح بالحالتين:
  • ok == true
  • already_in_channel

رفع ملف APK إلى Slack

بعد نجاح عملية البناء، بنبدأ نرفع ملف APK إلى Slack باستخدام External Upload API. العملية بتتم على 3 مراحل:
  • الحصول على Upload URL
  • رفع الملف إلى الرابط المؤقت
  • إكمال عملية الرفع ونشر الملف داخل القناة

التأكد من وجود ملف APK

[ -f "$APK_PATH" ] || { echo "APK not found at $APK_PATH"; ls -R build/app/outputs || true; exit 1; }
قبل أي شيء، الكود بيتأكد إن ملف APK موجود فعلًا في المسار المحدد.

الحصول على Upload URL

GET_RESP=$(curl -sS -X POST https://slack.com/api/files.getUploadURLExternal \
  -H "Authorization: Bearer $SLACK_BOT_TOKEN" \
  -H "Content-type: application/x-www-form-urlencoded" \
  --data "filename=${BASENAME}&length=${SIZE}")
هنا بنطلب من Slack رابط مؤقت نرفع عليه الملف. الاستجابة بترجع لنا قيمتين مهمتين:
  • upload_url: الرابط الذي سيتم رفع الملف عليه
  • file_id: رقم الملف داخل Slack

رفع ملف APK

curl -sS -X POST -F "file=@${APK_PATH}" "$UPLOAD_URL" >/dev/null
في الخطوة دي بنرفع ملف APK فعليًا إلى الرابط الذي حصلنا عليه من Slack.

إكمال الرفع وإرسال الملف للقناة

curl -sS -X POST https://slack.com/api/files.completeUploadExternal \
  -H "Authorization: Bearer $SLACK_BOT_TOKEN" \
  -H "Content-type: application/json" \
  -d "$BODY" | tee /tmp/slack_complete.json
بعد رفع الملف، لازم نستدعي files.completeUploadExternal حتى يظهر الملف داخل قناة Slack مع التعليق المطلوب.

إضافة تعليق مع رقم Commit

في الكود المستخدم، بيتم إرسال تعليق مع الملف يحتوي على رقم الـ Commit الحالي.
COMMENT=$(printf "🚀 New Android build\n• Commit: %s" "$GITHUB_SHA")
وده مفيد جدًا عشان الفريق يعرف النسخة المرفوعة مرتبطة بأي Commit في GitHub.

مشكلة jq في GitHub Actions

الكود يعتمد على أداة jq لمعالجة JSON داخل الـ Shell. غالبًا jq تكون موجودة بالفعل داخل ubuntu-latest. لكن لو ظهرت لك مشكلة إن jq غير مثبتة، أضف الخطوة التالية قبل رفع الملف:
- name: Install jq
  run: sudo apt-get update && sudo apt-get install -y jq

ملاحظات مهمة عند استخدام Slack API

  • تأكد إن البوت لديه صلاحية رفع الملفات
  • تأكد إن SLACK_CHANNEL_ID صحيح وليس اسم القناة
  • لو القناة Private، قم بدعوة البوت يدويًا
  • لا تكتب Slack Token مباشرة داخل ملف YAML
  • استخدم GitHub Secrets دائمًا لحماية البيانات الحساسة

مشاكل شائعة وحلولها

APK not found

لو ظهر لك الخطأ التالي:
APK not found at build/app/outputs/apk/release/app-release.apk
تأكد إن أمر البناء نجح، وتأكد من المسار الصحيح للملف الناتج.

not_in_channel

هذا الخطأ معناه إن البوت ليس موجودًا داخل قناة Slack. الحل:
  • لو القناة Public، استخدم conversations.join
  • لو القناة Private، قم بدعوة البوت يدويًا

invalid_auth

هذا الخطأ معناه إن Slack Token غير صحيح أو لم يتم إضافته بشكل صحيح داخل GitHub Secrets.

missing_scope

هذا الخطأ معناه إن البوت لا يمتلك الصلاحيات المطلوبة. تأكد من إضافة الصلاحيات التالية:
files:write
channels:read
channels:join

هل يمكن بناء AAB بدل APK؟

نعم، لو هدفك رفع التطبيق إلى Google Play، الأفضل استخدام Android App Bundle بدل APK.
flutter build appbundle --release
وفي هذه الحالة سيكون الملف الناتج غالبًا داخل:
build/app/outputs/bundle/release/app-release.aab
بعدها تحتاج تعديل APK_PATH إلى مسار ملف AAB.

أفضل الممارسات الأمنية

  • لا تضع أي Token داخل الكود مباشرة
  • استخدم GitHub Secrets للبيانات الحساسة
  • استخدم أقل صلاحيات ممكنة للبوت
  • راجع صلاحيات Slack App بشكل دوري
  • لا تشارك ملف Keystore داخل المستودع

مميزات هذا الحل

  • بناء APK تلقائيًا عند كل Push
  • توفير وقت الفريق
  • تقليل الأخطاء اليدوية
  • ربط عملية التطوير مباشرة مع Slack
  • سهولة معرفة رقم Commit الخاص بكل نسخة
  • إمكانية التوسع لاحقًا لإضافة اختبارات أو نشر على المتاجر

الخلاصة

استخدام GitHub Actions مع Flutter و Slack API يعتبر حل ممتاز لأي فريق يريد أتمتة عملية بناء التطبيق ومشاركة النسخ الجديدة بسرعة. بدل ما تقوم ببناء ملف APK يدويًا ورفعه على Slack كل مرة، تقدر تخلي GitHub Actions يقوم بالعملية بالكامل بمجرد حدوث Push على فرع معين. الطريقة دي هتوفر وقت كبير، وتقلل الأخطاء، وتخلي الفريق دائمًا على اطلاع بآخر نسخة من التطبيق. ومع الوقت تقدر تطور نفس Workflow بإضافة اختبارات تلقائية، توقيع التطبيق، رفع AAB إلى Google Play، أو إرسال إشعارات أكثر تفصيلًا للفريق.
شاهد أيضًا
مقالات ذات صلة

🚫 مانع الإعلانات مفعل

يجب إيقاف مانع الإعلانات لاستكمال تصفح الموقع