لماذا يحتاج كل بناء إلى هوية فريدة

لقد قمت للتو بتشغيل عملية بناء. يظهر ملف JAR في دليل المخرجات. أو ربما أرشيف ZIP، أو ملف ثنائي مُجمَّع. يبدو مثل أي بناء آخر قمت به هذا الأسبوع. تنسخه إلى خادم، وتنشره، وتنتقل إلى المهمة التالية.

بعد ثلاثة أيام، يُبلغ أحدهم عن خطأ في بيئة الإنتاج. تحتاج إلى معرفة أي قطعة أثرية (artifact) تعمل فعليًا. تتفقد الخادم، وتجد ملفًا باسم app-1.0.0.jar، وتدرك أنه ليس لديك أي فكرة عما إذا كان هذا هو بناء يوم الثلاثاء أم بناء يوم الخميس. كلاهما كان معنونًا بـ 1.0.0. كلاهما جاء من نفس المستودع. لكن هناك خطأ ما، ولا يمكنك تتبع المشكلة إلى الكود المصدري.

هذه هي اللحظة التي يصبح فيها فقدان هوية القطعة الأثرية مشكلة تشغيلية حقيقية.

المشكلة مع مجرد رقم إصدار

أرقام الإصدارات هي الشكل الأساسي للتعريف. إنها تعطيك فكرة تقريبية عما تتعامل معه. 1.0.0، 2.3.1، 3.0.0-beta — هذه التسميات تساعد البشر على فهم التقدم والتوافق.

لكن أرقام الإصدارات وحدها تنهار بسرعة في الممارسة العملية.

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

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

ما الذي يجعل هوية القطعة الأثرية جيدة

يجب أن تكون هوية القطعة الأثرية المفيدة فريدة، وقابلة للتتبع، ودائمة. يجب أن تجيب على ثلاثة أسئلة:

  • ما الكود الذي تم استخدامه؟
  • متى تم بناء هذا؟
  • أي تشغيلة بناء أنتجته؟

النهج الأكثر شيوعًا يجمع بين ثلاث قطع من المعلومات في معرف واحد.

معرف البناء (Build ID)

كل تشغيلة خط أنابيب (pipeline run) تحصل على رقم تسلسلي. Jenkins يسميه رقم البناء. GitLab CI يسميه معرف خط الأنابيب. GitHub Actions يسميه رقم التشغيل. مهما كان الاسم، فهو عدد صحيح متزايد بشكل رتيب (monotonically increasing) وفريد ضمن المشروع. البناء 142 يختلف دائمًا عن البناء 143.

لكن معرف البناء وحده لا يخبرك بالكود الذي تم بناؤه. أنت بحاجة إلى المزيد.

هاش الالتزام (Commit Hash)

كل التزام (commit) في Git له هاش SHA — سلسلة سداسية عشرية طويلة تحدد بشكل فريد الحالة الدقيقة للكود المصدري في تلك النقطة. عندما تدمج معرف البناء مع هاش الالتزام، تحصل على شيء قوي: "هذه القطعة الأثرية جاءت من البناء 142، الذي استخدم الالتزام a3f2c9e."

إذا حدث خطأ ما، يمكنك سحب (check out) ذلك الالتزام بالضبط ورؤية الكود الذي تم تجميعه. لا تخمين، ولا "أعتقد أن هذا هو الإصدار الذي استخدمناه."

الطابع الزمني (Timestamp)

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

مزيج معرف البناء، وهاش الالتزام، والطابع الزمني يمنحك هوية قوية وقابلة للقراءة البشرية. شيء مثل 142-a3f2c9e-20250321T143022. إنها ليست جميلة، لكنها لا لبس فيها.

إليك كيف يمكنك بناء تلك الهوية في خط أنابيب CI:

BUILD_ID="${CI_PIPELINE_ID:-142}"
COMMIT_HASH="${CI_COMMIT_SHA:-a3f2c9e}"
TIMESTAMP=$(date +%Y%m%d%H%M%S)
ARTIFACT_NAME="myapp-${BUILD_ID}-${COMMIT_HASH}-${TIMESTAMP}.jar"

echo "Building ${ARTIFACT_NAME}"
# ... build steps ...
cp target/app.jar "dist/${ARTIFACT_NAME}"

قاعدة القطعة الأثرية غير القابلة للتغيير (Immutable Artifact Rule)

بمجرد أن تحصل القطعة الأثرية على هويتها، يجب ألا تتغير هذه الهوية أبدًا. هذا هو مبدأ عدم القابلية للتغيير (immutability).

القطعة الأثرية غير القابلة للتغيير تعني:

  • لا تقوم أبدًا بالكتابة فوق قطعة أثرية موجودة.
  • لا تعيد أبدًا استخدام هوية لملف مختلف.
  • لا تعدل أبدًا قطعة أثرية بعد بنائها.

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

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

مع عدم القابلية للتغيير، يمكنك تتبع كل قطعة أثرية خلال دورة حياتها. أنت تعرف بالضبط أي قطعة أثرية ذهبت إلى التدريج، وأيها ذهبت إلى الإنتاج، وأيها لا تزال تنتظر الاختبار. يمكنك مقارنة القطع الأثرية عبر البيئات والتأكد من أنها متطابقة.

أين تعيش القطع الأثرية؟

الهوية وحدها ليست كافية. تحتاج أيضًا إلى مكان لتخزين القطع الأثرية بحيث تبقى بعد آلة البناء.

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

هذا التخزين يُسمى سجل (registry). يمكن أن يكون خادم ملفات بسيطًا، أو مستودع قطع أثرية مخصصًا مثل Nexus أو Artifactory، أو حلًا سحابيًا أصليًا مثل سجلات الحاويات (container registries) لصور Docker. المهم هو أنه مصدر واحد للحقيقة لجميع القطع الأثرية المبنية.

عندما تجمع بين هوية فريدة غير قابلة للتغيير مع سجل موثوق، فإنك تنشئ سلسلة عهدة (chain of custody) لكل قطعة برمجية تنتجها. يمكنك تتبع أي مثيل قيد التشغيل إلى بنائه الدقيق، وكوده المصدري، والظروف التي تم إنشاؤه فيها.

قائمة تحقق عملية

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

الخلاصة

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