نموذج الإصدارات
YY.M.patch.build-type، حيث يمثّل YY السنة بصيغة رقمين، ويمثّل M شهر الإصدار (من دون صفر بادئ)، ويمثّل patch رقم التصحيح ضمن الفرع، ويمثّل build رقم build متزايدًا باستمرار، ويكون type إما stable أو lts.
مثال: 25.3.8.23-lts — إصدار LTS لشهر مارس 2025، مع التصحيح 8 وbuild رقم 23.
يوجد مساران للإصدار:
- تُنشر إصدارات Stable شهريًا تقريبًا. وتتلقى أحدث ثلاثة إصدارات مستقرة تصحيحات، ما يوفّر نحو ثلاثة أشهر من الدعم النشط لكل إصدار.
- تُنشر إصدارات LTS (الدعم طويل الأمد) في مارس وأغسطس من كل عام. ويجري دعم إصدارين من LTS في الوقت نفسه، لمدة لا تقل عن 12 شهرًا لكل منهما.
سياسة النقل الرجعي
- إصلاحات الأمان — تُنقل رجعيًا دائمًا.
- إصلاحات الأخطاء الحرجة (الاستثناءات (الأخطاء المنطقية)، فقدان البيانات، النتائج غير الصحيحة، مشكلات RBAC) — تُحدَّد تلقائيًا للنقل الرجعي وفقًا للقواعد العامة للنقل الرجعي؛ ويُعرَف ذلك بالتصنيف
pr-critical-bugfix، الذي يؤدي إلى إضافةpr-must-backportتلقائيًا. - إصلاحات الاستقرار والتراجعات — تُنقل رجعيًا عندما تكون مخاطر التغيير منخفضة مقارنةً بمخاطر ترك الخطأ دون إصلاح؛ ويُشار إليها بالتصنيف
pr-must-backportالذي يضيفه القائمون على الصيانة يدويًا. - إصلاحات الأخطاء الطفيفة التي يتوفر لها حل بديل — لا تُنقل رجعيًا في العادة تجنبًا لزعزعة استقرار فروع الإصدار.
- الميزات الجديدة، والتحسينات، وأعمال تحسين الأداء — لا تُنقل رجعيًا.
pr-must-backport التجاوز اليدوي الذي يستخدمه القائمون على الصيانة لتمييز طلب سحب بغرض نقله رجعيًا. أما التصنيف pr-critical-bugfix فيؤدي إلى إضافة pr-must-backport تلقائيًا بواسطة CI hook (راجع pr_labels_and_category.py).
تصعيد التعارضات. عندما يتعذر على النقل الرجعي التلقائي حل تعارضات الدمج، يجب مع ذلك إنشاء طلب سحب cherry-pick وإسناده إلى المؤلف، والشخص الذي أجرى الدمج، والمكلّفين الحاليين في طلب السحب الأصلي، حتى يتمكن شخص من حل التعارضات وإكمال النقل الرجعي.
أداة النقل إلى الإصدارات الأقدم
tests/ci/cherry_pick.py. تعمل الأداة كسير عمل GitHub Actions على البنية التحتية لـ ClickHouse، وتغطي جميع المتطلبات: اكتشاف فروع الإصدار النشطة، واختيار طلبات السحب المؤهلة للنقل إلى الإصدارات الأقدم، وتنفيذ إجراءَي cherry-pick والنقل إلى الإصدارات الأقدم على مرحلتين، وإدارة التعارضات، وفرض سياسة التأخير، والحفاظ على تزامن التصنيفات.
الهدف على المدى الطويل هو استخلاص هذا التنفيذ في أداة بايثون مستقلة ومفتوحة المصدر يمكن للمشاريع الأخرى اعتمادها. والتصميم المستهدف هو:
- قابلة للتهيئة — تُعرَّف جميع معلمات السياسة (التصنيفات المؤهِّلة، ونافذة التأخير، وحدود طلبات السحب الراكدة، وما إلى ذلك) في ملف تهيئة، بحيث يمكن تكييف الأداة لتلائم متطلبات النقل إلى الإصدارات الأقدم لأي مشروع من دون إجراء تغييرات على الشيفرة.
- قابلة للتوزيع — تُحزَّم كحزمة wheel لبايثون مكتفية ذاتيًا وقابلة للتثبيت من PyPI، من دون أي اعتماد على البنية التحتية لـ CI الخاصة بـ ClickHouse.
- قابلة للبرمجة — توفّر نموذج كائنات واضحًا لطلبات السحب، والتصنيفات، وفروع الإصدار، بحيث يمكن للمستخدمين بناء سير عمل مخصَّصة فوق المحرك الأساسي.
الاختبار
- مجموعة قابلة للتهيئة من فروع الإصدار التي تمثل خطوط الإصدار،
- طلبات سحب تحمل توليفات مختلفة من تصنيفات backport،
- طلبات سحب الإصدار التي تحمل تصنيف
releaseوتشير إلى فروع الإصدار.
فروع الإصدار النشطة
release) مفتوحًا على GitHub. تكتشف أتمتة النقل إلى الإصدارات الأقدم هذه الفروع ديناميكيًا عند كل تشغيل، لذلك لا حاجة إلى أي تغييرات في الإعدادات عند إصدار نسخة جديدة أو وصول نسخة قديمة إلى end-of-life.
يحدّد التصنيف الخاص بالإصدار أقدم إصدار يجب أن يصل إليه طلب السحب: إذ يُنقَل إلى الإصدارات الأقدم إلى ذلك الإصدار وإلى كل فرع إصدار نشط أحدث منه، وليس إلى الإصدار المسمّى وحده. على سبيل المثال، إذا وُضع v25.3-must-backport على طلب سحب دُمج في فرع التطوير، فسيُنقَل إلى الإصدارات الأقدم إلى 25.3 وإلى كل فرع إصدار نشط بعده (25.4 و25.5 و…). وإذا وُجدت عدة تصنيفات خاصة بالإصدار، فالأدنى إصدارًا هو الذي يُعتدّ به، لأنه يشمل الإصدارات الأحدث أصلًا.
ليس من الضروري أن يكون الإصدار المسمّى نشطًا بحد ذاته. فالتصنيف الخاص بإصدار بلغ end-of-life (أي إصدار لا يملك طلب سحب إصدار مفتوحًا) يواصل دفع الإصلاح إلى كل إصدار نشط يليه، بحيث لا تؤدي الترقية من ذلك الإصدار إلى فقدان الإصلاح بصمت. على سبيل المثال، يظل v25.12-must-backport على طلب سحب ما يدفع النقل إلى الإصدارات الأقدم إلى 26.1 و26.2 و… حتى بعد أن يكون 25.12 نفسه قد بلغ end-of-life.
التنفيذ
نظرة عامة
CherryPick في GitHub Actions (.github/workflows/cherry_pick.yml)، والمُنفَّذ في tests/ci/cherry_pick.py. وهي تعمل عبر واجهة برمجة تطبيقات GitHub وعمليات git محلية على مشغّل style-checker-aarch64 ذاتي الاستضافة.
تجري العملية على مرحلتين لكل زوج من (original PR، فرع الإصدار):
- يُنشأ cherry-pick PR لعزل حلّ التعارضات عن هدف الدمج الفعلي. وإذا لم توجد تعارضات، يُدمج تلقائيًا.
- يُنشأ backport PR على فرع الإصدار الفعلي، مع ضغط التغييرات المنتقاة في commit واحد.
التصنيفات
تسمية الفروع وطلبات السحب
N وفرع إصدار release/X.Y:
- فرع
cherry-pick:cherrypick/release/X.Y/N - فرع
backport:backport/release/X.Y/N - عنوان طلب سحب
cherry-pick:Cherry pick #N to release/X.Y: <original title> - عنوان طلب سحب
backport:Backport #N to release/X.Y: <original title>
العملية خطوة بخطوة
1. اكتشاف الإصدارات النشطة
BackportPRs.receive_release_prs من GitHub عن جميع طلبات السحب المفتوحة التي تحمل التصنيف release. وتمثل مراجع head لهذه الطلبات أسماء فروع الإصدار (مثل release/25.3). ومن ثم يستنتج منها مجموعة التصنيفات الخاصة بالإصدار التي يجب البحث عنها: أي تصنيف v{VER}-must-backport موجود في المستودع ولا يكون إصداره أحدث من أحدث إصدار نشط. وتُدرج التصنيفات الأقدم حتى إذا لم يعد إصدارها نشطًا (ويُتجاوز أي تصنيف أحدث من جميع الإصدارات النشطة، لأنه لا يمكن توسيعه إلى أي فرع نشط)، لذلك يظل من الممكن العثور على طلب سحب موسوم لإصدار وصل إلى end-of-life ما دام هناك إصدار أحدث لا يزال نشطًا.
2. العثور على طلبات السحب المطلوب ترحيلها عكسيًا
BackportPRs.receive_prs_for_backport واجهة برمجة تطبيقات البحث في GitHub للعثور على طلبات السحب المدمجة التي:
- تحمل تصنيفًا واحدًا على الأقل للترحيل العكسي (
pr-must-backportأوpr-must-backport-forceأوpr-critical-bugfixأو تصنيفًا خاصًا بإصدار معيّن)، و - لا تحمل بالفعل
pr-backports-created، و - دُمجت بعد أقدم تاريخ commit جرى العثور عليه في أي فرع إصدار، و
- جرى تحديثها خلال آخر 90 يومًا (للحفاظ على كفاءة query البحث).
3. مرحلة cherry-pick (ReleaseBranch.create_cherrypick)
cherry-pick:
- انتقل إلى فرع الإصدار وأنشئ منه فرع backport (
backport/release/X.Y/N). - نفّذ
git merge -s oursعلى الأصل الأول لالتزام الدمج لإنشاء قاعدة دمج اصطناعية من دون أي تغييرات في المحتوى. - أنشئ بالقوة فرع cherry-pick (
cherrypick/release/X.Y/N) بحيث يشير مباشرةً إلى التزام الدمج الخاص بطلب السحب الأصلي. - حاول تنفيذ
git merge --no-commit --no-ffلدمج فرع cherry-pick في فرع backport:- إذا كان محدَّثًا بالفعل، فهذا يعني أن التغيير موجود أصلًا في فرع الإصدار — علِّمه كمكتمل وتجاوزه.
- وإلا (سواء وُجدت تعارضات أم لا)، فأعِد الضبط وادفع الفرعين.
- أنشئ طلب سحب
cherry-pickيستهدفbackport/release/X.Y/Nانطلاقًا منcherrypick/release/X.Y/N، مع الوسمينpr-cherrypickوdo not test. - مرِّر
pr-bugfixأوpr-critical-bugfixمن طلب السحب الأصلي عند الاقتضاء. - لا يتم تعيين مُسنَد إليهم في هذه المرحلة؛ إذ لا تتم إضافتهم إلا عند اكتشاف تعارضات.
4. الدمج التلقائي لطلبات سحب cherry-pick الخالية من التعارضات
cherry-pick قابلاً للدمج (من دون تعارضات)، يدمجه البوت تلقائيًا عبر واجهة برمجة تطبيقات GitHub ثم ينتقل فورًا إلى مرحلة backport.
5. مرحلة backport (ReleaseBranch.create_backport)
- انتقل إلى فرع backport واسحب أحدث التحديثات.
- اعثر على قاعدة الدمج بين فرع الإصدار وفرع backport.
- نفّذ
git reset --softإلى قاعدة الدمج، مع دمج جميع التزامات cherry-pick في التزام واحد. - أنشئ التزامًا مستخدمًا عنوان طلب سحب backport كرسالة.
- ادفع فرع backport بالقوة وافتح طلب سحب backport يستهدف فرع الإصدار الفعلي.
- أضف إلى طلب السحب الوسم
pr-backport(وpr-bugfix/pr-critical-bugfixعند الاقتضاء). - عيِّن طلب السحب إلى مؤلف طلب السحب الأصلي، ومن نفّذ دمجه، ومُسنَد إليهم حاليًا (باستثناء حسابات الروبوت).
6. الاكتمال
pr-backports-created إلى طلب السحب الأصلي.
7. فحص مسبق
ReleaseBranch.pre_check الأمر git merge-base --is-ancestor للتحقق من أن التزام الدمج ليس قابلًا للوصول إليه بالفعل من فرع الإصدار. وإذا كان الأمر كذلك، فيُعتبر طلب السحب نُقل إلى الإصدارات الأقدم بالفعل ويتم تجاوزه.
التعامل مع طلبات السحب Cherry-pick الراكدة
CherryPickPRs في بداية كل تشغيلٍ يُنفَّذ كل ساعة، وتتعامل مع حالتين:
- طلبات السحب cherry-pick اليتيمة: إذا لم يعد لفرع الإصدار الخاص بطلب سحب cherry-pick طلب سحب إصدار مفتوح (أي إن الإصدار أُغلق)، فسيُغلَق طلب السحب cherry-pick تلقائيًا.
- طلبات السحب cherry-pick المعاد فتحها: إذا كان طلب السحب الأصلي يحمل بالفعل الوسم
pr-backports-created، لكن طلب سحب cherry-pick الخاص به لا يزال مفتوحًا، فسيُزال الوسمpr-backports-createdمن طلب السحب الأصلي لكي يمكن إعادة معالجته.
- بعد 3 أيام من دون أي تحديثات، ينشر الروبوت تعليق تذكير مع الإشارة إلى المُسنَد إليهم.
- بعد 7 أيام من دون أي تحديثات، ينشر الروبوت تعليق إغلاق ويغلق طلب السحب.
حل التعارضات
cherry-pick في تعارضات، يُترك طلب السحب الخاص بـ cherry-pick مفتوحًا لكي يعالجها أحد الأشخاص. ويُسنِد الروبوت الطلب إلى مؤلف طلب السحب الأصلي، ومن قام بدمجه، والمُعيَّنين له. وبعد حل التعارضات ودمج طلب السحب الخاص بـ cherry-pick، يُنشئ الروبوت طلب سحب backport في التشغيل التالي المجدول كل ساعة.
للتخلّي عن backport بالكامل، أغلِق طلب السحب الخاص بـ cherry-pick. وسيتعامل الروبوت معه على أنه تم تخطيه عمدًا.
لإعادة إنشاء طلب سحب cherry-pick معطّل من الصفر:
- أزِل التصنيف
pr-cherrypickمن طلب السحب الخاص بـcherry-pick. - احذف الفرع
cherrypick/.... - أزِل
pr-backports-createdمن طلب السحب الأصلي إذا كان موجودًا.
CI لطلبات السحب الخاصة بالترحيل العكسي
BackportPR، والمُعرَّف في ci/workflows/backport_branches.py) بدلًا من سير عمل طلب السحب القياسي. يشغّل سير العمل هذا مجموعة فرعية ممثّلة من CI: عمليات بناء ASan/UBSan وTSan، وعمليات بناء الإصدار، وعمليات بناء macOS، والاختبارات الوظيفية تحت ASan، واختبارات الضغط تحت TSan، واختبارات التكامل. كما يتحقق من أن فرع الترحيل العكسي يحتوي على ما بين 1 و50 عملية commit، وعلى ملف واحد معدَّل على الأقل (ويُفرَض ذلك بواسطة check_backport_branch.py).
المصادقة
ROBOT_CLICKHOUSE_SSH_KEY) لعمليات git push. وتتم المصادقة على استدعاءات واجهة برمجة تطبيقات GitHub عبر get_best_robot_token، التي تختار الرمز المميز صاحب أكبر حصة متبقية من مجموعة مخزّنة في SSM (/github-tokens). ويُستخدم ROBOT_CLICKHOUSE_COMMIT_TOKEN في خطوة checkout ضمن سير عمل Actions، وليس لاستدعاءات واجهة برمجة التطبيقات. وتُستبعد حسابات الروبوت (robot-clickhouse, clickhouse-gh) عند إسناد المسؤولية إلى شخص معيّن.
ذاكرة التخزين المؤقت لواجهة برمجة تطبيقات GitHub
GitHubCache (من cache_utils.py) بحفظ ذاكرة التخزين المؤقت لكائن PyGithub في S3، مما يقلل من استدعاءات واجهة برمجة التطبيقات بين عمليات التشغيل كل ساعة. تُنزَّل ذاكرة التخزين المؤقت في بداية كل تشغيل وتُرفع في نهايته.
معالجة الأخطاء
BackportException. وفي CI، يؤدي ذلك إلى إرسال إشعار عبر CIBuddy إلى دردشة الفريق.