أتمتة Release Notes: افتح PR جاهز من commits
هتطلع مسودة Release Notes قابلة للمراجعة بدل ما تقرأ كل commits يدويًا قبل كل إصدار. المكسب العملي: تقليل وقت التحضير من حوالي 45 دقيقة إلى 7 دقائق في مشروع متوسط.
مستوى القارئ: متوسط
المشكلة باختصار
الطريقة الشائعة إنك تفتح GitHub، تقرأ آخر commits، تكتب Changelog يدوي، ثم تنسى commit مهم أو تخلط bug fix مع breaking change. الطريقة دي بتفشل لما الفريق يطلع إصدار كل أسبوع، أو لما عندك أكثر من خدمة وكل خدمة لها commits منفصلة.
الافتراض هنا إن عندك repository يستخدم Git، وفريق صغير أو متوسط يكتب commits بشكل قريب من Conventional Commits مثل feat: وfix:. لو الرسائل عشوائية جدًا، الأتمتة هتنتج مسودة ضعيفة، لكنها لا تزال تكشف لك أين المشكلة بالظبط.
الفكرة بمثال واضح
ركز في المثال ده: عندك تطبيق SaaS صغير. خلال أسبوع الفريق أضاف تصدير CSV، أصلح خطأ في صفحة الدفع، وعدّل docs. بدل ما مدير الإصدار يقرأ 38 commit، السكربت يقرأ الرسائل ويقسمها إلى Features وBug Fixes وDocumentation. بعد كده يفتح PR فيه ملف CHANGELOG.md محدث.
علميًا، الفكرة اسمها release-note generation من metadata موجودة بالفعل في Git history. الأداة لا تفهم نية الفريق بالسحر. هي تعتمد على convention واضح في أسماء commits. لذلك Conventional Commits مهم: feat تعني ميزة، fix تعني إصلاح، ووجود ! أو BREAKING CHANGE يرفع التغيير لقسم أخطر.
الخطوات العملية
- اتفق مع الفريق على رسائل commits بسيطة:
feat(auth): add magic link loginوfix(billing): handle failed invoice retry. - أضف
git-cliffلتوليد Changelog من Git history بدل الكتابة اليدوية. - شغّل workflow في GitHub Actions يدويًا قبل الإصدار، وليس على كل push.
- افتح PR تلقائي يحتوي على
CHANGELOG.mdعشان يراجعه شخص قبل tag النهائي.
هذا ملف إعداد بسيط لـ git-cliff يحافظ على الأقسام واضحة:
# cliff.toml
[changelog]
header = "# Changelog\n\n"
body = """
{% for group, commits in commits | group_by(attribute="group") %}
## {{ group | upper_first }}
{% for commit in commits %}
- {{ commit.message | upper_first }} ({{ commit.id | truncate(length=7, end="") }})
{% endfor %}
{% endfor %}
"""
trim = true
[git]
conventional_commits = true
filter_unconventional = true
commit_parsers = [
{ message = "^feat", group = "features" },
{ message = "^fix", group = "bug fixes" },
{ message = "^docs", group = "documentation" },
{ message = "^perf", group = "performance" },
]