GitHub Actions بعمق

سير العمل والوظائف والخطوات

18 دقيقة الدرس 1 من 30

سير العمل والوظائف والخطوات

GitHub Actions هي منصة CI/CD المدمجة مباشرة في GitHub. كل شركة تقنية كبرى — Google وMicrosoft وStripe وShopify — تستخدمها أو ما يعادلها. لاستخدامها بفعالية، يجب أن تفهم نموذج التنفيذ المكوّن من ثلاث طبقات: سير العمل (Workflows) تحتوي وظائف (Jobs)، والوظائف تحتوي خطوات (Steps). الخلط بين هذه الطبقات ينتج pipelines هشة وبطيئة وصعبة التصحيح في الإنتاج.

تشريح ملف سير العمل

سير العمل هو ملف YAML مخزن في مستودعك تحت المسار .github/workflows/. يفحص GitHub هذا المجلد تلقائياً — أي ملف .yml هناك يصبح workflow مسجلاً. المفاتيح الرئيسية التي يجب أن يحتوي عليها كل workflow هي name وon وjobs.

إليك workflow على مستوى الإنتاج يُظهر كل عنصر هيكلي رئيسي. اقرأه بعناية قبل أن نشرح كل جزء.

# .github/workflows/ci.yml # مثال كامل: بناء واختبار وتدقيق خدمة Python على كل PR إلى main. name: CI Pipeline on: push: branches: ["main", "release/**"] paths-ignore: - "docs/**" - "*.md" pull_request: branches: ["main"] types: [opened, synchronize, reopened] workflow_dispatch: inputs: run_slow_tests: description: "Include slow integration tests?" required: false default: "false" type: boolean concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: lint: name: Lint & Static Analysis runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.12" cache: "pip" - run: pip install ruff mypy - run: ruff check . - run: mypy src/ test: name: Unit Tests (Python ${{ matrix.python-version }}) runs-on: ubuntu-24.04 needs: lint strategy: fail-fast: false matrix: python-version: ["3.11", "3.12"] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: "pip" - run: pip install -e ".[dev]" - run: pytest -x --tb=short -q env: DATABASE_URL: sqlite:///test.db

الأحداث والمشغّلات: مفتاح on

يحدد مفتاح on ما يُسبّب تشغيل سير العمل. تدعم GitHub Actions أكثر من 35 نوعاً من الأحداث. اختيار أحداث خاطئة هو أحد أكثر أخطاء تصميم الـ pipeline شيوعاً — الـ workflows التي تعمل على نطاق واسع جداً تهدر الدقائق وتستنزف الحصة المجانية؛ تلك التي تعمل على نطاق ضيق جداً تُغفل الانتكاسات.

أهم الأحداث في العمل الهندسي اليومي هي:

  • push — يُطلَق عند دفع commits إلى فرع. استخدم فلاتر branches لتجنب الإطلاق على كل فرع في المستودع. أضف paths-ignore لتخطي التغييرات التوثيقية فقط (CI لا يحتاج للتشغيل لأن أحدهم أصلح خطأ إملائياً في README).
  • pull_request — يُطلَق عند فتح PR أو تحديثه أو مزامنته. هذا الحدث الأهم في CI: يُبوّب الدمج. دائماً اقرنه بفلتر branches لتُطلق فقط PRs التي تستهدف الفروع المهمة تشغيلات مكلفة.
  • workflow_dispatch — يُطلَق حين ينقر إنسان "Run workflow" في واجهة GitHub. استخدمه للعمليات عند الطلب: قطع الإصدارات، عمليات الترحيل المفردة، تشغيل مجموعات الاختبارات البطيئة. كتلة inputs تعرّف نموذجاً يُقدمه GitHub في واجهة المستخدم.
  • schedule — يُطلَق وفق جدول cron. استخدمه لعمليات الفحص الأمني الليلية، تدقيق التبعيات، أو أي فحص يجب تشغيله بشكل مستقل عن تغييرات الكود. التوقيت دائماً UTC.
مصيدة إنتاجية — عواصف التزامن: على مستودع مزدحم، push واحد إلى فرع PR يمكن أن يُطلق عدة تشغيلات workflow متزامنة (حدث push وحدث pull_request معاً). بدون كتلة concurrency، ستكون هناك خمس تشغيلات في الطابور لنفس PR، مما يهدر الـ runners ويُربك فحوصات الحالة. دائماً أضف مجموعة concurrency مرتبطة بـ github.workflow وgithub.ref، مع cancel-in-progress: true. هذا يضمن أن التشغيل الأخير فقط لفرع معين هو النشط، وتُلغى الأقدم تلقائياً.
GitHub Actions Three-Layer Execution Model Workflow (.github/workflows/ci.yml) triggered by: on: push / pull_request Job: lint runs-on: ubuntu-24.04 Step: checkout Step: setup-python Step: pip install ruff mypy Step: ruff check . Step: mypy src/ needs: lint Job: test (matrix: py 3.11, 3.12) runs-on: ubuntu-24.04 — two parallel runners Python 3.11 Step: checkout Step: setup-python 3.11 Step: pip install Step: pytest PASS Python 3.12 Step: checkout Step: setup-python 3.12 Step: pip install Step: pytest PASS
سير العمل يحتوي وظائف. تعمل الوظائف على runners معزولة. الخطوات داخل وظيفة تشترك في نفس نظام ملفات الـ runner. مفتاح needs يجعل test ينتظر نجاح lint؛ المصفوفة تُدوّر runnerين متوازيين لـ Python 3.11 و3.12.

الوظائف: وحدة العزل

الوظيفة هي وحدة العزل في GitHub Actions. كل وظيفة تعمل على آلة افتراضية جديدة — لا شيء من نظام ملفات وظيفة ما، أو متغيرات بيئتها، أو أدواتها المثبّتة ينتقل إلى وظيفة أخرى ما لم تُمرره صراحة عبر artifacts. هذا اختيار تصميمي متعمد: يمنع الاقتران الخفي بين مراحل CI.

حقول تهيئة الوظيفة الرئيسية:

  • runs-on — نظام تشغيل وإصدار الـ runner. ثبّت دائماً على إصدار محدد مثل ubuntu-24.04 بدلاً من علامة ubuntu-latest القابلة للتغيير. تُغيّر GitHub ما يشير إليه ubuntu-latest بدون إشعار، مما يسبب انتكاسات بيئة صامتة في pipeline.
  • needs — مصفوفة من معرّفات الوظائف التي يجب أن تكتمل بنجاح قبل أن تبدأ هذه الوظيفة. افتراضياً، جميع الوظائف في workflow تعمل بالتوازي. استخدم needs للتعبير عن التبعيات: الاختبار لا يجب أن يعمل إن فشل الـ lint؛ النشر لا يجب أن يعمل إن فشلت الاختبارات.
  • environment — يربط الوظيفة بـ GitHub Environment (مُغطى في درس لاحق)، مما يتيح قواعد حماية النشر والـ secrets المُحدّدة لبيئات بعينها (staging وproduction).
  • outputs — يتيح لوظيفة نشر أزواج key-value يمكن للوظائف التالية قراءتها. يُستخدم لتمرير أسماء حزم البناء، أرقام الإصدارات المحسوبة، أو نتائج الاختبارات بين الوظائف.
  • timeout-minutes — الوقت الأقصى المسموح به للوظيفة قبل أن تُلغيه GitHub. الافتراضي 360 دقيقة (6 ساعات). دائماً حدد حداً أضيق — اختبار تكامل متوقف لا يجب أن يستهلك ست ساعات من وقت الـ runner قبل إيقافه.
الفكرة الأساسية — عزل الوظيفة مقابل مشاركة الخطوة: الخطوات داخل وظيفة تشترك في نفس VM للـ runner، ونفس دليل العمل، ونفس حالة متغيرات البيئة. لهذا تُثبّت التبعيات في خطوة وتُشغّل الاختبارات في أخرى — كلاهما يرى نفس node_modules/ أو venv/. لكن الوظيفة B لا ترى ما كتبته الوظيفة A على القرص. إن بنت الوظيفة A صورة Docker تحتاجها الوظيفة B للدفع، يجب على A رفع الصورة كـ artifact (أو دفعها إلى registry) وتنزيلها B.

الخطوات: حيث يحدث العمل

الخطوة هي وحدة عمل واحدة داخل وظيفة. تعمل كأمر shell (run) أو تستدعي action جاهزة (uses). تُنفَّذ الخطوات بالتسلسل داخل الوظيفة، بالترتيب الذي عُرِّفت به — لا تزامن على مستوى الخطوة.

كل خطوة يمكن أن تحتوي اختيارياً على:

  • name — التسمية المعروضة في واجهة GitHub وفي مخرجات السجل. دائماً اكتب أسماء ذات معنى — "Run tests" مفيد بالحد الأدنى، "Unit tests (src/)" مفيد، "pytest src/ -x --tb=short -q" هو بالضبط ما تحتاجه حين تُصحّح فشلاً في الساعة الثانية صباحاً.
  • id — معرّف ثابت يُستخدم للإشارة إلى مخرجات هذه الخطوة ونتيجتها من الخطوات اللاحقة في نفس الوظيفة.
  • if — تعبير شرطي يحدد ما إذا كانت هذه الخطوة ستُنفَّذ. يُستخدم شائعاً للخطوات التي يجب تنفيذها عند الفشل فقط (if: failure()) أو على فروع محددة.
  • env — متغيرات بيئة مُحدّدة لهذه الخطوة، تتجاوز متغيرات مستوى الوظيفة أو سير العمل لهذه الخطوة فقط. لا تضع secrets مباشرة في قيم env — دائماً ارجع إليها عبر ${{ secrets.MY_SECRET }}.
  • continue-on-error — إن كانت true، تستمر الوظيفة حتى إن فشلت هذه الخطوة. مفيد للتشخيصات الاختيارية كتقرير التغطية التي لا يجب أن تعيق الـ pipeline.
# أنماط خطوات متقدمة: المخرجات والشروط ومعالجات الفشل jobs: build: runs-on: ubuntu-24.04 outputs: image_tag: ${{ steps.meta.outputs.version }} steps: - uses: actions/checkout@v4 # خطوة بمعرّف لكي يمكن الإشارة إلى مخرجاتها - name: Compute image tag id: meta run: | VERSION=$(git describe --tags --always --dirty) echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - name: Build Docker image run: | docker build \ --tag "myapp:${{ steps.meta.outputs.version }}" \ --build-arg GIT_SHA=${{ github.sha }} \ . # هذه الخطوة تعمل فقط إن فشلت الوظيفة — رفع سجلات تشخيصية - name: Upload build logs on failure if: failure() uses: actions/upload-artifact@v4 with: name: build-logs path: /tmp/build-*.log retention-days: 7
ممارسة احترافية — استخدم GITHUB_OUTPUT لا set-output: صيغة echo "::set-output name=key::value" القديمة مهجورة ومعطّلة على الـ runners الجديدة. دائماً اكتب المخرجات باستخدام نهج ملف البيئة: echo "key=value" >> "$GITHUB_OUTPUT". كذلك استخدم echo "MY_VAR=value" >> "$GITHUB_ENV" لتعيين متغيرات بيئة تستمر عبر الخطوات في نفس الوظيفة. كلا الملفين GITHUB_OUTPUT وGITHUB_ENV ملفات مؤقتة تنشئها GitHub؛ الإلحاق بهما هو الآلية المعتمدة الآمنة من الحقن.

رسم بيان التبعيات: تصميم ترتيب الوظائف

على نطاق الشركات الكبرى، رسم بيان تبعيات الوظائف هو الرافعة الأكبر لسرعة الـ pipeline. النموذج الذهني هو رسم بيان موجّه غير دوري (DAG): الوظائف بلا تبعيات تعمل بالتوازي؛ الوظائف ذات needs تُشكّل سلاسل. يُشغّل pipeline مُصمَّم جيداً أكبر قدر ممكن بالتوازي، ويُسلسل فقط ما هو تابع حقاً.

نمط إنتاجي شائع لخدمة ويب:

  • المرحلة الأولى المتوازية: lint وtype-check وdependency-audit — تعمل جميعها في آنٍ واحد بلا تبعيات. كلها رخيصة (تحت دقيقتين) ومستقلة تماماً.
  • المرحلة الثانية (تحتاج المرحلة الأولى): unit-tests — تعمل فقط إن نجح الـ lint والتحقق من النوع. تشغيل الاختبارات بعد فشل الـ lint ضوضاء لا معنى لها.
  • المرحلة الثالثة (تحتاج unit-tests): build — تجميع وتغليف الـ artifact.
  • المرحلة الرابعة (تحتاج build): integration-tests — تشغيل التبعيات (قاعدة البيانات، ناقل الرسائل) واختبار الـ artifact المغلّف من البداية للنهاية.
  • المرحلة الخامسة (تحتاج integration-tests، على main فقط): deploy-staging — النشر على بيئة staging، مُبوَّبة بموافقة GitHub Environment.
نمط فشل شائع — التسلسل الكامل: المهندسون الجدد على GitHub Actions كثيراً ما يضعون كل خطوة منطقية في وظيفة واحدة تعمل بالتسلسل. يبدو هذا بسيطاً لكن له مشكلتان جديتان: (1) خطوة اختبار بطيئة تُعيق جميع الخطوات اللاحقة حتى لو كانت فحوصات أبكر كالـ lint ستكتشف المشكلة أسرع؛ (2) فشل وظيفة واحدة لا يُعطيك إشارة عن نوع الفحص الذي فشل. قسّم إلى وظائف حسب النوع ودع الـ DAG يُعطيك السرعة المتوازية وتحديد الفشل الدقيق.

أنماط الإنتاج الشائعة

بعض إعدادات مستوى سير العمل التي يجب أن يتضمنها كل pipeline إنتاجي:

# هيكل workflow للإنتاج — إعدادات يجب أن تكون في كل pipeline name: CI/CD on: push: branches: ["main"] pull_request: branches: ["main"] # إلغاء أي تشغيل قيد التنفيذ لنفس الفرع حين يبدأ تشغيل جديد. # يمنع وضع خمسة تشغيلات في الطابور لخمسة commits سريعة التتالي لنفس PR. concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true # مبدأ أقل الصلاحيات: افتراضي لصلاحيات token القراءة فقط. # منح الكتابة فقط حيث تحتاجها وظيفة محددة (نشر، نشر حزمة). permissions: contents: read jobs: test: runs-on: ubuntu-24.04 timeout-minutes: 20 # لا تترك الافتراضي 360 دقيقة أبداً. permissions: contents: read # تجاوز على مستوى الوظيفة؛ فقط ما تحتاجه هذه الوظيفة فعلاً. steps: - uses: actions/checkout@v4 with: # الاستنساخ الضحل (العمق الافتراضي 1) هو الأسرع للـ CI. # زد فقط إن احتجت تاريخ git (مثلاً git describe للإصدار). fetch-depth: 1 - name: Run tests run: make test

كل إعداد في ذلك الهيكل له سبب. concurrency يمنع هدر الـ runner. permissions: contents: read يتبع مبدأ أقل الصلاحيات — GITHUB_TOKEN الافتراضي قوي جداً لوظيفة اختبار، وaction مخترقة في سلسلة تبعياتك لا يجب أن تكون قادرة على الدفع إلى مستودعك. timeout-minutes: 20 شبكة أمان ضد العمليات المتوقفة التي ستستهلك طاقة الـ runner ساعات.

فهم سير العمل والوظائف والخطوات بهذا العمق هو الأساس لكل ما يلي في هذا الدرس التعليمي: بنيات المصفوفة، والتخزين المؤقت، وسير العمل القابلة للإعادة الاستخدام، ومصادقة OIDC، والنشر الآمن — كلها تُبنى مباشرة على هذا النموذج المكوّن من ثلاث طبقات. اضبط الهيكل صح وسيبدو باقي الدرس التعليمي منطقياً. اضبطه خطأً وستُصارع الأداة في كل منعطف.