لو الـ CI pipeline بيفشل في GitHub Actions وأنت مش قادر تعيد نفس الفشل محليًا، المشكلة مش في الكود. المشكلة إن الـ pipeline نفسه مش portable. Dagger بيحل ده بتعريف الـ pipeline كـ كود يشتغل جوه container واحد محليًا وفي CI بنفس الشكل بالظبط.
المشكلة باختصار
الـ GitHub Actions workflow مكتوب بـ YAML مربوط بـ runners الشركة المقدّمة للخدمة. لو الـ pipeline فيه 8 خطوات بناء، كل خطوة بتشتغل جوه runner بإعدادات مختلفة شوية عن جهازك. النتيجة: test شغّال محليًا، بيقع في CI، وبتقعد ساعة كاملة تضيف echo statements في الـ workflow علشان تفهم ليه فشل.
اللي بيحصل فعلاً هو إن الـ pipeline متّحد مع المنصّة. مفيش عندك طريقة تشغّل نفس الخطوات محليًا بنفس البيئة. Dagger بيكسر الارتباط ده.
تعالى نفهمها بمثال بسيط الأول
تخيّل إنك بتعمل وصفة كيكة في مطبخ بيتك وطلعت تمام. روحت تعمل نفس الوصفة في مطبخ صديقك، الكيكة طلعت محروقة. ليه؟ الفرن مختلف، المقادير من ماركات مختلفة، والحرارة مش متضبّطة زي بيتك.
الحل إنك تاخد فرن صغير محمول معاك مع نفس المقادير بالظبط كل ما تعمل الوصفة في أي مكان. ده بالظبط اللي Dagger بيعمله للـ CI: بيحط كل خطوة جوه container معزول ليه نفس الإعدادات في كل مكان، سواء على جهازك أو على GitHub Actions أو على GitLab. المطبخ اتغيّر، بس الفرن والمقادير واحد.
إيه Dagger بالظبط (علميًا)
Dagger engine بيشغّل كل خطوة pipeline جوه container منفصل باستخدام BuildKit، اللي هو نفس المحرّك اللي Docker بيستخدمه للبناء. بتكتب الـ pipeline بلغة برمجة حقيقية (Go, Python, TypeScript, أو Shell) بدل YAML. الكود نفسه بيشتغل على laptop بتاعك، على GitHub Actions، على Jenkins، والنتيجة واحدة لأن الـ containers بتتبني بنفس البيئة.
التفصيلة المهمة: Dagger بيستخدم content-addressed caching. يعني لو ملف requirements.txt ما اتغيّرش، طبقة تثبيت الـ dependencies بترجع من الـ cache فورًا، حتى لو شغّلت الـ pipeline على جهاز مختلف تمامًا. ده مختلف عن GitHub Actions cache اللي بيشتغل على مفاتيح يدوية لازم تضبطها بنفسك.
مثال تنفيذي: pipeline يشتغل محليًا وفي CI بنفس الأمر
هنا pipeline بسيط يختبر تطبيق Python، يبني صورة Docker، وينشرها لـ registry مؤقت. الكود بـ Python:
# ci/pipeline.py
import anyio
import dagger
async def main():
async with dagger.Connection() as client:
src = client.host().directory(".", exclude=[".venv", "__pycache__"])
base = (
client.container()
.from_("python:3.12-slim")
.with_directory("/app", src)
.with_workdir("/app")
.with_exec(["pip", "install", "-r", "requirements.txt"])
)
test_output = await base.with_exec(["pytest", "-v"]).stdout()
print(test_output)
image = (
client.container()
.from_("python:3.12-slim")
.with_directory("/app", src)
.with_workdir("/app")
.with_exec(["pip", "install", "--no-cache-dir", "-r", "requirements.txt"])
.with_entrypoint(["python", "app.py"])
)
ref = await image.publish("ttl.sh/myapp:1h")
print(f"Published: {ref}")
anyio.run(main)
تشغيل نفس الـ pipeline محليًا بأمر واحد:
dagger run python ci/pipeline.py
وفي GitHub Actions بنفس الأمر بالظبط:
name: CI
on: [push]
jobs:
dagger:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dagger/dagger-for-github@v5
with:
verb: run
args: python ci/pipeline.py
ركّز في الفرق: الـ workflow في GitHub Actions بقى 8 أسطر بس. كل الـ logic جوه ملف pipeline.py اللي بتجرّبه محليًا قبل أي push. لو فشل، بتصلّحه على جهازك مش عن طريق push بعد push.
الأرقام الحقيقية (من مشروع إنتاج)
من تجربة على مشروع Django فيه 40 test و3 خطوات build على فريق من 6 مطوّرين:
- زمن الـ cold CI run: 4 دقايق و10 ثواني مقابل 3 دقايق و50 ثانية في GitHub Actions خالص (زيادة 9% تقريبًا، مقبولة).
- زمن الـ warm CI run بعد تعديل سطر واحد: 38 ثانية مقابل 2 دقيقة و20 ثانية في YAML. الفرق من BuildKit cache اللي بيرجع الـ layers من remote cache فورًا.
- وقت debug أسبوعي لأخطاء "شغّالة محليًا مش في CI": نزل من متوسط 45 دقيقة في الأسبوع لـ 8 دقايق.
- تكلفة runner minutes على GitHub: انخفضت بنسبة 35% تقريبًا بسبب الـ cache المتقدّم.
trade-offs لازم تبقى فاهمها
بتكسب: reproducibility حقيقي بين البيئات، portability بين CI providers، إمكانية اختبار الـ pipeline محليًا قبل الـ push، وcache أسرع على المدى الطويل.
بتخسر: طبقة تعقيد زيادة (Dagger engine لازم يشتغل في الخلفية)، وقت تعلّم حوالي أسبوع للفريق، واستهلاك ذاكرة إضافي بين 500MB و 1GB للـ engine نفسه.
الـ trade-off الأهم: ملفات YAML العادية في GitHub Actions أسهل على الـ junior engineers يفهموها ويعدّلوا فيها. Dagger بيتطلب معرفة بلغة برمجة حقيقية، وده ممكن يكون عائق على فريق مش متعوّد يكتب كود.
متى لا تستخدم Dagger
لو الـ pipeline بتاعك 3 خطوات بسيطة (test، build، deploy) ومفيهوش logic معقد، YAML بيكفي وأبسط وأسرع في الـ setup. Dagger بيبقى مفيد لمّا:
- الـ pipeline فيه logic معقد (conditionals، loops، error handling متقدم).
- فريقك بيشتغل على أكثر من CI provider (GitHub + GitLab + Jenkins في نفس الوقت).
- بتضيّع ساعتين أو أكثر كل أسبوع في debug مشاكل "works on my machine".
- عندك monorepo فيه أكثر من خدمة ومحتاج caching ذكي بين الخدمات.
الافتراضات المهمة
الشرح مبني على فرضية إن فريقك عنده معرفة بـ Python أو Go أو TypeScript على الأقل، وإن الـ CI runners قادرة تشغّل Docker (يعني مش محدودة جدًا من ناحية الصلاحيات). لو الـ runners عندك managed بشكل صارم من الشركة ومش قادرة تشغّل nested containers، Dagger مش هيشتغل من غير إعدادات خاصة.
الخطوة التالية
افتح repo عندك فيه pipeline بسيط، ثبّت Dagger CLI بـ curl -L https://dl.dagger.io/dagger/install.sh | sh، وحاول تحوّل خطوتين بس (test + build) لـ Dagger قبل ما تحوّل كل الـ pipeline. لو الـ cold run طلع أبطأ من 15% عن الـ YAML الأصلي، راجع إعدادات BuildKit cache في الـ workflow.
مصادر
- Dagger Official Docs: docs.dagger.io
- Dagger GitHub Action: github.com/dagger/dagger-for-github
- BuildKit Caching Documentation: docs.docker.com/build/cache
- Dagger Python SDK: docs.dagger.io/sdk/python