اكتشف كيف تحول GitHub Actions من مجرد أداة CI/CD إلى نظام أتمتة كامل لمشاريعك، مع نصائح عملية لتجنب الفخاخ التي يقع فيها حتى المطورون المحترفون.
في عام 2025، أصبح GitHub Actions أكثر من مجرد أداة لدمج الكود واختباره. إنه الآن العمود الفقري للأتمتة في المشاريع الحديثة، سواء كنت تبني مكتبة مفتوحة المصدر أو نظاماً معقداً في شركة تقنية. المشكلة؟ معظم المطورين يستخدمون 20% فقط من قدراته، ويضيعون ساعات في تصحيح workflows معقدة دون أن يعرفوا أن الحل يكمن في سطر واحد من YAML. لنبدأ بسؤال بسيط: كم مرة وجدت نفسك تنتظر 10 دقائق حتى ينتهي workflow ليعطيك خطأً سخيفاً مثل 'Module not found' لأنك نسيت تشغيل npm install؟ هذا بالضبط ما سنتجنبه اليوم.
الواقع المؤلم أن معظم الفرق تضيع ما بين 15% إلى 30% من وقت التطوير في مهام متكررة يمكن أتمتتها بسهولة. في شركة مثل Spotify، استخدموا GitHub Actions لتقليل وقت النشر من 45 دقيقة إلى 7 دقائق فقط، وذلك بإعادة هيكلة workflowsهم باستخدام matrix strategy وcaching ذكي. لكن قبل أن نتعمق، دعونا نتفق على شيء: YAML ليس مجرد ملف نصي، إنه برنامج كامل ينفذ على سيرفرات GitHub، وكل سطر فيه يمكن أن يكلفك دولارات أو ساعات من وقتك الثمين.
عندما تنشئ ملف workflow جديد في مجلد .github/workflows، فإنك في الواقع تنشئ برنامجاً صغيراً ينفذ على سيرفرات GitHub. هذا البرنامج ليس مجرد سكربت bash، بل هو نظام متكامل يدعم events، conditions، matrix builds، وdependencies بين jobs. لكن هنا تكمن المشكلة: معظم المطورين يعاملون workflow كسكربت عادي، مما يؤدي إلى مشاكل مثل: jobs تتكرر بلا داعٍ، أو steps تنفذ حتى لو فشلت الخطوات السابقة، أو أسوأ من ذلك - secrets تتسرب لأن أحدهم نسي إضافة condition.
لنأخذ مثالاً عملياً: تخيل أنك تريد تشغيل اختبارات الوحدة عند كل push إلى فرع main، لكنك تريد أيضاً تشغيل اختبارات التكامل فقط عند إنشاء pull request. الحل الساذج هو إنشاء workflowين منفصلين، لكن هذا سيؤدي إلى تكرار الكود وزيادة وقت التنفيذ. بدلاً من ذلك، يمكنك استخدام شرط واحد بسيط داخل workflow واحد:
name: CI Pipeline
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm install
- run: npm test
integration-tests:
needs: unit-tests
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm install
- run: npm run test:integrationلاحظ كيف استخدمنا if condition للتحكم في تنفيذ integration-tests. هذا ليس مجرد توفير للوقت، بل هو أيضاً توفير للمال، لأن GitHub يفرض عليك رسوماً بناءً على دقائق التنفيذ. في مشروع متوسط الحجم، يمكن لهذا الشرط البسيط أن يوفر لك 50% من تكلفة Actions شهرياً. لكن هناك خدعة أخرى: استخدام matrix strategy لتشغيل نفس الـ job على عدة بيئات دون تكرار الكود.
العديد من المطورين يخافون من matrix strategy لأنهم يعتقدون أنها معقدة، لكنها في الواقع أبسط مما تتصور. الفكرة الأساسية هي أنك تستطيع تشغيل نفس الـ job على عدة نسخ متوازية، كل نسخة لها متغيرات مختلفة. مثلاً، إذا كنت تريد اختبار تطبيقك على Node.js 18 و20، وعلى أنظمة تشغيل مختلفة، يمكنك كتابة:
jobs:
test:
strategy:
matrix:
node-version: [18, 20]
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npm testما يحدث خلف الكواليس هو أن GitHub ينشئ 4 jobs متوازية (2 node versions × 2 OS)، وكل job ينفذ نفس الخطوات لكن بقيم مختلفة. هذا ليس مجرد توفير للوقت، بل هو أيضاً ضمان أن تطبيقك يعمل على جميع البيئات المطلوبة. لكن هنا تأتي المشكلة: إذا كان لديك 10 متغيرات في matrix، فسيتم إنشاء 10 jobs، وهذا قد يكلفك الكثير إذا لم تكن حذراً.
الحل؟ استخدام max-parallel للتحكم في عدد الـ jobs المتوازية، واستخدام exclude لاستبعاد بعض التوليفات غير الضرورية. مثلاً، إذا كنت تعلم أن تطبيقك لا يعمل على Windows مع Node.js 18، يمكنك استبعاد هذا التوليف:
strategy:
matrix:
node-version: [18, 20]
os: [ubuntu-latest, windows-latest]
exclude:
- node-version: 18
os: windows-latest
max-parallel: 3هذه التقنية استخدمتها في مشروع مفتوح المصدر حيث كنا نحتاج لاختبار مكتبة على 5 إصدارات من Node.js و3 أنظمة تشغيل، لكننا لم نكن نريد تشغيل 15 job في نفس الوقت. باستخدام max-parallel: 5، قللنا وقت التنفيذ من 45 دقيقة إلى 15 دقيقة فقط، لأن الـ jobs كانت تنفذ على دفعات.
إذا كنت لا تستخدم caching في GitHub Actions، فأنت تضيع وقتك ومالك. الفكرة بسيطة: بدلاً من تحميل node_modules أو dependencies في كل مرة ينفذ فيها workflow، يمكنك تخزينها مؤقتاً واستعادتها في المرة التالية. لكن هنا تكمن المشكلة: معظم الأمثلة التي تراها على الإنترنت تستخدم caching بطريقة خاطئة، مما يؤدي إلى مشاكل مثل dependencies قديمة أو cache غير صالحة.
لنأخذ مثالاً عملياً: في مشروع يستخدم npm، يمكنك تخزين node_modules باستخدام:
- uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-ما يحدث هنا هو أن GitHub يحسب hash لملف package-lock.json، ويستخدم هذا الـ hash كمفتاح للـ cache. إذا لم يتغير الملف، فسيتم استعادة node_modules من الـ cache بدلاً من تحميله من جديد. لكن هناك مشكلة شائعة: إذا قمت بتحديث dependencies، قد لا يتم تحديث الـ cache بشكل صحيح، مما يؤدي إلى استخدام dependencies قديمة. الحل؟ إضافة خطوة إضافية للتحقق من صحة الـ cache:
- name: Verify cache
run: |
if [ ! -d "node_modules" ]; then
echo "Cache not restored, installing dependencies..."
npm install
else
echo "Cache restored, verifying..."
npm ls > /dev/null || npm install
fiفي شركة مثل Netflix، استخدموا هذه التقنية لتقليل وقت بناء مشروعهم من 20 دقيقة إلى 3 دقائق فقط. السر؟ ليس فقط في استخدام caching، بل في كيفية تنظيم الـ cache. مثلاً، بدلاً من تخزين node_modules بالكامل، يمكنك تخزين مجلد ~/.npm فقط، مما يقلل حجم الـ cache بشكل كبير ويزيد من فرص نجاح استعادته.
في المشاريع الكبيرة، قد تحتاج إلى تقسيم الـ cache حسب نوع الـ job. مثلاً، إذا كان لديك job لبناء الواجهة الأمامية وآخر للخلفية، يمكنك استخدام مفاتيح مختلفة للـ cache:
- uses: actions/cache@v3
with:
path: frontend/node_modules
key: ${{ runner.os }}-frontend-${{ hashFiles('frontend/package-lock.json') }}
- uses: actions/cache@v3
with:
path: backend/node_modules
key: ${{ runner.os }}-backend-${{ hashFiles('backend/package-lock.json') }}الجميع يعرف أنه يجب استخدام secrets لتخزين المفاتيح والمعلومات الحساسة، لكن القليل من يعرفون أن secrets يمكن أن تتسرب بطرق غير متوقعة. مثلاً، إذا قمت بطباعة secret في الـ logs عن طريق الخطأ، فسيتم تسجيله ويمكن لأي شخص لديه وصول للـ logs رؤيته. لكن المشكلة الأكبر هي أن secrets يمكن أن تتسرب عبر environment variables أو حتى عبر الـ context نفسه.
لنفترض أنك تريد استخدام secret في سطر الأوامر، فكتبت:
- run: echo "Deploying with token ${{ secrets.DEPLOY_TOKEN }}"هذا خطأ فادح، لأن الـ secret سيتم طباعته في الـ logs وسيظهر بشكل واضح. بدلاً من ذلك، يجب عليك تمرير الـ secret عبر environment variable دون طباعته:
- run: echo "Starting deployment..."
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}لكن حتى هذا ليس كافياً في بعض الحالات. مثلاً، إذا كان الـ secret يظهر في رسائل الخطأ أو في الـ stack traces، فقد يتسرب. الحل؟ استخدام mask لحجب الـ secret من الـ logs:
- name: Mask secret
run: echo "::add-mask::${{ secrets.DEPLOY_TOKEN }}"في شركة مثل GitLab، واجهوا مشكلة حيث كانت secrets تتسرب عبر الـ environment variables في الـ runners المشتركة. الحل الذي استخدموه كان إنشاء runners مخصصة لكل مشروع، وتقييد الوصول للـ secrets باستخدام conditions صارمة. مثلاً:
jobs:
deploy:
if: github.ref == 'refs/heads/main'
runs-on: self-hosted
steps:
- run: ./deploy.sh
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}أحد أسوأ الكوابيس للمطورين هو عندما يفشل workflow دون أي رسالة خطأ واضحة. مثلاً، قد ترى رسالة مثل 'Process completed with exit code 1' دون أي تفاصيل إضافية. في هذه الحالة، أول شيء يجب فعله هو تشغيل الـ job في وضع debug. يمكنك فعل ذلك ببساطة عن طريق إضافة متغير بيئة:
env:
ACTIONS_STEP_DEBUG: trueهذا سيجعل GitHub يطبع معلومات إضافية في الـ logs، مما يساعدك على تحديد المشكلة. لكن أحياناً المشكلة ليست في الكود، بل في الـ runner نفسه. مثلاً، قد يكون الـ runner قد نفد منه الذاكرة أو القرص الصلب. في هذه الحالة، يمكنك استخدام أداة مثل actions/toolkit لتشخيص المشكلة:
- name: Check disk space
run: df -h
- name: Check memory
run: free -mمشكلة أخرى شائعة هي عندما يفشل الـ job بسبب timeout. بشكل افتراضي، الـ job لديه timeout قدره 6 ساعات، لكن يمكنك تغييره باستخدام:
jobs:
test:
timeout-minutes: 30لكن أحياناً المشكلة تكون في الـ dependencies نفسها. مثلاً، قد يكون لديك dependency يتسبب في memory leak، مما يؤدي إلى فشل الـ job. في هذه الحالة، يمكنك استخدام أدوات مثل node --inspect لتشخيص المشكلة:
- run: node --inspect-brk ./test.jsعندما يفشل workflow، قد تحتاج إلى تحليل ملفات معينة مثل الـ logs أو الـ core dumps. يمكنك استخدام الـ artifacts لتحميل هذه الملفات:
- name: Upload logs
if: failure()
uses: actions/upload-artifact@v3
with:
name: logs
path: logs/هذا سيمكنك من تحميل الملفات بعد انتهاء الـ job، مما يساعدك على تحليل المشكلة بشكل أفضل. في مشروع مفتوح المصدر كنت أعمل عليه، استخدمنا هذه التقنية لتشخيص مشكلة في الـ CI حيث كان الـ job يفشل عشوائياً دون أي سبب واضح. بعد تحليل الـ logs، اكتشفنا أن المشكلة كانت في مكتبة خارجية تتسبب في segmentation fault على بعض الـ runners.
في عام 2025، لم يعد GitHub Actions مجرد أداة لاختبار الكود ونشره. أصبح بإمكانك استخدامه لأتمتة مهام إدارة المشروع بالكامل، مثل: تحديث الوثائق، إدارة الإصدارات، وحتى الرد على issues. مثلاً، يمكنك إنشاء workflow يقوم تلقائياً بإغلاق issues التي لم يتم الرد عليها منذ شهر:
name: Close stale issues
on:
schedule:
- cron: '0 0 * * *'
jobs:
close-stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v8
with:
stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity.'
days-before-stale: 30
days-before-close: 7يمكنك أيضاً استخدام GitHub Actions لإنشاء إصدارات تلقائية عند دمج الكود في فرع main. مثلاً، باستخدام semantic-release:
- name: Release
run: npx semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}في شركة مثل Microsoft، يستخدمون GitHub Actions لأتمتة عملية مراجعة الكود. مثلاً، عند إنشاء pull request، يقوم workflow تلقائياً بتشغيل أدوات تحليل الكود مثل SonarQube، ويرسل تقريراً بالنتائج إلى المراجع. هذا يقلل من وقت المراجعة بشكل كبير، لأن المراجع يرى المشاكل قبل أن يبدأ في قراءة الكود.
إحدى الميزات القوية لـ GitHub Actions هي القدرة على أتمتة تحديث الوثائق. مثلاً، إذا كان لديك مكتبة مفتوحة المصدر، يمكنك إنشاء workflow يقوم تلقائياً بتحديث ملف README عند كل إصدار جديد:
name: Update README
on:
release:
types: [published]
jobs:
update-readme:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: node scripts/update-readme.js
- uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: 'docs: update README for new release'هذا الـ workflow ينفذ عند كل إصدار جديد، ويقوم بتشغيل سكربت يقوم بتحديث ملف README بمعلومات الإصدار الجديد، ثم يقوم بعمل commit تلقائي للتغييرات. هذه التقنية استخدمناها في مشروع مفتوح المصدر لتوفير ساعات من العمل اليدوي في تحديث الوثائق.
بعد عشر سنوات من العمل مع GitHub Actions، هذه هي النصائح التي أتمنى أن أعرفها منذ اليوم الأول: أولاً، دائماً استخدم matrix strategy حتى لو كنت تعتقد أن مشروعك صغير، لأنك ستحتاجها عاجلاً أم آجلاً. ثانياً، لا تخزن node_modules بالكامل في الـ cache، بل استخدم ~/.npm أو ~/.cache بدلاً من ذلك، لأن هذا يقلل حجم الـ cache ويزيد من فرص نجاح استعادته. ثالثاً، دائماً استخدم conditions للتحكم في تنفيذ الـ jobs، حتى لو كنت تعتقد أن الـ job بسيط، لأنك ستوفر الكثير من الوقت والمال على المدى الطويل.
رابعاً، لا تعتمد فقط على secrets لتأمين معلوماتك الحساسة، بل استخدم أيضاً mask وconditions صارمة للتحكم في الوصول. خامساً، دائماً قم بتفعيل وضع debug عند مواجهة مشاكل، لأن الـ logs الإضافية يمكن أن توفر لك ساعات من البحث. وأخيراً، لا تعامل GitHub Actions كسكربت bash، بل تعامل معه كبرنامج كامل، لأن هذا سيجعلك تفكر في الـ architecture بدلاً من مجرد كتابة خطوات متتابعة.
الخطوة التالية؟ ابدأ بتطبيق ما تعلمته على مشروعك الحالي. اختر workflow واحد معقد، وقم بإعادة هيكلته باستخدام matrix strategy وcaching. ستندهش من الفرق الذي ستشهده في وقت التنفيذ وتكلفة الـ Actions. وإذا واجهتك مشكلة، تذكر: الـ logs هي صديقك، وdebug mode هو سلاحك السري.