أتمتة البناء وقابلية الاستنساخ
أتمتة البناء وقابلية الاستنساخ
خط أنابيب CI لا يكون موثوقاً إلا بقدر موثوقية عملية البناء التي يُشغّلها. أوضح لك الدرس الثاني تشريح خط الأنابيب — المراحل والمُشغّلات والمُحفّزات. يذهب هذا الدرس مستوىً أعمق: كيف تكتب سكريبتات البناء التي تُنفّذها تلك المراحل، وتُثبّت الاعتمادات حتى لا تنجرف أبداً، وتُهيكل بناءً يُنتج نفس البرنامج الثنائي بالضبط على حاسوب المطوّر وعلى مُشغّل GitHub Actions وعلى خادم الإنتاج؟ هذه الخصائص الثلاث — بناء آلي، واعتمادات مُثبَّتة، وبناء منعزل (hermetic) — هي الأساس الذي يُفرّق بين إعداد CI هاوٍ وإعداد على مستوى Google.
سكريبتات البناء: العقد بين الكود وخط الأنابيب
كل مرحلة في CI تستدعي سكريبتاً. ذلك السكريبت هو الوصف التنفيذي الموثوق لكيفية بناء برنامجك. القاعدة الذهبية: إذا نفّذ إنسان خطوةً مرةً واحدة يدوياً، يجب أن تكون قابلة للتعبير عنها كسكريبت يُشغّله خط الأنابيب تلقائياً — بلا مطالبات تفاعلية، ولا متغيرات بيئة ضمنية، ولا اعتمادات على مستوى النظام غير مُعلنة.
اكتب سكريبتات البناء بهذه الخصائص:
- الخروج عند أول خطأ: استخدم
set -euo pipefailفي Bash. بدونها، يُهمَل فشلnpm installبصمت ويُبلَّغ عن خط الأنابيب باللون الأخضر.-eتخرج عند الخطأ،-uتعامل المتغيرات غير المضبوطة كأخطاء،-o pipefailتصطاد الفشل داخل الأنابيب. - إصدارات الأدوات الصريحة: لا تستدعِ
nodeوتأمل في الأفضل. ثبّت الإصدار باستخدام.nvmrcأو.tool-versions(asdf) أو وسم صورة Docker. المُشغّل الذي يثبّت Node 18 اليوم قد يُحدَّث إلى Node 22 الربع القادم. - التثبيت الذي لا يعتمد على الشبكة: استخدم
npm ciبدلاً منnpm install. استخدمpip install --no-indexحين تتوفر مرآة. استخدمgo mod downloadمع وكيل وحدات نمطية. إخفاقات الشبكة في CI هي المصدر الأعلى للبنيات المتقلبة. - خطوات متكررة بأمان (idempotent): يجب أن تكون كل خطوة آمنة للتشغيل مرة أخرى. تجنب الآثار الجانبية كإلحاق الملفات أو تغيير الحالة المشتركة بين الخطوات.
run: في GitHub Actions غير قابل للاختبار ولا يمكن تشغيله محلياً. ضعه في scripts/build.sh واستدعِه من YAML بسطر واحد. يستطيع المهندسون الآن تشغيل نفس البناء تماماً محلياً بـ bash scripts/build.sh.
تثبيت الاعتمادات: لا مفاجآت في الإنتاج
الاعتمادات غير المُثبَّتة هي المصدر الأكثر شيوعاً لأعطال "كانت تعمل الأسبوع الماضي". النمط يتكرر عبر كل بيئة تقنية: تُصدر حزمة انتقالية تصحيحاً، ملف القفل غائب أو قديم، وفجأة يفشل بناؤك على قاعدة كود لم تتغير. في كبرى الشركات التقنية، كل اعتماد — مباشر وانتقالي — مُثبَّت بالضبط.
ماذا يعني التثبيت في كل بيئة تقنية:
- Node.js: ادفع
package-lock.jsonأوyarn.lockإلى المستودع. ثبّت باستخدامnpm ci(يُخطئ إذا كان ملف القفل غير متزامن معpackage.json). لا تدفعnode_modules/أبداً. - Python: ادفع
requirements.txtالمُولَّد بـpip-compile(منpip-tools)، الذي يُحل ويُثبّت جميع الاعتمادات الانتقالية. أو استخدمpoetry.lock. لا تستخدمrequirements.txtبنطاقات إصدار مفتوحة كـrequests>=2.0. - Go: ادفع
go.sum. تتحقق أدوات Go من المجموع الاختباري تشفيرياً — أي اعتماد مُعدَّل يُفشل البناء. شغّلgo mod tidyقبل الدفع للحفاظ على نظافته. - صور Docker الأساسية: لا تستخدم
FROM ubuntu:latestأبداً. ثبّت على digest:FROM ubuntu:24.04@sha256:abc123.... وسوم الصور قابلة للتغيير — نفس الوسم قد يشير إلى مجموعة طبقات مختلفة غداً. - إجراءات CI: في GitHub Actions، ثبّت إجراءات الطرف الثالث على SHA محدد لـ commit، لا على وسم.
uses: actions/checkout@v4قابل للتغيير؛uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683مُثبَّت للأبد.
البناء المنعزل (Hermetic): عزل كل متغير
البناء المنعزل هو البناء المُعزول تماماً عن البيئة المحيطة. بنفس الكود المصدري ونفس المدخلات، يُنتج البناء المنعزل مُخرجاً متطابقاً bit-for-bit بصرف النظر عن الجهاز الذي يُشغّله، أو ما هو مثبّت على ذلك الجهاز، أو وقت اليوم. هذا هو المعيار الذهبي. Bazel من Google وBuck2 من Meta هما نظاما بناء منعزل مبنيان لهذا الغرض ويفرضانه على مستوى سلسلة الأدوات.
لا تحتاج إلى Bazel للحصول على معظم الفائدة. قائمة التحقق العملية للبناء المنعزل:
- تشغيله داخل حاوية: ابنِ داخل صورة Docker تحتوي كل أداة بإصدار مُثبَّت. نظام تشغيل المضيف يصبح غير ذي صلة.
- لا شبكة أثناء البناء: يجب أن تكون جميع الاعتمادات موجودة في الصورة أو في طبقة الكاش قبل خطوة التصريف. البناء الذي يجلب من الإنترنت في منتصف التصريف غير منعزل — الإنترنت يتغير.
- حذف الطوابع الزمنية والبذور العشوائية: تُضمّن كثير من المصرّفات طوابع زمنية للبناء. استخدم
SOURCE_DATE_EPOCH(متغير بيئة معياري) لتجميد الطابع الزمني. استخدم بذرة عشوائية ثابتة في أي خطوة توليد. - تخزين المدخلات لا المخرجات مؤقتاً: خزّن طبقة الاعتمادات المُحمَّلة مؤقتاً مفهرسةً بهاش ملف القفل. لا تخزّن الحزم المبنية مؤقتاً عبر الفروع — الكاش القديم مصدر شائع لإخفاقات خفية.
التخزين المؤقت: مضاعف السرعة الذي يجب ألا يُفسد النتائج
الكاش ضروري لسرعة خط الأنابيب — تثبيت Node.js الذي يستغرق 3 دقائق يصبح 5 ثوانٍ عند الإصابة بالكاش. لكن كاشاً فاسداً يمكنه إخفاء الإخفاقات الحقيقية لأسابيع. طبّق هذه القواعد:
- مفتاح الكاش على المدخلات لا الوقت: يجب أن يتضمن مفتاح الكاش هاش ملف القفل (
hashFiles('**/package-lock.json'))، ونظام التشغيل، وإصدار Node. الكاش المفهرس فقط على اسم الفرع سيُقدّم نتيجة قديمة بعد تغيير الاعتمادات. - استعادة بلا ثقة عمياء: بعد استعادة الكاش، شغّل
npm ciدائماً — خيار--prefer-offlineيستخدم الكاش لكنه يتحقق من السلامة. لا تتخطَّ خطوة التثبيت لمجرد إصابة الكاش. - افصل كاش البناء عن كاش الاختبار: خزّن المُخرجات المُصرَّفة بشكل منفصل عن الحزم المُحمَّلة. مُخرج تصريف سيء في الكاش يُسبّب إخفاقات خفية يصعب تشخيصها جداً.
- مسح الكاش بقوة عند التغييرات الكبرى: أضف إلى بادئة مفاتيح الكاش رقماً تزيده يدوياً (
v2-deps-...). ازده كلما اشتبهت في فساد الكاش — مسح كاش واحد أسرع من ساعات تحقيق.
--no-cache والتحقق من تكرار الإخفاق في حالة نظيفة. إذا تكرر، البناء غير منعزل. إذا لم يتكرر، لديك مشكلة تسمّم كاش. كلاهما حرج ويجب إصلاحه.
Makefile ومُشغّلات المهام: الجسر بين البيئة المحلية و CI
أعلى نمط استثمارياً لقابلية الاستنساخ هو امتلاك أمر واحد يُنفّذ بالضبط ما يُنفّذه CI. يُعدّ Makefile (أو Taskfile.yml لبيئة Go/YAML) الواجهة المعيارية: make build، وmake test، وmake lint — نفس الأهداف، نفس الأوامر، سواء شغّلها إنسان أو خط أنابيب.
git clone ... && make ci والحصول على بناء أخضر خلال دقيقتين، فبناؤك قابل للاستنساخ. إذا واجه أخطاء خاصة بالبيئة، فتلك الأخطاء ستظهر على مُشغّل CI أيضاً في نهاية المطاف.
أنماط الإخفاق الشائعة في الإنتاج
حتى الفرق ذات النوايا الحسنة تصطدم بهذه المشاكل المتكررة:
- وسوم صور أساسية عائمة: كان
FROM node:ltsيشير إلى Node 18 العام الماضي؛ اليوم يشير إلى Node 22. كل اختباراتك تعمل الآن على وقت تشغيل مختلف عن الإنتاج. - أدوات نظام ضمنية: سكريبت بناء يستدعي
jqأوyqموجودين على حاسوب المطوّر لكن غائبين على مُشغّل جديد. البناء يفشل في CI برسالة "command not found" غامضة. - اختبارات حساسة للمنطقة الزمنية: اختبار يُؤكّد أن
new Date().toLocaleDateString() === "1/1/2025"ينجح في UTC+0 ويفشل في UTC+3. مُشغّل CI يستخدم UTC؛ أجهزة المطوّرين لا. - ظروف سباق في الخطوات المتوازية: خطوتا بناء تكتبان في نفس مجلد المُخرجات. على مُشغّل سريع تتصادمان؛ على مُشغّل بطيء لا. الإخفاق غير حتمي ومتقطّع.
كل هذه انتهاكات لقابلية الاستنساخ. الحل في كل حالة هو نفسه: تخلّص من الافتراض المحيطي الذي سبّبه — ثبّت الصورة، وأعلن الأداة في Dockerfile، وجمّد المنطقة الزمنية بـ TZ=UTC، وسَلسِل الخطوات المتعارضة.