الإدارة الدلالية لنُسخ البرمجيات 2.0.0

باختصار

بإعطاء الترقيم التالي ترقيع.بسيط.جذري أو كما يُعرف MAJOR.MINOR.PATCH لنُسخ البرمجيات، قم برفع درجة:

  1. ترقيم MAJOR (جذري) عند القيام بتغيرات جذرية غير متوافقة رجعيا للواجهة البرمجية (API)،
  2. ترقيم MINOR (بسيط) عند إضافة ميزات ووظائف جديدة لكن مُتوافقة رجعيًا، و
  3. ترقيم PATCH (ترقيع) عند القيام بإصلاحات للعلل والثغرات مُتوافقة رجعيًا.

الأوسمة الإضافية للنُسخ ما قبل النهائية (مثل: beta ،alpha …) والبيانات الوصفية لعمليات البناء والتجميع (build metadata) متوفرة كلواحق لصيغة MAJOR.MINOR.PATCH.

تمهيد

في عالم إدارة البرمجيات يتواجد هناك مكان مُفزع يُعرف بـ “جحيم الاعتماديات (dependency hell)”. كُلما كبُر نظامك وقمت بإدراج المزيد من الحزم في برمجيتك، كلما زاد احتمال أن تجد نفسك، يوما ما، في هاوية اليأس هذه.

في الأنظمة كثيرة الاعتماديات، إصدار نسخة جديدة من حزمة ما قد يتحول بشكل سريع إلى كابوس مُزعج. إذا كان وصف الاعتماديات في تلك الحزمة مُتشدّدًا جدًا، فإنك ستجد نفسك في مُواجهة خطر “قفل النُسخ” (version lock، أي عدم المقدرة على ترقية حزمة ما، دون الحاجة لإصدار نسخة جديدة من كل حزمة تعتمد عليها تلك الحزمة المُتشدّدة). أما في حال ما كان وصف الاعتماديات في تلك الحزمة مُتساهلا جدًا، فستجد نفسك -لا محالة- وقد تم لسعك بمُشكلة “انحلال النُسخ” (version promiscuity، أي فرط الإدّعاء بالتوافقية مع النسخ المُستقبلية بشكل يتجاوز الحد المعقول). جحيم الاعتماديات يكون عندما يمنعك قفل النُّسخ و/أو انحلال النُّسخ من المُضيّ قُدُما في مشروعك بسهولة وأمان.

كحل لهذه المعضلة، أَقترحُ بضعة قواعد ومتطلبات بسيطة من شأنها إملاء كيفية إسناد ترقيم لنُسخ البرمجيات وكيفية رفع درجة الترقيم.

هذه القواعد تعتمد -لكن ليس بالضرورة بشكل حصري- على الممارسات الشائعة الموجودة مُسبقا في كل من البرمجيات المفتوحة ومغلقة المصدر. وحتى يعمل هذا النظام، عليك أولا أن نُفصِح عن واجهة برمجية (API) للعامة (أي أن تقوم بنشرها ظاهرة)، قد تكون هذه الأخيرة عبارة عن مُجرد توثيق أو معززة بالشفرة المصدرية نفسها. أيًا كان الوضع، فإنه من المُهمّ أن تكون هذه الواجهة البرمجية واضحة ودقيقة. بمُجرد تحديد واجهتك البرمجية المٌُوجّهة للعامة (أي الظاهرة للعيان)، ستُشير إلى التغيّيرات التي تحصل عليها عن طريق رفع درجة ترقيم النسخة الحالية منها بشكل معين. تَبنّى صيغة ترقيم من الشكل X.Y.Z (أي Major.Minor.Patch). إصلاحات العِلل التي لا تؤثر على سير عمل الواجهة البرمجية تستوجب رفع درجة ترقيم “ترقيع” (patch)، التعديلات/الإضافات المُتوافقة رجعيًا على الواجهة البرمجية ترفع درجة ترقيم “بسيط” (minor)، أما التعديلات غير المتوافقة رجعيًا على الواجهة البرمجية تستوجب رفع درجة ترقيم “جذري” (major).

أُسمّي هذا الأمر بـ”الإدارة الدلالية لنُسخ البرمجيات” (Semantic Versioning واختصارًا SemVer). باتّباع هذا النمط، فإن أرقام نُسخ البرمجيات وطريقة الترقيم ستُضفي معنًى يدُلّ على الشفرة المصدرية ومدى التغيرات الحاصلة من نسخة إلى النسخة التي تليها.

مُواصفات الإدارة الدلالية لنُسخ البرمجيات (SemVer)

الكلمات المفتاحية: يجب (MUST)، يجب أن لا (MUST NOT)، يتطلّب (REQUIRED)، ينبغي (SHALL)، ينبغي أن لا (SHALL NOT)، يجدُر (SHOULD)، يجدر أن لا (SHOULD NOT), مُستحسن (RECOMMENDED)، قد (MAY)، و اختياري (OPTIONAL) في هذه الوثيقة تُحمَل على نفس التأويل المُوَضّح في RFC 2119.

  1. البرمجيات التي تعتمد الإدارة الدلالية للنُّسخ يجب أن تُصرّح بواجهة برمجية (API) عامة. قد تكون هذه الواجهة البرمجية عبارة عن الشفرة المصدرية بحد ذاتها، أو أن وجودها محصورٌ في التوثيق. أيًا كان الأمر، فإنه يجدر بها أن تكون دقيقة ومفهومة.

  2. الترقيم الطبيعي للنُّسخ يجب أن يكون من الشكل X.Y.Z حيث X ،Y و Z عبارة عن أعداد صحيحة غير سالبة، و يجب أن لا تحتوي على أصفار بادئة. X يُشير إلى ترقيم جذري (Major)، أما Y إلى ترقيم بسيط (Minor)، و Z إلى ترقيم ترقيع. كل جزء من الترقيم يجب أن يتم رفعه عدديا بدرجة. على سبيل المثال: 1.9.0 -> 1.10.0 -> 1.11.0.

  3. بمُجرّد إصدار نسخة جديدة من حزمة مُعيّنة بترقيم مُعيّن، يجب أن لا يُغيَّر محتوى تلك الحزمة. كل تغيير يتم إجراؤه عليها يجب أن يتم تصديره كنُسخة جديدة بترقيم مُغاير ومُناسب.

  4. الترقيم الجذري المُصفّر -(Major version zero (0.y.z- يُمثّل مرحلة التطوير الأوّلية. قد يتغير أي شيء في أي وقت. يجب أن لا تُعتبر الواجهة البرمجية (API) العامة-في هذه المرحلة- مُستقرة.

  5. النسخة 1.0.0 تُعرّف الواجهة البرمجية العامة. طريقة رفع درجات الترقيم بعد هذه الإصدارة يعتمد على الواجهة البرمجية العامة نفسها وطريقة إجراء التغييرات عليها.

  6. ترقيم الترقيع Z (أي x.y.Z بحيث x > 0) يجب أن تُرفع درجته فقط في حالة إدخال إصلاحات للعِلل مُتوافقة رجعيا، والمتمثلة في الإصلاحات الداخلية التي تُصحّح سُلوكًا غيرَ سويّ للبرمجية.

  7. ترقيم “بسيط” Y (أي x.Y.z بحيث x > 0) يجب أن تُرفع درجته إذا تم إدخال وظائف جديدة مُتوافقة رجعيًا للواجهة البرمجية العامة. ويجب أيضا رفع درجته في حال تم التخلي عن وظيفة ما (deprecating functionality) في الواجهة البرمجية العامة. قد يتم رفع درجته في حال تم إضافة وظيفية جوهرية أو تحسينات جديدة على نطاق خاص في الشفرة المصدرية. وقد يحتوي على تغييرات ترقيعية (Patch). ترقيم الترقيع (Patch) يجب أن يتم تصفيره (إرجاع قيمته إلى 0) عند رفع درجة ترقيم “بسيط” (Minor).

  8. ترقيم “جذري” X (أي X.y.z بحيث X > 0) يجب أن يتم رفع درجته في حال تم إدخال تغييرات غير متوافقة رجعيًا على الواجهة البرمجية العامة. قد يحتوي على تغييرات “ترقيعية” و “بسيطة”. يجب تصفير كل من ترقيم “بسيط” (Minor) وترقيم “ترقيع” (Patch) (أي إرجاع قيمتهما إلى 0) عند رفع درجة ترقيم “جذري” (Major).

  9. النُسخ المُعدّة لما قبل الإصدار قد يتم توسيمها بوصلة ‘-‘ تليها نقاط إضافية يُفصل بينها بمُعرّفات، وذلك مباشرة بعد ترقيم الترقيع (patch). يجب على هذه المُعرّفات أن تكون مكوّنة فقط من هجائيات عددية (alphanumerics) ووصلات ‘-‘ من ترميز ASCII وفقط، أي [0-9 A-Z a-z-]. يجب أن لا تكون هذه المُعرّفات فارغة. يجب أن لا تحتوي المعرفات الرقمية على أصفار بادئة. النُّسخ المُعدّة لما قبل الإصدار لها أولوية أقل من النسخ العادية المُصاحبة لها. النُّسخ المُعدّة لما قبل الإصدار تعني أنها نسخة غير مُستقرة وقد لا تُلبي التوافقية المرجوة منها كما هو مُشار إليه في الترقيم الطبيعي للنسخة المصاحبة لها. أمثلة:
    1.0.0-alpha,  1.0.0-alpha.1,   1.0.0-0.3.7,   1.0.0-x.7.z.92

  10. البيانات الوصفية لعمليات البناء والتجميع (build metadata) قد يتم تعليمها عن طريق إضافة علامة زائد (+) و سلسلة من المُعرّفات مفصول بينها بنقاط، وذلك مُباشرة بعد ترقيم ترقيع (Patch) أو ترقيم النُسخ ما قبل النهائية (مثل: beta ،alpha …). يجب على البيانات الوصفية هذه أن لا تحتوى سوى على هجائيات عددية من ترميز ASCII ووصلة - وفقط، أي: [0-9A-Za-z-]. يجب أن لا تكون هذه المعرّفات فارغة. يجدر إهمال ترقيم البيانات الوصفية لعميات البناء والتجميع عند تحديد أفضلية النُسخ. بالتالي، يُعتبر وجود نسختين مُختلفتين فقط في ترقيم البيانات الوصفية أن لهما نفس الأفضلية. أمثلة:
    1.0.0-alpha+001,  1.0.0+20130313144700,   1.0.0-beta+exp.sha.5114f85

  11. الأفضلية تُشير إلى كيفية المُقارنة بين النُسخ عند ترتيبها. يجب أن يتم حساب الأفضلية عن طريق تقسيم ترقيم النُّسخ إلى ترقيع، بسيط، جذري ثم مُعرفات النسخ ما قبل النهائية (alpha, beta) مع احترام هذا الترتيب (البيانات الوصفية لا تظهر في الأفضلية). يتم تحديد الأفضلية عن طريق أول اختلاف يظهر عند المقارنة بين هذه المعرّفات من اليسار إلى اليمين على النحو التالي: تتم دائما المقارنة بين جذري، بسيط وترقيع عدديا، مثال: 1.0.0 < 2.0.0 < 2.1.0 < 2.1.1. في حال تساوى ترقيم جذري، بسيط وترقيع، يتم احتساب ترقيم النسخ ما قبل النهائية (alpha, beta) على أن لها أفضلية أقل من النسخ العادية، مثال: 1.0.0 > 1.0.0-alpha . في حال تساوت النُسخ ما قبل النهائية بنفس ترقيم جذري، بسيط، ترقيع، يجب حساب الأفضلية عن طريق المقارنة بين المعرفات المفصول بينها بنقاط من اليسار إلى اليمين إلى غاية العثور على اختلاف، وذلك على النحو التالي: المعرفات المُكونة من أرقام فقط تتم مقارنتها عدديا، والمُعرّفات المُكوّنة من أحرف ووصلة (-) تتم مقارنتها أبجديًا وفق ترتيب ASCII. المُعرّفات الرقمية لها دائما أفضلية أقل من المعرفات غير العددية. كلما امتدت حقول النُّسخ ما قبل النهائية كلما زادت أفضليتها عن النّسخ ذوات حقول أقل إذا تساوت جميع المعرفات السابقة. مثال:
    1.0.0 > 1.0.0-rc.1 > 1.0.0-beta.11 > 1.0.0-beta.2 > 1.0.0-beta > 1.0.0-alpha.beta > 1.0.0-alpha.1 > 1.0.0-alpha

لماذا يجب استعمال الإدارة الدلالية لنُسخ البرمجيات ؟

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

مثال بسيط سيُوضّح كيف يُمكن للإدارة الدلالية لنُسخ البرمجيات أن تجعل من “جحيم الاعتماديات” شيئًا من الماضي. لنفرض أن هناك مكتبة برمجية تحمل الاسم “شاحنة الإطفاء”. تتطلّب هذه المكتبة حزمة برمجية تحمل الاسم “سُلّم” يتم إدارة نُسخها دلاليًا. في الوقت الذي تم فيه إنشاء مكتبة شاحنة الإطفاء، كانت حزمة سُلّم في نسختها 3.1.0. بما أن شاحنة الإطفاء تستعمل بعض الوظائف التي تم إدخالها أول مرة في النسخة 3.1.0، فإنه بإمكانك أن تُحدّد اعتمادية سُلّم لتكون أكبر من أو تُساوي 3.1.0 وأقل من 4.0.0 بكل أمان. الآن، عند إصدار النُسَخ 3.1.1 و 3.2.0 من حزمة سُلّم، يُمكنك نشرها في نظام إدارة الحزم خاصتك وأنت تعلم أنها مُتوافقة مع البرمجيات الحالية التي تعتمد عليها.

بصفتك مُطورًا مسؤولا، سيتوجّب عليك أن تتحقّق من أن كل عملية ترقية لحزمة مُعيّنة تعمل كما تم الإشهار لها. العالم الحقيقي مكان فوضوي، لا يوجد ما يمكننا فعله حيال ذلك سوى أن نكون يقظين. ما يمكنك فعله هو أن تنتهج طريقا بواسطة الإدارة الدلالية لنُسخ البرمجيات (Semantic Versioning) يسمح لك بإصدار وترقية الحزم من دون الحاجة لإصدار نُسخ جديدة من الحزم الأخرى المُعتمد عليها بكل رزانة، حافظًا لوقتك ومُتجنبًا لجميع المتاعب.

إذا بدى لك كل هذا الأمر مُحبّذًا، فإن كل ما تحتاج لفعله للبدء في استعمال الإدارة الدلالية لنُسخ البرمجيات هو أن تعلن أنك ستمتثل وتتّبع قواعدها. أشر إلى هذا الموقع في ملف README الخاص بمشروعك حتى يعلم البقية بهذه القواعد وتُمكّنهم من الاستفادة منها.

الأسئلة الأكثر شيوعًا

كيف يجدر بي التعامل مع مُراجعات O.y.z عند بداية مرحلة التطوير الأوّلية؟

أبسط شيء يُمكن القيام به هو أن تستهلّ مرحلة التطوير الأولية بإصدار النسخة 0.1.0 ثم ترفع درجة ترقيم “بسيط” (Minor) بعد كل إصدارة تليها.

متى أعرف أنه حان وقت إصدار النسخة 1.0.0؟

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

ألا يُثبّط هذا من عمليات التطوير المُتسارعة والتكرارية؟

الغرض من وجود النسخة الجذرية المصفّرة (Major version zero) هو عمليات التطوير السريعة. إذا كنت تغيّر من الواجهة البرمجية كل يوم، فإنه يجدر بك إما أن تبقى عند الترقيم O.y.z أو أن تكون في فرع تطوير مُنفصل (branch) تعمل فيه على النسخة الجذرية القادمة.

إذا كان حتى أصغر تغيير غير مُتوافق رجعيا للواجهة البرمجية العامة يتطلب رفع ترقيم جذري، ألن يقودَني ذلك إلى النسخة 42.0.0 بسرعة فائقة؟

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

عملية التوثيق الكامل للواجهة البرمجية (API) تُعتبر عملا مُنهكا!

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

ماذا يتوجب عليَّ فعله عندما أُصدر عن طريق الخطأ نسخةً بتغييرات غير مُتوافقة رجعيًا كنُسخة ترقيعية (Minor version)؟

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

ما يتوجب علي فعله عندما أحدّث الاعتماديات الخاصة بي دون تغيير الواجهة البرمجية العامة.

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

ماذا لو قمت عن غير قصد بتغيير الواجهة البرمجية العامة بطريقة لا تتماشى مع ترقيم النسخة (مثال: الشفرة تُدخِل عن طريق الخطأ تغييرات جذرية تكسر التوافقية الرجعية في إصدارة ترقيعية patch)

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

ماذا عن الوظائف المُتخلى عنها (deprecating functionality) ؟

التخلّي عن وظيفة موجودة مُسبقا شيء طبيعي في تطوير البرمجيات، بل يُعتبر أحيانا خطوة لابد منها للمُضي قُدما. عندما تتخلّى عن إحدى الوظائف في الواجهة البرمجية العامة، فإنه يجب عليك القيام بأمرين: (1) قم بتحديث التوثيق حتى يعلم بقية المُستخدمين بهذا التغيير، (2) أصدر نسخة بسيطة (Minor) جديدةً تحتوي على الوظيفة المُتخلى عنها. قبل حذف الوظيفة نهائيًا في نسخة جذرية (Major). يجب على الأقل أن تكون هناك نسخة بسيطة تحتوي على الوظيفة المُتخلى عنها حتى تُمكّن المستخدمين من الانتقال السلِس إلى الواجهة البرمجية الجديدة.

هل يفرض semver حدًا مُعينًا لطول السلسلة النصية عند ترقيم النُّسخ ؟

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

نُبذة

مُواصفات الإدارة الدلالية لنُسخ البرمجيات من تأليف Tom Preston-Werner, مُبتكر خدمة Gravatars وأحد مؤسسي GitHub.

لديك اقتراح؟ يُرجى فتح “خطب” على GitHub.

الرُخصة

المشاع الإبداعي ― CC BY 3.0