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

سجلات التغيير والالتزامات التقليدية

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

سجلات التغيير والالتزامات التقليدية

سجل التغيير عقدٌ مع مستخدميك. يُخبرهم بما تغيّر، وما انكسر، وما يجب عليهم فعله قبل الترقية. لكن على نطاق واسع — عشرات المهندسين ومئات الالتزامات أسبوعيًا — لا يستطيع أحد الحفاظ على سجل تغيير دقيق يدويًا. الحل الصناعي هو الالتزامات التقليدية (Conventional Commits): مواصفة خفيفة تمنح رسائل الالتزام هيكلًا قابلًا للتحليل آليًا، مما يُمكّن الأدوات من توليد سجلات التغييرات، وتحديث الإصدارات، وتشغيل عمليات الإطلاق دون أي تدخل بشري. هذه هي الطريقة التي تدير بها Google وMicrosoft ومعظم مشاريع المصدر المفتوح الحديثة التواصل مع المستخدمين حول الإصدارات.

مواصفة الالتزامات التقليدية

تُحدِّد المواصفة (conventionalcommits.org) تنسيقًا بسيطًا لرسائل الالتزام:

<النوع>[النطاق الاختياري]: <الوصف> [نص اختياري] [تذييل اختياري]

النوع هو الحقل الأساسي. تُحدِّد المواصفة نوعين إلزاميين وتحجز أخرى بالاتفاق:

  • fix — إصلاح خطأ. يُقابل رفع إصدار PATCH في SemVer.
  • feat — ميزة جديدة. يُقابل رفع إصدار MINOR.
  • feat! أو تذييل BREAKING CHANGE: ... — تغيير جوهري يكسر التوافق. يُقابل رفع إصدار MAJOR.
  • أنواع مجتمعية (من اتفاقية Angular، معتمدة على نطاق واسع): build، chore، ci، docs، perf، refactor، revert، style، test. لا تُؤدي هذه الأنواع إلى رفع رقم الإصدار بمفردها.

يُضيّق النطاق الاختياري السياق: feat(auth): add OIDC provider support. تظهر النطاقات في سجل التغيير مُجمَّعةً تحت نوعها، ويمكن استخدامها لتحديد قواعد الإصدار لكل مكوّن في المستودعات المتعددة.

لماذا يهم التنسيق: الآلة لا تفهم "أصلحت مشكلة تسجيل الدخول". إنها تحتاج إلى fix(auth): resolve session cookie SameSite mismatch. حقل النوع هو الإشارة؛ والوصف نثر بشري. كلاهما مهم — أحدهما للأتمتة والآخر للمهندس الذي يقرأ سجل التغيير في الساعة الثانية صباحًا خلال حادثة طارئة.

أمثلة حقيقية على الالتزامات

# رفع PATCH — إصلاح خطأ fix(payments): handle Stripe webhook signature validation on replay Previously, replayed webhooks could fail signature checks if the server clock drifted more than 5 seconds. Now using Stripe's recommended tolerance window. # رفع MINOR — ميزة جديدة feat(api): add pagination cursor support to /v2/events endpoint Closes #4821. Backward-compatible; existing offset-based callers are unaffected. # رفع MAJOR — تغيير جوهري (شكلان متكافئان) feat(sdk)!: drop support for Node.js 16 BREAKING CHANGE: minimum required Node.js version is now 18. Node 16 reached EOL in September 2023. # تغيير CI فقط — بدون رفع إصدار ci: migrate GitHub Actions runners to ubuntu-24.04 # تحديث اعتماديات — بدون رفع إصدار chore(deps): bump axios from 1.6.0 to 1.7.2

الأدوات: commitlint و Husky

المواصفة لا قيمة لها إذا تجاهلها المهندسون. commitlint يُطبِّق التنسيق عند الالتزام عبر خطاف Git، مما يُعطي ردود فعل فورية قبل وصول الرسالة إلى CI. husky يُوصِّل الخطاف تلقائيًا بعد npm install.

# تثبيت الأدوات npm install --save-dev \ @commitlint/cli \ @commitlint/config-conventional \ husky # commitlint.config.js module.exports = { extends: ['@commitlint/config-conventional'], rules: { 'type-enum': [2, 'always', [ 'build','chore','ci','docs','feat','fix', 'perf','refactor','revert','style','test' ]], 'subject-case': [2, 'always', 'lower-case'], 'header-max-length': [2, 'always', 100], } }; # ربط خطاف commit-msg (husky v9) npx husky init echo "npx --no -- commitlint --edit \$1" > .husky/commit-msg chmod +x .husky/commit-msg # اختبار — سيُرفض هذا الالتزام git commit -m "fixed stuff" # ✖ subject may not be empty [subject-empty] # ✖ type may not be empty [type-empty] # هذا سيُقبل git commit -m "fix(auth): resolve session cookie SameSite mismatch"
طبِّق في CI أيضًا. يعمل Husky فقط على جهاز المُلتزم — يتجاوزه git commit --no-verify أو الدفع المباشر. أضف commitlint كمهمة CI تعمل على طلبات السحب: npx commitlint --from origin/main --to HEAD. الخطاف هو تجربة المطوّر؛ CI هو البوابة الحقيقية.

توليد سجل التغيير تلقائيًا: standard-version و semantic-release

مع وجود الالتزامات المنظَّمة، تهيمن أداتان على مساحة توليد سجل التغيير:

  • standard-version — واجهة سطر أوامر محلية تُعدِّل package.json وتولِّد CHANGELOG.md وتُنشئ وسمًا في git. مناسبة للفرق التي تريد أن يُشغِّل الإنسان عمليات الإطلاق. مُهمَلة رسميًا لكنها لا تزال مستخدمة على نطاق واسع.
  • semantic-release — مؤتمتة بالكامل ومُشغَّلة بواسطة CI. لا يُشغِّلها إنسان؛ يُحدِّد مسار CI الإصدار التالي، ويُنشر القطعة الأثرية، ويُنشئ إصدارًا على GitHub، ويُسجِّل سجل التغيير. هذا هو المعيار الافتراضي للمستوى الكبير للمكتبات والخدمات ذات الإصدارات المتكررة.
Conventional Commits to Release Pipeline feat(api): ... MINOR bump fix(db): ... PATCH bump feat!: ... MAJOR bump chore: ... no bump Commit Analyzer semantic-release Next Version 1.4.0 → 2.0.0 determined CHANGELOG Git Tag npm publish GH Release
أنواع الالتزامات تُغذِّي المُحلِّل؛ الإصدار المُحدَّد يُشغِّل جميع قطع الإصدار تلقائيًا.

semantic-release في CI (GitHub Actions)

# .github/workflows/release.yml name: Release on: push: branches: [main] jobs: release: runs-on: ubuntu-24.04 permissions: contents: write # وسم + التزام سجل التغيير issues: write # تعليق على المشكلات عند إصلاحها pull-requests: write # تعليق على طلبات السحب المدمجة id-token: write # OIDC لإثبات الأصل في npm steps: - uses: actions/checkout@v4 with: fetch-depth: 0 # التاريخ الكامل مطلوب - uses: actions/setup-node@v4 with: node-version: 20 registry-url: https://registry.npmjs.org - run: npm ci - name: Release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} run: npx semantic-release
fetch-depth: 0 إلزامي. الاستنساخ السطحي (fetch-depth: 1، الافتراضي في GitHub Actions) يُعطي semantic-release الالتزام الأخير فقط. لا يستطيع تحديد الوسم السابق، فإما أن يتعطل أو يُحدِّد نوع الرفع بشكل خاطئ. هذا هو السبب الأكثر شيوعًا لإنتاج semantic-release إصدارات خاطئة في CI — دائمًا اجلب التاريخ الكامل.

تنسيق سجل التغيير واحتفظ بسجل تغيير

يتبع ملف CHANGELOG.md المُولَّد اتفاقية Keep a Changelog (keepachangelog.com): أقسام لكل إصدار، كل منها مُقسَّم إلى ### Added و### Fixed و### Changed و### Removed و### Breaking Changes. يتصفح القراء البشريون سجل التغيير من الأعلى؛ تُحلِّله الأدوات الآلية من الأسفل. كلا الجمهورين بالغا الأهمية.

لبيئات بيئات التطوير غير Node.js، تتوفر أدوات مكافئة:

  • Python: python-semantic-release (يقرأ pyproject.toml، ينشر إلى PyPI)
  • Go: goreleaser مع دعم الالتزامات التقليدية
  • Rust: cargo-release + git-cliff لتوليد سجل التغيير
  • عام / مستودعات متعددة: release-please (من Google)، changesets (npm)
استراتيجية المستودعات المتعددة: في مستودع متعدد مع إصدار مستقل للحزم، استخدم changesets (لـ JS) أو release-please (مستقل عن اللغة). يفتح كلاهما "طلب سحب إصدار" يجمع مدخلات سجل التغيير؛ دمج الطلب يُشغِّل النشر. يمنح هذا البشر نقطة مراجعة قبل شحن القطع الأثرية — وسط بين اليدوي الكامل والمؤتمت الكامل.

أنماط الفشل في بيئة الإنتاج

أبرز أنماط الفشل الشائعة على نطاق واسع:

  • تجاوز الدمج بالسحق (Squash): يولِّد زر "Squash and merge" في GitHub رسالة الالتزام الافتراضية مثل feat: my PR title (#123)، وهي غالبًا صحيحة — لكن فقط إذا كان عنوان طلب السحب مكتوبًا بالتنسيق التقليدي. طبِّق فحص عنوان طلب السحب مع amannn/action-semantic-pull-request في CI حتى تكون رسالة السحق دائمًا صحيحة.
  • تغيير جوهري مدفون في النص: يكتب المهندسون feat: update SDK مع BREAKING CHANGE: ... في النص لكن قاعدة commitlint في CI تتحقق فقط من الترويسة. يلتقطه المُحلِّل ما زال، لكن المراجعين يُفوِّتون الإشارة. اشترط بناء feat! في حقل النوع للوضوح.
  • التزامات الروبوت تُشغِّل حلقات: عندما يُسجِّل semantic-release ملف CHANGELOG.md المُحدَّث إلى main، يُعيد تشغيل سير عمل push. احمِ بـif: "!contains(github.event.head_commit.message, 'chore(release)')" أو استخدم الرمز skip_ci في رسالة الالتزام.
  • غياب NPM_TOKEN: تنجح مهمة الإصدار، يُنشأ الوسم، يُنشر إصدار GitHub — لكن خطوة نشر npm تفشل بصمت إذا لم يكن السر مُعيَّنًا. تحقق دائمًا من مخرجات المهمة الكاملة، وليس فقط العلامة الخضراء.