# الترقيات

يخبرك صباربيز في لوحة الإدارة حين يصدر إصدار موقّع أحدث. الإصدار الآمن يُثبَّت بنقرة واحدة، والإصدار الذي يغيّر قواعد بيانات البيئات يُثبَّت بعد أن تؤكد تحذيرًا. يبدأ التحديث من لوحة الإدارة، أو التحديث التلقائي، بانتظار انتهاء العمل الجاري، وتبدأ كل ترقية بنسخة احتياطية من بيئاتك. تنتظر حركة التطبيقات حتى يجتاز الإصدار الجديد فحوص السلامة، فإن لم يجتزها عاد صباربيز وحده إلى الإصدار السابق. يمكنك أيضًا أن تترك الإصدارات الآمنة تُثبَّت وحدها داخل نافذة صيانة تختارها، وهذا الخيار معطّل افتراضيًا.

لا شيء هنا جاهز للإنتاج. بُنيت قناة التحديث (فحص الإصدارات، وصفحة لوحة الإدارة، والتثبيت بنقرة، والتحديث التلقائي، وحارس التشغيل، والتصريف) في 2026-09-25، وتغطيها اختبارات الوحدات وثلاث حالات في CI نجحت في 2026-09-25 على جهاز CI نظيف، لا على خادم حقيقي ([الدليل](https://github.com/M7MMAD-OMAR/sbarbase/blob/main/docs/evidence/docker-upgrade-checks.json)، 45 فحصًا من 45): عاد وحده كلٌّ من إصدار يرحّل فهرس التحكم ثم يتوقف، وإصدار لا يجيب فيه PostgREST أبدًا، وإصدار لا ينجح في فحوص السلامة أبدًا، مع استعادة لقطة الفهرس، وكانت حركة التطبيقات تُجاب بالرمز 503 بينما تنتظر فحوص السلامة، ورُفض وسم غير موقّع ووسم موقّع بمفتاح غير مسرود دون أن يتحرك شيء. نقلت تلك التشغيلات نسخة المستودع بالأمر `lab/upgrade.py` من سطر الأوامر. وفي 2026-09-26 شغّلت الآلة الافتراضية التجريبية، تحت systemd، دورة سطر الأوامر ثم القناة نفسها كما يستخدمها المشغّل: إصدارات موقّعة ظهرت وثُبّتت عبر واجهة الإدارة التي تستخدمها لوحة الإدارة (لم تُشغَّل صفحة التحديثات نفسها) وبالتحديث التلقائي، وإصدار معطوب عاد منه صباربيز وحده، وإصدار يغيّر Auth ثُبّت بعد تأكيد تحذيره. وعلى الخادم الحقيقي كان 0.3.1 أول إصدار موقّع ثُبّت من سطر الأوامر بالأمر `lab/upgrade.py start --release`؛ **أما زر التثبيت في لوحة الإدارة والتحديث التلقائي والزر Roll back فلم تُشغَّل على خادم حقيقي بعد**؛ انظر [ما الذي شُغّل](https://base.sbarah.com/en/docs/guides/upgrades/#what-has-been-run).

## أين ترى التحديث

لا يرى شيئًا عن التحديثات إلا مشغّل التثبيت، أي مالك أو مدير في المنظمة التي أُنشئت عند الإعداد. بقية الأعضاء لا يرون شيئًا.

- **الإشعار:** حين يجد الفحص إصدارًا أحدث يمكنك تثبيته، يظهر أعلى لوحة الإدارة إشعار يذكر الإصدار وصنفه، مثل "Sbarbase 0.2.0 ready to install". وحين يوجد إصدار موقّع أحدث لا يستطيع هذا التثبيت أن يثبّته بعد، يقول الإشعار ذلك. يفتح الزر **View updates** صفحة التحديثات. يمكنك إخفاء الإشعار، ويعود مع الإصدار التالي.
- **صفحة التحديثات:** تعرض الإصدار المثبّت ووقت آخر فحص، مع زر **Check now**. وللإصدار المعروض تعرض الوسم والإيداع، وهل هو موقّع، وصنفه، وملاحظات الإصدار (بالعربية حين يفضّل متصفحك العربية، وإلا فبالإنجليزية)، وسبب تصنيفه، والصور المثبّتة التي تتغير، وما يمنع تثبيته الآن إن وُجد مانع. وتُسرد الإصدارات الأحدث التي تخطّاها الفحص مع السبب. وتحت ذلك آخر تحديث ونتيجته، ثم إعدادات التحديث.
- **الإشعارات:** إذا ضبطت [إشعارات المشغّل](https://base.sbarah.com/docs/reference/configuration/)، تصلك أيضًا `update.available` مرة واحدة لكل إصدار، و`update.applied` حين يُعتمد التحديث، و`update.rolled_back` حين يفشل إصدار جديد ويعود التثبيت وحده، و`update.rollback_failed` (حرجة) حين تفشل العودة أيضًا.

الخادم هو الذي يقرر هل يمكن تثبيت إصدار الآن، لا الصفحة: يحكم المشرف على ذلك وينشر حكمه، وتعرض لوحة الإدارة الحكم وسببه كما هما.

يفحص صباربيز كل 6 ساعات، وأول فحص بعد 5 دقائق من تشغيله. إذا فشل الفحص (خادم بلا اتصال مثلًا) أعاد المحاولة بعد 30 دقيقة، ثم بعد ساعة، ثم ساعتين، ثم 4 ساعات، ثم يعود إلى كل 6 ساعات، وتبقى آخر نتيجة سليمة في الصفحة. الفحص لا يثبّت شيئًا أبدًا.

## ثبّت إصدارًا من لوحة الإدارة

1. افتح **Updates** واقرأ ملاحظات الإصدار وقائمة ما يتغير.
2. اضغط **Install update**. لا يتاح الزر إلا حين يقول الخادم إن الإصدار قابل للتثبيت الآن.
3. في الإصدار الموسوم **Needs your confirmation**، اقرأ التحذير واختر المربع الذي يقول إن بيانات البيئات قد تحتاج استعادة من النسخ الاحتياطية إذا عاد التحديث إلى الإصدار الحالي. ثم اضغط **Install now**.
4. ابقَ في الصفحة. تعرض كل مرحلة: انتظار الخادم، ثم إنهاء العمل الجاري، ثم النسخ الاحتياطي والانتقال، ثم إعادة التشغيل، ثم فحوص السلامة. ينقطع اتصال لوحة الإدارة بضع دقائق أثناء إعادة التشغيل، ثم يعود وحده.
5. حين تقول الصفحة إن التحديث ثُبّت وإنه سليم، اضغط **Reload console** لتحمّل الصفحة الإصدار الجديد.

إذا لم يصبح الإصدار الجديد سليمًا، تخبرك الصفحة بذلك، ويكون صباربيز قد عاد إلى إصدارك السابق. وإذا لم تظهر نتيجة نهائية بعد 10 دقائق، فنفّذ `python3 lab/upgrade.py status` على الخادم.

## ما الذي يجري خطوة بخطوة

1. **الطلب:** لا تستطيع لوحة الإدارة ترقية نفسها، فهي تكتب طلبًا وينفّذه المشرف، وهو العملية التي تشغّل كل شيء، بعد أن يحكم عليه من جديد. لا يوجد إلا طلب واحد في كل وقت، والطلب الذي لا يلتقطه أحد خلال ساعة يسقط. وينتظر التحديث انتهاء النسخ الاحتياطي اليومي إن كان جاريًا.
2. **التصريف:** يتوقف عامل التجهيز عن التقاط مهام جديدة، وينتظر المشرف حتى تنتهي المهمة التي بين يديه، وأي تغيير في Studio أو تسجيل الدخول أو Realtime أو Edge Functions أو الوصول إلى قاعدة البيانات أو مفتاح التوقيع، والنسخ الاحتياطي اليومي، وحتى لا يبقى سجل عملية ينتظر التسوية. لا يبدأ شيء جديد في هذه الأثناء. وإذا استغرق ذلك أكثر من 10 دقائق فشل الطلب، ولا ينتقل شيء، ويستمر التجهيز.
3. **الفحوص:** يجلب الأمر `lab/upgrade.py start --release <tag>` وسم الإصدار من جديد، ويتحقق من توقيعه، ويحسب صنفه مرة أخرى من المصدر. ويرفض دون أن يغيّر شيئًا إذا كان في نسخة المستودع تعديلات محلية على ملفات متتبَّعة، أو إذا لم يحوِ الإصدار كل إيداع تشغّله نسخة المستودع، أو إذا غيّر الإصدار صورة PostgreSQL، أو إذا لم تكتمل بداية ترقية سابقة، أو إذا بقي سجل عملية ينتظر التسوية، أو إذا كان نسخ احتياطي أو استعادة قيد التنفيذ. ملفات الأدلة التي كتبها اختبار الاستلام والفحوص الحية داخل `docs/evidence/` لا تُعد تعديلات محلية: تُنسخ إلى `.lab/upgrades/evidence-<time>/` قبل أن تنتقل نسخة المستودع.
4. **الصور:** يسحب كل صورة يثبّتها الإصدار الجديد، فلا يوقف تنزيلٌ ناقص تثبيتًا يعمل. كل ما سبق لا يغيّر شيئًا.
5. **النسخة الاحتياطية:** يأخذ نسخة احتياطية من كل بيئة على هذا الخادم ومن قاعدة Storage المشتركة `storage_metadata` ([النسخ الاحتياطي والاستعادة](https://base.sbarah.com/docs/guides/backup-and-restore/))، وكلها بوقت تشغيل واحد. بداية هذه الخطوة هي نقطة اللاعودة للمحاولة التلقائية. تُوسم النسخ المأخوذة لترقية بهذا السبب. وحين تنتقل نسخة المستودع، يُسجَّل التشغيل أيضًا في `.lab/backups/upgrade-moved.json`: تُحفظ نسخ آخر 3 ترقيات نقلت نسخة المستودع مهما كان عمرها، ولا تُحسب ضمن `SBARBASE_BACKUP_KEEP`. أما المحاولة التي توقفت قبل الانتقال فنسخها عادية.
6. **لقطة التحكم والحارس:** ينسخ حالة التحكم (فهرس التحكم، وكل مخزن SQLite مباشرة تحت `.lab/upstream/`، ومخزن المفاتيح `.secrets/upstream/managed-keys.sqlite`) إلى `.lab/upgrades/snapshots/`، وينسخ حارس التشغيل الخاص بهذا الإصدار إلى `.lab/upgrades/guard.py` (انظر [حارس التشغيل](https://base.sbarah.com/en/docs/guides/upgrades/#the-start-guard)).
7. **الانتقال:** ينقل نسخة المستودع إلى إيداع الإصدار ويثبّت اعتمادياته.
8. **إعادة التشغيل:** يوقف المشرف لوحة الإدارة والحاويات المملوكة بترتيب، ثم يخرج برمز 42. يعيد systemd تشغيله (`RestartForceExitStatus=42` في الوحدة)، وكذلك Docker (`restart: unless-stopped`). أما من سطر الأوامر فتعيد التشغيل بنفسك.
9. **الحارس ثم الإصدار الجديد:** كل تشغيل يبدأ بالحارس. وإذا ترك الإصدار السابق سجل عملية دون تسوية، يعود أول تشغيل للإصدار الجديد فورًا قبل أن يمس أي شيء، ليسوّيه الإصدار السابق؛ ولا تُحسب هذه العودة على الإصدار في التحديث التلقائي.
10. **الحجز:** قبل أن يفتح أي شيء فهرس التحكم، يأخذ الإصدار الجديد لقطة جديدة لحالة التحكم، وهي التي تستعيدها العودة. وحتى تنجح فحوص السلامة:
    - تتلقى الطلبات عبر البوابة `503` مع `Retry-After: 5`، وتُرفض مقابس Realtime والوصول المباشر إلى قاعدة البيانات؛
    - تتلقى تغييرات الإدارة في لوحة الإدارة `409` مع جملة تقول إن التغييرات متوقفة مؤقتًا، لأن العودة كانت ستُسقطها؛
    - تبقى لوحة الإدارة تجيب على القراءة، وتسجيل الدخول والخروج، و**Roll back**، و**Check now**، وإعدادات التحديث؛
    - ينتظر عامل التجهيز، والنسخ الاحتياطي اليومي، وتشغيل Studio، وبقية التغييرات في الخلفية.
11. **فحوص السلامة:** بعد أن تعمل عملية لوحة الإدارة، يجب أن تجيب جولة فحوص كاملة خلال 120 ثانية: `/health` الخاص بلوحة الإدارة (وهو يقرأ فهرس التحكم ومخزن المفاتيح)، وAuth الإدارة، ولكل بيئة في الخدمة Auth وREST وStorage مباشرة، ثم REST وAuth مرة أخرى عبر البوابة كما يصل إليهما التطبيق، برمز صالح لهذا التشغيل وحده. حين تنجح جولة يُعتمد التحديث: تمر الحركة، ويبدأ العامل، ويُرسل `update.applied`. وبعدها يعود للعمل وحده Studio الذي استُخدم في آخر ثماني ساعات، ويُعاد تحميل تبويبه المفتوح فيكمل ([Studio](https://base.sbarah.com/docs/guides/studio/)).
12. **العودة:** إذا فشلت مرحلة من مراحل التشغيل، أو لم تنجح أي جولة خلال 120 ثانية، يعيد المشرف نسخة المستودع إلى الخلف، ويستعيد اللقطة من الخطوة 10، ثم يخرج بخطأ. تشغّل سياسة إعادة التشغيل الإصدار السابق بصوره السابقة، فيمر بالحجز وفحوص السلامة نفسها قبل أن يُسجَّل `rolled_back`، ويُرسل `update.rolled_back`. وإذا لم يعمل الإصدار السابق أيضًا، تصبح الحالة `rollback_failed` ويُرسل `update.rollback_failed`، والطريق حينها استعادة النسخ الاحتياطية من الخطوة 5.

عند إعادة التشغيل لا تُستبدل إلا حاويات Auth وREST وStorage وRealtime وEdge Functions التي تغيّرت صورتها المثبّتة، وتعيد العودة الصور السابقة. أما حاوية قاعدة البيانات فلا تستبدلها الترقية أبدًا.

## حارس التشغيل

الحارس سكربت صغير يعمل قبل أي شيفرة من الإصدار الذي تحمله نسخة المستودع: أول `ExecStartPre` في وحدة systemd، وأول خطوة في سكربت تشغيل الحاوية، ومن داخل `lab/dev.py` حين تشغّل صباربيز من الطرفية. تنسخ الترقية حارس الإصدار الذي تغادره إلى `.lab/upgrades/guard.py`، وتشغّل مرات التشغيل التالية تلك النسخة، فلا يستطيع إصدار معطوب أن يكسر طريق العودة إلى الإصدار الذي قبله.

ما دامت ترقية أو تراجع ينتظر فحوص السلامة، ولا يفعل ذلك في غير هذه الحالة، يعدّ الحارس كل تشغيل ويعيد نسخة المستودع إلى الخلف حين:

- ينتهي التشغيل السابق للإصدار الجديد قبل أن تنجح فحوص سلامته (ماتت العملية، أو فشلت خطوة قبل المشرف)؛
- لا يجتاز الإصدار الجديد فحوص السلامة في 3 مرات تشغيل؛
- لا تكون نسخة المستودع هي الإصدار المنتظر اعتماده، أو توقفت الترقية أثناء نقل نسخة المستودع.

ويكمل أيضًا عودة انقطعت. وإذا فشل الإصدار السابق بالطريقة نفسها، يسجّل الحارس `rollback_failed` ويترك الإصدار السابق يعمل دون فحوص السلامة.

كل عودة، سواء أجراها الحارس أو المشرف أو تراجع طلبه المشغّل، تنقل نسخة المستودع قسرًا ثم تتحقق من النتيجة:

- تذهب ملفات الأدلة داخل `docs/evidence/` إلى `.lab/upgrades/evidence-<time>/` كما في الترقية؛ وتُنسخ أولًا بقية التعديلات المحلية على الملفات المتتبَّعة، والملفات غير المتتبَّعة التي سيكتب الإصدار السابق فوقها، إلى `.lab/upgrades/aside-<time>/`، مجلدًا لكل عودة. لا شيء يحذف هذه المجلدات: ابحث فيها عن أي تعديل أجريته على الخادم، واحذف المجلد بنفسك حين لا تحتاجه؛
- يُحذف ملف `.git/index.lock` الذي تركته عملية Git قُتلت، لكن فقط حين لا يمكن أن تكون أي عملية Git تستخدم نسخة المستودع؛
- لا يُسجَّل شيء حتى تصبح نسخة المستودع هي الإصدار السابق، وملفاتها المتتبَّعة نظيفة.

وإذا فشلت العودة داخل `start` نفسه، تبقى الترقية في المرحلة `applied`، ويعيد التشغيل التالي نسخة المستودع إلى الخلف قبل أن تعمل أي شيفرة من الإصدارين.

والنقل الذي يستمر في الفشل تعيد مرات التشغيل التالية محاولته. وبعد 3 محاولات فاشلة يحدث واحد من ثلاثة:

- نسخة المستودع على الإصدار السابق: تصبح المرحلة `failed` (عودة `start`) أو `rollback_failed` (أي عودة أخرى)، ويعمل الإصدار السابق دون فحوص السلامة؛
- نسخة المستودع ما زالت على الإصدار المعتمد الذي حاول تراجعٌ طلبه المشغّل أن يغادره: يبقى على ذلك الإصدار، وتعود المرحلة `confirmed`، ويعرض `upgrade.py status` سطر `rollback` بسبب فشل التراجع؛
- غير ذلك: يبقى صباربيز متوقفًا بدل أن يشغّل شجرة ملفات لم يُتحقق منها. يطبع الحارس سطرًا واحدًا، وينتظر 5 دقائق، ثم يخرج، فتعيد كل إعادة تشغيل محاولة النقل، ويعرض `status` سطر `stuck` بالسبب. ويعود التثبيت وحده حين يزول السبب (قرص ممتلئ، أو ملكية ملفات خاطئة).

وبعد أن تفشل عملية تحديث فرعية بطريقة قد تترك نسخة المستودع بين إصدارين، لا يستأنف المشرف التجهيز عليها: يُفشل الطلب، ويقول إن صباربيز سيعيد التشغيل، ثم يخرج ليسوّي الحارس نسخة المستودع أولًا. ويُعلن عن العودة التي يجريها الحارس في أول تشغيل تالٍ يستطيع إرسال الإشعارات. تضبط وحدة systemd القيمة `StartLimitIntervalSec=0` حتى لا يتوقف systemd عن إعادة التشغيل بينما يحتاج الحارس عدة مرات، و`TimeoutStartSec=600` لأن العودة تعيد تثبيت اعتماديات الإصدار السابق قبل الفحص المسبق.

المشرف الذي يُقتل بدل أن يتوقف (SIGKILL، أو قاتل نفاد الذاكرة)، أثناء فحوص السلامة أو في أي وقت آخر، يترك الحاويات المملوكة تعمل، لأن Docker هو من يملكها لا الخدمة. وبعد الحارس يوقفها التشغيل التالي كما يوقفها المشرف نفسه، فتبقى كل حاوية وكل وحدة تخزين (`lab/leftover_runtime.py`، وهو ثاني `ExecStartPre` في الوحدة، ويشغّله المشرف مرة أخرى بعد أن يأخذ أقفاله). ويرفض بدل ذلك، ويذكر السبب، حين يمسك قفلها مشرف أو عامل ما زال يعمل، أو حين ينتظر إيصال تجهيز أو سجل HBA تسويةً. والوحدة المثبّتة قبل إضافة هذا السطر تبقى ترفض عند الفحص المسبق حتى يُعاد تثبيتها ([النشر على خادم](https://base.sbarah.com/docs/guides/server-deployment/)).

## الأصناف الأربعة

يُحسب الصنف من الفرق بين الإيداع الذي تشغّله وإيداع الإصدار، ولا يؤخذ مما يقوله الإصدار عن نفسه. كل ما يستطيعه الإصدار أن يجعل نفسه أشد، بأن يعلن ترحيل بيانات.

| الصنف | ما الذي يحدده | ماذا تفعل |
|---|---|---|
| آمن (Safe) | لا يتغير شيء مما في الصفوف التالية. قد تتغير الشيفرة والاعتماديات وتثبيتات PostgREST وEdge Functions وStudio، ولا يغيّر أي منها قاعدة بيانات بيئة | ثبّته من لوحة الإدارة أو من سطر الأوامر، أو اتركه للتحديث التلقائي |
| يحتاج تأكيدك (Needs your confirmation، `attended`) | يتغير تثبيت Auth أو Storage أو Realtime | ثبّته من لوحة الإدارة بعد أن تقرّ بالتحذير، أو بالخيار `--allow-class attended`. لا يُثبَّت تلقائيًا أبدًا |
| يحتاج إعادة بناء (Needs a rebuild) | يتغير `compose.yaml` أو `deploy/sbarbase.service` أو `.dockerignore` أو وكيل TLS (`deploy/console-tls-proxy.ts`) أو `Dockerfile`، أو أي ملف ينسخه `Dockerfile` الخاص بالإصدار إلى الصورة (أسطر `COPY` و`ADD` فيه). إعادة التشغيل لا تقرأ هذه من جديد | ثبّته على الخادم ثم أعد البناء (أدناه). لا يُثبَّت من لوحة الإدارة ولا تلقائيًا أبدًا |
| يحتاج ترحيلًا (Needs a migration) | تتغير صورة PostgreSQL، أو يعلن الإصدار ترحيل بيانات | ليس ترقية. لا يُثبَّت من لوحة الإدارة ولا تلقائيًا ولا بالأمر `start --release` أبدًا |

### إصدارات تحتاج تأكيدك

يشغّل كل من Auth وStorage وRealtime ترحيلات المخطط الخاصة به في كل قاعدة بيانات بيئة عند تشغيله، وقد لا تعمل الصورة السابقة على المخطط بعد ترحيله. والعودة لا تستعيد إلا حالة التحكم، لا قواعد بيانات البيئات. لذلك إذا عاد تحديث من هذا الصنف إلى الإصدار السابق، **فقد تحتاج بيانات البيئات إلى استعادة من النسخ الاحتياطية المأخوذة قبل الترقية**. ويرحّل Storage أيضًا قاعدة البيانات المشتركة `storage_metadata`، وفيها إعدادات Storage لكل البيئات، والنسخة الاحتياطية المأخوذة قبل الترقية تشملها أيضًا إلى جانب البيئات. لهذا لا يُثبَّت هذا الصنف إلا بعد إقرار صريح، ولا يُثبَّت تلقائيًا أبدًا.

ولإلغاء تحديث كهذا كليًا بعد العودة، استعد التشغيل المأخوذ قبله وصباربيز يعمل على الإصدار السابق. يَسِم الأمر `python3 lab/backup.py list` ذلك التشغيل بعبارة "before an upgrade". استعد كل بيئة أولًا، ثم `storage_metadata`، فاستعادتها تعيد تشغيل Storage في النهاية فيبدأ على تسجيلات تطابق البيئات المستعادة. وبعد تحديث Auth، استعد أيضًا قاعدة الإدارة من التشغيل نفسه: نطاق الإدارة يستخدم صورة Auth نفسها، وقد رحّلت حسابات المشغّلين:

```
python3 lab/backup.py restore <environment> <backup>     # once per environment
python3 lab/backup.py restore-storage <backup>           # Storage pauses for every environment meanwhile
python3 lab/backup.py restore-management <backup>        # after an Auth update; operators sign in again afterwards
```

### إصدارات تحتاج إعادة بناء

مع Docker، يشغّل المشغّل `start --release` مع `--allow-class rebuild`، ثم يعيد البناء:

```
sudo deploy/compose.sh upgrade vX.Y.Z
sudo deploy/compose.sh up
```

ومع خدمة systemd، مرّر إلى `supervise --apply` الخيارات نفسها التي ثُبّتت بها الخدمة ليثبّت الوحدة الجديدة، ثم أعد التشغيل:

```
/usr/bin/python3 lab/upgrade.py start --release vX.Y.Z --allow-class rebuild
sudo /usr/bin/python3 lab/install_server.py supervise --apply
sudo systemctl restart sbarbase
```

الإصدار الذي يحتاج إعادة بناء ويغيّر أيضًا تثبيت Auth أو Storage أو Realtime يحتاج الخيارين معًا: `--allow-class rebuild --allow-class attended`. ولا يمرّر مشغّل Docker إلا الأول، فشغّل مثل هذا الإصدار مع Docker بالأمر `docker compose exec sbarbase python3 lab/upgrade.py start --release vX.Y.Z --allow-class rebuild --allow-class attended`، ثم `sudo deploy/compose.sh up`. وحين يتغير وكيل TLS، أعد تشغيل وحدته الخاصة بعد التحديث أيضًا. تمر إعادة التشغيل بعدها بالحارس واللقطة والحجز وفحوص السلامة والعودة نفسها كأي ترقية.

### إصدارات تحتاج ترحيلًا

تغيير صورة PostgreSQL يستبدل حاوية قاعدة البيانات، وهذه مهمة `lab/migrate-generation.py` ([حاويات قواعد البيانات حالة خاصة](https://base.sbarah.com/en/docs/guides/upgrades/#database-containers-are-special)). أما ترحيل البيانات المعلن فاتبع فيه ما تذكره ملاحظات الإصدار. ليس في صباربيز أداة تطبّق أيًا منهما من صفحة التحديثات.

## التحديث التلقائي

معطّل افتراضيًا. فعّله من الإعدادات أسفل صفحة التحديثات: اختر **Install safe updates automatically**، وحدد نافذة الصيانة، ثم احفظ. النافذة بالتوقيت المحلي للخادم، وتظهر بنظام 12 ساعة مع المنطقة الزمنية التي يستخدمها الخادم، وقيمتها الافتراضية من 3:00 AM إلى 5:00 AM. والنافذة التي تنتهي قبل وقت بدئها تعبر منتصف الليل. مع Docker يكون التوقيت المحلي للخادم هو توقيت الحاوية: UTC ما لم تضبط `TZ` (مثل `sudo TZ=Asia/Dubai deploy/compose.sh up`)، ويمرره المشغّل إلى الحاوية، ولا يقرأ أي ملف `.env`، فأعطه القيمة مع كل `up` لاحق، والصورة تحمل قاعدة بيانات المناطق الزمنية.

لا يُثبَّت تحديث وحده إلا إذا تحققت كل هذه الشروط:

- فحص الإصدارات الجديدة مفعّل؛
- الوقت داخل النافذة؛
- الإصدار آمن، وحكم الخادم يقول إنه قابل للتثبيت الآن؛
- لا نسخ احتياطي ولا استعادة ولا ترقية أخرى ولا سجل عملية ينتظر التسوية (ينتظر حتى تنتهي)؛
- لم يستنفد هذا الإصدار محاولته التلقائية، ولم يُتراجع عنه قط.

تُستنفد المحاولة التلقائية حين تبلغ نقطة اللاعودة، أي بداية النسخ الاحتياطي. المحاولة التي توقفت قبلها (فشل في الشبكة أثناء جلب الإصدار أو سحب صوره، أو رفض، أو تصريف انتهت مهلته) تُعاد، 3 مرات على الأكثر لكل إصدار، بفاصل 10 دقائق ثم 20 دقيقة، داخل النافذة. أما المحاولة التي يفشل بعدها نسخها الاحتياطي أو لقطتها أو انتقالها فلا تتكرر. والإصدار الذي تُراجع عنه، وحده أو بطلب منك، لا يُثبَّت تلقائيًا أبدًا، إلا إذا كان سبب العودة الوحيد سجل عملية لم يسوّه الإصدار السابق. ويبقى بإمكانك تثبيت أي منها من لوحة الإدارة.

ساعة النسخ الاحتياطي اليومي (`SBARBASE_BACKUP_HOUR`) بتوقيت UTC، أما النافذة فبالتوقيت المحلي. على خادم ساعته بتوقيت UTC يتداخلان افتراضيًا، فينتظر التحديث حتى ينتهي النسخ الاحتياطي.

## إيقاف الفحص

على خادم بلا اتصال أو خادم خاص، ألغِ اختيار **Check for new releases** واحفظ. عندها لا يتصل صباربيز بمصدر الإصدارات من تلقاء نفسه، ويتوقف التحديث التلقائي معه. يبقى الزر **Check now** يشغّل فحصًا واحدًا حين تضغطه.

يقرأ الفحص المستودع الرسمي عبر HTTPS، لا `origin` الخاص بك. لقراءة نسخة أخرى (مرآة يصل إليها خادمك)، اضبط `SBARBASE_RELEASE_SOURCE` على عنوان Git الخاص بها أو مسارها في بيئة الخدمة، ومع Docker أعطه للمشغّل مع كل `up` (`sudo SBARBASE_RELEASE_SOURCE=... deploy/compose.sh up`)، فيمرره إلى الحاوية. يبقى التحقق من التواقيع بالمفاتيح التي تسردها نسخة مستودعك.

## التراجع

**قبل الاعتماد** تكون العودة تلقائية كما وُصفت أعلاه، ولا شيء عليك أن تضغطه.

**بعد الاعتماد** تعرض صفحة التحديثات الزر **Roll back** تحت آخر تحديث. يعيد نسخة المستودع إلى الإصدار الذي كنت تشغّله قبله ويعيد التشغيل، مرورًا بالتصريف والحارس والحجز وفحوص السلامة نفسها. وله حدود:

- يُبقي حالة التحكم كما هي الآن: كل ما كُتب منذ التحديث يبقى، ولا يستعيد اللقطة.
- لا يظهر إلا إذا كان الإصدار السابق قادرًا على فتح فهرس التحكم ومخزن المفاتيح. إذا رحّلهما التحديث إلى مخطط لا يعرفه الإصدار السابق، يُرفض مع ذكر السبب، والطريق حينها إصدار أحدث، أو النسخ الاحتياطية المأخوذة قبل الترقية.
- لا يظهر إلا لآخر تحديث، ولا يظهر أثناء طلب آخر قيد التنفيذ.
- يُرفض، من الصفحة أو من سطر الأوامر، ما دامت في نسخة المستودع تعديلات محلية على ملفات متتبَّعة خارج `docs/evidence/`، لأن العودة ستنقلها جانبًا؛ ويذكر الرفض أسماءها. أودعها أو تخلّص منها على الخادم أولًا. أما العودة التلقائية فلا ترفض لهذا السبب: تنقل التعديلات جانبًا.
- العودة لا تلغي تغييرًا أجراه Auth أو Storage أو Realtime الأحدث على قواعد بيانات البيئات عند تشغيله (انظر [إصدارات تحتاج تأكيدك](https://base.sbarah.com/en/docs/guides/upgrades/#releases-that-need-your-confirmation)). وإن لم يعمل الإصدار السابق عليها، فاتبع قسم «إذا لم يبدأ صباربيز بعد طريق العودة» في [النسخ الاحتياطي والاستعادة](https://base.sbarah.com/docs/guides/backup-and-restore/): هناك `storage_metadata` أولًا، ثم البيئات.

يخبرك `python3 lab/upgrade.py rollback --check` هل سيمضي التراجع، ولماذا لا، دون أن يغيّر شيئًا.

## ما الذي يبقى وما الذي قد يضيع

- **قبل الاعتماد تعود حالة التحكم كاملة:** حركة التطبيقات محجوزة، فلا تصل أي كتابة من تطبيق إلى الإصدار الجديد؛ وتُرفض تغييرات الإدارة بالرمز `409` بدل أن تُقبل ثم تسقط؛ وتعود حالة التحكم إلى اللقطة التي أخذها الإصدار الجديد عند تشغيله.
- **قواعد بيانات البيئات ليست جزءًا من تلك اللقطة:** في الإصدار الآمن لا يتغير فيها شيء عند تشغيل الإصدار الجديد. أما في الإصدار الذي يحتاج تأكيدك فتبقى ترحيلات Auth أو Storage أو Realtime بعد العودة، في البيئات وفي `storage_metadata`، حتى تستعيد الاثنتين من التشغيل المأخوذ قبل الترقية.
- **بعد الاعتماد الإصلاح إلى الأمام فقط:** تبقى النسخ الاحتياطية المأخوذة قبل الترقية في `.lab/backups/`، ونسخ آخر 3 ترقيات نقلت نسخة المستودع محمية من الحذف الدوري. والتراجع يُبقي ما كُتب منذ الاعتماد، أو يرفض حين لا يستطيع.
- **تغييرات صورة قاعدة البيانات لا تمر بالترقية أبدًا:** تُرفض، ومكانها النقل تحت إشراف (قسم «الانتقال إلى صورة PostgreSQL أخرى» أدناه).
- **التعديلات المحلية لا تُحذف أبدًا:** أي تعديل محلي على ملف متتبَّع خارج `docs/evidence/` يوقف الترقية والتراجع الذي يطلبه المشغّل حتى تودعه أو تتخلص منه بنفسك. والعودة التي تجري رغم ذلك (التلقائية، أو عودة الحارس) تنسخ التعديلات المحلية إلى `.lab/upgrades/aside-<time>/`، وملفات الأدلة إلى `.lab/upgrades/evidence-<time>/`، قبل أن تكتب فوق نسخة المستودع.

## من سطر الأوامر

مع Docker ضع `docker compose exec sbarbase` قبل كل أمر `python3`. ومع systemd شغّل كل أمر بحساب الخدمة وبقيم `HOME` و`PATH` نفسها التي في ملف الوحدة (`sudo -u sbarbase env HOME=/home/sbarbase PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/sbarbase/.bun/bin /usr/bin/python3 lab/upgrade.py ...`): فإن لم يجد bun في المسار، يتوقف `start` بعد النسخ الاحتياطي عند تثبيت اعتماديات الإصدار الجديد، وإعادة التشغيل عندها تثبّته على الإصدار السابق.

| المهمة | الأمر | بالأمر `sbarbase` |
|---|---|---|
| أحدث إصدار موقّع، وصنفه، وما يغيّره | `python3 lab/upgrade.py channel` (`--json` للنتيجة كاملة، و`--preview` لتضمين الإصدارات التجريبية) | `sbarbase upgrade channel` (`--json`؛ ولا يوجد `--preview`) |
| تثبيت إصدار موقّع | `python3 lab/upgrade.py start --release vX.Y.Z` | `sbarbase upgrade start --release vX.Y.Z` |
| تثبيت إصدار يحتاج تأكيدك | أضف `--allow-class attended` | بالطريقة نفسها |
| تثبيت إصدار يحتاج إعادة بناء | أضف `--allow-class rebuild`، ثم أعد البناء كما في الأعلى | بالطريقة نفسها |
| الانتقال إلى أي إيداع أو وسم، دون تحقق من التوقيع ولا تصنيف | `python3 lab/upgrade.py start --to <tag or commit>` (الافتراضي `origin/main`) | `sbarbase upgrade start --to <tag or commit>` |
| معرفة ما سيغيّره `--to`، دون تغيير أي شيء | `python3 lab/upgrade.py check --to <tag or commit>` | `sbarbase upgrade check --to <tag or commit>` |
| إعادة التشغيل عليه | `sudo deploy/compose.sh up` (أو `sudo systemctl restart sbarbase`) | |
| معرفة النتيجة | `python3 lab/upgrade.py status` | `sbarbase upgrade status` |
| هل سيمضي التراجع؟ | `python3 lab/upgrade.py rollback --check` | `sbarbase upgrade rollback --check` |
| العودة إلى الإصدار السابق | `python3 lab/upgrade.py rollback`، ثم إعادة التشغيل بالطريقة نفسها | `sbarbase upgrade rollback` |

الأمر `start --to` اختيار صريح من المشغّل: لا يتحقق من توقيع ولا يصنّف. لكنه يرفض تغيير صورة PostgreSQL، ويأخذ نسخة احتياطية أولًا، وله العودة نفسها.

البدء من سطر الأوامر لا يصرّف المشرف العامل، فيبقى يشغّل الإصدار السابق من نسخة المستودع المنقولة حتى إعادة التشغيل. وحين يكون صباربيز عاملًا يقول `start` ذلك ويطلب منك إعادة التشغيل فورًا، قبل أن يبدأ المشرف القديم أي شيء آخر. ويعرض `status` المرحلة، وبعد عودة تلقائية سطر `why back` بسببها.

التراجع من سطر الأوامر بينما ينتظر إصدار جديد فحوص السلامة يستعيد لقطة التحكم، فيحتاج إيقاف صباربيز أولًا (`sudo systemctl stop sbarbase` أو `docker compose stop`)، ويخبرك بذلك إن لم يكن متوقفًا.

إذا أردت الرجوع إلى تثبيتات أقدم بعد عودة تلقائية، وقال `rollback` إنه لا توجد ترقية للتراجع عنها، فرقِّ إلى الإصدار الأقدم نفسه: `python3 lab/upgrade.py start --to <earlier commit>`، ثم أعد التشغيل. يأخذ نسخة احتياطية أولًا مثل أي ترقية.

يمرّر الأمر `sbarbase` كل واحد من هذه إلى `lab/upgrade.py` في وسائط منفصلة ([الأمر sbarbase](https://base.sbarah.com/docs/guides/cli/)). ويرفض قبل أن يشغّل أي شيء إذا لم يكن الإصدار وسمًا بسيطًا على شكل `vX.Y.Z`، أو إذا سمّى `--allow-class` شيئًا غير `rebuild` أو `attended`، أو إذا لم يناسب خيارٌ الأمرَ الفرعي؛ وأعطِ `--allow-class` مرتين لإصدار يحتاج الاثنين.

## الانتقال إلى أول إصدار فيه قناة التحديث

الإصدار الذي لا يحوي قناة التحديث (لا يعرف `lab/upgrade.py` فيه الأمر `channel`) لا يستطيع رؤية إصدار ولا التحقق منه. والانتقال إلى أول إصدار فيه القناة يغيّر `Dockerfile` و`compose.yaml` وسكربت تشغيل الحاوية و`deploy/sbarbase.service`، فهو يحتاج إعادة بناء ولا يمكن بنقرة:

1. نفّذ `python3 lab/upgrade.py start --to <tag or commit>` (هذا المسار لا يتحقق من التوقيع).
2. مع Docker نفّذ `docker compose up -d --build`. من دون إعادة البناء لا يكون في الحاوية `ssh-keygen` ولا حارس في سكربت تشغيلها، فيُرفض كل إصدار بالرسالة "ssh-keygen is not installed, so no release signature can be checked; rebuild the container image".
3. مع systemd نفّذ `sudo /usr/bin/python3 lab/install_server.py supervise --apply` بخيارات الخدمة، ثم `sudo systemctl restart sbarbase`. وإلى أن تُثبَّت الوحدة الجديدة يعمل الحارس من داخل `lab/dev.py` لا قبل الفحص المسبق.

أول تشغيل للإصدار الجديد يأخذ لقطة التحكم ويحجز الحركة ويشغّل فحوص السلامة، حتى لو بدأ الانتقالَ الأمرُ الأقدم. وإن فشل، تصل العودة إلى الإصدار الأقدم، وليس فيه حارس، ويعتمد تشغيله دون فحوص سلامة ولا حجز، ولا يستطيع إيصال إشعارات `update.*`.

## ما الذي شُغّل

- اختبارات الوحدات على جهاز التطوير في 2026-09-26: 214 اختبار Python في `lab/test_release_channel.py` و`lab/test_updates.py` و`lab/test_upgrade.py` و`lab/test_upgrade_health.py` و`lab/test_upgrade_guard.py` و`lab/test_upgrade_drain.py` و`lab/test_upgrade_check.py` (ملف الترقية يضم أيضًا اختبارات الترقية الأقدم)، و58 اختبار Bun للحجز، وللفحص الذي يمر من خلاله، ولمسارات التحديث، ولصفحة لوحة الإدارة. نجحت كلها.
- أعادت الآلة الافتراضية التجريبية دورة سطر الأوامر في 2026-09-26 على شيفرة قناة التحديث، تحت systemd والحارس يعمل أولًا: ترقية إلى PostgREST أحدث بالأمر `start --to`، ثم تراجع طلبه المشغّل والترقية معتمدة، ثم إصدار معطوب عاد منه صباربيز وحده واستعاد لقطة التحكم، وبقيت البيانات كما هي في كل خطوة (26 فحصًا من 26، [الدليل](https://github.com/M7MMAD-OMAR/sbarbase/blob/main/docs/evidence/vm-upgrade-checks.json)؛ وانتهى التشغيل الأول في 2026-09-25 بـ[رجوع](https://github.com/M7MMAD-OMAR/sbarbase/blob/main/docs/evidence/vm-upgrade-return.json) إلى التثبيتات الأصلية). ويسجّل CI دورة `start --to` نفسها على جهاز نظيف، مع مقارنة المستخدمين والملفات وحاويات Storage قبل وبعد ([الدليل](https://github.com/M7MMAD-OMAR/sbarbase/blob/main/docs/evidence/docker-upgrade-checks.json)).
- نجحت حالات CI الثلاث الخاصة بالقناة في 2026-09-25 (تشغيل CI رقم 36195831433، 45 فحصًا من 45، [الدليل](https://github.com/M7MMAD-OMAR/sbarbase/blob/main/docs/evidence/docker-upgrade-checks.json))، على جهاز CI نظيف ببيانات اختبار: ترقية معتمدة؛ وإصدار يرحّل فهرس التحكم ثم يتوقف، عاد منه صباربيز ورجع مخطط الفهرس من 4 إلى 3؛ وإصدار لا يجيب فيه PostgREST أبدًا؛ وإصدار لا ينجح في فحوص السلامة أبدًا، وكانت حركة التطبيقات تُجاب خلال تلك المدة بالرمز 503؛ ووسوم غير موقّعة أو موقّعة بمفتاح غير مسرود، رُفضت دون أن يتحرك شيء.
- شغّلت الآلة الافتراضية التجريبية القناة نفسها في 2026-09-26 (`lab/vm-milestones.sh channel`، 114 فحصًا من 114، [الدليل](https://github.com/M7MMAD-OMAR/sbarbase/blob/main/docs/evidence/vm-channel-checks.json)). جاءت الإصدارات من مستودع محلي داخل الآلة، موقّعة بمفتاح مؤقت أُدرج للتجربة، وطلبها المشغّل عبر واجهة الإدارة: إصدار فيه PostgREST أحدث ثُبّت واعتُمد؛ وإصدار لا يجيب فيه PostgREST أبدًا، عاد منه صباربيز وحده واستعاد لقطة التحكم؛ ثم فُعّل التحديث التلقائي، فترك الإصدار الذي عاد منه وثبّت وحده الإصدار الآمن التالي داخل نافذة الصيانة؛ وأخيرًا إصدار يغيّر Auth، رُفض قبل تأكيد تحذيره وثُبّت بعده. في كل مرة أنهى المشرف العمل الجاري وخرج بالرمز 42، فأعاد systemd تشغيله، وعمل حارس التشغيل أولًا. وفي الإصدارات الثلاثة التي بدأت، أُجيبت حركة التطبيقات بالرمز 503 حتى نجحت فحوص السلامة؛ أما الإصدار المعطوب فلم يخدم شيئًا لأن تشغيله لم يبدأ. وبقي المستخدمون والملفات وفهرس التحكم كما هي. وسُجّلت أحداث `update.available` و`update.applied` و`update.rolled_back`.
- شُغّل على الخادم الحقيقي: النقل من سطر الأوامر (`start --to`) مع نسخته الاحتياطية ولقطته وفحوص الصحة، وإصدار موقّع من المستودع الرسمي بمفتاح توقيع الإصدارات الرسمي عبر `start --release`، أولها 0.3.1. ولم يُشغَّل بعد: الزر **Install update** في لوحة الإدارة هناك؛ والتحديث التلقائي؛ والزر **Roll back** في لوحة الإدارة؛ والعودة من إصدار يغيّر Auth أو Storage أو Realtime؛ وإصدار يحتاج إعادة بناء. وحتى يشغّلها خادم حقيقي لا يوصف التحديث التلقائي بأنه جاهز.

## للمطورين: نشر إصدار موقّع

الإصدارات وسوم `vX.Y.Z` مشروحة (annotated) وموقّعة بمفتاح SSH، ويحمل كل منها `release.json` (الإصدار، وأقدم إصدار يمكن الترقية منه، وملاحظات بالإنجليزية والعربية، والترحيلات المعلنة).

1. جهّز الملف وإصدار `package.json`:

   ```
   /usr/bin/python3 lab/release.py prepare --version X.Y.Z --notes-en TEXT --notes-ar TEXT
   ```

   أضف `--minimum-from X.Y.Z` حين يجب أن تمر التثبيتات الأقدم بإصدار سابق أولًا، و`--migration TEXT` لكل ترحيل بيانات (وهذا يجعل الإصدار يحتاج ترحيلًا). لا ينشئ الأمر وسمًا ولا يدفع شيئًا؛ يطبع الأوامر الدقيقة التي تودع التغيير، وتوقّع بالأمر `git tag -s` مع `gpg.format=ssh`، وتتحقق مقابل `deploy/release-signers`، وتدفع الوسم إلى المستودع الرسمي.
2. لا تثق التثبيتات إلا بالمفاتيح المسرودة في `deploy/release-signers` **في الإصدار الذي تشغّله**. أضف النصف العام من مفتاح التوقيع هناك، سطرًا لكل مفتاح بصيغة allowed signers الخاصة بـ ssh-keygen، في إيداع مستقل. التثبيت الذي ما زال على إصدار بلا ذلك المفتاح يرفض كل إصدار على أنه غير موقّع، ويصل إلى الإصدار الذي فيه المفتاح عبر `start --to`.

إلى أن يُسرد مفتاح، يُرفض كل إصدار على أنه غير موقّع. هذا هو الوضع الافتراضي المقصود، لا عطل. وتعديل ذلك الملف محليًا تعديل محلي يوقف الترقيات، فيجب أن يأتي المفتاح في إيداع.

## للمطورين: تغيير إصدار أصلي مثبّت

يشغّل صباربيز صور Supabase الأصلية بإصدارات مثبّتة. تغيير تثبيت واحد يكون عن قصد، بمراجعة مكتوبة وبعد اجتياز كل الاختبارات. **مرّت سبعة إصدارات أصلية بهذا الإجراء في أكتوبر 2026** (Auth وPostgREST وStorage وRealtime وEdge Runtime وStudio وpostgres-meta)، لكل منها مراجعة مكتوبة؛ وما زال فحص قبولها الحي معلقًا، مع أن صور Auth وREST وStorage الجديدة تعمل على الخادم الحقيقي منذ 2026-10-16. القواعد الملزمة موجودة في [سياسة تحديث الأصل](https://github.com/M7MMAD-OMAR/sbarbase/blob/main/docs/engineering/UPSTREAM-UPDATE-POLICY.md).

### ما المثبّت

كل صورة أصلية مثبّتة بالوسم والبصمة في ستة ملفات قفل: `lab/distro-image.lock.json` (صورة PostgreSQL من Supabase)، و`lab/images.lock.json` (Auth وPostgREST وPostgreSQL القياسي الذي تستخدمه البيئات الثابتة)، و`lab/storage-image.lock.json` (Storage)، و`lab/realtime-image.lock.json` (Realtime)، و`lab/functions-image.lock.json` (Edge Runtime)، و`lab/studio-image.lock.json` (Studio وpostgres-meta). اطبع المجموعة كاملة وتحقق أن لا شيء منها عائم:

```
/usr/bin/python3 lab/pin_update.py show
/usr/bin/python3 lab/pin_update.py verify
```

### تغيير مكوّن واحد

1. اقرأ ملاحظات الإصدار الأصلية للنسخة المرشحة.
2. حضّر التغيير. يكتب هذا الأمر مدخل مراجعة مؤرخًا تحت `docs/upstream/`، ويسجّل البصمة السابقة تثبيتًا للتراجع، ويغيّر مكوّنًا واحدًا بالضبط:

   ```
   /usr/bin/python3 lab/pin_update.py stage --file lab/images.lock.json --component rest \
       --tag public.ecr.aws/supabase/postgrest:vX.Y --digest sha256:<64 hex> --note "why"
   ```

3. أكمل مدخل المراجعة: ما الذي تغيّر، وأي أجزاء من صباربيز يمسّها، والتغييرات الكاسرة والترحيلات، وقرار الاعتماد أو التأجيل.
4. اجتز بوابة الاعتماد: مجموعتا اختبارات Python وBun كاملتين، والفحوص الحية للتكامل مع النسخة الجديدة. لا اعتماد بلا اختبارات.
5. اعتمد مكوّنًا واحدًا في كل مرة، فيدل أي فشل على تغيير إصدار واحد.

### التراجع عن تثبيت

أعد التثبيت السابق المسجّل في مدخل المراجعة وانشره في إصدار جديد، فتصل إليه التثبيتات عبر `lab/upgrade.py`. إذا رحّلت النسخة الأحدث البيانات، فقد يحتاج التراجع إلى ترحيل بيانات خاص به؛ سجّله في المدخل نفسه.

## حاويات قواعد البيانات حالة خاصة

لا يعيد التشغيل إنشاء حاوية قاعدة بيانات أبدًا كترقية ضمنية. حاوية القاعدة المُدارة مثبّتة بهويتها الدقيقة، واستبدالها (لصورة جديدة مثلًا) يمر عبر `lab/migrate-generation.py`، وهو إجراء بسجل عملية ومختبر ضد الانهيار على بيئات ثابتة مؤقتة. جرى أول تشغيل له تحت إشراف على بيانات محفوظة في 2026-09-25، وبقيت أعداد الصفوف كلها كما هي؛ انظر [الحالة](https://base.sbarah.com/docs/reference/status/).

### الانتقال إلى صورة PostgreSQL أخرى

يرفض `start` أي إصدار يختلف فيه `lab/distro-image.lock.json`. والوصول إليه يكون بنقل واحد تحت إشراف، على أمرين، وفقط إذا كانت تثبيتات الصور في الإصدار الهدف لا تختلف عن الإصدار المثبّت إلا في صورة PostgreSQL (انقل الصور الأخرى أولًا بالأمر `start`). والمقارنة بين تثبيتات الصور وحدها: أي تغيير آخر في ذلك الإصدار يبدأ معه دون فحوص السلامة ودون العودة التلقائية اللتين يمنحهما `start` للترقية، ولا شيء في هذا النقل يستعيد لقطة حالة التحكم التي تأخذها الخطوة 1، وقد تحذفها الترقيات اللاحقة. لذلك انتقل إلى إصدار لا يغيّر إلا القليل غير صورة PostgreSQL:

1. أوقف الكتابة في كل البيئات ([الإيقاف المؤقت للكتابة](https://base.sbarah.com/docs/guides/write-pause/))، ثم شغّل وصباربيز يعمل: `/usr/bin/python3 lab/upgrade.py database --to REF`. يرفض ما لم يكن الإيقاف قائمًا في كل بيئة منشورة، ثم يسحب صور الإصدار الهدف، ويأخذ نسخة احتياطية من كل البيئات ومن قاعدة الإدارة، ويلتقط حالة التحكم، ويسجّل النقل على أنه مُجهَّز. لم يتحرك شيء بعد: `database --abandon` يلغيه.
2. أوقف صباربيز، ثم شغّل: `/usr/bin/python3 lab/upgrade.py database --continue`. يرفض ما دام صباربيز يعمل أو كانت حاوية قاعدة البيانات القديمة تعمل، ويُبقي صباربيز متوقفًا حتى ينتهي. ينقل نسخة المستودع إلى الإصدار الهدف (النقل المتحقَّق منه نفسه الذي يستعمله طريق العودة)، ثم يستبدل حاوية قاعدة البيانات بصورة القفل الجديد عبر `lab/migrate-generation.py --image-from-lock`، مع الإبقاء على اسمها ومالكها ووحدة تخزين بياناتها. وحين يطلب منك ذلك، شغّل صباربيز، ونفّذ الفحوص التي تلي التغيير، ثم استأنف الكتابة. وفي Docker، والحاوية متوقفة، شغّل هذه الخطوة بالصيغة `docker compose run --rm --entrypoint /usr/bin/python3 sbarbase lab/upgrade.py database --continue` بدل `docker compose exec`؛ ولم تُجرَّب هذه الصيغة بعد.

إذا توقف أي من الأمرين في منتصفه، فشغّله مرة أخرى (عدا الحالات المذكورة في آخر هذه الفقرة): يعيد `--continue` نقل نسخة المستودع إن لم يكتمل، ويكمل تغيير الحاوية الذي انقطع بالصورة التي سجّلها (ومنه تغيير توقف بعد حذف الحاوية القديمة وقبل أن تجهز الجديدة: يتولى التشغيل التالي الحاوية الجديدة ويعيد تشغيلها إن كانت متوقفة)، ولا يعيد التغيير إن كان قد اكتمل. وبعد أن يبدأ `--continue` يرفض صباربيز أن يبدأ حتى يكتمل النقل (أما النقل المُجهَّز فلا يمسّ الإصدار العامل)، ويرفض `start` و`rollback` ونقل ثانٍ ما دام نقل مسجّلًا. والأمر `database` وحده يعرض أين وصل. وهناك انقطاع واحد لا يغطيه هذا: يكتب Git ملفات نسخة المستودع في أماكنها مباشرة، فقد يترك القتل أو إعادة تشغيل المضيف في أثناء نقل `--continue` لنسخة المستودع بعضها مكتوبًا إلى نصفه. عندها قد تفشل الأوامر قبل أن تقرأ سجل النقل، وقد لا يرفض صباربيز البدء أيضًا، لأن ذلك الفحص يعمل من الملفات نفسها. فإن فشل `--continue` بخطأ من Python بعد انقطاع كهذا، فأبقِ صباربيز متوقفًا، واقرأ الإيداع الهدف (`to`) من `.lab/upgrades/database-move.json`، ونفّذ `git checkout -f <to>` بحساب الخدمة، ثم شغّل `--continue` مرة أخرى. ولم تُجرَّب هذه الطريقة بعد. والأمر نفسه `git checkout -f <to>` هو ما يطلبه `--continue` حين يجد نسخة المستودع قد تغيّرت بعد أن نقلها.

طريق العودة إلى الصورة السابقة هو النقل نفسه نحو الإصدار السابق، ولا يمكن إلا نحو إصدار فيه هذا النقل (ترفض الخطوة 1 إصدارًا ليس في `lab/migrate-generation.py` الخاص به الخيار `--image-from-lock`). وهو آمن فقط إذا لم تغيّر الصورة الأحدث إصدارات الإضافات ولم تُعِد كتابة فهارس النظام على وحدة البيانات؛ وإلا فطريق العودة استعادة من النسخ الاحتياطية المأخوذة في الخطوة 1. وبعد أن تنتقل نسخة المستودع تُحفظ هذه النسخ من الحذف التلقائي كما تُحفظ نسخ الترقية (نسخ آخر 3 ترقيات نقلت نسخة المستودع)؛ وتسمح الترقيات اللاحقة بحذفها في النهاية. ولأن صباربيز يبقى يعمل بين الخطوتين، يعيد `--continue` الفحص قبل أن يسجّل بدء النقل، ويرفض دون أن يغيّر شيئًا في حالتين: ما دام سجل عملية ينتظر التسوية (شغّل صباربيز ليسوّي السجل، ثم أوقفه مرة أخرى وتابع)، وإذا نُشرت بيئة أو استؤنفت الكتابة في بيئة منذ النسخ الاحتياطية في الخطوة 1، فتلك النسخ تفوتها كتابات عندها (ألغِ النقل بالأمر `--abandon` وأعد الخطوة 1). وهذا الفحص يخص البيئات وحدها: تبقى لوحة الإدارة تعمل في أثناء ذلك، وما يغيّره المشغّلون عندها (الحسابات، وعوامل تسجيل الدخول، والجلسات، والدعوات) لا تحمله نسخة قاعدة الإدارة المأخوذة في الخطوة 1، فلا تغيّر شيئًا منها حتى ينتهي النقل. ويقبل كذلك صور الإصدار الهدف من جديد، فينزّل ما حُذف منها. وبعد أن يسجّل `--continue` بدء النقل، لا طريق إلى الخلف: يرفض `--abandon` من تلك اللحظة. وبعد ذلك لا مخرج هنا للنقل الذي يبقى عالقًا: تغيير جيل يظل يرفض، أو صورة جديدة لا تجهز أبدًا على وحدة البيانات بعد حذف الحاوية القديمة، يجعل كل تشغيل جديد يفشل، بينما يرفض `--abandon` والنقل إلى الخلف و`rollback` و`lab/backup.py start-database`، ولا يبدأ صباربيز. وإذا لم تجهز الصورة الجديدة أبدًا بعد حذف الحاوية القديمة، فالمخرج أن تعيد بناء الكتلة من النسخ الاحتياطية المأخوذة في الخطوة 1 ثم تعتمدها، وصباربيز متوقف: `/usr/bin/python3 lab/cluster_rebuild.py <دفعة النسخ من الخطوة 1> --image-at <الإصدار الذي بدأ منه النقل>`، ثم `/usr/bin/python3 lab/cluster_takeover.py`. يعيد أمر الاعتماد نسخة المستودع إلى ذلك الإصدار، بشرط أن يكون فيه هذا المخرج أصلًا (بروتوكول الاستبدال نفسه في `lab/hba_migration.py` الخاص به؛ فالنقل الذي بدأ من إصدار أقدم لا يستطيع استخدامه)، ويؤرشف تغيير الجيل العالق والنقل على أنهما استُبدلا (ولا يُحذف شيء قبل أرشفته)، ويحذف الحاوية البديلة التي لم تجهز، ويشغّل قاعدة البيانات على وحدة التخزين المعاد بناؤها ([استعادة الكتلة كاملة](https://github.com/M7MMAD-OMAR/sbarbase/blob/main/docs/engineering/WHOLE-CLUSTER-RESTORE.md)). وإذا انقطع أمر الاعتماد، يدلّك بدء التشغيل و`database --continue` عليه من جديد؛ فشغّله مرة أخرى. وإذا تعذّر عليه المتابعة قبل أن يبدأ تغيير الجيل (كأن تتبيّن أن وحدة التخزين المعاد بناؤها غير صالحة)، فالأمر `lab/cluster_takeover.py --release` يعيد النقل إلى `database --continue`. تبقى وحدة البيانات القديمة، مع أن الحاوية الفاشلة عملت عليها، وتضيع الكتابات التي جاءت بعد النسخ الاحتياطية في الخطوة 1، ولهذا تبقى الكتابة متوقفة. أما تغيير الجيل الذي يظل يرفض والحاوية القديمة ما زالت موجودة فلا مخرج له بعد، ولا مخرج كذلك لاعتماد لا تجهز فيه قاعدة البيانات الجديدة نفسها على وحدة التخزين المعاد بناؤها. للأمرين اختبارات وحدات فقط، لذلك جرّب النقل على نسخة من التثبيت أولًا. لهذا النقل اختبارات وحدات فقط: لم يُشغَّل على قاعدة بيانات حقيقية، ولا في تجربة كاملة، ولا على خادم.

## الحدود

- عدا اختبارات الوحدات، شُغّلت قناة التحديث في حالات CI الخاصة بها على جهاز CI نظيف ([الدليل](https://github.com/M7MMAD-OMAR/sbarbase/blob/main/docs/evidence/docker-upgrade-checks.json))، وفي الآلة الافتراضية التجريبية ([الدليل](https://github.com/M7MMAD-OMAR/sbarbase/blob/main/docs/evidence/vm-channel-checks.json))، بإصدارات من مستودع محلي موقّعة بمفتاح مؤقت. وعلى الخادم الحقيقي نُفّذت الترقيات من 0.2.0 فما بعد من سطر الأوامر (`lab/upgrade.py start --to`، و0.3.1 بالأمر `start --release`) وأكدتها فحوص الصحة؛ ولم تُجرَّب أي ترقية على بيانات عملاء حقيقية؛ كانت في الآلة الافتراضية التجريبية حسابات اختبار فقط.
- المشرف الذي يُقتل بينما ينتظر إصدار جديد فحوص السلامة (أو في أي وقت آخر) يترك الحاويات المملوكة تعمل. ويوقفها التشغيل التالي قبل الفحص المسبق كما في قسم «حارس التشغيل» أعلاه، إلا إذا أمسك قفلها مشرف أو عامل ما زال يعمل، أو انتظر إيصال أو سجل تسويةً: عندها يرفض ويذكر السبب، ويظهر ذلك في `journalctl -u sbarbase`. هذا مغطى باختبارات الوحدات فقط، ولم يُجرَّب قتل حي. والوحدة المثبّتة قبل إضافة هذه الخطوة تبقى ترفض عند الفحص المسبق: أعد تثبيتها بالأمر `supervise --apply`، أو شغّل `/usr/bin/python3 lab/leftover_runtime.py` مرة واحدة بحساب الخدمة.
- حين يتوقف صباربيز بعد أن ينقل تحديث نسخة المستودع، يشغّل الإيقاف الأخير لطبقة التشغيل المملوكة سكربت الإيقاف من نسخة المستودع المنقولة، لا من الإصدار الذي شغّلها.
- تغطي فحوص السلامة لوحة الإدارة وAuth الإدارة، ولكل بيئة Auth وREST وStorage، ثم REST وAuth عبر البوابة. أما Realtime وEdge Functions وStudio فلا تُفحص. والإصدار الذي يجتاز الفحوص ثم يسيء التصرف لا يُعاد وحده: استخدم **Roll back** أو `rollback`.
- قد يترك الإصدار الذي يحتاج تأكيدك قواعد بيانات البيئات وقاعدة Storage المشتركة `storage_metadata` مرحَّلة بعد العودة. وإلغاء ذلك استعادة تشغّلها بنفسك من النسخ الاحتياطية المأخوذة قبل الترقية؛ العودة لا تفعلها. واستعادة `storage_metadata` توقف Storage لكل البيئات في أثنائها، ولا تغطيها إلا اختبارات الوحدات. وإن لم تستطع صورة Storage السابقة أن تعمل على `storage_metadata` المرحَّلة، فلا يعمل الإصدار السابق (`rollback_failed`)؛ وتحتاج الاستعادة عندها أن تكون حاوية قاعدة البيانات عاملة، ويشغّلها الأمر `lab/backup.py start-database` وحدها (انظر قسم «إذا لم يبدأ صباربيز بعد طريق العودة» في [النسخ الاحتياطي والاستعادة](https://base.sbarah.com/docs/guides/backup-and-restore/)).
- الانتقال إلى أول إصدار فيه قناة التحديث يحتاج إعادة بناء وسطر الأوامر (أعلاه). والعودة التي تصل إلى إصدار أقدم من القناة ليس فيها حارس ولا فحوص سلامة ولا حجز، ولا تستطيع إيصال إشعارات `update.*`.
- يعيد الأمر `lab/install_server.py supervise`، مع `--apply` أو دونه، كتابة الملف المتتبَّع `docs/evidence/supervisor-unit.json`. على إصدار فيه قناة التحديث لا يوقف ذلك الترقية: يُنسخ جانبًا مع بقية الأدلة. أما التثبيت الذي لا يحوي `lab/upgrade.py` فيه الدالة `set_aside_evidence` (أُضيفت في 2026-09-25) فما زال يرفض الترقية إذا تغيّرت ملفات أدلته. انسخ `docs/evidence/` إلى مكان آخر، ثم نفّذ `git checkout -- docs/evidence` بحساب الخدمة مرة واحدة، ثم رقِّ.
- لا يثبّت التحديث التلقائي إلا الإصدارات الآمنة. الإصدار الذي يحتاج تأكيدك أو إعادة بناء أو ترحيلًا ينتظرك دائمًا.
- العودة القسرية، وحد المحاولات الثلاث، والبقاء متوقفًا، كلها في الحارس، والعودة تشغّل الحارس المنسوخ من الإصدار الذي غادرته الترقية. لذلك لا تحمي إلا الترقيات التي بدأت من إصدار يحويها.
- الحارس الذي لا يستطيع كتابة `state.json` أصلًا (قرص ممتلئ أو للقراءة فقط) لا يستطيع عدّ محاولاته الفاشلة، فتدور الخدمة في إعادة تشغيل متكررة حتى يصبح القرص قابلًا للكتابة.
