كيفية إعداد التكامل المستمر لمستودع Monorepo باستخدام Buildkite

دقائق القراءة: 10

مقدمة: لماذا يُعد Monorepo خيارًا قويًا؟

يشير مفهوم Monorepo إلى استخدام مستودع Git واحد لاحتواء عدة مشاريع أو خدمات ضمن قاعدة شيفرة موحدة. هذا الأسلوب يمنح الفرق مرونة كبيرة في إدارة الواجهات الخلفية والخدمات والواجهات الأمامية من مكان واحد، كما يقلل من تعقيد تتبع التغييرات عبر مستودعات متعددة ومزامنة الاعتمادات بينها.

لكن هذا التنظيم لا يخلو من تحديات، خصوصًا عند بناء نظام Continuous Integration. فعندما يتغير جزء محدد داخل المستودع، نحتاج إلى معرفة المشروع المتأثر فقط، ثم تشغيل البناء أو الاختبارات أو النشر المناسب له دون بقية المشاريع. هنا تبرز قوة Buildkite عند دمجه مع GitHub وAWS.

في هذا الدليل العملي ستتعرف على طريقة احترافية لإعداد تكامل مستمر لمستودع Monorepo بحيث:

  • يتم إنشاء بنية CI قابلة للتوسع داخل AWS EC2.
  • تُفعَّل خطوط التنفيذ تلقائيًا من خلال GitHub Webhooks.
  • يتم اكتشاف المجلدات المتغيرة داخل المستودع وتشغيل المسارات المناسبة فقط.
  • تُؤتمت أغلب الخطوات عبر سكربتات Bash قابلة لإعادة الاستخدام.

إعداد التكامل المستمر لمستودع مونوربو باستخدام Buildkite وAWS وGitHub

المتطلبات الأساسية قبل البدء

  • حساب على AWS لنشر وكلاء Buildkite Agents.
  • إعداد AWS CLI وربطه بالحساب بشكل صحيح.
  • حساب على Buildkite لإنشاء خطوط التكامل المستمر.
  • حساب على GitHub لاستضافة الشيفرة المصدرية.

قبل التطبيق، تأكد من أن بيئة العمل لديك تسمح بتشغيل أوامر bash، وإنشاء مفاتيح SSH، واستخدام أدوات مثل curl وjq وenvsubst.

فهم بنية العمل في Buildkite

يعتمد Buildkite على مفهومين رئيسيين:

  • Pipelines: وهي الحاويات العليا التي تصف سير العمل.
  • Steps: وهي المهام الفردية التي تُنفَّذ داخل كل خط.

في هذا السيناريو سنبني مجموعة من الخطوط:

  • خط خاص بطلبات السحب pull-request.
  • خط خاص بالدمج merge.
  • خطوط فرعية لكل خدمة مثل foo-service وbar-service.
  • خطوط نشر deploy تُفعَّل عند الحاجة فقط.

مخطط خطوط Buildkite لمستودع Monorepo وتدفق التشغيل بين المسارات المختلفة

آلية العمل عند إنشاء Pull Request

عند إنشاء طلب سحب جديد في GitHub، يتم تشغيل خط pull-request داخل Buildkite. هذا الخط ينفذ أمرًا مثل git diff لتحديد المجلدات التي تغيرت داخل المستودع.

إذا اكتشف النظام تغييرات داخل مشروع فرعي معين، فإنه يُفعّل خط Pull Request المخصص لذلك المشروع فقط. وبذلك لا يتم استهلاك الموارد في بناء خدمات لم تتأثر أصلًا.

مخطط تدفق عمل طلب السحب في Buildkite مع اكتشاف التغييرات داخل Monorepo

آلية العمل بعد الدمج إلى الفرع الرئيسي

بعد نجاح جميع الفحوصات في GitHub ودمج طلب السحب، يتم تشغيل خط merge. هذا الخط يعيد تحليل التغييرات ثم يشغّل خط deploy المناسب لكل خدمة متأثرة.

تبدأ عملية النشر عادة نحو بيئة staging أولًا، ثم يُسمح بالإطلاق إلى production يدويًا بعد التحقق من سلامة النتائج.

مخطط تدفق الدمج والنشر التدريجي إلى staging ثم production باستخدام Buildkite

الهيكل النهائي للمشروع

لضمان تنظيم واضح، يمكن أن يكون هيكل المشروع على النحو التالي:

├── .buildkite
│   ├── diff
│   ├── merge.yml
│   ├── pipelines
│   │   ├── deploy.json
│   │   ├── merge.json
│   │   └── pull-request.json
│   └── pull-request.yml
├── bar-service
│   ├── .buildkite
│   │   ├── deploy.yml
│   │   ├── merge.yml
│   │   └── pull-request.yml
│   └── bin
│       └── deploy
├── bin
│   ├── create-pipeline
│   ├── create-secrets-bucket
│   ├── deploy-ci-stack
│   └── stack-config
└── foo-service
    ├── .buildkite
    │   ├── deploy.yml
    │   ├── merge.yml
    │   └── pull-request.yml
    └── bin
        └── deploy

إنشاء المشروع ورفعه إلى GitHub

ابدأ بإنشاء مشروع Git جديد، ثم ادفعه إلى GitHub:

mkdir buildkite-monorepo-example
cd buildkite-monorepo-example
git init
echo node_modules/ > .gitignore
git add .
git commit -m "initialize repository"
git remote add origin <YOUR_GITHUB_REPO_URL>
git push origin master

إنشاء مشروع Git جديد ورفعه إلى GitHub لاستخدامه مع Buildkite

إعداد بنية Buildkite التحتية على AWS

إنشاء سكربتات التشغيل الأساسية

أنشئ مجلد bin وأضف داخله السكربتات التنفيذية المطلوبة:

mkdir bin
cd bin
touch create-pipeline create-secrets-bucket deploy-ci-stack
chmod +x ./*

إنشاء حاوية أسرار ومفاتيح SSH

أضف المحتوى التالي إلى الملف create-secrets-bucket:

#!/bin/bash
set -eou pipefail
CURRENT_DIR=$(pwd)
ROOT_DIR="$( dirname "${BASH_SOURCE[0]}" )/.."
BUCKET_NAME="buildkite-secrets-adikari"
KEY="id_rsa_buildkite"

echo "creating bucket $BUCKET_NAME .."
aws s3 mb s3://$BUCKET_NAME

# Generate SSH Key
ssh-keygen -t rsa -b 4096 -f $KEY -N ''

# Copy SSH Keys to S3 bucket
aws s3 cp --acl private --sse aws:kms $KEY "s3://$BUCKET_NAME/private_ssh_key"
aws s3 cp --acl private --sse aws:kms $KEY.pub "s3://$BUCKET_NAME/public_key.pub"

if [[ "$OSTYPE" == "darwin"* ]]; then
  pbcopy < id_rsa_buildkite.pub
  echo "public key contents copied in clipboard."
else
  cat id_rsa_buildkite.pub
fi

# Move SSH Keys to ~/.ssh directory
mv ./$KEY* ~/.ssh
chmod 600 ~/.ssh/$KEY
chmod 644 ~/.ssh/$KEY.pub
cd $CURRENT_DIR

يقوم هذا السكربت بإنشاء حاوية S3 لتخزين مفاتيح SSH التي يستخدمها Buildkite للوصول إلى مستودع GitHub. كما يُنشئ المفتاحين العام والخاص ويضبط صلاحياتهما بالشكل الصحيح.

بعد تشغيله، تحقق من وجود الحاوية والملفات داخلها، ثم أضف محتوى المفتاح العام id_rsa_buildkite.pub إلى صفحة مفاتيح SSH في GitHub.

تشغيل سكربت إنشاء حاوية S3 ومفاتيح SSH الخاصة بـ Buildkiteالتحقق من وجود مفاتيح SSH داخل حاوية S3 الجديدة على AWSإضافة المفتاح العام SSH إلى إعدادات GitHub لربط Buildkite بالمستودع

نشر حزمة Elastic CI Stack عبر CloudFormation

يوفر فريق Buildkite قالبًا جاهزًا باسم Elastic CI Stack for AWS لإنشاء عنقود خاص من وكلاء Buildkite مع التوسع التلقائي. أضف السكربت التالي إلى الملف bin/deploy-ci-stack:

#!/bin/bash
set -euo pipefail
[ -z $BUILDKITE_AGENT_TOKEN ] && { echo "BUILDKITE_AGENT_TOKEN is not set."; exit 1; }
CURRENT_DIR=$(pwd)
ROOT_DIR="$( dirname "${BASH_SOURCE[0]}" )/.."
PARAMETERS=$(cat ./bin/stack-config | envsubst)
cd $ROOT_DIR

echo "downloading elastic ci stack template.."
curl -s https://s3.amazonaws.com/buildkite-aws-stack/latest/aws-stack.yml -O
aws cloudformation deploy \
  --capabilities CAPABILITY_NAMED_IAM \
  --template-file ./aws-stack.yml \
  --stack-name "buildkite-elastic-ci" \
  --parameter-overrides $PARAMETERS
rm -f aws-stack.yml
cd $CURRENT_DIR

يمكنك الحصول على قيمة BUILDKITE_AGENT_TOKEN من تبويب Agents داخل لوحة Buildkite.

الحصول على Buildkite Agent Token من لوحة التحكم لاستخدامه في النشر

بعد ذلك أنشئ الملف bin/stack-config وضع فيه إعدادات CloudFormation:

BuildkiteAgentToken=$BUILDKITE_AGENT_TOKEN
SecretsBucket=buildkite-secrets-adikari
InstanceType=t2.micro
MinSize=0
MaxSize=3
ScaleUpAdjustment=2
ScaleDownAdjustment=-1

شغّل السكربت التالي لنشر البنية التحتية:

./bin/deploy-ci-stack

قد يستغرق التنفيذ بعض الوقت، ويمكن متابعة التقدم من خلال لوحة AWS CloudFormation. بعد انتهاء النشر، سيتم إنشاء مجموعة Auto Scaling مسؤولة عن تشغيل مثيلات EC2 التي تستضيف وكلاء Buildkite وعمليات البناء.

متابعة نشر قالب CloudFormation الخاص بـ Buildkite على AWSنجاح إنشاء مكونات CloudFormation لتشغيل وكلاء Buildkite تلقائيًامجموعة Auto Scaling في AWS لتشغيل مثيلات EC2 الخاصة بوكلاء Buildkite

إنشاء خطوط التنفيذ في Buildkite آليًا

بعد تجهيز البنية التحتية، ننتقل إلى إنشاء خطوط التنفيذ. يفضَّل تنفيذ ذلك برمجيًا بدلًا من الإعداد اليدوي حتى تكون البيئة قابلة لإعادة الإنتاج.

أنشئ API Access Token من Buildkite مع الصلاحيات التالية:

  • write_builds
  • read_pipelines
  • write_pipelines

ثم تأكد من ضبط المتغير BUILDKITE_API_TOKEN في البيئة.

أضف السكربت التالي إلى الملف bin/create-pipeline:

#!/bin/bash
set -euo pipefail
export SERVICE="."
export PIPELINE_TYPE=""
export REPOSITORY=git@github.com:adikari/buildkite-docker-example.git
CURRENT_DIR=$(pwd)
ROOT_DIR="$( dirname "${BASH_SOURCE[0]}" )/.."
STATUS_CHECK=false
BUILDKITE_ORG_SLUG=adikari
USAGE="USAGE: $(basename "$0") [-s|--service] service_name [-t|--type] pipeline_type
Eg:
  create-pipeline --type pull-request
  create-pipeline --type merge --service foo-service
  create-pipeline --type merge --status-checks
NOTE: BUILDKITE_API_TOKEN must be set in environment
ARGUMENTS:
  -t | --type buildkite pipeline type <merge|pull-request|deploy> (required)
  -s | --service service name (optional, default: deploy root pipeline)
  -r | --repository github repository url (optional, default: buildkite-docker-example)
  -c | --status-checks enable github status checks (optional, default: true)
  -h | --help show this help text"
[ -z $BUILDKITE_API_TOKEN ] && { echo "BUILDKITE_API_TOKEN is not set."; exit 1; }
while [ $# -gt 0 ]; do
  if [[ $1 =~ "--"* ]]; then
    case $1 in
      --help|-h) echo "$USAGE"; exit ;;
      --service|-s) SERVICE=$2 ;;
      --type|-t) PIPELINE_TYPE=$2 ;;
      --repository|-r) REPOSITORY=$2 ;;
      --status-check|-c) STATUS_CHECK=${2:-true} ;;
    esac
  fi
  shift
done
[ -z "$PIPELINE_TYPE" ] && { echo "$USAGE"; exit 1; }
export PIPELINE_NAME=$([ $SERVICE == "." ] && echo "" || echo "$SERVICE-")$PIPELINE_TYPE
BUILDKITE_CONFIG_FILE=.buildkite/pipelines/$PIPELINE_TYPE.json
[ ! -f "$BUILDKITE_CONFIG_FILE" ] && { echo "Invalid pipeline type: File not found $BUILDKITE_CONFIG_FILE"; exit; }
BUILDKITE_CONFIG=$(cat $BUILDKITE_CONFIG_FILE | envsubst)
if [ $STATUS_CHECK == "false" ]; then
  pipeline_settings='{ "provider_settings": { "trigger_mode": "none" } }'
  BUILDKITE_CONFIG=$((echo $BUILDKITE_CONFIG; echo $pipeline_settings) | jq -s add)
fi
cd $ROOT_DIR
echo "Creating $PIPELINE_TYPE pipeline.."
RESPONSE=$(curl -s POST "https://api.buildkite.com/v2/organizations/$BUILDKITE_ORG_SLUG/pipelines" \
  -H "Authorization: Bearer $BUILDKITE_API_TOKEN" \
  -d "$BUILDKITE_CONFIG")
[[ "$RESPONSE" == *errors* ]] && { echo $RESPONSE | jq; exit 1; }
echo $RESPONSE | jq
WEB_URL=$(echo $RESPONSE | jq -r '.web_url')
WEBHOOK_URL=$(echo $RESPONSE | jq -r '.provider.webhook_url')
echo "Pipeline url: $WEB_URL"
echo "Webhook url: $WEBHOOK_URL"
echo "$PIPELINE_NAME pipeline created."
cd $CURRENT_DIR
unset REPOSITORY
unset PIPELINE_TYPE
unset SERVICE
unset PIPELINE_NAME

عرض المساعدة الخاصة بسكربت create-pipeline لإنشاء خطوط Buildkite تلقائيًا

إعداد ملفات تعريف Pipelines

ملف pull-request.json

{
  "name": "$PIPELINE_NAME",
  "description": "Pipeline for $PIPELINE_NAME pull requests",
  "repository": "$REPOSITORY",
  "default_branch": "",
  "steps": [
    {
      "type": "script",
      "name": ":buildkite: $PIPELINE_TYPE",
      "command": "buildkite-agent pipeline upload $SERVICE/.buildkite/$PIPELINE_TYPE.yml"
    }
  ],
  "cancel_running_branch_builds": true,
  "skip_queued_branch_builds": true,
  "branch_configuration": "!master",
  "provider_settings": {
    "trigger_mode": "code",
    "publish_commit_status_per_step": true,
    "publish_blocked_as_pending": true,
    "pull_request_branch_filter_enabled": true,
    "pull_request_branch_filter_configuration": "!master",
    "separate_pull_request_statuses": true
  }
}

ملف merge.json

{
  "name": "$PIPELINE_NAME",
  "description": "Pipeline for $PIPELINE_NAME merge",
  "repository": "$REPOSITORY",
  "default_branch": "master",
  "steps": [
    {
      "type": "script",
      "name": ":buildkite: $PIPELINE_TYPE",
      "command": "buildkite-agent pipeline upload $SERVICE/.buildkite/$PIPELINE_TYPE.yml"
    }
  ],
  "cancel_running_branch_builds": true,
  "skip_queued_branch_builds": true,
  "branch_configuration": "master",
  "provider_settings": {
    "trigger_mode": "code",
    "build_pull_requests": false,
    "publish_blocked_as_pending": true,
    "publish_commit_status_per_step": true
  }
}

ملف deploy.json

{
  "name": "$PIPELINE_NAME",
  "description": "Pipeline for $PIPELINE_NAME deploy",
  "repository": "$REPOSITORY",
  "default_branch": "master",
  "steps": [
    {
      "type": "script",
      "name": ":buildkite: $PIPELINE_TYPE",
      "command": "buildkite-agent pipeline upload $SERVICE/.buildkite/$PIPELINE_TYPE.yml"
    }
  ],
  "provider_settings": {
    "trigger_mode": "none"
  }
}

إنشاء الخطوط الأساسية وربطها مع GitHub Webhooks

بعد إعداد ملفات التعريف، نفّذ الأوامر التالية لإنشاء الخطوط الأساسية:

./bin/create-pipeline --type pull-request --status-checks
./bin/create-pipeline --type merge --status-checks

إنشاء خطوط pull-request وmerge في Buildkite عبر السكربت الآلي

انسخ قيمة Webhook URL الناتجة من المخرجات، ثم انتقل إلى إعدادات المستودع في GitHub وأضف تكامل Webhook لكل من خطي pull-request وmerge. اختر الحدث Just the push event.

إعداد GitHub Webhook لربط المستودع مع خطوط Buildkite الأساسيةظهور خطوط Buildkite الجديدة داخل لوحة التحكم بعد الإنشاء

بعد ذلك، فعّل تكامل GitHub من داخل Buildkite للسماح بإرسال تحديثات الحالة إلى المستودع. يكفي تنفيذ هذه الخطوة مرة واحدة على مستوى الحساب.

إعداد تكامل GitHub داخل Buildkite لإرسال حالة الفحوصات إلى المستودع

إنشاء خطوط الخدمات الفرعية

الآن أنشئ خطوط كل خدمة. هذه الخطوط سيتم تشغيلها ديناميكيًا، لذلك لا تحتاج إلى Webhook منفصل:

# foo service pipelines
./bin/create-pipeline --type pull-request --service foo-service
./bin/create-pipeline --type merge --service foo-service
./bin/create-pipeline --type deploy --service foo-service

# bar service pipelines
./bin/create-pipeline --type pull-request --service bar-service
./bin/create-pipeline --type merge --service bar-service
./bin/create-pipeline --type deploy --service bar-service

إنشاء خطوط الخدمات الفرعية foo-service وbar-service داخل Buildkiteاكتمال قائمة خطوط Buildkite بعد إضافة جميع خطوط الخدمات والنشر

إعداد خطوات التشغيل الذكية بحسب التغييرات

إنشاء سكربت diff

هذا السكربت مسؤول عن مقارنة التغييرات وتحديد الملفات أو المجلدات المتأثرة:

#!/bin/bash
[ $# -lt 1 ] && { echo "argument is missing."; exit 1; }
COMMIT=$1
BRANCH_POINT_COMMIT=$(git merge-base master $COMMIT)
echo "diff between $COMMIT and $BRANCH_POINT_COMMIT"
git --no-pager diff --name-only $COMMIT..$BRANCH_POINT_COMMIT

ثم امنحه صلاحية التنفيذ:

chmod +x .buildkite/diff

ملف .buildkite/pull-request.yml

steps:
  - label: "Triggering pull request pipeline"
    plugins:
      chronotc/monorepo-diff#v1.1.1:
        diff: ".buildkite/diff ${BUILDKITE_COMMIT}"
        wait: false
        watch:
          - path: "foo-service"
            config:
              trigger: "foo-service-pull-request"
          - path: "bar-service"
            config:
              trigger: "bar-service-pull-request"

يعتمد هذا الإعداد على الإضافة chronotc/monorepo-diff لاكتشاف التغييرات ورفع الخطوط المناسبة تلقائيًا.

ملف .buildkite/merge.yml

steps:
  - label: "Triggering merge pipeline"
    plugins:
      chronotc/monorepo-diff#v1.1.1:
        diff: "git diff --name-only HEAD~1"
        wait: false
        watch:
          - path: "foo-service"
            config:
              trigger: "foo-service-merge"
          - path: "bar-service"
            config:
              trigger: "bar-service-merge"

تهيئة خطوط خدمة foo-service

ملف foo-service/.buildkite/pull-request.yml

steps:
  - label: "Foo service pull request"
    command:
      - "echo linting"
      - "echo testing"

يمكنك هنا استبدال أوامر echo بأوامر حقيقية مثل npm test أو npm run lint بحسب تقنية المشروع.

ملف foo-service/.buildkite/merge.yml

steps:
  - label: "Run sanity checks"
    command:
      - "echo linting"
      - "echo testing"

  - label: "Deploy to staging"
    trigger: "foo-deploy"
    build:
      env:
        STAGE: "staging"

  - wait

  - block: ":rocket: Release to Production"

  - label: "Deploy to production"
    trigger: "foo-deploy"
    build:
      env:
        STAGE: "production"

هذا التصميم ممتاز من منظور الاستقرار؛ إذ يفصل بين التحقق الأولي والنشر إلى staging ثم يسمح بإطلاق production يدويًا لتقليل المخاطر.

ملف foo-service/.buildkite/deploy.yml

steps:
  - label: "Deploying foo service to ${STAGE}"
    command: "./foo-service/bin/deploy ${STAGE}"

سكربت النشر foo-service/bin/deploy

#!/bin/bash
set -euo pipefail
STAGE=$1
echo "Deploying foo service to $STAGE"

ثم امنحه صلاحية التنفيذ:

chmod +x ./foo-service/bin/deploy

بعد ذلك، كرر الفكرة نفسها لإنشاء ملفات pull-request.yml وmerge.yml وdeploy.yml الخاصة بخدمة bar-service.

اختبار سير العمل بالكامل

بعد اكتمال الإعداد، يمكنك اختبار النظام عمليًا بإنشاء فرع جديد وتعديل ملف داخل foo-service:

git checkout -b change-foo-service
cd foo-service && touch test.txt
echo testing >> test.txt
git add .
git commit -m 'making some change'
git push origin master

اختبار التغييرات داخل foo-service لدفع Buildkite إلى تشغيل الخط المناسب

عند دفع التغييرات إلى GitHub، يجب أن يعمل خط pull-request ثم يفعّل خط foo-service-pull-request. وستظهر حالة الفحوصات داخل GitHub Checks. كما يمكنك تفعيل حماية الفروع Branch Protection لفرض نجاح هذه الفحوصات قبل الدمج.

ظهور حالة فحوصات Buildkite داخل GitHub Checks بعد إنشاء Pull Request

بعد نجاح جميع الفحوصات ودمج طلب السحب، سيبدأ خط merge، ثم يتم اكتشاف التغييرات في foo-service وتشغيل خط foo-service-merge.

تشغيل خط merge في Buildkite بعد دمج طلب السحب في GitHub

لاحقًا سيتوقف الخط عند خطوة الإطلاق إلى الإنتاج، ويمكنك المتابعة يدويًا بالنقر على زر Release to Production بعد التأكد من نجاح النشر إلى staging.

الإطلاق اليدوي إلى production بعد نجاح النشر إلى staging في Buildkite

أفضل الممارسات لتحسين الاعتمادية والأداء

  • اعزل كل خدمة داخل مسار واضح حتى يسهل على أدوات المقارنة اكتشاف التغييرات بدقة.
  • استخدم أوامر فحص واختبار حقيقية بدل أوامر تجريبية مثل echo.
  • فعّل حماية الفروع في GitHub لفرض نجاح الفحوصات قبل الدمج.
  • احتفظ بالسكربتات داخل المستودع حتى تصبح بنية CI/CD جزءًا من الشيفرة نفسها.
  • فكّر في استخدام إضافات مثل buildkite-docker-compose-plugin لعزل بيئات البناء داخل حاويات Docker.

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

يوفر الجمع بين Buildkite وGitHub وAWS حلًا قويًا ومرنًا لبناء نظام Continuous Integration مخصص لمشاريع Monorepo. الميزة الأهم هنا ليست فقط الأتمتة، بل القدرة على تشغيل المسارات المتأثرة بالتغييرات دون هدر الموارد على بقية الخدمات. هذا النهج مناسب جدًا للفرق التي تدير عدة خدمات داخل مستودع واحد وتبحث عن توازن بين السرعة، وقابلية التوسع، والتحكم الدقيق في النشر إلى البيئات المختلفة.

اترك تعليقاً

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