أتمتة توليد Changelog و GitHub Releases بـ release-please
لو بتقعد كل أسبوع 20 دقيقة تفتح CHANGELOG.md وتلصق فيه "أضفنا كذا، صلّحنا كذا" يدويًا، وبعدين تفتح تاب GitHub Releases وتلصق نفس الكلام تاني، وتزوّد رقم الـ version في package.json بإيدك — ده شغل روبوت بتعمله إنت. release-please من Google بيعمل الثلاثة دول من commits الموجودة عندك أصلاً، بدون ما تتعلم أداة جديدة. الناتج: Release PR واحد بيقعد مفتوح على main، لمّا تعمله merge، بيتولّد CHANGELOG محدّث، tag جديد، و GitHub Release رسمي — كله في 15 ثانية.
المشكلة باختصار
الـ CHANGELOG اليدوي بيفشل لأربع أسباب. الأول: الناس بتنساه. الثاني: بيتكتب يوم الـ release بس، فبيعتمد على ذاكرة اللي بيكتب. الثالث: الـ version في package.json والـ tag في git والـ release على GitHub — ثلاثتهم لازم يطابقوا، وده بيحصل مرة من كل عشرة. الرابع: في monorepo فيه 5 packages، الشغل ده بيتضرب في 5. الحل المنطقي إن الـ source of truth يكون الـ commits نفسها، طالما إنت بتكتبها بصيغة منظّمة.
مثال بسيط الأول: ليه Conventional Commits أصلاً
تخيّل إنك قاعد في مطعم، والجرسون بدل ما يقولّك "عندنا أكل"، بيقولّك: "الطبق الجديد: مكرونة بالبشاميل. التعديل على الطبق القديم: الكبسة بقت بتوابل أقل. مشكلة اتحلّت: الحمّص مبقاش فيه ملح زيادة." ده بالظبط اللي Conventional Commits بتعمله للـ git: بتخلّي كل commit يقول لنفسه هو feature جديد، تصليح bug، ولا مجرد تنسيق.
كل commit بيبدأ بـ نوع محدد:
feat:ميزة جديدة → بيزوّد الـ minor version (1.2.0 → 1.3.0)fix:تصليح bug → بيزوّد الـ patch version (1.2.0 → 1.2.1)feat!:أوBREAKING CHANGE:→ بيزوّد الـ major version (1.2.0 → 2.0.0)chore:,docs:,refactor:,test:→ مش بتظهر في الـ changelog غالبًا
التعريف العلمي الدقيق
Conventional Commits هي مواصفة (specification) لصياغة رسائل الـ commits بصيغة قابلة للقراءة آليًا، مبنية فوق Semantic Versioning (SemVer). كل رسالة بتتكوّن من type + scope اختياري + description. الأدوات اللي بتلتزم بالمواصفة دي تقدر تستنتج رقم الإصدار التالي بدون تدخّل بشري. release-please بيطبّق نفس المنطق ده: بيفحص كل الـ commits من آخر tag، يحدّد أكبر نوع فيهم (major > minor > patch)، ويحسب الـ version الجديد.
إزاي release-please بيشتغل فعلاً
الـ flow بسيط لدرجة مدهشة:
- إنت بتدفع commits عادي على
mainبصيغة Conventional Commits. - release-please بيشتغل كـ GitHub Action على كل push.
- بيلاقي الـ commits اللي بعد آخر release، يحسب الـ version الجديد، ويفتح (أو يحدّث) Release PR فيه: CHANGELOG جديد +
package.jsonمعدّل +.release-please-manifest.jsonمتغيّر. - إنت بتعمل review للـ PR ده عادي. لو جاهز، بتضغط merge.
- عند الـ merge، release-please بيتحرّك تاني: بيعمل git tag، ينشر GitHub Release بنفس محتوى الـ CHANGELOG، وبيقفل الـ PR.
نقطة مهمة: الـ Release PR دايمًا موجود. طول ما في commits مفيهاش release، هو مفتوح. ده الـ trade-off الرئيسي: في كل مشروع هتلاقي PR اسمه "chore: release 1.x.x" دايمًا في القائمة. ناس بتحبه، ناس بتكرهه.
الإعداد الكامل — ملف واحد workflow + ملفّين config
افتح repo فيه package.json وحط الملفات دي.
1) .github/workflows/release-please.yml
name: release-please
on:
push:
branches: [main]
permissions:
contents: write
pull-requests: write
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: googleapis/release-please-action@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
2) release-please-config.json — بيحدد نوع الـ release وإعدادات الـ changelog:
{
"packages": {
".": {
"release-type": "node",
"package-name": "my-app",
"changelog-sections": [
{ "type": "feat", "section": "المزايا الجديدة" },
{ "type": "fix", "section": "تصليحات" },
{ "type": "perf", "section": "تحسينات أداء" },
{ "type": "refactor", "section": "إعادة هيكلة", "hidden": true },
{ "type": "chore", "section": "صيانة", "hidden": true }
],
"include-v-in-tag": true,
"bump-minor-pre-major": true
}
}
}
3) .release-please-manifest.json — الـ source of truth للـ version الحالي:
{
".": "0.1.0"
}
ادفع الثلاثة على main. بعد أول push، استنى 30 ثانية. هتلاقي PR جديد اسمه "chore(main): release 0.2.0" فيه الـ CHANGELOG. افتحه واعمله merge. خلاص — أول Release رسمي اتعمل.
مثال Commits حقيقية وناتجها
لو عملت الـ commits دي على main خلال أسبوع:
feat(auth): add OAuth login with Google
fix(api): handle 429 rate limit errors gracefully
feat(ui): dark mode toggle in settings
fix(db): prevent connection pool leak on timeout
docs: update README installation steps
chore: bump eslint to 9.2
release-please هيفتحلك PR ناتجه:
- version: من
0.1.0لـ0.2.0(لأن فيه feat → minor bump) - CHANGELOG.md: قسم "المزايا الجديدة" فيه بندين، قسم "تصليحات" فيه بندين. الـ
docsوchoreمش هيظهروا. - GitHub Release v0.2.0: بنفس المحتوى، بيتعمل أوتوماتيكيًا لما تـ merge.
Monorepo — لو عندك أكتر من package
release-please v13 بيدعم monorepos بشكل first-class. لو عندك packages/api و packages/web و packages/shared، الـ config بيبقى:
{
"packages": {
"packages/api": { "release-type": "node", "package-name": "@myorg/api" },
"packages/web": { "release-type": "node", "package-name": "@myorg/web" },
"packages/shared": { "release-type": "node", "package-name": "@myorg/shared" }
},
"plugins": [
{ "type": "node-workspace" },
{ "type": "linked-versions", "group-name": "main", "components": ["@myorg/api", "@myorg/web"] }
],
"separate-pull-requests": false
}
الـ plugin node-workspace بيحدّث الـ internal dependencies بين الـ packages تلقائيًا. لو api بتستخدم shared ونزل Release جديد لـ shared، الـ api/package.json هيتحدّث أوتوماتيك.
الأرقام اللي تهمّك
- الوقت اليدوي قبل: ~20 دقيقة/release × 4 releases شهريًا = 80 دقيقة شهريًا.
- الوقت مع release-please: ~2 دقيقة review لكل PR × 4 = 8 دقائق.
- التوفير الشهري: 72 دقيقة (90%).
- أخطاء version mismatch: من ~2 شهريًا لـ 0 (بحكم إن الـ bump deterministic).
- تكلفة GitHub Actions: صفر للمشاريع العامة؛ للخاصة ~3 دقائق/شهر × $0.008 = أقل من 3 سنت/شهر.
Trade-offs حقيقية لازم تعرفها
هتكسب: CHANGELOG دقيق، version صحيح دايمًا، GitHub Release تلقائي، source of truth واحد هو الـ commits، دعم monorepos قوي.
هتخسر:
- انضباط في صياغة الـ commits. لو فريقك بيكتب "update stuff" في كل commit، release-please مش هيفيدك.
- الـ Release PR هيقعد مفتوح دايمًا، ناس بتشوفه ضوضاء.
- الـ CHANGELOG بالإنجليزي افتراضيًا. تعريبه محتاج
changelog-sectionsكـ ما فوق، مش اللغة كلها. - مش بيشوف الـ commits اللي قبل ما تفعّله. أول run بيبدأ من نقطة الصفر.
متى لا تستخدم release-please
تجنّبه لو: (1) المشروع شخصي ومش بتعمل releases أصلاً، (2) فريقك مش ملتزم بـ Conventional Commits ومش هتقدر تفرضها (استخدم commitlint مع pre-commit hook لو عاوز تفرضها)، (3) الـ release لديك محتاج خطوات يدوية حرجة زي migrations أو approvals من Compliance — release-please ممكن يقفّزك عليها. في الحالة دي استخدم publish_now: false أو semantic-release اللي بيدي تحكّم أكتر في مراحل النشر.
الخطوة التالية
اختار أصغر repo عندك دلوقتي، وحط الـ 3 ملفات اللي فوق عليه. ادفعهم على branch منفصل، افتح PR، و شوف release-please بيتصرّف إزاي. لو في commits موجودة مش بصيغة Conventional Commits، حوّل الفريق للصيغة دي في الـ commits الجديدة فقط — مش لازم تعدّل التاريخ. بعد أول release ناجح، طبّقه على بقية المشاريع.