مقدمة
هندسة البرمجيات هي مجال سريع الخطى وتنافسي. إن طرح منتجاتك للمستخدمين بشكل أسرع سيمنحك ميزة تفوق على منافسيك. على الجانب المشرق، تتوفر أفضل الممارسات في الصناعة لمساعدة الشركات على الحصول على فرص متكافئة.
التكامل المستمر والتطوير المستمر (CICD) هو مثال على استراتيجية تستفيد من أفضل الممارسات في الصناعة لمنح الشركات ميزة في هذا المجال التنافسي.
يتيح GitHub، وهو مستودع قائم على الويب مع Git (أداة التحكم في الإصدار)، لمطوري البرمجيات والمهندسين والمعماريين تنفيذ CI/CD. التطوير المستمر (CD) هو ممارسة أتمتة عمليات البناء والاختبار والنشر. يتيح التكامل المستمر (CI) للعديد من الأشخاص التعاون في نفس المشروع، والتحقق من أن الكود يعمل دون القلق بشأن تعارضات الدمج.
تتيح لنا GitHub Actions كتابة خطوات تعمل على أتمتة عمليات البناء والاختبار والنشر.
في هذا البرنامج التعليمي، ستتعلم كيفية إعداد التكامل المستمر باستخدام GitHub Actions. سنبدأ بإعداد مستودع Git لاستضافة الكود الخاص بنا. بعد ذلك، سنقوم بتكوين عملية GitHub CI لمراقبة التغييرات في الكود الخاص بنا، وبدء تشغيل CI runner لتشغيل الاختبارات، وبناء تطبيقنا ونشره على خادم Ubuntu 22.04 الذي يعمل بنظام Nginx.
المتطلبات الأساسية
لمتابعة هذا البرنامج التعليمي، ستحتاج إلى ما يلي:
-
خادم يعمل بنظام Ubuntu 22.04. يمكنك متابعة هذا البرنامج التعليمي من أجل إعداد خادم Ubuntu الأولي, إضافة مستخدم ليس جذراً (non-root)، و تمكين جدار حماية UFW الخاص بنظام Ubuntu.
-
ستحتاج إلى تثبيت Node.js على خادمك، ويفضل أن يكون الإصدار 14 فما فوق. لدينا برنامج تعليمي حول كيفية تثبيت Node.js على Ubuntu.
-
ستحتاج إلى تثبيت برنامج خادم Nginx. لدينا دليل حول كيفية تثبيت Nginx على خادم يعمل بنظام Ubuntu.
-
ستحتاج إلى Docker و Docker Compose مثبتين على جهازك المحلي لتشغيل بيئة تطوير معزولة. يرجى اتباع برنامجنا التعليمي لمعرفة كيفية تثبيت وتشغيل Docker في السحاب.
الآن بعد أن أصبح لدينا كل ما نحتاجه، دعونا نبدأ.
الخطوة 1. استنساخ مستودع المشروع.
سنقوم ببناء هذا البرنامج التعليمي على Reactjs، وهي مكتبة Javascript تعريفية لبناء واجهات المستخدم. إذا كنت ترغب في إعداد مشروع جديد من الصفر، يمكنك استخدام هذا المصدر حول إعداد تطبيق React. للاختصار، سنستخدم نسخة مستنسخة من مستودع React.js الذي قمنا بإعداده بالفعل على GitHub.
التطبيق الذي نقوم باستنساخه هو تطبيق react بسيط مع react-router 6 واختبار تم إجراؤه باستخدام React Testing Library، والتي تمنحنا طرقاً للوصول إلى DOM.
لاستنساخ المستودع، انقر فوق الزر الأخضر وانسخ عنوان URL.

افتح الطرفية في مساحة العمل الخاصة بك، وقم بتشغيل الأمر أدناه لاستنساخ التطبيق:
|
1 |
git clone git@github.com:EspiraMarvin/cicd-tut.git |
بمجرد استنساخ المستودع، انتقل إلى دليل المشروع:
|
1 |
cd cicd-tut |
قم بتشغيل الأمر docker-compose up لبناء التطبيق وتشغيله:
|
1 |
docker-compose up --build --force-recreate |
عند اكتمال العملية، قم بزيارة http://localhost:3000
يجب أن ترى شيئاً مشابهاً لهذا:

الخطوة 2. فهم ملف Node.js.yml.
في هذه الخطوة، سنقوم بتعريف التوجيهات في ملف GitHub Yaml لمساعدتنا في فهم ما يحدث. في المستودع، يوجد دليل .github/workflows يحتوي على node.js.yml ملف، يحتوي على خطوات سير العمل التي يتبعها مشغلو github بعد دفع التغييرات إلى مستودع الكود الخاص بك على GitHub. YAML تُستخدم صيغة YAML لكتابة سير العمل لـ GitHub Actions. تنتهي ملفات YAML بامتداد yaml أو yml.
افتح node.js.yml ملف، يجب أن يبدو كالتالي:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
name: cicd-tut on: push: branches: [ "main" ] pull_request: branches: [ "main" ] jobs: build: # نوع المشغل الذي ستعمل عليه الوظيفة runs-on: self-hosted strategy: matrix: node-version: [16.x] # تمثل الخطوات سلسلة من المهام التي سيتم تنفيذها كجزء من الوظيفة steps: - name: 'checkout' uses: actions/checkout@v3 - name: 'setup node actions' uses: actions/setup-node@v3 with: node-version: "16" cache: 'npm' - run: npm i - run: npm test - run: npm run build # - run: cp -r ~/actions-runner/cicd-react/react-tut-test/react-tut-test/build /var/www/react-cicd |
في وقت كتابة هذا البرنامج التعليمي، كنا نستخدم الإصدار 16 من Node.js 16. الآن، دعنا نفهم سير عمل GitHub Actions:
-
name
name: cicd-tut
اسم سير العمل الخاص بك، سيتم عرض هذا الاسم في مستودعك Actions تبويب.
-
on
|
1 2 3 4 5 |
on: push: branches: [ "main" ] pull_request: branches: [ "main" ] |
on يُستخدم لتحديد الأحداث. يمكن للأحداث تشغيل سير العمل تلقائيًا أو جدولتها لوقت لاحق. يمكن أن تكون الأحداث فردية أو متعددة، كما يمكنك أيضًا تحديد عمليات تشغيل سير العمل لفروع أو علامات أو ملفات معينة. يعمل هذا مثل عامل التصفية.
في ملف YAML الخاص بنا، نقوم بتعريف أحداث متعددة تلقائية، وهي:
-
يتم تشغيل حدث push عندما يتم إرسال الكود إلى مستودع
-
يتم تشغيل حدث pull_request عندما يتم إنشاء طلب سحب على الفرع الرئيسي (main).
نحن نحدد اسم الفرع main من أجل قصر تنفيذ سير العمل على ذلك الفرع فقط. يمكننا أيضًا تحديد الفروع التي سيتم تجاهلها باستخدام علامة ! متبوعة باسم الفرع.
-
jobs
يتكون سير العمل بشكل أساسي من وظيفة واحدة أو عدة وظائف. تعمل هذه الوظائف بالتتابع من الأولى إلى الأخيرة.
|
1 2 3 4 |
jobs: build: # نوع المشغل الذي ستعمل عليه الوظيفة runs-on: self-hosted |
تعمل كل وظيفة في بيئة مشغل (runner) محددة بواسطة runs-on. يمكنك اختيار تشغيل الوظائف إما على مشغلات GitHub المشار إليها بواسطة ubuntu-latest أو مشغل self-hosted مشار إليه بواسطة self-hosted. سيعتمد اختيارك على عدد الوظائف التي تحتاجها. مع المشغلات المستضافة ذاتيًا (self-hosted runners)، سيكون لديك مرونة وتحكم أكبر في الموارد.
في خطوتنا التالية، سنقوم بتشغيل وظائفنا على مشغلات GitHub أولاً، ثم لاحقًا، سنقوم بإعداد مشغل GitHub مستضاف ذاتيًا على خادمنا الخاص.
-
strategy
تتيح لنا الإستراتيجية (Strategy) استخدام المتغيرات في تعريف وظيفة واحدة لإنشاء عمليات تشغيل متعددة للوظائف تلقائيًا بناءً على مجموعات المتغيرات.
في ملف YAML الخاص بنا، لدينا متغير واحد لإصدار node-version، ولكن إذا أضفنا متغيرًا آخر لإصدار node 18، مثل هذا: node-version: [16.x, 18.x]، فستقوم إستراتيجية المصفوفة (matrix strategy) بإنشاء عمليتي تشغيل لوظيفتنا لتطبيق react لكل من إصداري node 16 و 18.
|
1 2 3 |
strategy: matrix: node-version: [16.x] |
-
steps
الخطوات (Steps) هي سلسلة من المهام التي تشكل وظيفة. يمكن للخطوات تشغيل الأوامر، أو إعداد المهام، أو تشغيل الإجراءات (actions) في مستودعك العام، أو الإجراءات المنشورة في سجل docker.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
steps: - name: 'checkout' uses: actions/checkout@v3 - name: 'setup node actions' uses: actions/setup-node@v3 with: node-version: "16" cache: 'npm' - run: npm i - run: npm test - run: npm run build |
تتضمن الخطوة اسمًا. على الرغم من أنه اختياري، يمكنك استخدامه لتحديد طريقة سهلة للتعرف على اسم خطوتك التي ستظهر على Github.
في الخطوة، يمكنك تحديد إجراء لتشغيله في وظيفتك، فالإجراءات قابلة لإعادة الاستخدام. يتم تحديد إصدارات الإجراء عند تعريفه، وهذا أمر مهم لأنه يمنع تعطل سير العمل الخاص بك عندما ينشر مالك الإجراء تحديثًا.
في مقتطف الكود أعلاه، checkout@v3 هو مثال على إجراء بإصدار محدد 3. يقوم هذا الإجراء بفحص مستودعك (checkout) حتى يتمكن سير العمل الخاص بك من الوصول إليه.
بعض الإجراءات مثل actions/setup-node@v3 أعلاه تحتوي على مدخلات يشار إليها باستخدام with الكلمة المفتاحية، تتطلب إجراءات setup node إصدار node 16 وتخزين npm مؤقتًا.
-
run
يقوم هذا الإجراء بتشغيل برامج سطر الأوامر باستخدام واجهة تفاعل (shell) نظام التشغيل.
في ملف YAML أعلاه، لدينا ثلاثة أوامر، وسيتم تشغيلها جميعًا باستخدام نفس واجهة التفاعل (shell) في بيئة التشغيل (runner).
-
الأمر الأول npm i يقوم بتثبيت جميع التبعيات التي يحتاجها تطبيق react الخاص بنا.
-
الثاني npm test، يقوم بتشغيل الاختبار الذي كتبناه. يتوقع الاختبار أن يتم عرض النص learn react على الصفحة الرئيسية.
-
أخيرًا، npm run build يقوم الأمر بإنشاء دليل بناء (build directory) يحتوي على نسخة الإنتاج من تطبيقنا. لاحقًا، سنستخدم دليل البناء هذا في Nginx server block.
الخطوة 3. اختبار Github CI باستخدام GitHub Runners.
في هذه الخطوة، سنقوم باختبار عملية التكامل المستمر (Continuous Integration) باستخدام GitHub runners. ابدأ بفتح ملف node.js.yml. قم بتعديل نوع بيئة التشغيل (runner) التي ستعمل عليها الإجراءات إلى ubuntu-latest. الغرض من ذلك هو اختبار ما إذا كان سير عمل GitHub يعمل بشكل مثالي على GitHub runners قبل إعداد بيئات التشغيل المستضافة ذاتيًا (self-hosted runners) الخاصة بنا.
|
1 2 3 |
jobs: build: runs-on: ubuntu-latest |
أنشئ مستودعًا جديدًا على GitHub account. قبل أن نتابع، ارجع إلى دليل مساحة العمل الخاص بك واحذف دليل .git، إذا لم تتمكن من رؤيته، فتحقق من ملفاتك المخفية. يمكنك استخدام الأمر التالي على الطرفية (terminal) لحذف الدليل:
|
1 |
rm -rf .git |
الآن يمكنك إضافة جميع ملفات مشروعك باستخدام git add، ثم عمل commit وpush لها إلى مستودعك. إذا واجهت صعوبة، استخدم هذا الدليل حول connecting your project folder to the GitHub repository الذي أنشأته أعلاه.
عند الانتهاء، انقر فوق علامة التبويب Code في مستودعك، وسترى نقطة برتقالية صغيرة بجوار إجمالي عدد الالتزامات (commits)، وعند النقر عليها، سترى نافذة منبثقة مشابهة للنافذة أدناه، تشير إلى أن سير العمل الخاص بك قد تم وضعه في قائمة الانتظار:

الآن انقر فوق رابط Details في النافذة المنبثقة أو انتقل إلى علامة التبويب Actions، وسترى كل خطوة من خطوات سير عمل node.js.yml يتم تشغيلها بواسطة GitHub runners:

إذا نجحت العملية، انقر فوق علامة التبويب Actions، ويجب أن تبدو هكذا:

وهذا كل شيء، يجب أن تتحول النقطة البرتقالية الصغيرة في علامة التبويب Code الآن إلى علامة صح خضراء. لقد قام GitHub runner ببناء تطبيقنا بنجاح.
الآن، دعنا نذهب خطوة أبعد ونغير بيئة البناء لاستخدام GitHub self-hosted runners في Ubuntu Server Infrastructure.
الخطوة 4. إعداد سير عمل GitHub لاستخدام بيئة تشغيل مستضافة ذاتيًا (self-hosted runner).
قبل تثبيت بيئة التشغيل المستضافة ذاتيًا (self-hosted runner) في خادمنا، دعنا نعود إلى مساحة العمل الخاصة بنا، ونقوم بتعديل ملف سير العمل node.js.yml ليعمل على GitHub self-hosted runners.
|
1 2 3 |
jobs: build: runs-on: self-hosted |
في هذه المرحلة، عند عمل commit للتغييرات، سيتم وضع الوظيفة في قائمة الانتظار نظرًا لعدم تحديد بيئة تشغيل مستضافة ذاتيًا (self-hosted runner) بعد.
في مستودعك، انقر فوق علامة التبويب
Settings، وفي الشريط الجانبي الأيسر انقر فوق
Actions، ثم انقر فوق
Runners:

انقر فوق New self-hosted runner، وحدد Linux كنظام تشغيل.
سترى تعليمات توضح لك كيفية تنزيل بيئة التشغيل (runner) وتثبيتها على خادمك.
الخطوة 5. تثبيت وتكوين بيئة تشغيل مستضافة ذاتيًا لـ Github (Github self-hosted runner) على خادمنا.
في هذه الخطوة، سنقوم بإعداد بيئة تشغيل مستضافة ذاتيًا لـ GitHub (self-hosted GitHub runner). بيئة التشغيل المستضافة ذاتيًا هي نظام يمكنه نشر وإدارة تنفيذ المهام من Github Actions على موقع Github الإلكتروني. إحدى الميزات التي تتمتع بها بيئة التشغيل المستضافة ذاتيًا مقارنة ببيئة التشغيل المستضافة على GitHub هي المرونة. فهي توفر تحكمًا أكبر في نظام التشغيل والأجهزة والأدوات الأخرى التي يمكن تخصيصها لتلبية احتياجات تطبيقك المطلوبة.
يمكن إضافة بيئات التشغيل المستضافة ذاتيًا على مستويات مختلفة مثل:
-
بيئات التشغيل على مستوى المستودع (Repository-level runners)، وهي مخصصة لمستودع واحد.
-
بيئات التشغيل على مستوى المؤسسة (Organization-level runners)، ويمكنها معالجة المهام لمستودعات متعددة في المؤسسة.
-
على مستوى المؤسسة الكبرى (Enterprise-level) والتي يمكن تخصيصها لمؤسسات متعددة.
للمتابعة، قم بتسجيل الدخول إلى خادمك عبر ssh:
|
1 |
ssh username@server_ip |
انتقل إلى الدليل الرئيسي الخاص بك باستخدام الأمر:
|
1 |
cd ~ |
ثم، قم بإنشاء دليل يسمى action-runners وانتقل إليه:
|
1 |
mkdir actions-runner && cd actions-runner |
بعد ذلك، قم بتنزيل أحدث إصدار من حزمة بيئة التشغيل (runner package):
|
1 |
curl -o actions-runner-linux-x64-2.298.2.tar.gz -L https://github.com/actions/runner/releases/download/v2.298.2/actions-runner-linux-x64-2.298.2.tar.gz |
ثم قم بفك ضغط الحزمة التي تم تنزيلها باستخدام الأمر:
|
1 |
tar xzf ./actions-runner-linux-x64-2.298.2.tar.gz |
بالعودة إلى المستودع الخاص بك، في علامة التبويب Settings، في اللوحة الجانبية اليسرى انقر فوق Actions، ثم Runners، تمامًا كما فعلنا في Step 4.
سترى أمرًا مدرجًا يتضمن رمزًا مميزًا (token) يربط بيئة التشغيل المستضافة ذاتيًا بمستودع GitHub الخاص بك. بينما لا تزال داخل الدليل الذي قمت بفك ضغط كود بيئة تشغيل GitHub فيه، استخدم الأمر المدرج لربط بيئة التشغيل الخاصة بك، على سبيل المثال:
|
1 |
./config.sh --url https://github.com/EspiraMarvin/react-tut-test --token XXXXXXXXXXXXXXXXXXXXXXXXXXX |
يجب أن تتم عملية المصادقة بنجاح:

اضغط على Enter لمجموعة بيئة التشغيل الافتراضية (Default runner group).
ثم أدخل اسمًا لبيئة التشغيل الخاصة بك، على سبيل المثال، my-runner، واضغط على enter.
اضغط على Enter لتخطي إضافة تسميات إضافية، وستظهر لك رسالة النجاح في لقطة الشاشة أدناه:

أدخل اسم دليل العمل الخاص بك، على سبيل المثال، react-cicd، ويجب أن ينجح ذلك مع ظهور النص settings saved.
أخيرًا، قم بتشغيل ./run.sh، ويجب أن يظهر هذا Connected to Github:

ولكن هذا لا يعمل في الخلفية، فإذا قمنا بكتابة ctrl+c في الطرفية (terminal) الخاصة بنا، فسيتم إيقاف بيئة التشغيل، ونحن بحاجة إلى استبدالها بخدمة .svc.sh لإبقاء بيئة التشغيل تعمل كخدمة حتى نتمكن من مواصلة التفاعل معها.
قم بإيقاف بيئة التشغيل باستخدام ctrl+c. يمكنك تثبيت خدمة .svc.sh عن طريق تشغيل الأمر:
|
1 |
sudo ./svc.sh install |
بمجرد تثبيتها، ابدأ الخدمة باستخدام الأمر:
|
1 |
sudo ./svc.sh start |
يجب أن ينجح هذا، ويظهر active (running).
لتأكيد أن خدمة svc.sh تعمل وتنشط، قم بتنفيذ الأمر:
|
1 |
sudo ./svc/sh status |

عند هذه النقطة، فإن أي سير عمل (workflow) قد يكون قيد الانتظار في انتظار بيئة تشغيل مستضافة ذاتيًا يجب أن يعمل بنجاح. يمكنك أيضًا تعديل ملف وتجربة الأمور. على سبيل المثال، قم بتعديل ملف حول.tsx، ثم قم بعمل commit و push للتغييرات، وستقوم بيئة التشغيل المستضافة ذاتيًا بإكمال المهمة بنجاح.
الخطوة 6. إعداد كتلة خادم Nginx (Nginx server block)
في هذه الخطوة، سنقوم بإعداد كتلة خادم (server block) في Nginx لعرض نسخة البناء (build version) لتطبيق react الخاص بنا. لدينا دليل تعليمي حول إعداد كتل خادم Nginx (Nginx server blocks) قد تجده مفيدًا.
أدناه مثال على كتلة خادم مستخدمة في هذا الدليل التعليمي:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
server { listen 80; listen [::]:80; server_name react.test; root /var/www/react-cicd/build; index index.html index.htm index.nginx-debian.html; add_header X-Frame-Options "SAMEORIGIN"; add_header X-XSS-Protection "1; mode=block"; add_header X-Content-Type-Options "nosniff"; charset utf-8; location / { try_files $uri $uri/ =404; } } |
ستقوم بإنشاء ملف تكوين كتلة خادم Nginx داخل /etc/nginx/sites-available دليل.
افتح ملفًا لتكوين كتلة خادم موقعك باستخدام محرر nano باستخدام الأمر:
|
1 |
sudo nano /etc/nginx/sites-available/react-cicd |
انسخ كتلة الخادم المشتركة أعلاه وقم بتعديلها وفقًا لمسارات الدليل الخاصة بك، ثم الصقها في الملف المفتوح. عند الانتهاء من التعديل، اضغط على ctrl+x ثم اضغط على y و enter للحفظ والخروج.
بمجرد الحفظ، قم بإنشاء رابط رمزي (symlink) لتكوين كتلة خادم react-cicd من /etc/nginx/sites-available إلى /etc/nginx/sites-enabled عن طريق تشغيل الأمر:
|
1 |
sudo ln -s /etc/nginx/sites-available/react-cicd /etc/nginx/sites-enabled/ |
لكي تدخل التغييرات حيز التنفيذ، تحتاج إلى إعادة تشغيل Nginx. ومع ذلك، قبل أن تتمكن من إعادة تشغيل خدمة Nginx، اختبر ما إذا كانت تكوينات Nginx صالحة، عن طريق تشغيل الأمر:
|
1 |
sudo nginx -t |
إذا كان التكوين صحيحًا، فيجب أن ينجح الاختبار.
لاحظ قيمة توجيه server_name “ react.test ” في كتلة الخادم؟ ستقوم بإضافة هذه القيمة في ملف hosts على جهازك المحلي. سيمكنك هذا من فتح التطبيق في متصفحك. هذه الخطوة ضرورية فقط للنطاقات الافتراضية المستخدمة في بيئات التطوير المحلية. إذا كان لديك اسم نطاق مسجل مرتبط بعناوين IP العامة لخادمك، فيجب أن يكون اسم النطاق متاحًا بالفعل في متصفحك.
في جهازك المحلي، افتح ملف hosts باستخدام الأمر:
|
1 |
sudo nano /etc/hosts |
داخل ملف hosts، أضف عنوان IP الخاص بخادمك، على سبيل المثال 127.0.0.1، متبوعًا باسم النطاق الافتراضي الخاص بك.
يظهر مثال أدناه. ثم أغلق الملف واحفظه:
|
1 |
192.168.3.123 react.test |
بالعودة إلى خادمك داخل /var/www ، قم بإنشاء دليل جديد، يمكنك تسميته react-cicd عن طريق تشغيل:
|
1 |
mkdir react-cicd |
في هذه المرحلة، سنقوم بإلغاء تعليق الأمر الأخير في ملف node.js.yml .
يقوم هذا الأمر بنسخ مجلد البناء الخاص بتطبيق react الخاص بنا من المكان الذي حددنا فيه مجلد العمل الخاص بنا داخل دليل actions-runner في الخطوة 5.
ويضع مجلد public في دليل /var/www/react-cicd .
يمكن لكتلة الخادم الآن الوصول إلى تطبيقنا وعرضه على المتصفح.
أخيرًا، أعد تشغيل خدمة Nginx باستخدام الأمر:
|
1 |
sudo service nginx restart |
الآن، يمكنك إجراء تغيير في ملف حول.tsx ، ثم عمل commit و push لتغييراتك إلى المستودع الخاص بك. بعد نجاح عملية البناء، سترى نسخة البناء لتطبيق react الخاص بك على http://react.test أو على اسم النطاق الذي حددته. تجنب تعديل عنصر href في ملف Home.tsx لأن ذلك قد يتسبب في فشل الاختبار المكتوب بالفعل. يجب أن تبين علامة التبويب Actions في مستودعك أيضًا عملية بناء المهمة المكتملة.

الخاتمة
يأتي التكامل المستمر مع Github Actions بالعديد من المزايا بما في ذلك تجربة مطور جيدة، ويساعد في الاختبار المستمر، ويسمح بتعاون أسهل في الفرق الأكبر حجمًا، ويقصر وقت التطوير، والإصدار السريع للميزات الجديدة، والتعليقات في الوقت الفعلي، ورضا العملاء مما يمنحك ميزة تفوق على منافسيك. للبناء على هذه المعرفة، قد ترغب أيضًا في التعرف على إعداد خطوط أنابيب التكامل المستمر (CI) لـ GitLab على Ubuntu. واستخدام مثيل GitLab مدار ذاتيًا لاستضافة صور Docker الخاصة بك وتشغيل عمليات بناء خاصة.
حوسبة سعيدة!
التعليقات
لا توجد تعليقات بعد. كن أول من يعلق.