الـ Pipelines المكتوبة بالسكريبت و Groovy
الـ Pipelines المكتوبة بالسكريبت و Groovy
يوفر Jenkins صيغتين لكتابة الـ Pipeline. تعرّفت في الدرس الثالث على صيغة Declarative — وهي لغة DSL منظّمة ومقيّدة تُغطي 80-90% من احتياجات CI/CD. أما Scripted Pipeline فهي الصيغة الأخرى: كود Groovy نقي يعمل داخل بيئة Jenkins CPS (Continuation-Passing Style)، وتمنحك لغة كاملة بمرونة شاملة لكنها أقل تقييدًا. في بيئات Google الواسعة وفي البنوك الكبرى التي تعتمد Jenkins، ستجد كلا الصيغتين — وأحيانًا في نفس الملف.
ما الذي يجعل Scripted مختلفة؟
يُغلَّف الـ Scripted Pipeline بكتلة node لا بكتلة pipeline. كل ما بداخلها هو كود Groovy يعمل على الـ agent الذي تحدده. لا يوجد هيكل stages إلزامي، ولا مفتاح post تلقائي — أنت من يكتب try/catch/finally بنفسك.
ملاحظات أساسية: node('linux && docker') تختار agent بناءً على تعبير الـ label. stage() هي استدعاء دالة وليس مفتاح. معالجة الأخطاء هي Groovy نقية — عليك تعيين currentBuild.result يدويًا ثم إعادة رمي الخطأ حتى يُعلّم Jenkins الإجراء بالفشل.
متى تحتاج فعلًا إلى Scripted؟
الإجابة: أقل مما تتوقع، لكن حين تحتاجها تكون ضرورة حقيقية. إليك حالات الاستخدام الإنتاجية المشروعة:
- توليد stages ديناميكي — تحتاج إنشاء stages من قائمة لا تُعرف إلا وقت التشغيل (مثل stage لكل microservice في monorepo). كتلة
stagesفي Declarative ثابتة؛ Scripted تتيحfor (svc in services) { stage("Deploy ${svc}") { … } }. - تفريع شرطي معقد — حين يعجز
whenDSL في Declarative عن التعبير عن منطق متعدد المستويات يشمل البيئة والـ tag وفرع المصدر ومساعدات الـ shared library. - توازٍ بفروع ديناميكية — بناء
Mapمن الفروع المتوازية وقت التشغيل وتمريره إلىparallel(branches). - هجرة pipelines قديمة — إرث من قبل صيغة Declarative (حقبة Jenkins قبل الإصدار 2.5) يصعب إعادة كتابته كليًا.
الـ Parallel الديناميكي — القوة الحقيقية
السبب الكلاسيكي للجوء إلى Scripted هو التوازي المحسوب وقت التشغيل. افترض أن لديك مستودعًا متعدد الخدمات ومساعدًا يُعيد الخدمات التي تغيّرت في الـ commit الحالي. لا يمكنك التعبير عن هذا في Declarative.
for، المتغير svc مشترك بين جميع الـ closures. حين تعمل الـ closure تكون الحلقة قد انتهت وأصبح svc يحمل آخر قيمة فقط. انسخه دائمًا بـ def s = svc داخل الحلقة. هذا أكثر أخطاء إنتاج Scripted Pipeline شيوعًا.
دمج Scripted داخل Declarative
لا تضطر لاختيار صيغة واحدة للملف بأكمله. تتيح Jenkins تضمين كتلة script { } في أي مكان داخل كتلة steps في Declarative — وذلك الكتلة هي كود Groovy Scripted نقي. هذا هو نمط أفضل ما في العالمين الذي تتبناه معظم تثبيتات Jenkins الناضجة.
كتلة script { } تمنحك Groovy كاملًا. خارجها أنت في Declarative DSL الآمنة مع معالجة post التلقائية وكتل options وparameters وenvironment. هذا الهجين هو ما تبدو عليه قوالب pipelines Netflix و Airbnb في الواقع.
حماية CPS — ما لا يمكنك فعله
يُشغّل Jenkins الـ Scripted Pipelines في بيئة CPS محوّلة تُسلسل حالة البناء على القرص حتى تتمكن من الاستمرار بعد إعادة تشغيل المتحكم. هذا يفرض قيودًا حقيقية تلدغ فرق الإنتاج:
- عدم قابلية تسلسل الكائنات — لا يمكنك تخزين
FileInputStreamمفتوح أوClosureفي حقل يتجاوز حدودnode. محوّل CPS سيفشل في تسلسلها. - قيود
@NonCPS— الطرق المُزيّنة بـ@NonCPSتتجاوز تحويل CPS وتعمل كـ JVM عادي، لكنها لا تستطيع استدعاء طرق CPS محوّلة (مثلshوecho). افصل الكود: منطق بحت في مساعدات@NonCPS، وخطوات Jenkins في طرق CPS عادية. - قيود مكتبة Groovy القياسية — كثير من الفئات محجوبة بحماية الـ sandbox. ستواجه
RejectedAccessException. إما اعتمد التوقيع في Manage Jenkins → In-process Script Approval، أو انقل المنطق إلى Shared Library (الدرس 6) التي تعمل بصلاحيات أوسع.
@NonCPS، وأبقِ الطرق الرئيسية في الـ pipeline رفيعة (استدعاءات خطوات Jenkins فقط). هذا ينتج pipelines أسرع وأكثر أمانًا وقابلية للصيانة، ويتجنب معظم أخطاء تسلسل CPS.
أنماط Groovy التي تستحق المعرفة
لا تحتاج أن تكون خبيرًا في Groovy، لكن هذه الأنماط تظهر في كل Scripted Pipeline حقيقي:
readFile('path')/writeFile file: 'path', text: content— قراءة مخرجات البناء إلى سلاسل Groovy للمنطق الشرطي.- GStrings:
"Deploy to ${env.BRANCH_NAME}"— إدراج متغيرات Groovy. استخدم الاقتباس المفرد داخل خطواتshحين تريد توسيع متغيرات الشِّل لا Groovy. def result = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim()— التقاط مخرجات الأمر في متغير Groovy.error('message')— فشل pipeline صريح؛ يضبط النتيجة ويرمي استثناءً، وهو أنظف من رمي الاستثناءات الخام.
sh '…' بالاقتباس المفرد، تذهب علامة الدولار إلى الشِّل. داخل sh "…" بالاقتباس المزدوج، يُوسّع Groovy أولًا — مما يعني أن ${MY_VAR} تُوسَّع بـ Groovy قبل أن يرى الشِّل الأمر، فمتغير Groovy المفقود يُدرج فراغًا بدل قيمة الشِّل التي توقعتها.
الـ Scripted Pipelines أداة قوية في ترسانة Jenkins. استخدمها بتعمّد: الافتراضي هو Declarative، الغ إلى كتل script { } حين تحتاج منطق Groovy داخل stage واحد، وارقَ إلى Scripted الكامل فقط حين تحتاج رسومات pipeline محسوبة وقت التشغيل. الدرس القادم يتناول Agents والبناء الموزع، وهو ينطبق على كلا الصيغتين.