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

الـ Pipelines التصريحية

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

الـ Pipelines التصريحية

الـ Declarative Pipeline هو لغة DSL منظّمة وموجَّهة تُوفّرها Jenkins كطريقةٍ موصى بها لتعريف أنابيب CI/CD في الكود. تُغلّف هذه اللغة Groovy داخل قواعد نحوية صارمة تجعل الـ Pipelines قابلةً للقراءة والتدقيق والصيانة على نطاق واسع — وهو ما تحتاجه فرق التقنية الكبرى حين يوجد Jenkinsfile في كل مستودع microservice ويُراجَع كأي كود تطبيقي. يُشرّح هذا الدرس كل بنية أساسية: pipeline وagent وstages/steps وpost وwhen.

الهيكل الإلزامي

يجب أن يلتزم أي Declarative Pipeline ببنية محددة على المستوى الأعلى. أي انحراف عنها يُعدّ خطأً في التحليل النحوي يُكتشف وقت التحميل — وهذا في الواقع ميزة، لأنه يكشف الأخطاء قبل تشغيل أي بناء.

// Jenkinsfile — أبسط declarative pipeline صالح pipeline { agent any // أين يعمل — إلزامي على المستوى الأعلى stages { // قائمة مرحلة بالترتيب — إلزامية stage('Build') { steps { // خطوة واحدة على الأقل — إلزامية echo 'Compiling...' } } } post { // تنظيف / إشعارات — اختياري always { echo 'Pipeline finished.' } } }

يُطبّق المحلّل النحوي القواعد بصرامة: لا يمكنك وضع كود Groovy مجرّد على المستوى الأعلى كما تسمح به الـ Scripted Pipeline. هذا القيد هو المقايضة — تحصل على تغذية راجعة فورية بشأن الأخطاء النحوية ودعم الأدوات (Blue Ocean، Replay)، وتتنازل عن قدر بسيط من المرونة الخام التي نادرًا ما تحتاجها على أي حال.

تعليمة agent

يُجيب بلوك agent على سؤال واحد: على أي مُنفِّذ تعمل هذه الـ Pipeline (أو المرحلة)؟ يُحدّد Jenkins الـ agent قبل تنفيذ أي خطوات، لذا فإن إعداده الصحيح أمر جوهري.

  • agent any — جدوِل التنفيذ على أي مُنفِّذ متاح. مناسب للإعدادات الصغيرة؛ غير موصى به في الإنتاج حيث تريد بيئات بناء قابلة للتكرار.
  • agent none — لا agent على المستوى الأعلى؛ كل stage يجب أن يُعلن عن agent خاص به. هذا هو النمط الإنتاجي: مراحل مختلفة تعمل على agents مختلفة.
  • agent { label 'linux && docker' } — جدوِل على أي عقدة تطابق تعبير التسمية. التسميات هي آلية توجيه المراحل إلى الأجهزة أو أنظمة التشغيل الصحيحة.
  • agent { docker { image 'node:20-alpine' } } — شغّل حاوية Docker جديدة لكل مرحلة؛ يُركَّب workspace داخلها تلقائيًا. هذا هو النمط الموصى به لمراحل البناء في تثبيتات Jenkins الحديثة.
  • agent { kubernetes { yaml '...' } } — أنشئ Pod في Kubernetes كمُنفِّذ. يجعل هذا إضافة Jenkins Kubernetes معيار الشركات الأصيلة للسحاب.
النمط الإنتاجي — agent none مع agents لكل مرحلة: أعلن agent none على المستوى الأعلى وعيّن agents صريحة لكل مرحلة. يمنع هذا الـ controller من احتجاز فتحة مُنفِّذ وهي خاملة أثناء تشغيل المراحل البطيئة (اختبارات التكامل، النشر).
pipeline { agent none // لا فتحة منفّذ عامة محجوزة stages { stage('Build') { agent { docker { image 'gradle:8-jdk21' } } steps { sh './gradlew assemble' stash name: 'jars', includes: 'build/libs/**/*.jar' } } stage('Test') { agent { docker { image 'gradle:8-jdk21' } } steps { unstash 'jars' sh './gradlew test' } post { always { junit 'build/test-results/**/*.xml' } } } stage('Deploy to Staging') { agent { label 'deploy' } // عقدة بها بيانات اعتماد السحاب steps { unstash 'jars' sh './scripts/deploy.sh staging' } } } }

المراحل والخطوات

كل وحدة عمل ذات معنى تعيش داخل stage. المراحل هي مستوى الدقة الذي يعرضه Blue Ocean وتستوعبه فرقك: Build، Unit Test، Integration Test، Security Scan، Publish، Deploy. داخل المرحلة، يحتوي بلوك steps على استدعاءات دوال الخطوات.

الخطوات المدمجة الرئيسية التي ستستخدمها باستمرار:

  • sh 'command' — نفّذ أمر shell على agents لينوكس/macOS.
  • bat 'command' — نفّذ أمر batch على agents ويندوز.
  • echo 'message' — اطبع في سجل البناء.
  • checkout scm — سحب الكود المصدري الذي أطلق هذا البناء.
  • stash / unstash — مرّر artifacts بين مراحل تعمل على agents مختلفة.
  • withCredentials([...]) — حقن الأسرار في البيئة طوال مدة البلوك.
  • dir('path') — غيّر دليل العمل للخطوات المتداخلة.
  • timeout(time: 10, unit: 'MINUTES') — فشِّل خطوة أو مرحلة إذا تجمّدت.
Declarative Pipeline structure overview pipeline { } agent environment options parameters post stages { } stage('Build') agent steps { } stage('Test') when { } steps { } stage('Deploy') when { } steps { } post { } always success failure cleanup
نظرة عامة على بنية الـ Declarative Pipeline: كل مرحلة تحمل agent وwhen الخاصة بها؛ يعمل post بعد انتهاء جميع المراحل.

قسم post

يُعرّف بلوك post الإجراءات التي تُنفَّذ بعد انتهاء الـ Pipeline (أو المرحلة الفردية)، بصرف النظر عن النتيجة. يمكن أن يظهر على المستوى الأعلى وداخل أي stage. الشروط المتاحة تغطي كل حالة نهائية ممكنة:

  • always — غير مشروط؛ استخدمه للتنظيف وأرشفة السجلات.
  • success — فقط عند نجاح البناء؛ استخدمه لنشر الـ artifacts أو الإشعارات.
  • failure — فقط عند الفشل؛ استخدمه لتنبيه فريق الاستجابة أو التراجع.
  • unstable — اكتمل البناء لكن فشل في الاختبارات فأصبحت نتيجته UNSTABLE.
  • changed — يُطلَق حين تختلف النتيجة الحالية عن السابقة؛ مثالي لرسائل "عاد إلى الأخضر".
  • fixed — اختصار لـ success-after-failure.
  • regression — اختصار لـ failure-after-success.
  • aborted — توقّف البناء يدويًا.
  • cleanup — يعمل أخيرًا بعد كل شروط post الأخرى؛ استخدمه لتنظيف الـ workspace لضمان تنفيذه حتى لو فشلت خطوة إشعار.
نصيحة — افصل التنظيف عن الإشعارات: ضع deleteDir() أو إيقاف الحاوية في cleanup لا في always. إذا رمى إشعار Slack خطأً، قد يتوقف always قبل الوصول إلى التنظيف؛ أما cleanup فمضمون التنفيذ بعد كل ما في post.

تعليمة when

يتيح لك بلوك when تخطّي مرحلة بالكامل استنادًا إلى شروط تُقيَّم قبل تخصيص agent المرحلة. هذا ضروري لأنابيب فعّالة: لماذا تُشغّل حاوية نشر في كل commit على فرع feature في حين أنك تنشر فقط من main؟

الشروط المدمجة الشائعة:

  • branch 'main' — يطابق اسم الفرع (يدعم الـ glob: 'release/*').
  • tag 'v*' — يطابق وسم Git.
  • environment name: 'DEPLOY_ENV', value: 'production' — يفحص متغير بيئة.
  • expression { return params.RUN_INTEGRATION_TESTS } — تعبير Groovy عشوائي يُعيد قيمة boolean.
  • changeRequest() — صحيح عندما يكون البناء Pull Request.
  • not { branch 'main' } — نفي.
  • allOf { branch 'main'; environment name: 'CI', value: 'true' } — AND منطقي.
  • anyOf { branch 'main'; branch 'staging' } — OR منطقي.
pipeline { agent none environment { REGISTRY = 'registry.example.com' IMAGE = "${REGISTRY}/myapp" } stages { stage('Build & Push Image') { agent { docker { image 'docker:24-dind' } } steps { sh "docker build -t ${IMAGE}:${env.BUILD_NUMBER} ." withCredentials([usernamePassword( credentialsId: 'registry-creds', usernameVariable: 'USER', passwordVariable: 'PASS' )]) { sh "echo $PASS | docker login ${REGISTRY} -u $USER --password-stdin" sh "docker push ${IMAGE}:${env.BUILD_NUMBER}" } } } stage('Deploy to Staging') { agent { label 'deploy' } when { anyOf { branch 'main' branch 'release/*' } } steps { sh "./scripts/deploy.sh staging ${IMAGE}:${env.BUILD_NUMBER}" } } stage('Deploy to Production') { agent { label 'deploy' } when { allOf { branch 'main' tag pattern: 'v\\d+\\.\\d+\\.\\d+', comparator: 'REGEXP' } } steps { timeout(time: 5, unit: 'MINUTES') { input message: 'Deploy to production?', ok: 'Ship it' } sh "./scripts/deploy.sh production ${IMAGE}:${env.BUILD_NUMBER}" } post { success { slackSend channel: '#deployments', color: 'good', message: "Deployed ${IMAGE}:${env.BUILD_NUMBER} to production" } failure { slackSend channel: '#deployments', color: 'danger', message: "Production deploy FAILED for ${IMAGE}:${env.BUILD_NUMBER}" } } } } post { always { echo 'Pipeline complete.' } cleanup { deleteDir() } } }

متغيرات البيئة والخيارات

يُكمل تعليمتان إضافيتان القواعد النحوية التصريحية. يحقن بلوك environment المتغيرات في env لنطاق الـ Pipeline أو المرحلة. يضبط بلوك options سلوكيات مستوى الـ Pipeline:

  • timeout(time: 30, unit: 'MINUTES') — أنهِ الـ Pipeline بالكامل إذا تجاوزت الميزانية الزمنية.
  • retry(3) — أعد المحاولة عند الفشل (استخدم retry على مستوى المرحلة للتحكم الدقيق).
  • buildDiscarder(logRotator(numToKeepStr: '10')) — يمنع نفاد مساحة القرص بتحديد عدد البنات المحتفظ بها.
  • disableConcurrentBuilds() — يضمن تشغيل نسخة واحدة فقط من هذه الـ Pipeline في وقت واحد؛ ضروري لأنابيب النشر.
  • skipDefaultCheckout() — يُعطّل السحب التلقائي للكود المصدري لتتحكم فيه صراحةً بـ checkout scm.
فخ إنتاجي — غياب buildDiscarder: خدمة ذات وتيرة commits عالية بلا تدوير للسجلات ستملأ قرص controller Jenkins في غضون أيام. يجب أن يُعرّف كل Jenkinsfile كحد أدنى: buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '5')).

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

فهم كيفية الفشل بالقواعد النحوية لا يقل أهمية عن معرفة الاستخدام الصحيح:

  • أخطاء وقت التحليل: تُكتشف أخطاء Groovy النحوية والمتغيرات غير المعلنة والتعليمات المكتوبة في غير مكانها حين يُحمّل Jenkins الـ Jenkinsfile — قبل تخصيص أي منفّذ. استخدم دائمًا ميزة Replay للتكرار السريع دون الحاجة لـ commit.
  • انتهاء مهلة تخصيص الـ agent: إن لم يتوفر agent مطابق، سيضع Jenkins البناء في الانتظار إلى أجل غير مسمى ما لم تضبط options { timeout(...) }.
  • تقييم when قبل الـ agent: افتراضيًا يُقيَّم when قبل تخصيص agent المرحلة — جيد للكفاءة. إن احتجت الـ agent جاهزًا مسبقًا (مثلًا لفحص workspace)، أضف beforeAgent false داخل بلوك when.
  • stash/unstash بين agents: يُخزَّن الـ stash على الـ controller؛ للـ artifacts الكبيرة (مئات الميجابايت) يصبح عنق الزجاجة. استخدم مخزن artifacts خارجيًا (Artifactory، S3، Nexus) ومرّر الإحداثيات فقط بين المراحل.

القواعد الصارمة للـ Declarative Pipeline ليست قيدًا — بل هي العقد الذي يجعل ملفات Jenkinsfile قابلة للتدقيق والمقارنة والصيانة عبر مئات المستودعات. أتقن هذه القواعد قبل اللجوء إلى مخارج الـ Scripted Pipeline.