SSD Nodes Learn Hosting plans →
الأدلة Matt Connorبقلم Matt Connor · آخر تحديث في 2026-10-21

إعداد مشغّل GitHub Actions على VPS بنظام Ubuntu 24.04

سجّل مشغّل GitHub Actions ذاتي الاستضافة على Ubuntu 24.04: أنشئ مستخدماً مخصصاً، تحقّق من checksum، شغّل config.sh وخدمة systemd، وافهم خطر طلبات السحب.

Verified Every command ran end-to-end on a fresh Ubuntu 24.04 server, July 30, 2026.

ما يفعله مشغّل GitHub Actions مستضاف ذاتياً

مشغّل GitHub Actions مستضاف ذاتياً هو برنامج تثبّته على VPS تملكه، ويطلب المهام من GitHub وينفّذها على أجهزتك. تسجّله في مستودع واحد، وتثبّته كخدمة systemd، ثم يعود للعمل بعد كل إعادة تشغيل. يحدّد GitHub المهمة التي سيجري تشغيلها. وينفّذ خادمك العمل.

يستحق CI (التكامل المستمر) على خادم تملكه ذلك لسببين. لا تعود دقائق البناء خاضعة للحساب، ويمكن للمهمة الوصول إلى موارد لا يملكها إلا جهازك، مثل ذاكرة تخزين مؤقتة للبناء ومهيّأة مسبقاً أو شبكة خاصة. لكن المقابل هو الأمان. ينفّذ المشغّل كل ما يحدده ملف سير العمل، باستخدام المستخدم الذي منحته إياه، ولذلك فإن ملف سير العمل يتيح تنفيذ التعليمات البرمجية عن بُعد بحكم تصميمه. يكون ذلك مناسباً في مستودع خاص، لأن الأشخاص الذين تثق بهم فقط يمكنهم إضافة ملف سير عمل. أما في مستودع عام، فذلك يمثل خطراً حقيقياً، ويشرح القسم الخاص بطلبات السحب من fork آلية حدوثه.

تستخدم جميع الخطوات التالية Ubuntu 24.04 ونسخة المشغّل 2.338.0، وهي الإصدار الحالي في 12 October 2026.

ما تحتاج إليه قبل البدء

ابدأ من VPS يضم حساب مسؤول عادي مع صلاحيات sudo، وبالحالة التي تصل إليها في الدقائق العشر الأولى على VPS جديد. لا تحتاج إلى فتح منفذ وارد. يفتح المشغّل اتصال HTTPS (بروتوكول نقل النص التشعبي الآمن) صادراً إلى GitHub، ويبقيه مفتوحاً أثناء انتظار المهام، لذلك لا يتصل GitHub بخادمك أبداً. يمكن أن يظل جدارك الناري مغلقاً أمام العالم، ومع ذلك تصل المهام.

تحتاج أيضاً إلى صلاحيات المسؤول على المستودع، لأن رمز التسجيل يظهر في إعدادات المستودع.

أنشئ مستخدماً مخصصاً للـrunner

لا تشغّل الـrunner مطلقاً بصفة root أو باستخدام حسابك الإداري. ترث كل مهمة صلاحيات مستخدم الـrunner، لذلك ينجح سير عمل يستدعي sudo إذا كان مستخدم الـrunner يستطيع استخدام sudo. أنشئ مستخدماً واحداً غير مميّز لا يملك شيئاً باستثناء دليله الرئيسي. يشرح حسابات المستخدمين ذات أقل الصلاحيات على VPS النمط العام. فيما يلي الإعداد المحدد.

sudo useradd -m -s /bin/bash gharunner
sudo passwd -l gharunner
sudo chmod 750 /home/gharunner
sudo install -d -m 700 -o gharunner -g gharunner /home/gharunner/actions-runner

يقفل passwd -l كلمة المرور، لذلك لا يستطيع أحد تسجيل الدخول بصفة gharunner باستخدام كلمة مرور. ويهم ضبط الوضع 700 على دليل الـrunner لأن الـrunner يخزّن بيانات الاعتماد فيه بنص واضح، وقد يحتوي checkout على مصدر خاص.

تحقق من الخاصيتين قبل المتابعة:

sudo passwd -S gharunner
sudo -l -U gharunner

يطبع passwd -S سطراً يبدأ بـ gharunner L، حيث يعني L أن كلمة المرور مقفلة. يجب أن يعرض sudo -l -U gharunner النتيجة is not allowed to run sudo. إذا طبع بدلاً من ذلك قائمة بالأوامر المسموح بها، فهذا يعني أن الحساب ينتمي إلى مجموعة sudo وأن العزل الذي أنشأته للتو لم يعد قائماً.

نزّل runner وتحقّق من ملف tarball

استخدم حساب runner من الآن فصاعداً.

sudo -iu gharunner
cd ~/actions-runner
RUNNER_VERSION=2.338.0
curl -fL -o actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz \
  "https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz"

شغّل uname -m أولاً إذا لم تكن متأكداً من البنية. يأخذ x86_64 ملف linux-x64 المذكور أعلاه. ويأخذ aarch64 ملف actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz.

تحقّق الآن مما نزّلته. قيمة SHA256 (خوارزمية التجزئة الآمنة، 256 بت) أدناه مخصّصة لملف tarball بإصدار 2.338.0 وبنية x64. يعرض GitHub القيمة الخاصة بالإصدار الحالي في صفحة الإصدار وعلى شاشة New self-hosted runner. تتغير القيمة مع كل إصدار، لذلك انسخها من هناك عند تثبيت إصدار مختلف.

echo "af4b794c1bc41d73d40535e3fe092a39f9679cd8d965954c2aca25a05ca41d32  actions-runner-linux-x64-2.338.0.tar.gz" | sha256sum -c

يعرض التنزيل السليم سطراً واحداً:

actions-runner-linux-x64-2.338.0.tar.gz: OK

يعرض الملف المبتور أو المعدّل رسالة فشل وتحذيراً:

actions-runner-linux-x64-2.338.0.tar.gz: FAILED
sha256sum: WARNING: 1 computed checksum did NOT match

لا تتخطَّ هذا التحقق وتدع tar يكتشف المشكلة بدلاً منك. يفشل الأرشيف المكتوب جزئياً مع gzip: stdin: unexpected end of file وtar: Unexpected EOF in archive، وهذا يخبرك بأن الملف تالف، لكنه لا يحدد ما إذا كان قد بُتر أو استُبدل.

tar xzf ./actions-runner-linux-x64-2.338.0.tar.gz
ls

محتويات tarball وما لا يحتوي عليه

بعد الاستخراج، يحتوي الدليل على config.sh وrun.sh وenv.sh وsafe_sleep.sh وbin/ وexternals/. يحتوي bin/ على الملفات الثنائية الخاصة بالتشغيل وbin/installdependencies.sh. ويحتوي externals/ على بيئة Node المضمّنة التي تُنفَّذ عليها إجراءات JavaScript.

لا يوجد svc.sh بعد. تصف وثائق GitHub هذا الملف بأنه البرنامج النصي «الذي يُنشأ بعد إضافة runner بنجاح»، لأنه يُكتب من قالب يتضمن اسم المستودع واسم runner ضمن اسم الخدمة. لذلك يفشل sudo ./svc.sh install قبل ./config.sh مع sudo: ./svc.sh: command not found. سجّل runner أولاً، ثم ثبّت الخدمة.

ثبّت تبعيات الـrunner

الـrunner تطبيق .NET، لذلك يحتاج إلى بعض المكتبات المشتركة. اترك shell الخاص بمستخدم الـrunner وثبّت هذه المكتبات باستخدام sudo، لأن السكربت يكتب في قاعدة بيانات حزم النظام.

exit
cd /home/gharunner/actions-runner
sudo ./bin/installdependencies.sh

في Ubuntu 24.04، يثبّت ذلك libkrb5-3 وzlib1g وliblttng-ust1t64 وlibssl3t64 وlibicu74. يجرّب السكربت عدة أسماء للإصدارات لكل مكتبة، ويحتفظ بالاسم الذي توفره توزيعتك، ولذلك يعمل السكربت نفسه على إصدارات Ubuntu الأقدم وعلى Debian.

إذا تخطيت هذه الخطوة وكانت إحدى هذه المكتبات مفقودة، يتوقف ./config.sh قبل أن ينفذ أي إجراء:

Dependencies is missing for Dotnet Core 6.0
Execute sudo ./bin/installdependencies.sh to install any missing Dotnet Core 6.0 dependencies.

يعطيك غياب libicu الإرشاد نفسه تحت سطر أول مختلف، وهو Libicu's dependencies is missing for Dotnet Core 6.0. يصدر كلا الخطأين من المكان نفسه: يفحص config.sh المكتبات المضمّنة باستخدام ldd، ويفحص libicu باستخدام ldconfig، قبل أن يبدأ. لذلك يوقف الرابط غير المحلول السكربت بدلاً من التسبب في تعطل مربك لاحقاً.

سجّل الـrunner في مستودعك

احصل على token من المستودع. افتح Settings، ثم Actions، ثم Runners، ثم New self-hosted runner. تعرض الصفحة registration token. تنتهي صلاحيته بعد ساعة واحدة من إنشائه، لذلك أنشئه عندما تكون مستعداً للصقه.

سجّل الدخول باستخدام مستخدم الـrunner. يرفض config.sh العمل باستخدام sudo.

sudo -iu gharunner
cd ~/actions-runner
./config.sh --url https://github.com/YOUR-USER/YOUR-REPO \
  --token PASTE_REGISTRATION_TOKEN_HERE \
  --name vps-runner-1 \
  --labels vps \
  --work _work \
  --unattended \
  --replace

وظيفة هذه الخيارات. يحدد --name الاسم الذي يظهر به الـrunner في المستودع، لذلك اختر اسماً ستظل قادراً على التعرّف إليه بعد ستة أشهر. يضيف --labels التسميات التي تحددها أنت؛ ويحمل الـrunner مسبقاً self-hosted وLinux وX64 من دون طلب ذلك. يحدد --work الدليل الذي توضع فيه عمليات checkout، داخل دليل الـrunner. يجيب --unattended عن المطالبات التفاعلية باستخدام قيمها الافتراضية، وهذا هو المطلوب عندما يكون الأمر داخل script. يستبدل --replace تسجيلًا موجوداً بالاسم نفسه بدلاً من الفشل، وهذا هو المطلوب عند إعادة بناء الخادم.

تنتهي العملية الناجحة بهذه الأسطر:

√ Runner successfully added
√ Runner connection is good
√ Settings Saved.

يوجد التسجيل الآن داخل دليل الـrunner في الملفات .runner و.credentials و.credentials_rsaparams. يعرّف الملفان الأخيران هذا الـrunner لدى GitHub، لذلك يمكن لأي شخص يستطيع قراءتهما انتحال هويته. ولهذا السبب يكون نمط الدليل 700 ولا يملك المستخدم صلاحية sudo.

ثبّت الـrunner كخدمة systemd

يعمل ./run.sh في الطرفية لاختبار واحد، لكنه يتوقف بانتهاء جلسة SSH. ثبّت الخدمة لكي يبدأ الـrunner عند الإقلاع. يشرح خدمات systemd والمؤقتات على VPS ملفات الوحدة نفسها. أما svc.sh فينشئ ملف الوحدة نيابةً عنك.

exit
cd /home/gharunner/actions-runner
sudo ./svc.sh install gharunner
sudo ./svc.sh start
sudo ./svc.sh status

يتطلب svc.sh صلاحيات root لأنه يكتب وحدة في /etc/systemd/system ويمكّنها. الوسيط الذي يلي install هو المستخدم الذي تعمل الخدمة باسمه. مرّر gharunner صراحةً. إذا لم تمرّر وسيطاً، يعود البرنامج النصي إلى $SUDO_USER، وهو حساب المسؤول لديك، وعندها تُشغّل كل مهمة باسم مستخدم يمكنه استخدام sudo.

تُسمّى الوحدة باسم المستودع والـrunner بالصيغة actions.runner.YOUR-USER-YOUR-REPO.vps-runner-1.service. لا تحتاج إلى كتابة ذلك يدوياً:

systemctl list-units 'actions.runner.*'
sudo journalctl -u 'actions.runner.*' -n 20 --no-pager

يسجّل الـrunner السليم √ Connected to GitHub ثم سطراً ينتهي بـListening for Jobs، وتعرض صفحة Runners في المستودع حالته Idle. إذا ظهر الـrunner بالحالة Offline، فهذا يعني أنه لا يعمل أو لا يستطيع الوصول إلى GitHub عبر المنفذ 443.

إرسال مهمة إلى المنفّذ

يحدّد runs-on منفّذاً وفقاً للتسمية. اطلب self-hosted مع التسمية الخاصة بك، حتى لا تُسنَد المهمة إلى منفّذ لم تقصده.

name: build
on:
  push:
    branches: [main]
jobs:
  build:
    runs-on: [self-hosted, linux, vps]
    steps:
      - uses: actions/checkout@v5
      - run: uname -a

إذا انتظرت المهمة عند Waiting for a runner to pick up this job، فهذا يعني أن التسميات غير متطابقة. يجب أن تكون كل تسمية في runs-on موجودة على المنفّذ، لذلك قد تؤدي كلمة إضافية واحدة إلى بقاء المهمة في قائمة الانتظار من دون ظهور أي خطأ. قارن القائمة بالتسميات الظاهرة بجانب المنفّذ في إعدادات المستودع.

لماذا لا ينبغي الجمع بين runners ذاتية الاستضافة والمستودعات العامة

هذا هو الجزء الذي يتجاهله الناس. إرشادات GitHub واضحة: ينبغي «تقريباً عدم استخدام runners ذاتية الاستضافة مع المستودعات العامة»، كما أن هذه runners «لا تضمن التشغيل داخل أجهزة افتراضية نظيفة ومؤقتة، وقد تتعرض للاختراق المستمر بواسطة تعليمات برمجية غير موثوقة في workflow».

الآلية بسيطة. يجلب pull request من fork نسخته الخاصة من ملف workflow. إذا كان المستودع العام يشغّل workflows الخاصة بـpull request على runner لديك، فيمكن لأي شخص إنشاء fork للمستودع واقتراح workflow يشغّل أوامره على VPS لديك. لا يحتاج إلى صلاحية كتابة، لأن الشيء الذي يقترحه هو نفسه الشيء الذي سيُشغَّل.

تخفف إعدادات الموافقة من هذا الخطر، لكنها لا تعالجه. تطلب السياسة الافتراضية للمستودع العام من أحد المشرفين الموافقة على workflow الخاص بـfork لمساهم يرسل مساهمة للمرة الأولى. تعتبر GitHub الشخص مساهمًا للمرة الأولى فقط إلى أن يُدمج أي commit أو pull request له في المستودع. بعد ذلك، تُشغَّل pull requests اللاحقة الخاصة به دون طلب موافقة، وتحذّر GitHub نفسها من أن إصلاحًا بسيطًا ومدمجًا يكفي للوصول إلى هذه المرحلة. لذلك تعتمد الحماية على أن يقرأ شخص diff، ومن السهل أن يفوته payload مخفي على عمق ثلاثة مستويات داخل build script.

لا يحصل pull request من fork على الأسرار الخاصة بك، وتكون GITHUB_TOKEN له للقراءة فقط. يحد ذلك من الضرر داخل GitHub، لكنه لا يحمي خادمك. يمتلك المهاجم shell بصلاحيات gharunner، لذلك يمكنه قراءة كل ملف يستطيع ذلك المستخدم قراءته، والوصول إلى أي مورد يستطيع VPS الوصول إليه عبر شبكته الخاصة، وترك شيء في ~/.bashrc أو في وحدة user systemd تُشغَّل أثناء المهمة التالية.

يؤدي التسجيل باستخدام --ephemeral إلى جعل runner يقبل مهمة واحدة ثم يلغي تسجيله، لذلك لا تستطيع مهمة قراءة مساحة عمل المهمة التالية. يفيد ذلك فقط إذا أُعيد إنشاء الجهاز أو الحاوية لكل مهمة، لأن backdoor مكتوبًا في الدليل الرئيسي لمستخدم runner يبقى بعد التسجيل من جديد.

القواعد التالية قصيرة. استخدم runners ذاتية الاستضافة مع المستودعات الخاصة. إذا اضطررت إلى ربط runner بمستودع عام، فلا تشغّل pull requests من forks عليه، ولا تضع أي شيء آخر على ذلك الخادم، وتعامل مع الجهاز باعتباره قابلًا للتخلص منه.

مهام Docker، والمجموعة التي تعادل root فعلياً

تحتاج مهام الحاويات وحاويات الخدمات وأي خطوة في سير العمل تستدعي docker build إلى Docker daemon على مضيف runner. ثبّت Docker بالطريقة المعتادة، كما يوضّح ذلك Docker وDocker Compose على VPS، ثم أضف مستخدم runner إلى مجموعة docker.

افهم المقايضة قبل تنفيذ ذلك. تعادل العضوية في مجموعة docker امتيازات root، لأن الحاوية يمكنها إجراء bind mount للمسار / وتشغيل عمليات بصلاحيات root داخلها. لذلك، يمكن لسير عمل يتصل بـDocker socket قراءة كل ملف على VPS وكتابته، بما في ذلك /etc/shadow. قد يكون ذلك مقبولاً في مستودع خاص يساهم فيه أشخاص موثوقون. أما في أي سياق آخر، فهو يلغي فائدة المستخدم غير ذي الامتيازات. يحافظ Rootless Docker على عمليات إنشاء الحاويات ضمن صلاحيات مستخدم runner نفسه، لكن على حساب استخدام برنامج تشغيل تخزين أبطأ وعدم دعم الحاويات ذات الامتيازات.

التحديثات وإزالة الـrunner بطريقة صحيحة

يُحدّث الـrunner المستضاف ذاتياً نفسه افتراضياً. يكتشف الإصدار الجديد، ويستبدل ملفاته، ويعيد تشغيل الخدمة، لذلك لا تحتاج عادةً إلى تنفيذ أي إجراء. لتعطيل التحديث الذاتي، مرّر --disableupdate إلى ./config.sh عند تسجيل الـrunner. وإذا كان الـrunner مسجلاً مسبقاً، فعليك تسجيله مرة أخرى باستخدام هذا الخيار. هذا لا يتيح لك البقاء على إصدار واحد بشكل دائم. توضح وثائق GitHub صراحةً أن الـrunner الذي تم ضبطه باستخدام --disableupdate يجب تحديثه يدوياً خلال 30 يوماً من صدور إصدار جديد، وإلا تتوقف GitHub عن وضع المهام في قائمة الانتظار له.

يحافظ التحديث اليدوي على التسجيل، لأن .runner و.credentials غير موجودين في ملف tarball. أوقف الخدمة، ونزّل ملف tarball الجديد وتحقق من سلامته باستخدام checksum كما في gharunner، ثم استخرجه فوق الدليل نفسه باستخدام tar xzf، وبعد ذلك شغّل الخدمة مرة أخرى:

cd /home/gharunner/actions-runner
sudo ./svc.sh stop
sudo ./svc.sh start

لإزالة الـrunner، أزل الخدمة أولاً، ثم ألغِ تسجيله. تحصل على رمز الإزالة من صفحة Runners نفسها، ضمن زر Remove الخاص بالـrunner.

cd /home/gharunner/actions-runner
sudo ./svc.sh stop
sudo ./svc.sh uninstall
sudo -iu gharunner
cd ~/actions-runner
./config.sh remove --token PASTE_REMOVAL_TOKEN_HERE

يؤدي حذف الدليل دون إلغاء التسجيل إلى بقاء الـrunner ظاهراً بالحالة Offline في المستودع. ويبقى كذلك إلى أن ينقر أحد المسؤولين على Force remove this runner، أو إلى أن تزيله GitHub تلقائياً بعد 14 يوماً دون اتصال.

أوضاع الفشل، مع الرسائل التي ستظهر لك

Must not run with sudo. تطبع config.sh هذه الرسالة ثم تنهي التنفيذ عند تشغيلها بصلاحيات root. هذا الفحص مقصود، لأن الملفات التي يملكها root في _work تعطل كل مهمة لاحقة تعمل بحساب الخدمة. شغّل ./config.sh بصفة gharunner. يتجاوز المتغير RUNNER_ALLOW_RUNASROOT هذا الفحص، لكن استخدامه يؤخر حدوث المشكلة فقط.

sudo: ./svc.sh: command not found. أنت في الدليل الصحيح. لا يوجد svc.sh بعد، لأن config.sh لم يكمل تسجيله. سجّل الـrunner، ثم ثبّت الخدمة.

Http response code: NotFound from 'POST https://api.github.com/actions/runner-registration'. غالباً لا يكون الرمز المميز رمز تسجيل صالحاً. قد يكون قد انتهت صلاحيته، لأن صلاحيته تستمر ساعة واحدة فقط، أو قد تكون لصقت personal access token بدلاً من رمز التسجيل الموجود في صفحة Runners. أنشئ رمزاً جديداً والصقه مرة أخرى، وتحقق من أن --url هو عنوان URL الكامل للمستودع.

Dependencies is missing for Dotnet Core 6.0. شغّل sudo ./bin/installdependencies.sh من دليل الـrunner بصفة root، ثم أعد التسجيل.

الـrunner غير متصل بعد إعادة التشغيل. شغّل systemctl is-enabled 'actions.runner.*'. إذا لم يظهر شيء، فهذا يعني أن ./svc.sh install لم يُشغَّل مطلقاً، ولذلك لم يكن الـrunner موجوداً إلا داخل جلسة الطرفية. إذا كانت الوحدة مفعّلة وما زال الـrunner غير متصل، فاقرأ journalctl -u 'actions.runner.*' وتحقق من اتصالات HTTPS الصادرة.

يمتلئ القرص. تتراكم عمليات checkout وذاكرات التخزين المؤقت للبناء وصور Docker ضمن _work وفي الدليل الرئيسي لمستخدم الـrunner، ولا يحذفها شيء تلقائياً. راقب du -sh /home/gharunner/actions-runner/_work وأضف عملية تنظيف مجدولة قبل أن يقرر امتلاء القرص ذلك نيابةً عنك.

FAQ

لماذا يعرض sudo ./svc.sh install الرسالة command not found؟

لأن svc.sh غير موجود في أرشيف runner المضغوط. يُنشأ هذا الملف في دليل runner عند اكتمال تسجيل ./config.sh، باستخدام اسم المستودع واسم runner لإنشاء اسم الخدمة. شغّل ./config.sh أولاً بصفته مستخدم runner. بعد ذلك، يعثر sudo ./svc.sh install gharunner على البرنامج النصي ويكتب وحدة باسم actions.runner.OWNER-REPO.RUNNER-NAME.service داخل /etc/systemd/system.

هل أحتاج إلى فتح منفذ في جدار الحماية لاستخدام runner مستضاف ذاتياً؟

لا. يفتح runner اتصال HTTPS صادراً إلى GitHub ويُبقيه مفتوحاً أثناء انتظار المهام، لذلك لا يبدأ GitHub أي اتصال بخادم VPS لديك. اسمح بالاتصالات الصادرة على المنفذ 443 واترك قواعد الاتصالات الواردة مغلقة. إذا ظهر runner بحالة Offline أثناء تشغيل خدمته، فتحقق من تصفية الاتصالات الصادرة وDNS بدلاً من قواعد الاتصالات الواردة.

هل يمكنني استخدام runner مستضاف ذاتياً مع مستودع عام؟

يمكنك ذلك، لكن GitHub لا ينصح به. يحتوي طلب السحب من fork على ملف workflow الخاص به، لذلك يمكن لأي شخص إنشاء fork لمستودعك واقتراح أوامر تُنفَّذ على جهازك. يغطي طلب الموافقة الافتراضي المساهمين الذين لم يُدمج لهم أي commit أو pull request في المستودع من قبل فقط. إذا ربطت runner بمستودع عام، فعطّل عليه workflows الخاصة بطلبات السحب من fork، ولا تُبقِ أي شيء آخر على ذلك الخادم، وأعد بناء الجهاز وفق جدول محدد.

لماذا يفشل التسجيل مع Http response code: NotFound؟

تعيد مكالمة التسجيل الإجابة NotFound عندما تكون بيانات الاعتماد خاطئة، وليس فقط عندما يكون عنوان URL خاطئاً، لذلك تكون الرسالة مضللة. تنتهي صلاحية رموز التسجيل بعد ساعة واحدة من عرضها، ولا يُقبل personal access token لهذه المكالمة. افتح Settings ثم Actions ثم Runners ثم New self-hosted runner مرة أخرى، وانسخ الرمز الجديد، وتأكد من أن قيمة --url تشير إلى مستودع تملك فيه صلاحيات المسؤول.

هل كان هذا المقال مفيدًا؟

#github-actions#ci#self-hosted#runner#ubuntu-24-04