إدارة المخرجات وهندسة الإصدارات

الإصدار الدلالي وأنظمة الإصدار

18 دقيقة الدرس 2 من 28

الإصدار الدلالي وأنظمة الإصدار

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

SemVer: العقد الثلاثي

الإصدار الدلالي (semver)، المحدد في semver.org، يستخدم ثلاثة أرقام: MAJOR.MINOR.PATCH. القواعد دقيقة وقابلة للإنفاذ آلياً.

  • PATCH — إصلاح خطأ متوافق للخلف. لا تغييرات على الـ API العام. يمكن للمستهلكين الترقية التلقائية بأمان.
  • MINOR — وظيفة جديدة متوافقة للخلف. الاستدعاءات الحالية تستمر في العمل. يمكن للمستهلكين الترقية التلقائية بأمان.
  • MAJOR — تغيير غير متوافق في الـ API. يجب على المستهلكين قراءة ملاحظات الإصدار والموافقة صراحةً. رفع MAJOR يُشير إلى ضرورة مسار هجرة.

هذه القواعد تجعل مديري التبعيات موثوقين: مستهلك يُعلن ^2.3.0 (نطاق caret في npm) سيثبت تلقائياً أي 2.x.y حيث x >= 3 بدون تدخل بشري، ثقةً في أن رفعَي MINOR وPATCH آمنان. يكسر العقد — ويكسر الإنتاج — بمجرد انتهاكه. إذا حذف إصدار MINOR دالة، تنكسر كل الخدمات المنبثقة التي تستخدمها عند npm install أو pip install --upgrade التالي، بصمت، بعد أيام من شحن الإصدار.

الانتهاك الأكثر شيوعاً لـ semver في الصناعة: معاملة MINOR كـ"ميزة جديدة مهمة بما يكفي" بدلاً من "متوافق للخلف تماماً." إعادة تسمية أو حذف أو تغيير توقيع أي رمز API عام — دالة، نقطة نهاية REST، حقل gRPC — هو رفع MAJOR، نقطة. ألم رفع MAJOR مقصود: يجبر المهندسين على تصميم واجهات مستقرة بدلاً من تطفيرها باستهتار. المكتبات التي ترفع MAJOR كثيراً (كـ React 16 → 17 → 18) توفر عادةً codemods وأدلة هجرة لتسهيل الانتقال.

معرّفات ما قبل الإصدار وسلّم الترقية

يدعم SemVer لاحقتين اختياريتين بدلالات صارمة. فهمهما ضروري لأن خط أنابيب CI/CD للترقية يتطابق مباشرةً مع سلّم ما قبل الإصدار.

  • معرّف ما قبل الإصدار — يُلحق بشرطة: 2.0.0-alpha.1، 2.0.0-beta.3، 2.0.0-rc.1. تُصنَّف ما قبل الإصدارات أدنى من الإصدار ذاته: 2.0.0-rc.1 < 2.0.0. لن يُرقّي مدير الحزم بشكل افتراضي مستهلكاً مستقراً إلى ما قبل إصدار — يجب على المستهلك الموافقة صراحةً (مثل npm install mylib@next أو pip install --pre mylib).
  • بيانات ميتا للبناء — تُلحق بعلامة زائد: 2.0.0+20260611.sha.a3f9d12. تُتجاهل بيانات ميتا للبناء تماماً عند مقارنة الإصدارات؛ نسختان ببيانات ميتا مختلفة متساويتان من حيث semver. استخدمها لتضمين SHA الـ git، ومعرّف تشغيل CI، أو طابع زمني للبناء لإمكانية التتبع البشري — دون التأثير على ترتيب الإصدارات أو حل التبعيات.

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

SemVer pre-release promotion ladder SemVer Pre-release Promotion Ladder alpha.1 internal only unit + smoke tests gate beta.2 opt-in users integration tests gate rc.1 feature-frozen canary + load tests gate 2.0.0 GA release 100% traffic Same binary SHA throughout — only the version tag changes at each gate
سلّم ما قبل الإصدار يتطابق مباشرةً مع بوابات الترقية. alpha ثم beta ثم rc ثم GA، بنفس الملف التنفيذي طوال الوقت.

CalVer: الإصدار بالتاريخ

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

  • YYYY.MM — Ubuntu: 24.04 (إبريل 2024 LTS)، 24.10. التاريخ يُشير مباشرةً إلى تاريخ انتهاء نافذة الدعم.
  • YYYY.MM.MICRO — Django استخدم هذا تاريخياً: 2024.6.0، 2024.6.1. جزء micro هو عداد رقع ضمن الشهر.
  • YYYY.MINOR — pip: 24.1، 24.2. تزايدات طفيفة ضمن السنة، بدون دقة الشهر.
  • YYYY.0M.DD — بعض أدوات Google الداخلية تستخدم تواريخ كاملة بتنسيق ISO لقطارات الإصدار اليومية.

CalVer ملائم بشكل سيء للمكتبات التي لها كثير من المستهلكين المنبثقين وتغييرات كسرية متكررة: لا توجد إشارة مدمجة تدل على أن 2024.072024.08 آمن للترقية التلقائية أم يتطلب هجرة. للمكتبات ذات الـ API المستقرة لكن مع تحولات كبرى متفرقة، يعمل الهجين جيداً: 24.6.0 حيث 24.6 هو CalVer (سنة.شهر) والجزء الأخير عداد رقع بأسلوب semver. Kubernetes يستخدم semver خالصاً (1.30.2) لكنه ينشر إصدارات minor بوتيرة ربع سنوية تقريباً — يتصرف كقطار إصدار بتسميات semver.

قاعدة الإبهام لاختيار المخطط: استخدم semver لأي برنامج يُستورد كمكتبة من قِبَل كود آخر. استخدم CalVer للمنتجات الموجهة للمستخدم، وصور الأنظمة، أو توزيعات أدوات LTS حيث نوافذ الدعم أهم من إشارات استقرار الـ API. عند الشك، semver هو الافتراضي الأكثر أماناً — عقده مفهوم عالمياً ودعم الأدوات له أوسع.

أتمتة رفع الإصدار: semantic-release

semantic-release هو الأداة المعيارية لأتمتة semver دون تدخل بشري. تقرأ تاريخ commits الـ git — تحديداً commits مُنسَّقة كـ Conventional Commits (feat:، fix:، feat!:) — تحدد الإصدار الصحيح التالي، تُنشئ سجل تغييرات، تنشر الحزمة، تُنشئ إصداراً على GitHub، وتُعلّم الـ commit. لا يُعدّل أي مهندس رقم إصدار بيده.

# تثبيت semantic-release الأساسي والإضافات المطلوبة (بيئة Node.js) npm install --save-dev \ semantic-release \ @semantic-release/commit-analyzer \ @semantic-release/release-notes-generator \ @semantic-release/changelog \ @semantic-release/npm \ @semantic-release/git \ @semantic-release/github # .releaserc.json — إعداد إنتاجي أدنى { "branches": [ "main", {"name": "next", "prerelease": true}, {"name": "beta", "prerelease": "beta"}, {"name": "release/+([0-9]).x", "range": "${major}.x", "channel": "${major}.x"} ], "plugins": [ "@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", "@semantic-release/changelog", ["@semantic-release/npm", {"npmPublish": true}], ["@semantic-release/git", {"assets": ["package.json", "CHANGELOG.md"], "message": "chore(release): ${nextRelease.version} [skip ci]"}], "@semantic-release/github" ] } # أمثلة رسائل commit وما تُحفّز: # fix: correct null dereference in token parser -> رفع PATCH (1.2.3 -> 1.2.4) # feat: add gzip response compression -> رفع MINOR (1.2.3 -> 1.3.0) # feat!: remove deprecated /v1 API endpoints -> رفع MAJOR (1.2.3 -> 2.0.0) # (footer) BREAKING CHANGE: /v1 removed -> رفع MAJOR (نفس التأثير) # docs: update README -> لا إصدار (توثيق فقط) # chore: bump dev dependency -> لا إصدار

إعداد الفروع أعلاه يُنفّذ استراتيجية متعددة القنوات كاملة: main يُنتج إصدارات GA مستقرة، فرع next يُنتج ما قبل إصدارات x.y.z-next.N للمستهلكين المتقدمين، beta يُنتج بنيات x.y.z-beta.N، وفروع الصيانة كـ release/2.x تستقبل رقع الاستعادة لخط 2.x دون الإخلال بـ main. هذا هو النموذج المستخدم بالضبط في المشاريع مفتوحة المصدر الكبيرة كـ Babel وJest وsemantic-release ذاتها.

خصائص بيئتَي Python والحاويات

ليس كل مشروع يستخدم أدوات Node.js. نفس انضباط semver-from-commits ينطبق عبر البيئات، لكن الأدوات تختلف:

  • Python: python-semantic-release يقرأ Conventional Commits ويرفع pyproject.toml. ينشر على PyPI ويُنشئ إصدارات GitHub. صياغة ما قبل الإصدار في PEP 440 مختلفة قليلاً: 2.0.0a1 (alpha)، 2.0.0b2 (beta)، 2.0.0rc1 — بدون شرطة. أدوات كـ pip تفهم كلا صيغتَي PEP 440 وsemver.
  • وحدات Go: semver مُفرَض من أدوات Go. الوحدة بإصدار v2+ يجب أن يكون لها لاحقة مسار استيراد /v2 (مثل github.com/myorg/mylib/v2). وكيل Go يُخزّن إصدارات الوحدة غير القابلة للتغيير — لا يمكن إعادة وسم v2.0.0 بعد تخزينه مؤقتاً.
  • صور الحاويات: لا توجد أداة معيارية لإصدار-من-commits لصور OCI، لكن الاتفاقية هي وسم الصورة بنص semver جنباً إلى جنب مع SHA الـ git. وظيفة CI نموذجية تُنتج myimage:2.3.1 وmyimage:sha-a7f3d91 يُشيران إلى نفس الـ digest. علامة semver للمستهلكين البشريين؛ علامة SHA تُستخدم في مانيفستات النشر لضمان عدم القابلية للتغيير.
# GitHub Actions: احسب علامة semver من git، ابنِ وادفع صورة الحاوية # هذا النمط يُستخدم حين يتولى semantic-release احتساب الإصدار # وخطوة منفصلة تُعلّم صورة الحاوية. - name: Get version from package.json (set by semantic-release) id: version run: echo "VERSION=$(node -p \"require('./package.json').version\")" >> "$GITHUB_OUTPUT" - name: Build and push image with semver + SHA tags uses: docker/build-push-action@v6 with: push: true tags: | ghcr.io/${{ github.repository }}:${{ steps.version.outputs.VERSION }} ghcr.io/${{ github.repository }}:${{ github.sha }} ghcr.io/${{ github.repository }}:latest # للمشاريع غير Node.js، استخدم python-semantic-release (Python) أو # goreleaser (Go) لاحتساب الإصدار وقيادة بناء الصورة. # مثال goreleaser (.goreleaser.yaml): # dockers: # - image_templates: # - "ghcr.io/myorg/myapi:{{ .Tag }}" # - "ghcr.io/myorg/myapi:{{ .ShortCommit }}" # dockerfile: Dockerfile # build_flag_templates: # - "--label=org.opencontainers.image.version={{ .Version }}" # - "--label=org.opencontainers.image.revision={{ .FullCommit }}"

مخطط الإصدار على نطاق واسع: ما الذي ينكسر فعلاً

النظرية نظيفة؛ الإنتاج أكثر فوضى. هذه حالات الفشل التي تحدث فعلاً على نطاق واسع عند انهيار انضباط الإصدار:

  • الإصدارات ذات الأساس الصفري: semver ينص على أن 0.y.z مخصص للتطوير الأولي — الـ API غير مستقر وأي شيء قد يتغير. كثير من الفرق تبقي المكتبات عند 0.x لسنوات، مما يمنح المستهلكين انطباعاً زائفاً بأن رفعَي minor آمنان. إذا استُخدمت مكتبتك في الإنتاج، أطلق 1.0.0.
  • إعادة وسم الإصدارات المنشورة: على npm وPyPI ووكيل Go، الإصدارات غير قابلة للتغيير عموماً بمجرد نشرها. إعادة الوسم (حذف وإعادة نشر نفس الإصدار) يكسر البنيات الحتمية لكل مستهلك حلّ ذلك الإصدار. على GitHub Container Registry، إعادة دفع علامة قابلة للتغيير يكسر هذا الضمان لمستهلكي الصور. لا تُعد وسم إصدار منشور أبداً.
  • انجراف الإصدار في الـ monorepos: في monorepo به 50 حزمة، يُغري تحديد إصدار كل الحزم بشكل متزامن (جميعها عند 2.3.0 بغض النظر عما تغير). هذا ينتهك عقد semver: الحزم التي لم يطرأ عليها تغيير لا ينبغي أن ترفع. أدوات كـ Lerna وNx وChangesets تدعم الإصدار المستقل — كل حزمة تُصدر بشكل منفصل بناءً على تاريخ commits الخاص بها.
  • غياب قناة ما قبل الإصدار: الفرق التي تتخطى سلّم ما قبل الإصدار وتشحن مباشرةً من alpha إلى GA لا تمنح المستهلكين أي فرصة للتحقق من الترقية في بيئات غير إنتاجية. قناة ما قبل الإصدار لا تقل أهمية عن القناة المستقرة — هي صمام الأمان الذي يمسك التغييرات الكسرية قبل وصولها لقاعدة التثبيت الكاملة.
الانضباط في الإنتاج: ضع حماية على علامات الإصدار. على GitHub، أضف قاعدة حماية علامة لـ v* بحيث يستطيع إنشاء علامات الإصدار أو حذفها فقط بوت CI (حساب الخدمة الذي يشغّل semantic-release). هذا يمنع مهندساً من دفع v2.0.0 يدوياً فوق العلامة التي يديرها CI، مما يُفسد تاريخ الإصدار ويتسبب في حصول المستهلكين على الحزمة الخاطئة.

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