في مجال تطوير البرمجيات تمثل الإصدارات ركيزة أساسية ومهمة في تنظيم العمل على البرمجيات والتعديل عليها، ولأجل آخذ صورة عن أهمية الإصدارت تخيل معي أنك تقوم بتطوير برنامج جديد بغض النظر عن التقنية واللغة المستخدمة ففي بداية مرحلة التطوير لن تشعر بحجم المشكلة لأن البرمجية ما زالت صغيرة لكن كلما توسع البرنامج وزات الإعتماديات التي تُدمج مع هذا البرنامج أو النظام زاد تعقيد هذا البرنامج وبالتالي زاد تعقيد إخراج إصدارات جديدة.
- ما الإصدارت(Versions) أو ماذا يُقصد بالإصدار في مجال تطوير البرمجيات؟
- ما أهمية الإصدارات ولمَ هي شيء أساسي في تطوير البرمجيات؟
- المنهجيات والقواعد المتبعة في تحديد الإصدارات
- ما الإصدارت الدلالية
Semantic Versioning (SemVer)؟ وما طريقتها في إدارة الإصدارات؟ - أتمتة الإصدارات بإستخدام
semantic-release - مثال تفصيلي مع الشرح النظري والتطبيق العملي
كل منتج برمجي سواءً كان برنامج، واجهة تطبيق برمجية(API)، مكتبة(library) أو حزمة(package) يمتلك رقم إصدار يعكس الحالة الحالية للبرمجية. فالإصدار يُخبر المطورين، الأنظمة والمستخدمين بالتالي:
- ما الذي تغيّر.
- هل الترقية لهذا الإصدار آمن.
- هل الإصدار الحالي متوافق مع الإصدار السابق.
- ما الميزات والإصلاحات المضافة.
- إذا ما كان هناك تغييرات جذرية(Breaking Changes). لذلك فالإصدار ليس شيئا عشوائيا بل شيء واضح وقابل للتوقع.
- الإصدارات تعكس صورة واضحة عن التغييرات والتعديلات لفريق التطوير ذاته وللمستخدمين.
- التحديثات الغير مخطط لها والعشوائية تُسبب أخطاء وتوقفات غير محسوب حسابها.
- الأنظمة الكبيرة التي لديها إعتماديات كثيرة تتطلب آلية إصدارات متوقعة وواضحة.
- الإصدارات تضمن أن جميع البيئات(local, CI, production) تستخدم ذات الكود.
- الإصدارات تسهل التشاركية بين المطورين وتزيل التشتت بين النسخ العشوائية من ذات المنتج.
هناك الكثير من المنهجيات الطرق المتبعة لإدارة إصدارات البرمجيات وجميعها تدور حول تنظيم العمل على الإصدارت ووضع قواعد وتعليمات واضحة لأجل ذلك والحول دون وصول البرمجية إلى مرحلة من التعقيد يكون إخراج إصدار جديد بها صعب أو شبه مستحيل، وقد يقوم المطور أو فريق التطوير في وضع قواعدهم الخاصة بإدارة إصدارت البرمجية التي يتم العمل عليها، لكن هناك منهجية شهيرة جدا وذات قواعد واضحة وبسيطة وهي منهجية [الإصدارت الدلالية](# Semantic Versioning) Semantic Versioning وهي التي سيدور الشرح عنها في هذا المقال.
تقوم هذه الطريقة على تمثيل الإصدار بثلاثة أرقام مفصول بينها بنقطة ولكل رقم معنى معين فشكلها العام كالتالي:
MAJOR.MINOR.PATCH
مثال: 3.0.2
فلكل قسم أو رقم من الأرقام الثلاثة معنى معين:
إصدار كبير(MAJOR): يُزاد عندما تحتوي التعديلات على إضافة تغييرات جذرية قد يكون فيها تغيير على ميزات موجودة مسبقا أو إزالة ميزات سابقة أو تغييرات على واجهة التطبيقات البرمجيةAPI غير متوافقة مع الإصدارات السابقة، مما يعني أن التحديث إلى هذا الإصدار أمر غير آمن.
إصدار صغير(MINOR): يُزاد عند تحتوي التعديلات على إضافة ميزات ووظائف جديدة دون الإخلال بطريقة العمل السابقة أو تغيير سلوكها، وغالبا يكون التحديث لهذا الإصدار آمن.
إصدار إصلاح(PATCH): يُزاد عندما تحتوي التعديلات على إضافة إصلاحات على الميزات الموجودة دون التغيير على طريقة العمل السابقة مما يعني أن التحديث لهذا الإصدار آمن تماما.
بعض من القواعد التي تتبعها الإصدارت الدلالية:
- عند زيادة رقم الإصدار الكبير(MAJOR) يتم تغيير الإصدار الصغير والإصلإح إلى صفر 0، وإيضا عند تغيير رقم الإصدار الصغير(MINOR) يتم تغيير رقم إصدار الإصلاح(PATCH) إلى صفر 0.
- قد يتم إضافة لواحق إضافية بعد الإصدار ذات معنى خاص(قبل نشر(pre-release)، بناء(build)...) مثل:
1.1.0.build,2.3.1.pre-release,1.0.0-alpha,1.0.0-alpha.1 - رقم الإصدار الكبير يجب أن لا يكون صفر وإذا كان صفرا فهذا يعني أن المشروع في مرحلة ما قبل النشر أي أن الكود أو محتويات المشروع أو واجهة التطبيق البرمجية
APIغير مستقرة وأنها عرضة للتعديل والتغيير في أي وقت.
فمن خلال هذه المعايير يمكن لأي شخص توقع التغييرات الآتية مع الإصدار الجديد من خلال ملاحظة أماكن الأرقام المُتغيرة في الإصدار.
🟢 يمكنك الإنتقال إلى المثال التفصيلي لكي تتضح الصورة أكثر ثم تواصل القراءة لاحقا من هنا.
يمكن القيام بأتمتة الإصدارات التي تتبع منهجية الإصدارات الدلالية بإستخدام المكتبة semantic-release
يمكن القيام بأتمتة عملية إدارة الإصدارات بجميع مراحلها التي تتضمن:
- تحديد رقم الإصدار القادم.
- توليد سجل التغييرات المرافق للإصدار الجديد.
- نشر التطبيق أو المكتبة أو الحزمة إلى مسجل الحزم مثل npm أو حتى إلى gitlab, github. ويمكن من خلال هذه المكتبة أتمتة كل ما يتعلق بالإصدارات ومراحلها مما يسهم في تسهيل عملية إدارة الإصدارات وإزالة هامش الأخطاء البشرية.
تعتمد هذه المكتبة على الإلتزامات(Commits) المكتوبة حسب قواعد الإلتزامات الإعتيادية Conventional Commits حيث تعمل على تحليل هذه الإلتزمات وتقرر رقم الإصدار بناء على المعلومات المستنتجة، فمثلا كل نوع من الإلتزامات يتسبب في تحديد جزء معين من الإصدار الدلالي:
- النوع
featيتسبب في إصدار صغيرMINOR - النوع
fixيتسبب في إصدار إصلاحيPATCH - الجملة
BREAKING CHANGE:في ذيل الإلتزام أو علامة التعجب بعد نوع الإلتزام، يتسبب في إصدار كبيرMAJORمثل:
feat!: تغيير طريقة التحكم بالشريط الجانبي في التطبيق
----
feat: إرسال بريد إلكتروني للمستخدم عند شراء المنتج
BREAKING CHANGE: إزالة الطريقة القديمة لإرسال الإيميلات
semantic-release فهمها وتحليلها، كما أنه لابد للمطور أن يكون على علم بأن كل إلتزام يكتبه قد يتسبب في إنشاء إصدار جديد ويجب أن يكون هذا الإصدار متوافق مع محددات الإصدارات الدلالية(SemVer).
- نقوم بإنشاء ملف من نوع
jsonداخل المشروع في المستوى الأول يحتوي على إعدادت الـsemantic-release.
//.releaserc.json
{
// هذه الإعدادت تُخبر المكتبة أن تعتمد الفرع
// main للإصدارات الرئيسية
// والفروع:
// 1.x, 2.x
// فروع لإصدارات الصيانة
// والفروع:
// beta, alpha, next
// كفروع لإصدارات ما قبل النشر
"branches": [
"main",
"1.x",
"2.x",
{
"name": "beta",
"prerelease": true
},
{
"name": "alpha",
"prerelease": true
},
{
"name": "next",
"prerelease": true
}
],
// هذه الإعدادت تحدد الإضافات التي يمكن تشغيلها مع تشغيل الـ`semantic-release`
// مثل إضافة توليد سجل التغييرات ونشر الإصدار الجديد على الجيتهب أو الجيتلاب وغيره من الإضافات
// مع إمكانية تخصيص إعدادت كل إضافة
"plugins": [
["@semantic-release/commit-analyzer"],
[
"@semantic-release/release-notes-generator",
{
"preset": "conventionalcommits",
"presetConfig": {
"types": [
{
"type": "feat",
"section": "الميزات الجديدة"
},
{
"type": "fix",
"section": "الإصلاحات"
},
{
"type": "perf",
"section": "تحسينات الأداء"
},
{
"type": "revert",
"section": "التراجعات"
},
{
"type": "docs",
"section": "التوثيق"
},
{
"type": "style",
"section": "Styles"
},
{
"type": "refactor",
"section": "أكواد أُعيد كتابتها"
},
{
"type": "test",
"section": "الإختبارات"
},
{
"type": "build",
"section": "تعديلات البناء"
},
{
"type": "ci",
"section": "الدمج المستمر"
}
]
}
}
],
[
"@semantic-release/changelog",
{
"changelogFile": "CHANGELOG.md",
"changelogTitle": "# سجل التغييرات\n\nجميع التغييرات على هذا المشروع يتم توثيق ملخصها في هذا الملف.\n\nشكل الملف يستند للمرجع [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nكما يتبع هذا المشروع آلية الإصدارت الدلالية [Semantic Versioning](https://semver.org/spec/v2.0.0.html)."
}
],
[
"@semantic-release/npm",
{
"npmPublish": false
}
],
[
"@semantic-release/exec",
{
"prepareCmd": "npm run build"
}
],
[
"@semantic-release/git",
{
"assets": [
"CHANGELOG.md",
"dist/**/*"
],
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}
],
[
"@semantic-release/github",
{
"releasedLabels": ["released", "released-on-${nextRelease.channel}"],
"addReleases": "bottom"
}
]
],
"tagFormat": "${version}",
"preset": "conventionalcommits",
"ci": true,
"dryRun": false,
"debug": false
}
📌 للإستزادة والتوسع في إعدادت semantic-release راجع هذه الصفحة
2. تشغيل semantic-release كجزء من عملية الدمج المستمر(Continuous Integration)
- حيث نقوم في الدمج المستمر(gitlab ci/cd, github workflow...) بتثبيت إعتماديات الـ
semantic-releaseثم نقوم بتشغيل الأمر الخاص به، مثلا: - من خلال gitlab ci/cd:
stages:
- release
# Only run semantic-release on these branches
.release_branches:
only:
- main
- beta
- alpha
- next
- /^([0-9]+)\.x$/ # matches 1.x, 2.x, 3.x
- /^([0-9]+)\.x\.x$/ # matches 1.x.x
release:
stage: release
image: node:24-alpine
extends: .release_branches
before_script:
- echo "📥 Installing dependencies..."
- apk add --no-cache git jq curl
- npm config set fund false
- npm config set audit false
# Initialize project if package.json doesn't exist
- |
# Create minimal package.json
if [ ! -f "package.json" ]; then
npm init -y
# Update with minimal config
jq '. + {
"version": "0.0.0-development",
"scripts": {
"test": "echo \"Running tests...\" && exit 0",
"build": "echo \"Building...\" && exit 0"
}
}' package.json > package.json.tmp && mv package.json.tmp package.json
fi
# Create package-lock.json if missing
- npm install --package-lock-only --no-audit
script:
- echo "📦 Installing semantic-release..."
- |
npm install \
semantic-release@^24.2.3 \
@semantic-release/changelog@^6.0.3 \
@semantic-release/commit-analyzer@^11.1.0 \
@semantic-release/exec@^6.0.3 \
@semantic-release/git@^10.0.1 \
@semantic-release/gitlab@^13.2.4 \
@semantic-release/npm@^11.0.2 \
@semantic-release/release-notes-generator@^12.1.0 \
conventional-changelog-conventionalcommits@^7.0.2 \
--no-audit --progress=false
- echo "🚀 Running semantic-release..."
- npx semantic-release --debug- من خلال github workflow:
name: Release
on:
push:
branches:
- main
- beta
- alpha
- next
- 1.x
- 2.x
- 3.x
- 4.x
- 1.x.x
pull_request:
branches:
- main
jobs:
release:
permissions:
contents: write
issues: write
pull-requests: write
name: Semantic Release
runs-on: ubuntu-latest
if: github.event_name == 'push'
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Initialize project
run: |
# Create minimal package.json
if [ ! -f "package.json" ]; then
npm init -y
# Update with minimal config
jq '. + {
"version": "0.0.0-development",
"scripts": {
"test": "echo \"Running tests...\" && exit 0",
"build": "echo \"Building...\" && exit 0"
}
}' package.json > package.json.tmp && mv package.json.tmp package.json
fi
# Generate lock file
npm install --package-lock-only --no-audit
- name: Install semantic-release
run: |
echo "Installing semantic-release packages..."
npm install \
semantic-release@^22.0.5 \
@semantic-release/changelog@^6.0.3 \
@semantic-release/commit-analyzer@^11.1.0 \
@semantic-release/exec@^6.0.3 \
@semantic-release/git@^10.0.1 \
@semantic-release/github@^9.2.5 \
@semantic-release/npm@^11.0.2 \
@semantic-release/release-notes-generator@^12.1.0 \
conventional-changelog-conventionalcommits@^7.0.2 \
--no-audit --progress=false
- name: Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "Running semantic-release..."
npx semantic-release --debug- هذا مثال مبسط يشرح تطبيق الإصدارات على مثال عملي، في هذا القسم تجد الشرح النظري وفي ذات المخزن(Repository) تجد التطبيق العملي مع جميع الإلتزامات(Commits) والإصدارت(Releases) وجميع الملفات المتعلقة.
- لنقل أننا نريد عمل مكتبة بسيطة بإستخدام الجافا سكربت للقيام بالعمليات الحسابية الأساسية(الضرب، الطرح...) ولتكن هذه المكتبة مكونة من ملف واحد فقط
math-lite.js. - عند كل إصدار سنقوم بإنشاء
ReleaseوTagونربطهما مع بعض، أيضا سنقوم بإنشاء ملف سجل التغييراتChangelogبحيث نكتب بداخله التغييرات التي تمت في كل إصدار يتم نشره، لكن بعد الإصدار1.0.0سنقوم بأتممتة للعملية بأكملها بواسطةsemantic-release - لكي تتضح لك الصورة جيدا قم إنشاء مخزن(Repository) خاص بك وتطبيق جميع الخطوات الواردة عليه
//math-lite.js
export function add(a, b) {
return a + b;
}الإصدار: 0.1.0
نجعل الإصدار 0.1.0 لأن المكتبة:
- غير مستقرة بعد
- قد يتغير الـ API الخاص بها في أي وقت
الآن يمكن للمستخدمين الوثوق في الـ API واعتبار المكتبة أصبحت مستقرة وأن أي تغييرات جذرية لا يمكن أن تتم إلا في إصدار كبير قادم الذي هو 2.0.0.
//math-lite.js
export function add(a, b) {
return a + b;
}الإصدار: 1.0.0
افترض أن المستخدم استخدم الدالة كالتالي:
add(1, undefined)
ستقوم الدالة بإعادة NaN بينا نريدها أن تُرجع خطأ(Exception)
//math-lite.js
export function add(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new Error("add: يجب أن يكون كلا المعاملين رقمين");
}
return a + b;
}🔹 ضمن التعديلات الحالية تم إضافة إعدادت أتمتة الإصدارات بواسطةsemantic-release من خلال الملفين:
الإصدار: 1.0.1
لماذا PATCH؟
- لا تغييرات في الـ API
- لا ميزات جديدة
- فقط إصلاح آمن ومتوافق مع الإصدارات السابقة
- fix: دالة الجمع لا تتحقق من نوع المعاملات
- ci: أتمتت إعدادات الإصدارات الدلالية مع تكوين GitHub Actions
أضفنا دالة الطرح:
//math-lite.js
export function add(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new Error("add: يجب أن يكون كلا المعاملين رقمين");
}
return a + b;
}
export function subtract(a, b) {
return a - b;
}هذا التعديل لا يُغير شيء في الميزات السابقة لكنه يضيف ميزة جديدة.
الإصدار: 1.1.0
إضافة مدخل اختياري للدالة add():
//math-lite.js
export function add(a, b, options = { absolute: false }) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new Error("add: يجب أن يكون كلا المعاملين رقمين");
}
const result = a + b;
return options.absolute ? Math.abs(result) : result;
}
export function subtract(a, b) {
return a - b;
}الاستخدام القديم لا يزال يعمل → متوافق مع الإصدارات السابقة.
الإصدار: 1.2.0
قررنا إعادة تسمية الدالة subtract إلى minus مع تصحيح بعض الأخطاء.
سيؤدي هذا إلى أخطاء لدى المستخدمين الذين يستخدمون الدالة subtract لذلك ستؤدي هذه التعديلات إلى إنشاء إصدار كبير MAJOR.
//math-lite.js
export function add(a, b, options = { absolute: false }) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new Error("add: يجب أن يكون كلا المعاملين رقمين");
}
const result = a + b;
return options.absolute ? Math.abs(result) : result;
}
export function minus(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new Error("minus: يجب أن يكون كلا المعاملين رقمين");
}
return a - b;
}الإصدار: 2.0.0
بعض المستخدمين لا يزالون يعتمدون على الإصدار 1.x.x، ولنقل أن جميع التعديلات التي تتم على آخر إصدار يتم إضافتها في الفرع(branch) المسمي main فسنقوم بالمحافظة على فرع منفصل يحتوي على الميزات الموجودة في الإصدار 1 فقط ولتكن تسمية الفرع 1.x
لنفترض وجود خطأ في الدالة subtract بالإصدار `1.x.
//math-lite.js
export function add(a, b, options = { absolute: false }) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new Error("add: يجب أن يكون كلا المعاملين رقمين");
}
const result = a + b;
return options.absolute ? Math.abs(result) : result;
}
export function subtract(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new Error("subtract: يجب أن يكون كلا المعاملين رقمين");
}
return a - b;
}الإصدار: 1.2.1
نستمر في دعم الإصدارات 1.x بالتصحيحات فقط، دون إضافة ميزات جديدة.
| الإصدار | النوع | السبب |
|---|---|---|
| 0.1.0 | تطوير | إصدار غير مستقر مبكر |
| 1.0.0 | كبير MAJOR | أول إصدار مستقر |
| 1.0.1 | إصلاحي PATCH | إصلاح خطأ |
| 1.1.0 | صغير MINOR | ميزة جديدة متوافقة مع الإصدارات السابقة (subtract) |
| 1.2.0 | صغير MINOR | إضافة معامل اختياري إلى add() |
| 2.0.0 | كبير MAJOR | تغيير جذري: إعادة تسمية subtract إلى minus |
| 1.2.1 | إصلاحي PATCH | إصلاح في الإصدار القديم (1.x) |
جميع التغييرات التي تمت تجدها هنا.