أتمتة التحقق من طلبات السحب (Pull Requests) في AWS لتسهيل مراجعات الكود

دقائق القراءة: 9
مع نمو المشاريع البرمجية وتزايد وتيرة دفع المطورين للكود، يزداد احتمال حدوث مشكلات في طلبات السحب (Pull Requests). قد ينجم ذلك عن دمج طلب سحب قبل آخر، أو تقدم الفرع المستهدف (destination branch) بعدة التزامات (commits) مما يؤدي إلى تعارضات، أو ربما لأن مطوراً لم يقم بتشغيل الاختبارات قبل الدفع، فقام بإدخال خطأ غير مقصود في جزء آخر من المنتج. والقائمة تطول.
لا ينبغي أن تكون هذه المشكلات عائقاً كبيراً، فكل مؤسسة لديها سير عمل (workflow) لمراجعات الكود (Code Reviews)، أليس كذلك؟ ومع ذلك، فإن هذه المراجعات تستغرق وقتاً طويلاً، خاصةً لطلبات السحب التي تحتوي على أخطاء وليست جاهزة للمراجعة. يمكننا بالطبع بناء واختبار الكود يدوياً في كل مرة قبل مراجعة الكود الفعلية، ولكن بعد نقطة معينة، يصبح الأتمتة هي الحل الأمثل.
تخيل مؤسسة متوسطة الحجم تتعامل مع 100-150 طلب سحب أسبوعياً. إن الوقت الذي يُهدر في التحقق المتكرر من هذه الطلبات يمكن أن يُستثمر في تطوير مجموعة كاملة من الميزات الجديدة للشركة. إذاً، دعونا نستكشف كيف يمكننا تحقيق هذه الميزات من خلال الأتمتة!

المتطلبات الأساسية

لتحقيق أقصى استفادة من هذا الدليل، يجب أن تكون لديك دراية بخدمات AWS. نفترض أنك تعرف كيفية إنشاء وإدارة دوال Lambda Functions، ومشاريع CodeBuild Projects، وأحداث CloudWatch Events، وأدوار IAM Roles، وأنك تستخدم CodeCommit لإدارة إصدارات الكود الخاص بك.

فهم البنية المعمارية للحل

دعونا نفهم، على مستوى عالٍ، كيف سنتعامل مع هذا المشروع لضمان التحقق الفعال من طلبات السحب.
مخطط تدفق البنية المعمارية لأتمتة التحقق من طلبات السحب في AWS

آلية سير العمل خطوة بخطوة

لنستعرض سير العمل بمزيد من التفصيل:

  1. عند إنشاء طلب سحب جديد (Pull Request) أو تحديث طلب سحب موجود في مستودع الكود الخاص بك (Code Repository).
  2. يتم تفعيل حدث CloudWatch Event الذي يراقب المستودع، ويرسل البيانات ذات الصلة إلى دالة Lambda.
  3. تقوم دالة Lambda هذه بمهمتين رئيسيتين:
    • تشغيل مشروع CodeBuild Project لبناء أحدث التزام (commit) وتشغيل الاختبارات.
    • إضافة تعليق مخصص (custom message) على طلب السحب الخاص بك.
  4. بعد انتهاء CodeBuild من عملية البناء والاختبار، يتم تفعيل حدث CloudWatch Event آخر يرسل نتائج البناء إلى دالة Lambda ثانية.
  5. تقوم هذه الدالة بإضافة تعليق على طلب السحب يتضمن نتائج البناء.

الآن بعد أن فهمنا الصورة الكبيرة، دعنا نبدأ في التنفيذ!

إعداد تطبيقنا النموذجي

لتبسيط الشرح، قمت بإنشاء تطبيق Node.js بسيط مكتوب بلغة TypeScript. كل ما تفعله مرحلة البناء (build phase) هو تجميع الملف app.ts إلى app.js. يمكنك العثور على رابط المستودع (repo) هنا – قم باستنساخه (clone) واستخدامه إذا أردت متابعة الخطوات. جميع الأكواد ذات الصلة المستخدمة في هذه المقالة موجودة هناك.
لقطة شاشة لتطبيق Express بسيط يستخدم في الشرح
أمر البناء (build command) المستخدم هنا هو ببساطة tsc app.ts، ولكن يمكنك تعديله ليتناسب مع أمر البناء الخاص بمشروعك. وللحفاظ على البساطة، لم أقم بتضمين حالات اختبار (test cases). يمكنك ربطها بـ test في قسم scripts ضمن ملف package.json ومتابعة الشرح.

إعداد مشروع CodeBuild

الخطوة الأولى هي إعداد مشروع CodeBuild أساسي لمستودع الكود الخاص بك. للقيام بذلك، اتبع الإرشادات التالية:

  • قم بتعيين المصدر (source) ليكون مستودع CodeCommit الخاص بك.
  • يجب أن يكون نوع المرجع (Reference type) هو branch.
  • يجب أن تكون البيئة (Environment) متوافقة مع متطلبات مشروعك.
  • يجب عليك استخدام ملف buildspec.
  • بقية الإعدادات يمكن أن تكون افتراضية.

تأكد من وجود ملف buildspec.yml في المجلد الجذر لمستودعك.

ملاحظة هامة: قد يختلف هذا الإعداد إذا كنت تتعامل مع مستودع أحادي (MonoRepo). في هذه الحالة، قد يكون لديك ملفات buildspec.yml منفصلة لكل تطبيق، وسيتعين عليك تمرير مسار ملف buildspec بشكل انتقائي كمتغير بيئة (environment variable) بناءً على الملفات التي تم تغييرها داخل الالتزام (commit). لدينا إعداد مشابه في مؤسستنا، ونحن راضون جداً عن النتائج حتى الآن!

 version: 0.2
 phases:
   install:
     commands:
       - n 12.12
       - curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
       - echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
       - apt update
       - apt install yarn
       - yarn install
   # pre_build:
   #   commands:
   #     - yarn test
   build:
     commands:
       - yarn build

شرح ملف buildspec.yml

يقوم ملف buildspec.yml بتمرير أوامر وقت التشغيل (runtime commands) لكل عملية بناء إلى مشروع CodeBuild الخاص بنا. وإليك ما يفعله بالتحديد:

  • يثبت Node.js بالإصدار 12.12.0.
  • يثبت Yarn.
  • يثبت تبعيات المشروع (project’s dependencies).
  • yarn test: (يقوم بتشغيل حالات الاختبار الخاصة بنا. لا توجد حالات اختبار هنا، ولكن يمكنك إلغاء التعليق على هذا القسم إذا كنت بحاجة إليه).
  • yarn build: (يقوم ببناء مشروعنا).

دوال Lambda

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

  1. دالة TriggerCodebuildStart: ستتلقى هذه الدالة حدث CloudWatch Event (الذي سنقوم بإعداده لاحقاً) وستقوم بتشغيل مشروع CodeBuild الخاص بنا لبدء عملية بناء جديدة. كما أنها ستنشر تعليقاً “Build Started” يتضمن الطابع الزمني (timestamp) ورابطاً مباشراً لسجلات البناء (build logs) في قسم التعليقات الخاص بطلب السحب (PR).
  2. دالة TriggerCodebuildResult: ستتلقى هذه الدالة حدث CloudWatch Event من مشروع CodeBuild الخاص بنا، والذي سيحتوي على نتائج البناء. وستقوم أيضاً بنشر تعليق “CodeBuild Results” يتضمن الطابع الزمني ورابطاً مباشراً لسجلات البناء في قسم التعليقات الخاص بطلب السحب.

إليكم الكود الخاص بالدالتين، هذا ما كنتم تنتظرونه، أليس كذلك!

 const AWS = require ( 'aws-sdk' );
 const codecommit = new AWS.CodeCommit();
 const codebuild = new AWS.CodeBuild();
 
 exports .handler = async (event) => {
   try {
     console .log( 'Received Event: ' , event);
     const { destinationCommit } = event.detail;
     const { sourceCommit } = event.detail;
     const { pullRequestId } = event.detail;
     const pullRequestName = event.detail.title;
     const sourceBranch = event.detail.sourceReference.split( '/' ).pop();
 
     const triggerCodeBuildParameters = {
       sourceBranch,
       sourceCommit,
       destinationCommit,
       pullRequestId,
       pullRequestName
     };
 
     const codeBuildResult = await triggerCodebuild(triggerCodeBuildParameters);
     const buildId = codeBuildResult.build.id;
 
     const postBuildStartedCommentOnPRParameters = {
       sourceCommit,
       destinationCommit,
       pullRequestId,
       buildId
     }
     await postBuildStartedCommentOnPR(postBuildStartedCommentOnPRParameters);
 
     return { statusCode : 200 };
   } catch (error) {
     console .log( 'An Error Occured' , error);
     return { error };
   }
 };
 
 async function postBuildStartedCommentOnPR ( postBuildStartedCommentOnPRParameters ) {
   const { sourceCommit, destinationCommit, pullRequestId, buildId } = postBuildStartedCommentOnPRParameters;
   const logLink = `https:// ${process.env.REGION} .console.aws.amazon.com/codesuite/codebuild/projects/ValidatePullRequest/build/ ${buildId} ` ;
 
   const parameters = {
     afterCommitId : sourceCommit,
     beforeCommitId : destinationCommit,
     content : `Build For Validating The Pull Request has been started. Timestamp: ** ${ Date .now()} ** Check [CodeBuild Logs]( ${logLink} )` ,
     pullRequestId,
     repositoryName : process.env.REPOSITORY_NAME
   };
 
   const request = await codecommit.postCommentForPullRequest(parameters);
   const promise = request.promise();
 
   return promise.then(
     ( data ) => data,
     ( error ) => {
       console .log( 'Error In Commenting To Pull Request' , error);
       throw new Error (error);
     }
   );
 }
 
 async function triggerCodebuild ( triggerCodeBuildParameters ) {
   const { sourceBranch, sourceCommit, destinationCommit, pullRequestId, pullRequestName } = triggerCodeBuildParameters;
   console .log( `Triggering Codebuild, Branch: ${sourceBranch} ` );
 
   const parameters = {
     projectName : process.CODEBUILD_PROJECT,
     sourceVersion : `refs/heads/ ${sourceBranch} ^{ ${sourceCommit} }` ,
     environmentVariablesOverride : [
       {
         name : 'pullRequestId' ,
         value : pullRequestId,
         type : 'PLAINTEXT'
       },
       {
         name : 'sourceCommit' ,
         value : sourceCommit,
         type : 'PLAINTEXT'
       },
       {
         name : 'destinationCommit' ,
         value : destinationCommit,
         type : 'PLAINTEXT'
       },
       {
         name : 'pullRequestName' ,
         value : pullRequestName,
         type : 'PLAINTEXT'
       }
     ]
   };
 
   const request = await codebuild.startBuild(parameters);
   const promise = request.promise();
 
   return promise.then(
     ( data ) => data,
     ( error ) => {
       console .log( 'Error In Starting Codebuild' , error);
       throw new Error (error);
     }
   );
 }
 const AWS = require ( 'aws-sdk' );
 const codecommit = new AWS.CodeCommit();
 
 exports .handler = async (event) => {
   try {
     console .log( 'Event' , event);
     const parameters = await getParameters(event);
     console .log( 'Parameters For Comment:' , parameters);
     await commentCodeBuildResultOnPR(parameters);
     return { statusCode : 200 };
   } catch (error) {
     console .log( 'An Error Occured' , error);
     return { error };
   }
 };
 
 async function getParameters ( event ) {
   try {
     const buildId = event.detail[ 'build-id' ].split( '/' )[ 1 ];
     const buildStatus = event.detail[ 'build-status' ];
     const environmentVariableList = event.detail[ 'additional-information' ].environment[ 'environment-variables' ];
     let afterCommitId, beforeCommitId, content, pullRequestId;
 
     for (element of environmentVariableList) {
       if (element.name === 'pullRequestId' ) pullRequestId = element.value;
       if (element.name === 'sourceCommit' ) afterCommitId = element.value;
       if (element.name === 'destinationCommit' ) beforeCommitId = element.value;
       if (element.name === 'pullRequestName' ) pullRequestName = element.value;
     }
 
     const logLink = `https:// ${process.env.REGION} .console.aws.amazon.com/codesuite/codebuild/projects/ValidatePullRequest/build/ ${buildId} ` ;
     content = `Build Result: ** ${buildStatus} ** Timestamp: ** ${ Date .now()} ** Check [CodeBuild Logs]( ${logLink} )` ;
 
     return { afterCommitId, beforeCommitId, content, pullRequestId, repositoryName : process.env.REPOSITORY_NAME };
   } catch (error) {
     throw error;
   }
 }
 
 async function commentCodeBuildResultOnPR ( parameters ) {
   const request = await codecommit.postCommentForPullRequest(parameters);
   const promise = request.promise();
 
   return promise.then(
     ( data ) => data,
     ( error ) => {
       console .log( 'Error In Commenting To Pull Request' , error);
       throw new Error (error);
     }
   );
 }

ستحتاج إلى ملء متغيرات البيئة (environment variables) المناسبة قبل استخدام هذه الدوال. بقراءة الكود مرة واحدة، ستعرف ما يجب فعله. في حال احتجت إلى الرجوع إلى الوثائق، يمكنك زيارة هذه الروابط هنا و هناك.

تكوين أحداث CloudWatch

لربط جميع المكونات معاً، سنقوم الآن بتكوين أحداث CloudWatch Events. سننشئ حدثين رئيسيين:

  1. حدث لاستقبال بيانات الالتزامات الجديدة (new commit data) من مستودع الكود الخاص بنا.
  2. حدث آخر لاستقبال نتائج CodeBuild.

ستكون الأهداف (targets) لهذه الأحداث هي دوال Lambda الخاصة بنا. سأرفق لقطات شاشة كاملة للصفحات لتسهيل فهم المراجع.
لقطة شاشة لصفحة CloudWatch Events مع التركيز على الأحداث الخضراء
ركز على الأحداث المظللة باللون الأخضر.
لقطة شاشة لإعداد حدث CloudWatch لبدء عملية بناء CodeBuild
تأكد من استبدال ARN الخاص بمشروع CodeBuild الخاص بك هنا.
لقطة شاشة لإعداد حدث CloudWatch لنتائج بناء CodeBuild
لقد اخترت تشغيل دالة Lambda عند أحداث FAILED (فشل) و SUCCEEDED (نجاح). ولكن يمكنك أيضاً تحديد “All Events” (جميع الأحداث) وتخصيصها لتناسب احتياجاتك. والآن، حان وقت التنفيذ!

المشاهدة العملية: التنفيذ!

إذا وصلت إلى هذه النقطة، فأنت رائع حقاً! بعد كل هذا العمل، دعنا نرى ما حققناه. سنقوم بإنشاء طلبي سحب (Pull Requests): أحدهما يعمل بشكل صحيح، والآخر يحتوي على خطأ بناء (build error) متعمد.

طلب سحب خالٍ من الأخطاء

لقطة شاشة لطلب سحب جديد يعمل بشكل صحيح
لقطة شاشة لنتائج بناء ناجحة لطلب سحب
ممتاز! لقد تم البناء بنجاح.

طلب سحب مع أخطاء متعمدة

الآن، دعنا ننشئ طلب سحب يحتوي على أخطاء. لاحظ هنا، بدلاً من app.get، كتبنا ap.get. إنه خطأ متعمد وبسيط، لكنه سيؤدي الغرض المطلوب حالياً.
لقطة شاشة لطلب سحب يحتوي على خطأ بناء متعمد
لقطة شاشة لرسالة بناء فاشلة في طلب السحب
رسالة بناء فاشلة، ومراجعون سعداء! لم يضطروا إلى سحب الفرع (checkout the branch) واختباره يدوياً.
لقطة شاشة لسجلات بناء فاشلة في CodeBuild
أيها المطورون، كالعادة، السجلات متاحة لكم لمراجعة الأخطاء!

خاتمة وتطلعات مستقبلية

للارتقاء بهذا الحل خطوة إلى الأمام، يمكنك دمج إشعارات فورية. على سبيل المثال، يمكن تشغيل استدعاء API إلى رابط webhook URL الخاص بـ Slack لإرسال تنبيه فوري إلى قناة مخصصة في حالة فشل البناء (build failures). أليس هذا رائعاً؟
تجدر الإشارة إلى أن هذا الإعداد بسيط نسبياً، وقد تكون المشاريع الواقعية أكثر تعقيداً. فمثلاً، قد تحتوي المستودعات الأحادية (MonoRepo) على تطبيقات متعددة، وتختلف عمليات البناء والاختبار لكل تطبيق منها. في هذه الحالات، لن يكون تشغيل جميع الاختبارات في كل مرة أمراً فعالاً، بل سيكون مكلفاً ويسبب ارتباكاً. قد تحتاج إلى تشغيل عمليات البناء والاختبار بشكل انتقائي بناءً على الملفات التي تم الالتزام بها (committed files) والتطبيقات المتأثرة.
ومع ذلك، توفر هذه المقالة أساساً متيناً يمكنك البناء عليه وتوسيعه بلا شك. شكراً لقراءتكم! إذا احتجت إلى أي مساعدة بخصوص هذا الموضوع، فلا تتردد في التواصل معي عبر LinkedIn. أتطلع إلى تقديم المساعدة بكل الطرق الممكنة.

الخلاصة التقنية

تُظهر هذه المقالة بوضوح كيف يمكن لأتمتة التحقق من طلبات السحب في AWS أن تُحدث ثورة في سير عمل مراجعة الكود. من خلال الاستفادة من قوة AWS Lambda لتنسيق المهام، و AWS CodeBuild لتنفيذ عمليات البناء والاختبار، و Amazon CloudWatch Events كآلية تشغيل، يمكن للفرق تقليل التدخل اليدوي بشكل كبير. هذا لا يقتصر فقط على توفير الوقت الثمين للمطورين والمراجعين، بل يضمن أيضاً جودة كود أعلى عن طريق اكتشاف الأخطاء مبكراً في دورة التطوير. إن القدرة على تقديم ملاحظات فورية على طلبات السحب تعزز ثقافة التطوير المستمر (Continuous Development) وتسرع من وتيرة التسليم، مما يجعلها استراتيجية لا غنى عنها لأي فريق تطوير حديث يسعى للتميز في بيئة سحابية.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *