لو الـ 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)