كيفية إعداد التكامل المستمر لمستودع Monorepo باستخدام Buildkite
مقدمة: لماذا يُعد Monorepo خيارًا قويًا؟
يشير مفهوم Monorepo إلى استخدام مستودع Git واحد لاحتواء عدة مشاريع أو خدمات ضمن قاعدة شيفرة موحدة. هذا الأسلوب يمنح الفرق مرونة كبيرة في إدارة الواجهات الخلفية والخدمات والواجهات الأمامية من مكان واحد، كما يقلل من تعقيد تتبع التغييرات عبر مستودعات متعددة ومزامنة الاعتمادات بينها.
لكن هذا التنظيم لا يخلو من تحديات، خصوصًا عند بناء نظام Continuous Integration. فعندما يتغير جزء محدد داخل المستودع، نحتاج إلى معرفة المشروع المتأثر فقط، ثم تشغيل البناء أو الاختبارات أو النشر المناسب له دون بقية المشاريع. هنا تبرز قوة Buildkite عند دمجه مع GitHub وAWS.
في هذا الدليل العملي ستتعرف على طريقة احترافية لإعداد تكامل مستمر لمستودع Monorepo بحيث:
- يتم إنشاء بنية
CIقابلة للتوسع داخلAWS EC2. - تُفعَّل خطوط التنفيذ تلقائيًا من خلال
GitHub Webhooks. - يتم اكتشاف المجلدات المتغيرة داخل المستودع وتشغيل المسارات المناسبة فقط.
- تُؤتمت أغلب الخطوات عبر سكربتات
Bashقابلة لإعادة الاستخدام.

المتطلبات الأساسية قبل البدء
- حساب على
AWSلنشر وكلاءBuildkite Agents. - إعداد
AWS CLIوربطه بالحساب بشكل صحيح. - حساب على
Buildkiteلإنشاء خطوط التكامل المستمر. - حساب على
GitHubلاستضافة الشيفرة المصدرية.
قبل التطبيق، تأكد من أن بيئة العمل لديك تسمح بتشغيل أوامر bash، وإنشاء مفاتيح SSH، واستخدام أدوات مثل curl وjq وenvsubst.
فهم بنية العمل في Buildkite
يعتمد Buildkite على مفهومين رئيسيين:
Pipelines: وهي الحاويات العليا التي تصف سير العمل.Steps: وهي المهام الفردية التي تُنفَّذ داخل كل خط.
في هذا السيناريو سنبني مجموعة من الخطوط:
- خط خاص بطلبات السحب
pull-request. - خط خاص بالدمج
merge. - خطوط فرعية لكل خدمة مثل
foo-serviceوbar-service. - خطوط نشر
deployتُفعَّل عند الحاجة فقط.

آلية العمل عند إنشاء Pull Request
عند إنشاء طلب سحب جديد في GitHub، يتم تشغيل خط pull-request داخل Buildkite. هذا الخط ينفذ أمرًا مثل git diff لتحديد المجلدات التي تغيرت داخل المستودع.
إذا اكتشف النظام تغييرات داخل مشروع فرعي معين، فإنه يُفعّل خط Pull Request المخصص لذلك المشروع فقط. وبذلك لا يتم استهلاك الموارد في بناء خدمات لم تتأثر أصلًا.

آلية العمل بعد الدمج إلى الفرع الرئيسي
بعد نجاح جميع الفحوصات في GitHub ودمج طلب السحب، يتم تشغيل خط merge. هذا الخط يعيد تحليل التغييرات ثم يشغّل خط deploy المناسب لكل خدمة متأثرة.
تبدأ عملية النشر عادة نحو بيئة staging أولًا، ثم يُسمح بالإطلاق إلى production يدويًا بعد التحقق من سلامة النتائج.

الهيكل النهائي للمشروع
لضمان تنظيم واضح، يمكن أن يكون هيكل المشروع على النحو التالي:
├── .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

إعداد بنية 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.



نشر حزمة 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.

بعد ذلك أنشئ الملف 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 وعمليات البناء.



إنشاء خطوط التنفيذ في Buildkite آليًا
بعد تجهيز البنية التحتية، ننتقل إلى إنشاء خطوط التنفيذ. يفضَّل تنفيذ ذلك برمجيًا بدلًا من الإعداد اليدوي حتى تكون البيئة قابلة لإعادة الإنتاج.
أنشئ API Access Token من Buildkite مع الصلاحيات التالية:
write_buildsread_pipelineswrite_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

إعداد ملفات تعريف 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

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


بعد ذلك، فعّل تكامل 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


إعداد خطوات التشغيل الذكية بحسب التغييرات
إنشاء سكربت 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

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

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

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

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