GitHub Actions بعمق

التعبيرات والسياقات والشروط

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

التعبيرات والسياقات والشروط

حين تحفظ ملف workflow، فأنت قد كتبت YAML — لكن GitHub Actions يُقيّم لغةً ثانية مضمّنة داخل ذلك الـ YAML: لغة التعبيرات. فهمُها هو الفرق بين نسخ مقتطفات ومحادثات وبين تأليف workflows تتصرف بالضبط كما تنوي في كل الأحوال. يغطي هذا الدرس تلك اللغة بالكامل: صياغتها، والسياقات المدمجة التي تقرأ منها، والمفتاح if: الذي يتحكم في تنفيذ الـ jobs والخطوات.

صياغة التعبيرات

تُحاط التعبيرات بـ ${{ }}. يمكن استخدامها في أي موضع قيمة داخل ملف الـ workflow — حقول على مستوى الـ job مثل runs-on، وحقول الخطوات مثل run وenv وwith، وكذلك مفتاح if:. قيمة if: المجردة هي تعبير ضمني في حد ذاتها (الأقواس اختيارية هناك).

تدعم اللغة:

  • القيم الحرفية — منطقية (true/false)، ونول، وأرقام، وسلاسل بأقواس مفردة: 'main'. الأقواس المزدوجة ليست محددات سلاسل صالحة.
  • العوامل==، !=، <، <=، >، >=، &&، ||، !. المقارنة غير حساسة لحالة الأحرف في السلاسل.
  • الوصول للخصائص — النقطة (github.event_name) أو الأقواس المربعة للمفاتيح الديناميكية (fromJson(inputs.matrix)[0]).
  • الدوال — مكتبة صغيرة مدمجة: contains()، startsWith()، endsWith()، format()، join()، toJson()، fromJson()، hashFiles()، always()، success()، failure()، cancelled().
تحويل الأنواع: يُحوّل GitHub Actions الأنواع بصمت في المقارنات. السلسلة الفارغة تساوي false؛ أي سلسلة غير فارغة تساوي true. هذا مقصود ومفيد — if: inputs.flag صحيحة متى كان المدخل قد أُعطي — لكنها تُخطئ المهندسين الذين يتوقعون مساواة صارمة.

السياقات

السياق (context) هو كائن مُسمّى تملؤه Actions قبل تشغيل الـ workflow. تصل إلى خصائصه بالتعبيرات. السياقات السبعة الأكثر استخداماً هي:

  • github — بيانات عن الحدث المُطلِق: github.event_name، github.ref، github.sha، github.actor، github.repository، وحمولة الحدث الكاملة في github.event.
  • env — متغيرات البيئة المُعرّفة على مستوى الـ workflow أو الـ job أو الخطوة. داخل run يمكن قراءتها كمتغيرات shell ($VAR)، لكن ${{ env.VAR }} متاحة في أي مكان بما فيه شروط if:.
  • secrets — القيم المخزّنة في مخزن الأسرار. الوصول إليها عبر التعبيرات (${{ secrets.TOKEN }}) هو الطريقة الوحيدة الآمنة لتمريرها للخطوات. تُخفي Actions قيم الأسرار تلقائياً من السجلات.
  • vars — متغيرات الإعداد غير الحساسة (على مستوى المستودع أو المنظمة). استخدمها لأشياء مثل AWS_REGION أو NODE_VERSION التي تختلف بين البيئات لكنها ليست سرية.
  • jobs — قيم المخرجات من jobs أخرى في نفس الـ workflow. متاحة فقط في الـ jobs التي تُعلن needs:.
  • steps — قيم المخرجات والنتيجة (success/failure/skipped/cancelled) للخطوات السابقة داخل نفس الـ job.
  • runner — معلومات وقت التشغيل: runner.os، runner.arch، runner.temp، runner.tool_cache.
على نطاق شركات التقنية الكبرى، استخدم vars لكل ما يتغير بين البيئات وليس حساساً — سجلات Docker الأساسية، وأسماء الـ clusters، وعلامات الميزات، وقنوات الإشعارات. هذا يبقي secrets صغيرة وقابلة للتدقيق، ويتيح لك تغيير الإعداد غير السري دون تدوير أي اعتمادات.
GitHub Actions contexts and where they are populated Context Sources & Availability github.* Event payload, ref, sha secrets.* Encrypted secret store vars.* Non-secret config vars env.* Workflow / job / step env steps.* Outputs & outcomes jobs.* Cross-job outputs (needs) runner.* OS, arch, temp path Expression Evaluator ${{ ... }} if: conditions env: values with: inputs run: commands
جميع السياقات تُغذّي مُقيّم التعبيرات الذي يحلّ القيم في وقت تشغيل الـ workflow.

مفتاح if: — التنفيذ الشرطي

كل job وكل خطوة تقبل مفتاح if:. حين يُقيّم التعبير إلى قيمة خاطئة، تتخطى GitHub تلك الـ job أو الخطوة كلياً — وتظهر كـ "skipped" في الواجهة. الشرط الضمني الافتراضي هو success()، مما يعني أن الـ jobs والخطوات تعمل فقط حين تنجح جميع الخطوات السابقة في نفس الـ job (أو جميع الـ jobs المطلوبة).

دوال التحقق من الحالة الأربع بالغة الأهمية:

  • success() — صحيحة حين تنتهي كل خطوة/job سابقة دون خطأ (الافتراضية).
  • failure() — صحيحة حين تفشل خطوة/job سابقة واحدة على الأقل. استخدمها لخطوات الإشعار أو التنظيف.
  • always() — صحيحة بصرف النظر عن النتيجة السابقة، حتى لو أُلغي الـ workflow. استخدمها لخطوات التنظيف التي يجب أن تعمل مهما حدث.
  • cancelled() — صحيحة فقط حين أُلغي تشغيل الـ workflow صراحةً.
خطأ شائع: كتابة if: failure() على خطوة تحتاج أيضاً إلى مخرجات من خطوة سابقة. إذا فشلت الخطوة السابقة، فمخرجاتها فارغة أو غير معرّفة — وقد يُخطئ مُعالج الفشل بدوره. احرص دائماً على أن معالجات الفشل تعتمد على الحد الأدنى من البيانات التي تحتاجها فعلاً.

أنماط عملية

يوضح الـ workflow التالي المفاهيم الثلاثة معاً — التعبيرات، والسياقات المتعددة، والخطوات الشرطية — في سيناريو إنتاجي واقعي: نشر يعمل فقط على main، ويُخطر Slack عند الفشل، ويرفع السجلات دائماً كـ artifacts.

name: Production Deploy on: push: branches: [main] pull_request: branches: [main] env: AWS_REGION: ${{ vars.AWS_REGION }} # متغير إعداد غير سري IMAGE_TAG: ${{ github.sha }} jobs: build-and-push: runs-on: ubuntu-latest outputs: image-uri: ${{ steps.push.outputs.uri }} steps: - uses: actions/checkout@v4 - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }} aws-region: ${{ env.AWS_REGION }} - name: Build & push image id: push run: | IMAGE="${{ vars.ECR_REGISTRY }}/myapp:${{ env.IMAGE_TAG }}" docker build -t "$IMAGE" . docker push "$IMAGE" echo "uri=$IMAGE" >> "$GITHUB_OUTPUT" deploy: needs: build-and-push runs-on: ubuntu-latest # النشر فقط عند push حقيقي إلى main، ليس في PRs if: github.event_name == 'push' && github.ref == 'refs/heads/main' environment: production # يُطلق المراجعين المطلوبين إن تم إعدادهم steps: - name: Deploy to ECS run: | aws ecs update-service \ --cluster ${{ vars.ECS_CLUSTER }} \ --service myapp \ --force-new-deployment \ --region ${{ env.AWS_REGION }} - name: Collect deploy logs if: always() # ارفع السجلات حتى عند فشل النشر run: aws ecs describe-services --cluster ${{ vars.ECS_CLUSTER }} --services myapp > deploy-logs.json - name: Upload logs artifact if: always() uses: actions/upload-artifact@v4 with: name: deploy-logs-${{ github.sha }} path: deploy-logs.json - name: Notify Slack on failure if: failure() uses: slackapi/slack-github-action@v1 with: payload: | { "text": "Deploy FAILED for ${{ github.repository }} @ ${{ github.sha }} by ${{ github.actor }}" } env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_DEPLOY_WEBHOOK }}

لاحظ التفاعل: vars.* يحمل الإعداد الخاص بالبيئة وغير الحساس؛ secrets.* يحمل ARN الدور ورابط الـ webhook؛ github.* يقود منطق الشرط؛ ومخرجات steps.* تُمرّر عنوان الصورة بين الـ jobs.

التحقق من نتائج الخطوات

لكل خطوة خاصية outcome: success، failure، cancelled، أو skipped. يمكنك التفريع بناءً عليها صراحةً حين تحتاج تحكماً دقيقاً:

- name: Run tests id: tests run: npm test continue-on-error: true # لا توقف الـ job فوراً - name: Post coverage comment if: steps.tests.outcome == 'success' run: gh pr comment --body "$(cat coverage-summary.txt)" - name: Fail the job now if: steps.tests.outcome == 'failure' run: exit 1

يتيح لك continue-on-error: true أن تفشل خطوة دون إيقاف الـ job فوراً — تلتقط النتيجة وتقرر ما تفعله بعدها. هذا مفيد لمجموعات الاختبارات المتقلبة التي تريد رفع نتائجها قبل التوقف.

دوال التعبيرات في الممارسة

دالتان تستحقان اهتماماً خاصاً على نطاق واسع. hashFiles('**/package-lock.json') تُعيد تجزئة حتمية للملفات المطابقة — مثالية كمفتاح كاش يبطل فقط حين تتغير التبعيات فعلاً. fromJson() وtoJson() تتيحان تمرير بيانات هيكلية بين الخطوات والـ jobs، متجاوزتَين القيد النصي لـ GITHUB_OUTPUT.

استخدم contains(github.event.pull_request.labels.*.name, 'skip-ci') للسماح للمهندسين بوضع تسمية على الـ PR وتخطي الـ jobs المكلفة. حرف البدل *.name يُسطّح مصفوفة كائنات التسميات إلى أسمائها — أسلوب أنيق تراه في workflows الإنتاج على نطاق واسع.