GitHub Actions بعمق

التخزين المؤقت والقطع الأثرية في GitHub Actions

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

التخزين المؤقت والقطع الأثرية في GitHub Actions

مشروع Node.js يحتوي على مجلد node_modules ضخم قد يستهلك ثلاث دقائق في تنفيذ npm ci عند كل تشغيل، حتى لو لم يتغير شيء في ملف package-lock.json. اضرب ذلك في 50 طلب سحب يوميًا وستجد نفسك تحرق 150 دقيقة من وقت المشغّل (والمال) على عمليات إدخال/إخراج الشبكة وحدها. يوفر GitHub Actions آليتين تكمّل كل منهما الأخرى لحل هذه المشكلة: التخزين المؤقت (إعادة استخدام شجرة الملفات عبر تشغيلات متعددة لنفس الفرع) والقطع الأثرية (نقل مخرجات البناء بين الوظائف داخل تشغيل سير عمل واحد). فهم متى تستخدم كلًا منهما — وكيف تفشل — مهارة أساسية في بيئات الإنتاج.

التخزين المؤقت باستخدام actions/cache

تقوم الإجراء actions/cache بتخزين أرشيف مضغوط على خادم GitHub للتخزين المؤقت واستعادته في التشغيلات اللاحقة. المفهوم المحوري هنا هو مفتاح التخزين المؤقت: سلسلة نصية تُعرّف مدخل التخزين بشكل فريد. إذا تطابق المفتاح، تُستعاد البيانات (إصابة في المخزن المؤقت)؛ وإذا لم يتطابق، يُتخطى الخطوة وتعمل الوظيفة من الصفر (عدم إصابة)، ثم تحفظ الإجراء مدخلًا جديدًا تحت ذلك المفتاح.

المفتاح الجيد يتكون من جزأين: بادئة ثابتة (تحدد نظام التشغيل والغرض) وتجزئة محتوى لملف القفل أو ملف التبعيات. تضمن التجزئة إبطال المخزن المؤقت تحديدًا عند تغيير التبعيات — لا أقل ولا أكثر.

name: CI with Dependency Cache on: [push, pull_request] jobs: build: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '22' # استعادة المخزن المؤقت قبل تثبيت التبعيات - name: Cache node_modules uses: actions/cache@v4 id: npm-cache with: path: ~/.npm # مجلد تخزين npm العالمي، ليس node_modules key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}-npm- - name: Install dependencies run: npm ci - name: Run tests run: npm test

restore-keys: سلسلة الاحتياطية

حقل restore-keys عبارة عن قائمة من بادئات المفاتيح يتم تجربتها بالترتيب عند عدم تطابق المفتاح الأساسي. يجد GitHub أحدث مدخل تخزين مؤقت تبدأ مفتاحه بالبادئة المعطاة ويستعيده كـإصابة جزئية. بعد اكتمال الوظيفة، يُحفظ مدخل تخزين مؤقت جديد تحت المفتاح الأساسي الدقيق. هذا النمط يعني أن الفرع المنشأ حديثًا يحصل دائمًا على بداية دافئة من مخزن فرعه الأصلي بدلًا من تثبيت بارد.

قواعد نطاق التخزين المؤقت: يقتصر التخزين المؤقت على الفرع. يمكن لتشغيل على فرع ميزة قراءة مخازن الفرع الافتراضي (main)، لكن لا يمكن لتشغيل على main قراءة مخازن فروع الميزات. خطط لـrestore-keys وفق ذلك — بادئة تطابق مخازن main تُعدّ احتياطية قوية.

ما الذي يجب تخزينه مؤقتًا (وما الذي لا يجب)

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

  • npm: ~/.npm — ذاكرة التخزين العالمية للحزم
  • pip / poetry: ~/.cache/pip أو مجلد البيئة الافتراضية
  • Maven: ~/.m2/repository
  • Gradle: ~/.gradle/caches
  • Go modules: ~/go/pkg/mod
  • Cargo (Rust): ~/.cargo/registry وtarget/ (بحذر — target/ كبير الحجم)
التخزين المؤقت المدمج في إجراءات الإعداد: تقبل actions/setup-node@v4 وactions/setup-python@v5 وactions/setup-java@v4 وما شابهها إدخالًا باسم cache: (مثل cache: 'npm') يستخدم داخليًا actions/cache بالمسارات واستراتيجية المفاتيح الصحيحة. فضّل هذه على كتابة خطوات التخزين المؤقت بنفسك — فهي تتعامل مع الحالات الحدية التي قد تنساها.

حدود التخزين المؤقت والإخلاء

يفرض GitHub حدًا إجماليًا لحجم التخزين المؤقت يبلغ 10 جيجابايت لكل مستودع. تُخلى المدخلات التي لم يُصل إليها خلال سبعة أيام تلقائيًا؛ وعند الوصول للحد، تُحذف المدخلات الأقدم أولًا. في الممارسة العملية، أبقِ المفاتيح محددة وتجنب تخزين مخرجات البناء التي تتغير عند كل تشغيل (تلك تنتمي إلى القطع الأثرية). للمستودعات أحادية البنية التي تحتوي على ملفات قفل متعددة، نسّق مفاتيحك حسب مساحة العمل: monorepo-frontend-${{ hashFiles('apps/frontend/package-lock.json') }}.

Cache hit vs cache miss flow in GitHub Actions Cache Hit vs Cache Miss Cache HIT actions/cache restore Archive extracted to path Skip install step Build / Test runs fast Cache MISS actions/cache restore Try restore-keys prefix (partial hit or full miss) Run install step (cold) Cache saved under new key next run will hit
مسار الإصابة (يسار) يتخطى التثبيت؛ مسار عدم الإصابة (يمين) يُشغّل التثبيت من الصفر ويحفظ مدخلًا جديدًا للمرة التالية.

القطع الأثرية: نقل البيانات بين الوظائف

على عكس التخزين المؤقت، تقتصر القطع الأثرية على تشغيل سير عمل واحد. غرضها مختلف: نقل ملف ثنائي مُجمَّع، أو تقرير اختبار، أو صورة Docker مُضغوطة من وظيفة build إلى وظيفة test أو deploy الأسفل منها. تعمل الوظائف على مشغّلات مؤقتة منفصلة، لذا بدون القطع الأثرية لا توجد نظام ملفات مشترك بينها.

name: Build then Deploy on: push: branches: [main] jobs: build: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - name: Build application run: | npm ci npm run build # المخرجات في مجلد dist/ - name: Upload dist artifact uses: actions/upload-artifact@v4 with: name: dist-${{ github.sha }} path: dist/ retention-days: 7 # الاحتفاظ 7 أيام؛ الافتراضي 90 deploy: runs-on: ubuntu-24.04 needs: build # ينتظر نجاح وظيفة build steps: - name: Download dist artifact uses: actions/download-artifact@v4 with: name: dist-${{ github.sha }} path: dist/ - name: Deploy to server run: rsync -az dist/ user@prod:/var/www/app/

تسمية القطع الأثرية والاحتفاظ بها وحدود الحجم

استخدم مخطط عنونة بالمحتوى لأسماء القطع الأثرية — تضمين ${{ github.sha }} أو معرّف التشغيل يمنع التصادم العرضي بين التشغيلات المتزامنة. الاحتفاظ الافتراضي هو 90 يومًا للمستودعات العامة وقابل للتكوين في إعدادات المؤسسة؛ اضبط retention-days قيمة ضيقة لمخرجات البناء المؤقتة لتجنب الوصول إلى حصة التخزين. يفرض GitHub 500 ميجابايت لكل قطعة أثرية و2 جيجابايت لكل تشغيل (الأرقام الدقيقة تعتمد على خطتك). للصور الكبيرة من Docker، ادفعها إلى سجل وأرسل نص الوسم كقطعة أثرية.

لا تخزّن الأسرار مطلقًا في القطع الأثرية. أرشيفات القطع الأثرية قابلة للتنزيل من أي شخص يمتلك صلاحية القراءة على المستودع. يشمل ذلك بيانات الاعتماد المؤقتة، ومفاتيح API المضمنة في مخرجات البناء، وملفات .env. احذف الأسرار قبل الرفع؛ وللملفات الثنائية الحساسة حقًا، وقّعها وادفعها إلى سجل خاص.

بنية المصفوفة + القطع الأثرية: جمع النتائج

عندما تنتج وظيفة مصفوفة مخرجات لكل منصة، يجب أن يرفع كل خلية من خلايا المصفوفة بـاسم فريد يتضمن متغير المصفوفة، ثم تقوم وظيفة أسفل منها بتنزيلها جميعًا وتجميع النتيجة النهائية — حزمة إصدار، أو دمج تغطية، أو بيان Docker متعدد المنصات.

jobs: test: strategy: matrix: os: [ubuntu-24.04, windows-latest, macos-14] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - run: npm ci && npm test -- --reporter=json > results.json - uses: actions/upload-artifact@v4 with: name: test-results-${{ matrix.os }} path: results.json report: runs-on: ubuntu-24.04 needs: test steps: - uses: actions/download-artifact@v4 with: pattern: test-results-* # تنزيل جميع القطع الأثرية المطابقة merge-multiple: true # وضع الملفات مسطحة في مجلد العمل path: all-results/ - name: Merge and publish report run: node merge-reports.js all-results/
نمط الإنتاج — فصل التخزين المؤقت عن القطع الأثرية: استخدم actions/cache لكل ما هو قابل للإعادة (التبعيات، ذاكرات تخزين المُجمِّع، ذاكرات طبقات Docker) وactions/upload-artifact لكل ما هو ناتج تشغيل محدد (الملفات الثنائية، تقارير الاختبار، HTML التغطية). تعامل مع التخزين المؤقت كتحسين يمكنك حذفه بأمان؛ وتعامل مع القطع الأثرية كتسليمات بالغة الأهمية بين الوظائف.

أنماط الفشل الشائعة

  • مخزن مؤقت قديم بعد ترقية نظام التشغيل: إذا رقّيت من ubuntu-22.04 إلى ubuntu-24.04 ولم يتضمن مفتاح التخزين runner.os، ستستعيد ذاكرة تخزين ثنائية غير متوافقة. دائمًا ابدأ المفاتيح بـ${{ runner.os }}.
  • تسميم المخزن المؤقت عبر PR من fork: يمكن لـPRs المنقولة من fork قراءة المخازن المؤقتة من المستودع الأصلي لكن لا تستطيع الكتابة إليها. هذا حد أمني لا خلل — لكنه يعني أن أول تشغيل من fork سيستلزم تثبيتًا باردًا. تقبّل ذلك ولا تحاول التحايل عليه بتوكنات صريحة.
  • تنزيل القطعة الأثرية قبل رفعها: إذا نزّلت وظيفة B قطعة أثرية من وظيفة A لكنك نسيت needs: [job-a]، تفشل خطوة التنزيل برسالة غامضة "artifact not found". دائمًا أعلن عن needs الصريحة للوظائف التي تستهلك قطعًا أثرية.
  • رفع المسار الخاطئ: path: dist/ يرفع محتويات مجلد dist/ بدون المجلد نفسه؛ بينما path: dist (بدون شرطة مائلة) يتضمن المجلد ذاته. اعرف ماذا تريد قبل كتابة خطوة النشر.