نظرة عامة
الـ runners المستضافة على GitHub مشتركة ومؤقتة (ephemeral) افتراضياً — فكل مهمة تحصل على جهاز افتراضي جديد يُدمَّر بعد اكتمال المهمة. أما الـ runners ذاتية الاستضافة، فهي دائمة ومشتركة عبر عمليات تشغيل الـ workflow. وهذا يخلق خطراً أمنياً كبيراً: إذ يمكن أن تتسرّب الأسرار والرموز وartifacts الـ build من مهمة إلى المهمة التالية. ويمكن لـ workflow مخترَق أن يسمّم بيئة الـ runner لكل المهام المستقبلية.
Actions Runner Controller (ARC) يحل هذه المشكلة. فـ ARC مشغّل (operator) أصيل في Kubernetes يمنحك runners مؤقتة، ذاتية التوسّع، قائمة على الحاويات، وذاتية الاستضافة. فكل مهمة تحصل على بود (pod) جديد يُدمَّر عند اكتمال المهمة — تماماً مثل الـ runners المستضافة على GitHub، لكنها تعمل على بنيتك التحتية الخاصة بأدواتك وسياساتك الشبكية.
في هذا المختبر العملي، ستقوم بـ:
- نشر ARC على عنقود Kubernetes محلي
- تهيئة مجموعات توسّع runner مؤقتة
- إثبات العزل بين المهام (الفائدة الأمنية الجوهرية)
- بناء صور runner مخصّصة
- تطبيق عزل مجموعات الـ runner لفصل المسؤوليات
- تهيئة التوسّع التلقائي (autoscaling)
- تطبيق سياسات شبكية لتقييد وصول الـ runner إلى الشبكة
المتطلبات المسبقة
قبل بدء هذا المختبر، تأكّد من توفّر ما يلي:
- عنقود Kubernetes — kind، أو minikube، أو عنقود مُدار سحابياً (EKS، GKE، AKS)
- Helm 3 — ثبّته من helm.sh
- kubectl — مُهيَّأ للتواصل مع عنقودك
- حساب GitHub — بصلاحية مسؤول (admin) على مستودع أو مؤسسة
- GitHub App أو رمز وصول شخصي (PAT) — بنطاقات
repoوadmin:org(لـ PAT) أو أذونات GitHub App مناسبة - Docker — لبناء صور runner مخصّصة (التمرين 4)
إعداد البيئة
سنستخدم kind (Kubernetes in Docker) لإنشاء عنقود محلي. وهذا يُبقي المختبر مكتفياً ذاتياً وسهل التنظيف.
إنشاء عنقود kind
kind create cluster --name arc-lab
تحقّق من أن العنقود يعمل:
kubectl cluster-info --context kind-arc-lab
إنشاء مستودع GitHub اختباري
أنشئ مستودعاً جديداً (مثل arc-lab-test) في حساب GitHub لديك. أضف ملف workflow بسيطاً في .github/workflows/test.yml:
name: ARC Test Workflow
on:
push:
branches: [main]
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Hello from GitHub-hosted runner
run: echo "This runs on a GitHub-hosted runner"
ادفع (push) هذا إلى مستودعك. سنعدّله لاحقاً لاستهداف runners الخاصة بـ ARC.
التمرين 1: تثبيت ARC عبر Helm
يستخدم Actions Runner Controller v2 مخططات Helm لنشر مكوّنين: وحدة تحكم (controller) تدير دورة حياة بودات الـ runner، وواحدة أو أكثر من مجموعات توسّع الـ runner (runner scale sets) التي تسجّل لدى GitHub وتقبل المهام.
الخطوة 1: إضافة مستودع Helm
helm repo add actions-runner-controller \
https://actions-runner-controller.github.io/actions-runner-controller
helm repo update
الخطوة 2: تهيئة المصادقة
يحتاج ARC إلى المصادقة مع واجهة API الخاصة بـ GitHub. ولديك خياران:
الخيار A: GitHub App (موصى به للإنتاج)
أنشئ GitHub App في إعدادات مؤسستك أو حسابك:
- اذهب إلى Settings → Developer settings → GitHub Apps → New GitHub App
- اضبط الأذونات التالية:
- Repository:
Actions(قراءة)،Administration(قراءة/كتابة)،Metadata(قراءة) - Organization:
Self-hosted runners(قراءة/كتابة)
- Repository:
- ولّد مفتاحاً خاصاً ونزّله
- ثبّت التطبيق (App) على مؤسستك أو مستودعك
- سجّل قيمة App ID وقيمة Installation ID
الخيار B: رمز وصول شخصي (أبسط للمختبرات)
أنشئ PAT (كلاسيكي) بنطاقات repo وadmin:org، أو PAT دقيقاً (fine-grained) بأذونات Actions وAdministration. وفي هذا المختبر، سنستخدم PAT للتبسيط.
الخطوة 3: تثبيت وحدة تحكم ARC
helm install arc \
actions-runner-controller/gha-runner-scale-set-controller \
--namespace arc-systems \
--create-namespace
تحقّق من أن وحدة التحكم تعمل:
kubectl get pods -n arc-systems
ينبغي أن ترى مخرجات مشابهة لـ:
NAME READY STATUS RESTARTS AGE
arc-gha-runner-scale-set-controller-xxx 1/1 Running 0 30s
الخطوة 4: تثبيت مجموعة توسّع runner
الآن انشر مجموعة توسّع runner تسجّل لدى مستودع GitHub لديك:
helm install arc-runner-set \
actions-runner-controller/gha-runner-scale-set \
--namespace arc-runners \
--create-namespace \
--set githubConfigUrl="https://github.com/<org>/<repo>" \
--set githubConfigSecret.github_token="<PAT>"
استبدل <org>/<repo> بمسار مستودعك و<PAT> برمز الوصول الشخصي لديك.
تحقّق من مجموعة توسّع الـ runner:
kubectl get pods -n arc-runners
عند هذه النقطة، قد لا تكون هناك بودات runner بعد — فـ ARC يستخدم نموذج التوسّع إلى الصفر (scale-to-zero). فلا تُنشأ البودات إلا عند وجود مهام في طابور الانتظار.
الخطوة 5: التحقق في GitHub
انتقل إلى مستودعك في GitHub: Settings → Actions → Runners. ينبغي أن ترى مجموعة توسّع الـ runner مدرجة باسم arc-runner-set. وتُظهر الحالة أنها جاهزة لقبول المهام.
التمرين 2: تشغيل workflow على runners الخاصة بـ ARC
الآن حدّث الـ workflow الاختباري ليستهدف مجموعة توسّع runner الخاصة بـ ARC بدلاً من الـ runners المستضافة على GitHub.
الخطوة 1: تحديث الـ workflow
عدّل .github/workflows/test.yml لاستخدام وسم runner الخاص بـ ARC:
name: ARC Test Workflow
on:
push:
branches: [main]
workflow_dispatch:
jobs:
test:
runs-on: arc-runner-set
steps:
- name: Hello from ARC runner
run: |
echo "This runs on an ephemeral ARC runner!"
echo "Hostname: $(hostname)"
echo "Runner OS: $(uname -a)"
- name: Show environment
run: env | sort
التغيير الأساسي هو runs-on: arc-runner-set — وهذا يطابق اسم إصدار Helm لمجموعة توسّع الـ runner.
الخطوة 2: إطلاق الـ workflow
ادفع ملف الـ workflow المُحدَّث أو استخدم زر “Run workflow” (workflow_dispatch) في واجهة GitHub Actions.
الخطوة 3: مراقبة بود الـ runner
راقب مساحة الأسماء arc-runners أثناء تشغيل الـ workflow:
kubectl get pods -n arc-runners -w
سترى بوداً يُنشأ للمهمة:
NAME READY STATUS RESTARTS AGE
arc-runner-set-xxxxx-runner 1/1 Running 0 5s
بعد اكتمال المهمة، يُنهى البود ويُزال:
NAME READY STATUS RESTARTS AGE
arc-runner-set-xxxxx-runner 0/1 Completed 0 45s
شغّل kubectl get pods -n arc-runners مجدداً — لقد اختفى البود. هذا هو النموذج المؤقت: فكل مهمة تحصل على حاوية جديدة، وتُدمَّر الحاوية عند انتهاء المهمة. ولا توجد استمرارية للحالة بين المهام.
التمرين 3: إثبات الأمان المؤقت
يبرهن هذا التمرين على الفائدة الأمنية الجوهرية للـ runners المؤقتة: غياب التلوث بين المهام.
الخطوة 1: إنشاء workflow يكتب بيانات حساسة
أنشئ .github/workflows/ephemeral-test.yml:
name: Ephemeral Security Test
on: workflow_dispatch
jobs:
write-secret:
runs-on: arc-runner-set
steps:
- name: Write sensitive data
run: |
echo "SECRET_API_KEY=sk-prod-abc123xyz" > /tmp/secret-data
echo "DB_PASSWORD=super-secret-password" >> /tmp/secret-data
echo "Written sensitive data to /tmp/secret-data"
cat /tmp/secret-data
read-secret:
runs-on: arc-runner-set
needs: write-secret
steps:
- name: Attempt to read previous job data
run: |
echo "Checking if /tmp/secret-data exists from previous job..."
if [ -f /tmp/secret-data ]; then
echo "SECURITY RISK: Found data from previous job!"
cat /tmp/secret-data
else
echo "SECURE: /tmp/secret-data does not exist."
echo "Each job gets a fresh container — no cross-job contamination."
fi
الخطوة 2: تشغيل الـ workflow
أطلق الـ workflow عبر workflow_dispatch. تكتب المهمة الأولى (write-secret) بيانات حساسة في /tmp/secret-data. وتعمل المهمة الثانية (read-secret) في بود جديد وتحاول قراءة ذلك الملف.
الخطوة 3: التحقق من النتائج
في سجلات GitHub Actions، سترى:
- مهمة write-secret: تكتب الملف بنجاح وتطبع محتوياته
- مهمة read-secret: الملف غير موجود — تُظهر المخرجات
SECURE: /tmp/secret-data does not exist.
عملت كل مهمة في بود منفصل أُنشئ حديثاً. وعندما دُمِّر بود write-secret، دُمِّرت معه جميع البيانات — بما في ذلك الملف الحساس.
لماذا يهم هذا
على runner ذاتي الاستضافة دائم، سيظل ملف /tmp/secret-data على القرص عند تشغيل المهمة الثانية. ويمكن لـ workflow خبيث في طلب دمج (pull request) أن يقرأ أسراراً أو رموزاً أو بيانات اعتماد تركتها المهام السابقة. ومع الـ runners المؤقتة، يُقضى على متجه الهجوم هذا.
التمرين 4: صور runner مخصّصة
تستخدم runners الخاصة بـ ARC صورة حاوية أساسية. وللاستخدام الواقعي، تحتاج إلى تخصيص هذه الصورة لتضمين أدوات الـ build لديك.
الخطوة 1: إنشاء Dockerfile مخصّص
أنشئ ملف Dockerfile للـ runner المخصّص لديك:
FROM ghcr.io/actions/actions-runner:latest
USER root
# Install build tools
RUN apt-get update && apt-get install -y \
curl \
wget \
git \
jq \
unzip \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Install Go
RUN wget -q https://go.dev/dl/go1.22.4.linux-amd64.tar.gz \
&& tar -C /usr/local -xzf go1.22.4.linux-amd64.tar.gz \
&& rm go1.22.4.linux-amd64.tar.gz
ENV PATH="$PATH:/usr/local/go/bin"
# Install cosign
RUN curl -sSL -o /usr/local/bin/cosign \
https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64 \
&& chmod +x /usr/local/bin/cosign
# Install Docker CLI (for Docker-in-Docker workflows)
RUN curl -fsSL https://get.docker.com | sh
USER runner
الخطوة 2: بناء الصورة ودفعها
# Build the image
docker build -t ghcr.io/<org>/custom-runner:latest .
# Authenticate to GitHub Container Registry
echo "<PAT>" | docker login ghcr.io -u <username> --password-stdin
# Push the image
docker push ghcr.io/<org>/custom-runner:latest
الخطوة 3: تهيئة ARC لاستخدام الصورة المخصّصة
أنشئ ملف قيم custom-runner-values.yaml:
githubConfigUrl: "https://github.com/<org>/<repo>"
githubConfigSecret:
github_token: "<PAT>"
template:
spec:
containers:
- name: runner
image: ghcr.io/<org>/custom-runner:latest
command: ["/home/runner/run.sh"]
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "2"
memory: "2Gi"
حدّث مجموعة توسّع الـ runner بالصورة المخصّصة:
helm upgrade arc-runner-set \
actions-runner-controller/gha-runner-scale-set \
--namespace arc-runners \
-f custom-runner-values.yaml
الخطوة 4: التحقق من الأدوات المخصّصة
أنشئ workflow يستخدم الأدوات المخصّصة:
name: Custom Runner Tools Test
on: workflow_dispatch
jobs:
verify-tools:
runs-on: arc-runner-set
steps:
- name: Verify Go
run: go version
- name: Verify cosign
run: cosign version
- name: Verify Docker CLI
run: docker --version
الفائدة الأمنية: ببناء صورة الـ runner الخاصة بك، تتحكم بدقة في الأدوات والتبعيات الموجودة في بيئة الـ build. فلا توجد ملفات ثنائية غير متوقعة، ولا برمجيات مثبّتة مسبقاً لم توافق عليها، ويمكنك تثبيت كل أداة على إصدار محدّد. ويمكنك أيضاً فحص الصورة بحثاً عن الثغرات قبل نشرها.
التمرين 5: عزل مجموعات الـ runner
للـ workflows المختلفة مستويات ثقة مختلفة. فالتحقق من طلبات الدمج (pull request validation) لا ينبغي أن يملك وصولاً إلى أسرار الإنتاج. وworkflows النشر تحتاج إلى الأسرار لكن ينبغي أن تعمل فقط من الفرع الرئيسي. ويتيح لك ARC تطبيق هذا الفصل بإنشاء مجموعات توسّع runner متمايزة بوسوم وتهيئات مختلفة.
الخطوة 1: إنشاء مجموعة توسّع runner للتحقق من الـ PR
أنشئ pr-runner-values.yaml:
githubConfigUrl: "https://github.com/<org>/<repo>"
githubConfigSecret:
github_token: "<PAT>"
template:
spec:
containers:
- name: runner
image: ghcr.io/<org>/custom-runner:latest
command: ["/home/runner/run.sh"]
env:
- name: RUNNER_GROUP
value: "pr-validation"
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "1"
memory: "1Gi"
helm install arc-runner-pr \
actions-runner-controller/gha-runner-scale-set \
--namespace arc-runners \
-f pr-runner-values.yaml
الخطوة 2: إنشاء مجموعة توسّع runner للنشر
أنشئ deploy-runner-values.yaml:
githubConfigUrl: "https://github.com/<org>/<repo>"
githubConfigSecret:
github_token: "<PAT>"
template:
spec:
containers:
- name: runner
image: ghcr.io/<org>/custom-runner:latest
command: ["/home/runner/run.sh"]
env:
- name: RUNNER_GROUP
value: "deployment"
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "2"
memory: "2Gi"
serviceAccountName: deploy-runner-sa
nodeSelector:
runner-type: deployment
helm install arc-runner-deploy \
actions-runner-controller/gha-runner-scale-set \
--namespace arc-runners \
-f deploy-runner-values.yaml
الخطوة 3: تهيئة الـ workflows للعزل
استخدم وسوم runner مختلفة بناءً على مُشغِّل الـ workflow:
name: CI/CD Pipeline
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
validate:
if: github.event_name == 'pull_request'
runs-on: arc-runner-pr
steps:
- uses: actions/checkout@v4
- name: Run tests
run: make test
- name: Run linter
run: make lint
deploy:
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: arc-runner-deploy
steps:
- uses: actions/checkout@v4
- name: Deploy to production
run: make deploy
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
يطبّق هذا فصل المسؤوليات على مستوى الـ runner. فمهام التحقق من الـ PR تعمل على runners لا تملك وصولاً إلى أسرار النشر أو أجزاء الشبكة المميزة. أما مهام النشر فتعمل على مجموعة منفصلة من الـ runners تملك بيانات الاعتماد والوصول الشبكي اللازمين، لكنها تُطلَق فقط عند الدفع (push) إلى الفرع main.
التمرين 6: التوسّع التلقائي
يدعم ARC التوسّع التلقائي بشكل أصيل. فبودات الـ runner تُنشأ عند الطلب وتُدمَّر عند الخمول. ويمكنك تهيئة الحدّ الأدنى والأقصى من النسخ (replicas) للتحكم في التكلفة وسرعة الاستجابة.
الخطوة 1: تهيئة معاملات التوسّع التلقائي
حدّث ملف قيم مجموعة توسّع الـ runner لتضمين معاملات التوسّع:
githubConfigUrl: "https://github.com/<org>/<repo>"
githubConfigSecret:
github_token: "<PAT>"
minRunners: 0
maxRunners: 10
template:
spec:
containers:
- name: runner
image: ghcr.io/actions/actions-runner:latest
command: ["/home/runner/run.sh"]
helm upgrade arc-runner-set \
actions-runner-controller/gha-runner-scale-set \
--namespace arc-runners \
-f autoscale-values.yaml
الخطوة 2: توليد حمل
أنشئ workflow يُطلق عدة مهام متوازية:
name: Autoscale Test
on: workflow_dispatch
jobs:
parallel-job:
runs-on: arc-runner-set
strategy:
matrix:
id: [1, 2, 3, 4, 5]
steps:
- name: Simulate work
run: |
echo "Job ${{ matrix.id }} running on $(hostname)"
sleep 60
أطلق هذا الـ workflow وراقب توسّع البودات:
kubectl get pods -n arc-runners -w
سترى خمسة بودات تُنشأ — واحداً لكل مهمة في المصفوفة (matrix):
NAME READY STATUS RESTARTS AGE
arc-runner-set-abcde-runner 1/1 Running 0 5s
arc-runner-set-fghij-runner 1/1 Running 0 5s
arc-runner-set-klmno-runner 1/1 Running 0 5s
arc-runner-set-pqrst-runner 1/1 Running 0 5s
arc-runner-set-uvwxy-runner 1/1 Running 0 5s
بعد اكتمال المهام (60 ثانية)، تُنهى جميع البودات. وتعود مساحة الأسماء إلى صفر بودات.
الخطوة 3: تهيئة تأخير التقليص
لتحسين التكلفة، قد ترغب في أن تبقى البودات دافئة (warm) لفترة قصيرة بعد اكتمال المهمة. وهذا يتجنّب زمن بدء التشغيل البارد (cold-start) للأحمال المتقطعة. وسلوك التوسّع إلى الصفر في ARC هو الخيار الافتراضي والأكثر أماناً. وإذا احتجت إلى runners دافئة، فأبقِ النافذة قصيرة (أقل من 5 دقائق) وتأكّد من أن الوضع المؤقت لا يزال مفروضاً.
التمرين 7: السياسات الشبكية للـ runners
تتيح لك NetworkPolicies في Kubernetes تقييد الوصول الشبكي لبودات الـ runner. وهذا دفاع بالغ الأهمية ضد تسريب البيانات من عمليات build مخترَقة.
الخطوة 1: إنشاء NetworkPolicy
طبّق NetworkPolicy التالية على مساحة الأسماء arc-runners:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: runner-egress-policy
namespace: arc-runners
spec:
podSelector: {}
policyTypes:
- Egress
egress:
# Allow DNS resolution
- to:
- namespaceSelector: {}
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
# Allow GitHub API and Actions services
- to:
- ipBlock:
cidr: 140.82.112.0/20
- ipBlock:
cidr: 143.55.64.0/20
- ipBlock:
cidr: 185.199.108.0/22
- ipBlock:
cidr: 4.0.0.0/8
ports:
- protocol: TCP
port: 443
# Allow your container registry (example: ghcr.io)
- to:
- ipBlock:
cidr: 140.82.112.0/20
ports:
- protocol: TCP
port: 443
# Allow your artifact storage (replace with your CIDR)
# - to:
# - ipBlock:
# cidr: 10.0.0.0/8
# ports:
# - protocol: TCP
# port: 443
kubectl apply -f runner-network-policy.yaml
ملاحظة: تنشر GitHub نطاقات IP الخاصة بها على https://api.github.com/meta. استخدم نطاقات actions وapi. والقيم أعلاه أمثلة — تحقّق من النطاقات الحالية وحدّثها وفقاً لذلك.
الخطوة 2: اختبار NetworkPolicy
أنشئ workflow يحاول الوصول إلى رابط خارجي:
name: Network Policy Test
on: workflow_dispatch
jobs:
test-network:
runs-on: arc-runner-set
steps:
- name: Test GitHub API (should work)
run: curl -s -o /dev/null -w "%{http_code}" https://api.github.com
- name: Test external URL (should be blocked)
run: |
if curl -s --connect-timeout 5 https://evil-exfiltration-server.example.com; then
echo "FAIL: External access was allowed"
exit 1
else
echo "PASS: External access was blocked by NetworkPolicy"
fi
عند تشغيل هذا الـ workflow:
- ينجح طلب واجهة API الخاصة بـ GitHub (HTTP 200) لأن NetworkPolicy تسمح بالحركة إلى نطاقات IP الخاصة بـ GitHub.
- ينتهي طلب الرابط الخارجي بانتهاء المهلة ويفشل لأنه ليس في قائمة الحركة الصادرة المسموح بها.
يمنع هذا build مخترَقاً من تسريب الشيفرة المصدرية أو الأسرار أو artifacts الـ build إلى خادم يتحكم فيه المهاجم. وحتى لو نفّذت تبعية خبيثة شيفرة عشوائية أثناء الـ build، فلن تتمكن من الاتصال بالخارج.
التنظيف
أزل جميع الموارد التي أُنشئت أثناء هذا المختبر:
# Delete Helm releases
helm uninstall arc-runner-set -n arc-runners
helm uninstall arc-runner-pr -n arc-runners
helm uninstall arc-runner-deploy -n arc-runners
helm uninstall arc -n arc-systems
# Delete namespaces
kubectl delete namespace arc-runners
kubectl delete namespace arc-systems
# Delete the kind cluster
kind delete cluster --name arc-lab
إذا أنشأت GitHub App لهذا المختبر، فيمكنك حذفه من Settings → Developer settings → GitHub Apps. وألغِ أي رموز PAT أنشأتها.
أبرز الخلاصات
- الـ runners المؤقتة تقضي على التلوث بين المهام. فكل مهمة تحصل على حاوية جديدة — وتُدمَّر الأسرار والرموز وartifacts الـ build عند اكتمال المهمة.
- يوفّر ARC مزايا الـ runner ذاتي الاستضافة دون المخاطر الأمنية. فتحصل على أدوات مخصّصة، ووصول شبكي خاص، وتحكم في التكلفة، مع الحفاظ على نموذج الأمان المؤقت.
- صور الـ runner المخصّصة تمنحك تحكماً كاملاً في بيئة الـ build. ثبّت إصدارات الأدوات، وافحص بحثاً عن الثغرات، واقضِ على خطر سلسلة التوريد الناجم عن البرمجيات المثبّتة مسبقاً.
- عزل مجموعات الـ runner يطبّق فصل المسؤوليات. فworkflows التحقق من الـ PR والنشر تعمل على مجموعات runner منفصلة بامتيازات ووصول شبكي مختلفين.
- السياسات الشبكية طبقة دفاع بالغة الأهمية. فتقييد الحركة الصادرة للـ runner يمنع تسريب البيانات حتى لو اختُرقت خطوة build.
- التوسّع التلقائي إلى الصفر يقلّل التكلفة وسطح الهجوم. فبودات الـ runner لا توجد إلا طوال مدة المهمة — فلا توجد بنية تحتية دائمة يجب صيانتها أو تأمينها.
الخطوات التالية
واصل تعزيز وضعيتك الأمنية في CI/CD مع هذه الأدلة ذات الصلة:
- تأمين GitHub Actions Runners — تعمّق في أفضل ممارسات أمن الـ runner، وإدارة الرموز، والمراقبة، للـ runners المستضافة على GitHub وذاتية الاستضافة على حدّ سواء.
- فصل المسؤوليات والحدّ الأدنى من الامتيازات في خطوط أنابيب CI/CD — دليل شامل لتطبيق مبادئ الحدّ الأدنى من الامتيازات عبر خط أنابيب CI/CD بأكمله، من التحكم في المصدر إلى النشر في الإنتاج.