Jenkins والتكامل المستمر المؤسسي

الـ Pipelines المكتوبة بالسكريبت و Groovy

18 دقيقة الدرس 4 من 28

الـ 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 بنفسك.

// أبسط Scripted Pipeline node('linux && docker') { try { stage('Checkout') { checkout scm } stage('Build') { sh 'make build' } stage('Test') { sh 'make test' junit 'build/reports/**/*.xml' } stage('Push') { withCredentials([usernamePassword( credentialsId: 'dockerhub-creds', usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASS' )]) { sh ''' echo "$DOCKER_PASS" | docker login -u "$DOCKER_USER" --password-stdin docker push myorg/myapp:${GIT_COMMIT[0..7]} ''' } } } catch (err) { currentBuild.result = 'FAILURE' throw err } finally { cleanWs() if (currentBuild.result == 'FAILURE') { slackSend channel: '#alerts', message: "Build FAILED: ${env.BUILD_URL}" } } }

ملاحظات أساسية: 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}") { … } }.
  • تفريع شرطي معقد — حين يعجز when DSL في Declarative عن التعبير عن منطق متعدد المستويات يشمل البيئة والـ tag وفرع المصدر ومساعدات الـ shared library.
  • توازٍ بفروع ديناميكية — بناء Map من الفروع المتوازية وقت التشغيل وتمريره إلى parallel(branches).
  • هجرة pipelines قديمة — إرث من قبل صيغة Declarative (حقبة Jenkins قبل الإصدار 2.5) يصعب إعادة كتابته كليًا.
Declarative هي الاختيار الافتراضي في كل الشركات الكبرى. توصيات Jenkins وأنماط Google/Netflix تؤكد على Declarative ما لم تصطدم بعائق حقيقي. Scripted تمنحك القوة؛ Declarative تمنحك الأمان والوضوح للمهندسين العشرة الذين سيصونون الملف بعدك.

الـ Parallel الديناميكي — القوة الحقيقية

السبب الكلاسيكي للجوء إلى Scripted هو التوازي المحسوب وقت التشغيل. افترض أن لديك مستودعًا متعدد الخدمات ومساعدًا يُعيد الخدمات التي تغيّرت في الـ commit الحالي. لا يمكنك التعبير عن هذا في Declarative.

// Dynamic parallel fan-out — Scripted Pipeline @Library('myorg-shared') _ def changedServices = [] node('controller') { stage('Detect Changes') { checkout scm changedServices = detectChangedServices() // مساعد shared-library } } // بناء Map من الفروع المتوازية def branches = [:] for (String svc in changedServices) { def s = svc // نسخ متغير الحلقة — تفادي خطأ Groovy closure الشهير! branches["Build ${s}"] = { node('linux && docker') { checkout scm dir("services/${s}") { sh "docker build -t myorg/${s}:${env.GIT_COMMIT[0..7]} ." sh "docker push myorg/${s}:${env.GIT_COMMIT[0..7]}" } } } } stage('Build Services') { parallel branches }
خطأ التقاط متغير closure في Groovy: في حلقة for، المتغير svc مشترك بين جميع الـ closures. حين تعمل الـ closure تكون الحلقة قد انتهت وأصبح svc يحمل آخر قيمة فقط. انسخه دائمًا بـ def s = svc داخل الحلقة. هذا أكثر أخطاء إنتاج Scripted Pipeline شيوعًا.

دمج Scripted داخل Declarative

لا تضطر لاختيار صيغة واحدة للملف بأكمله. تتيح Jenkins تضمين كتلة script { } في أي مكان داخل كتلة steps في Declarative — وذلك الكتلة هي كود Groovy Scripted نقي. هذا هو نمط أفضل ما في العالمين الذي تتبناه معظم تثبيتات Jenkins الناضجة.

pipeline { agent any stages { stage('Build Matrix') { steps { script { // Groovy كامل متاح هنا def envs = ['dev', 'staging', 'prod'] def deployBranches = [:] envs.each { e -> def env = e deployBranches["Deploy to ${env}"] = { sh "helm upgrade --install myapp-${env} chart/ \ --namespace ${env} \ --set image.tag=${GIT_COMMIT[0..7]}" } } if (env.BRANCH_NAME == 'main') { parallel deployBranches } else { echo "Skipping multi-env deploy on feature branch" } } } } } post { failure { slackSend channel: '#deploys', message: "Deploy failed: ${env.BUILD_URL}" } } }

كتلة script { } تمنحك Groovy كاملًا. خارجها أنت في Declarative DSL الآمنة مع معالجة post التلقائية وكتل options وparameters وenvironment. هذا الهجين هو ما تبدو عليه قوالب pipelines Netflix و Airbnb في الواقع.

Declarative vs Scripted Pipeline decision flow Which Pipeline Syntax? Need dynamic stage generation? No Declarative pipeline { } Yes Complex logic in one stage? No Declarative + script { } block Yes Full Scripted node { } — max power When to use each Declarative — default Hybrid — complex steps Scripted — dynamic stages
شجرة القرار: Declarative ثم Hybrid ثم Scripted الكامل، بترتيب الأفضلية.

حماية 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 صريح؛ يضبط النتيجة ويرمي استثناءً، وهو أنظف من رمي الاستثناءات الخام.
قاعدة الاقتباس المفرد مقابل المزدوج هي أكثر مشاكل إنتاج Jenkins إثارةً للتساؤل. داخل sh '…' بالاقتباس المفرد، تذهب علامة الدولار إلى الشِّل. داخل sh "…" بالاقتباس المزدوج، يُوسّع Groovy أولًا — مما يعني أن ${MY_VAR} تُوسَّع بـ Groovy قبل أن يرى الشِّل الأمر، فمتغير Groovy المفقود يُدرج فراغًا بدل قيمة الشِّل التي توقعتها.

الـ Scripted Pipelines أداة قوية في ترسانة Jenkins. استخدمها بتعمّد: الافتراضي هو Declarative، الغ إلى كتل script { } حين تحتاج منطق Groovy داخل stage واحد، وارقَ إلى Scripted الكامل فقط حين تحتاج رسومات pipeline محسوبة وقت التشغيل. الدرس القادم يتناول Agents والبناء الموزع، وهو ينطبق على كلا الصيغتين.

ES
Edrees Salih
منذ ساعة

We are still cooking the magic in the way!