GitHub Actions لمشاريع بايثون: دليل عملي لإعداد CI

تنسى تشغيل pytest قبل الدمج، فيصل كود مكسور إلى الفرع الرئيسي ولا تكتشفه إلا لاحقاً. يلزمك شيئان: ملف واحد في مستودعك يجعل GitHub يشغّل اختباراتك مع كل دفعة وكل طلب سحب، وإعداد في صفحة المستودع يمنع الدمج ما لم تنجح الاختبارات. بعدها لا يبقى تشغيلها قراراً بشرياً. لكنّ بين ملف CI منسوخ على عجل وملف مضبوط فرقاً ثمنه دقائق ودولارات، وهذا الدليل يبني المضبوط.
أين يوضع الملف وما بنيته
الثابت هو المجلد لا الاسم: ضع الملف في .github/workflows/ci.yml أو سمِّه ما شئت. GitHub يقرأ كل ملف YAML داخل .github/workflows ويشغّله بوصفه سير عمل.
المفاتيح الأساسية أربعة: on يحدد متى يعمل السير، وjobs يضم المهام وكل مهمة تعمل على آلة مستقلة، وruns-on يحدد نظام تلك الآلة وubuntu-latest يقابل حالياً Ubuntu 24.04، وsteps قائمة الخطوات بالترتيب: إجراء جاهز عبر uses أو أمر طرفية عبر run. وسقف المهمة الواحدة 6 ساعات على مشغّلات GitHub — وستعود إليه لاحقاً خطراً لا ضمانة.
أول سير عمل يعمل فعلاً
name: CI
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: astral-sh/[email protected]
with:
enable-cache: true
- run: uv sync --locked --all-extras --dev
- run: uv run pytest tests
انتبه لاختلاف صيغة الوسم بين السطرين: actions/checkout@v7 صالح لأن هذا الإجراء ينشر وسماً رئيسياً متحركاً، أما setup-uv فتوقّف عن ذلك منذ إصداره الثامن؛ كتابة @v10 تفشل، والصحيح تثبيت الإصدار كاملاً @v10.0.1.
الحاسم هنا --locked: تفشل المهمة إن كان uv.lock غير محدَّث ويحتاج إلى إعادة توليد، أي أن أحدهم أضاف اعتمادية ونسي تحديث القفل. البديل --frozen يتجاهل هذا التحقق ويثبّت ما في القفل كما هو. في CI اختر --locked دائماً؛ الفشل الصريح أرخص من بناء صامت بحزم قديمة. أساسيات القفل نفسه تجدها في أداة uv لإدارة حزم بايثون: دليل عملي للانتقال من pip وvenv، أما كتابة الاختبارات التي يشغّلها السطر الأخير فموضوع شرح pytest: من أول اختبار إلى fixture وparametrize.
تشغيل واحد لا اثنان: ضبط on وconcurrency
الصيغة الشائعة on: [push, pull_request] بلا تقييد تشغّل كل فحص مرتين على أي فرع له طلب سحب مفتوح من المستودع نفسه: مرة لحدث الدفع ومرة لحدث طلب السحب، فتُحرق الدقائق مرتين. الحل كما تراه أعلاه: قيّد push بالفرع الرئيسي واترك فحص الفروع لحدث pull_request، فيُفحص كل فرع عبر طلب سحبه مرة واحدة، ويُفحص main بعد الدمج — وهو منطق قالب GitHub الرسمي لبايثون الذي يقيّد الحدثين معاً بالفرع الرئيسي.
والحدثان لا يفحصان الشيء نفسه: pull_request لا يختبر رأس فرعك بل commit دمج تجريبياً لفرعك مع الفرع الهدف (refs/pull/N/merge)، ولهذا قد ينجح الفحص على الفرع ويفشل في طلب السحب أو العكس. ليس خللاً؛ إنه فحص لنتيجة الدمج الفعلية.
أما concurrency فيعالج الدفعات المتتابعة: مع cancel-in-progress: true، دفعتان متتاليتان على الفرع نفسه تلغيان التشغيل الأول بدل تركه يستهلك دقائق على نتيجة لن يقرأها أحد.
سطران يحرسان التوكن والرصيد
permissions: contents: read سطر واحد يقيّد توكن سير العمل كله، لأن القاعدة الموثقة أن تحديد أي إذن صراحة يصفّر كل إذن لم يُذكر. المستودعات المنشأة منذ 2023 افتراضيها مقيّد أصلاً، لكن الأقدم قد تحمل الافتراضي المتساهل، والسطر الصريح يحميك في الحالين.
أما timeout-minutes: 10 فيقطع الطريق على الافتراضي: 360 دقيقة، أي سقف الساعات الست نفسه. اختبار واحد معلّق — انتظار شبكة لا يرد أو حلقة لا تنتهي — على مستودع خاص بلا مهلة قد يستهلك 360 دقيقة في تشغيل واحد، أي 18% من رصيد الخطة المجانية الشهري البالغ 2000 دقيقة. 10 دقائق سقف مريح لاختبارات بايثون عادية، ووسّعه إن ضاق.
Ruff: خطوة فحص وخطوة تنسيق
أضف Ruff اعتمادية تطوير: uv add --dev ruff. عندها يثبّت uv.lock إصداره، فلا تتغيّر نتيجة الفحص من تشغيل إلى آخر. ثم أضف خطوتين قبل سطر pytest في المهمة نفسها:
- run: uv run ruff check --output-format=github .
- run: uv run ruff format --check
الوسيط --output-format=github يجعل الأخطاء تظهر معلّمة على أسطرها داخل واجهة طلب السحب بدل أن تدفنها السجلات.
Ruff 0.16.0، الصادر في 23 يوليو 2026، رفع القواعد المفعّلة افتراضياً من 59 إلى 413. ترقيته على مشروع قائم قد تُفشل CI فجأة بمئات الأخطاء في كود لم يتغيّر. للعودة إلى السلوك السابق ثبّت select = ["E4", "E7", "E9", "F"] تحت جدول [tool.ruff.lint] في pyproject.toml ثم وسّع تدريجياً. التفاصيل في الانتقال إلى Ruff: فحص وتنسيق بايثون بأداة واحدة.
وخذ أرقام الإصدارات من صفحات إصدارات المستودعات نفسها لا من أمثلة التوثيق المتقادمة مثل ruff-action@v3 وcheckout@v4؛ الأحدث اليوم actions/checkout v7.0.1 وastral-sh/setup-uv v10.0.1 وastral-sh/ruff-action v4.1.0.
Ruff قبل pytest أم مهمة موازية؟
فصل Ruff في مهمة مستقلة يعطيه علامة خاصة في واجهة طلب السحب، لكن افهم الفوترة أولاً: كل مهمة تُقرَّب لأعلى إلى أقرب دقيقة كاملة على حدة، فمهمة Ruff منفصلة تنتهي في 20 ثانية تُفوتر دقيقة إضافية كاملة في كل تشغيل على المستودعات الخاصة.
| Ruff خطوة قبل pytest | Ruff مهمة موازية | |
|---|---|---|
| الدقائق المفوترة | تقريب واحد لأعلى | تقريب لكل مهمة |
| زمن النتيجة الكاملة | مجموع الخطوتين | أطول المهمتين فقط |
| الأنسب له | مستودع خاص محدود الدقائق | مستودع عام مجاني |
لهذا يعتمد قالب GitHub الرسمي التسلسل: فشل Ruff يوقف المهمة مبكراً قبل pytest بلا كلفة إضافية. وعلى مستودع عام لا تُحسب الدقائق أصلاً، فالتوازي ربح صافٍ في الوقت.
مصفوفة إصدارات بايثون ومتى تستحق
إن كانت مكتبتك يستخدمها آخرون، اختبرها على أكثر من إصدار:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v7
- uses: astral-sh/[email protected]
with:
enable-cache: true
python-version: ${{ matrix.python-version }}
- run: uv sync --locked --all-extras --dev
- run: uv run pytest tests
هذا المقطع يحل محل مهمة test كاملة، ولا يُلصق فوقها. python-version في setup-uv يضبط UV_PYTHON ويتقدّم على .python-version وعلى ما في pyproject.toml. والحد الأعلى للمصفوفة 256 مهمة لكل تشغيل.
القيمة الافتراضية لـ fail-fast هي true، فأول إصدار يفشل يلغي بقية المهام قبل أن تكتمل؛ اضبطه على false إن أردت رؤية الصورة كاملة. وإن كان مشروعك تطبيقاً داخلياً يعمل على إصدار واحد، فالمصفوفة ترف يضاعف الدقائق بلا فائدة.
حدود الكاش قبل أن تراهن عليه
قيمة enable-cache الافتراضية auto مفعّلة أصلاً على مشغّلات GitHub لحدثي الدفع وطلب السحب، وكتابتها صراحة توضيح لا أكثر؛ ويُبطَل الكاش عند أي تغيير في uv.lock أو pyproject.toml. لكن لا تعوّل على بقائه: حد كاش GitHub 10 GB لكل مستودع، وأي مدخلة لا تُقرأ لمدة 7 أيام تُحذف. ووثائق uv نفسها تقول: في CI غالباً ما يكون تنزيل عجلات بايثون الجاهزة من جديد أسرع من استعادتها من الكاش، وإنما يستحق الكاش عناءه للحزم التي تُبنى من المصدر. خيار prune-cache في setup-uv — يشغّل uv cache prune --ci — يقصر المخزَّن على هذا الغرض.
اجعل الفحص إلزامياً قبل الدمج
سير العمل وحده لا يمنع أحداً من الدمج فوق فحص فاشل. المسار: Settings ← Rules ← Rulesets ← New ruleset ← New branch ruleset، ثم فعّل قاعدة «Require status checks before merging» واختر مهمة الاختبار من القائمة. GitHub يوصي اليوم بـ Rulesets، لكن نظام Branch protection rules لم يُوقَف، والاثنان يعملان معاً على المستودع نفسه. Rulesets متاحة للمستودعات العامة مجاناً، أما الخاصة فتتطلب خطة مدفوعة من Pro فما فوق، وقواعد الحماية القديمة تعمل هناك.
مصيدتان بعد التفعيل. الأولى أن الفحص لا يظهر في قائمة الاشتراط قبل أن يعمل مرة واحدة على الأقل، فادفع تغييراً أولاً ثم اضبط القاعدة. والثانية سلوكان متعاكسان يخلط الناس بينهما: الفحص المطلوب يمر إذا كانت حالته نجاحاً أو تخطّياً أو حياداً، فمهمة تخطّاها شرط if: غير محقق تُحتسب مروراً وتسمح بالدمج، أما سير عمل لم يُطلق أصلاً — لأن فلتر مسارات استبعد التغيير مثلاً — فيبقى فحصه معلّقاً ويمنع الدمج. الأول ثقب صامت في حمايتك، والثاني طلب سحب محشور لا يُدمج.
يمر محلياً ويسقط على المشغّل
الأسباب المتكررة قليلة:
uv.lockغير مرفوع إلى المستودع أو غير محدّث.- ملفات موجودة على جهازك لكن
.gitignoreيستبعدها. - حساسية حالة الأحرف: لينكس يفرّق بين
Test_utils.pyوtest_utils.pyبينما ويندوز وماك لا يفرّقان، وgit يضبطcore.ignoreCaseتلقائياً عليهما فلا يلتقط إعادة تسمية غيّرت حالة الأحرف فقط — استيراد يعمل عندك ينفجر على المشغّل. - اختبارات تعتمد على الشبكة أو على متغيرات بيئة موجودة عندك فقط.
- اختبارات تقارن تواريخ بتوقيت جهازك المحلي؛ المشغّل لا يشاركك منطقتك الزمنية، فثبّت المنطقة داخل الاختبار بدل افتراضها.
- الأسرار لا تُمرَّر إلى طلبات السحب القادمة من نسخة متفرّعة، باستثناء
GITHUB_TOKENبصلاحيات مقيّدة.
وأحياناً العيب ليس في كودك: في 6 أغسطس 2026 تعطلت GitHub Actions نحو 10 ساعات، وفشل في الذروة 71% من التشغيلات بأعطال بنية تحتية. شغّل Ruff وpytest محلياً قبل الدفع دائماً؛ CI شبكة أمان، لا حَكم وحيد.
هل يستحق مشروعك ذلك؟
إن كان المستودع عاماً فنعم: GitHub Actions مجاني على المشغّلات القياسية للمستودعات العامة، ولا تحتاج إلى بطاقة دفع لتشغيله، وهذه عقبة تسقط عن كثير من المطوّرين العرب.
للمستودعات الخاصة، الخطة المجانية تمنحك 2000 دقيقة شهرياً، والأنظمة لا تُحسب بالتساوي: دقيقة لينكس ×1 وويندوز ×2 وماك ×10، وأسعار التجاوز على الترتيب 0.006 و0.010 و0.062 دولاراً للدقيقة. وبلا وسيلة دفع مسجلة لا تجاوز أصلاً: يتوقف التشغيل فور نفاد الحصة، ولهذا ليس ضبط المهلة ومنع التشغيل المزدوج ترفاً. أبقِ CI على ubuntu-latest وحده ما لم يكن مشروعك يستهدف ويندوز أو ماك فعلاً، ولا تفعّل المصفوفة قبل أن تحتاج إليها.
وما يوفّر دقائق CI أكثر من أي ضبط آخر هو العدّة نفسها: أدوات بايثون الحديثة: دليل uv وRuff وpyproject.toml تشرح البديل الأسرع لكل خطوة تثبيت وفحص في المسار أعلاه، وشرح pyproject.toml يجمع إعداداتها في ملف واحد يقرؤه المسار.