Semantic Versioning چیست؟ به طور خلاصه یعنی نسخه را به صورت MAJOR.MINOR.PATCH بنویسیم و بر اساس تاثیر تغییرات آن را افزایش دهیم: هر شکست ناسازگاری به گذشته MAJOR را زیاد می‌کند، افزودن قابلیت سازگار MINOR را، و رفع باگ یا بهبود داخلی PATCH را. این روش نسخه گذاری قابل پیش بینی است و به توسعه دهنده و ابزارها می‌گوید با هر انتشار چه انتظاری از سازگاری داشته باشند.

Semantic Versioning چیست؟

Semantic Versioning یا به اختصار SemVer یک قرارداد نسخه گذاری نرم افزار است که قالب آن X.Y.Z است:

  • MAJOR یا X: وقتی تغییر ناسازگار با نسخه های قبلی ایجاد می‌کنید.
  • MINOR یا Y: وقتی قابلیت جدید سازگار با نسخه قبلی اضافه می‌کنید.
  • PATCH یا Z: وقتی باگ را رفع یا تغییرات داخلی بدون تاثیر بر API عمومی انجام می‌دهید.

افزون بر این، می‌توانید پیش نسخه ها و متادیتا را اضافه کنید:

  • پیش نسخه: 1.4.0-alpha.1 یا 2.0.0-rc.1 برای نسخه های آزمایشی تا پیش از انتشار پایدار.
  • متادیتا: 1.4.0+build.5 یا 2.0.0-rc.1+sha.5114f85 که فقط اطلاعات تکمیلی است و در ترتیب نسخه ها اثری ندارد.

ترتیب نسخه ها بر اساس اعداد انجام می‌شود: 1.10.0 از 1.9.9 جدیدتر است. پیش نسخه ها همیشه از انتشار نهایی همان نسخه قدیمی ترند: 1.4.0-alpha.3 از 1.4.0 کوچک تر است.

چگونه نسخه را درست افزایش دهیم؟

معیار تصمیم گیری ساده است، اما باید دقیق اجرا شود:

  • MAJOR: هر تغییری که قرارداد عمومی شما را می‌شکند؛ مثلا حذف یا تغییر پارامتر تابع، تغییر رفتار بازگشتی، تغییر نام اندپوینت API، یا تغییر قالب خروجی که مصرف کننده را مجبور به اصلاح کند.
  • MINOR: افزودن قابلیت جدید که استفاده فعلی را نمی‌شکند؛ مثلا اضافه شدن پارامتر اختیاری با مقدار پیش فرض، اضافه شدن رویداد یا متد جدید، یا پشتیبانی از قالب اضافی.
  • PATCH: رفع باگ، بهبود عملکرد بدون تغییر قرارداد، به روز رسانی داخلی وابستگی ها که اثری بر API ندارد، یا تصحیح مستندات همراه با تغییرات جزئی کد در حد عدم تاثیر بر مصرف کننده.

مثال های سریع افزایش نسخه

  • رفع NullPointer در تابع موجود بدون تغییر امضا: 1.2.3 ← 1.2.4
  • افزودن متد جدید به کلاس عمومی: 1.2.3 ← 1.3.0
  • حذف فیلد از پاسخ API: 1.2.3 ← 2.0.0
  • آماده سازی انتشار کاندیدا: 2.0.0-rc.1 ← 2.0.0-rc.2 ← 2.0.0

قواعد دقیق که نباید نقض شوند

  • هر بخش عددی یک عدد صحیح غیرمنفی بدون صفر پیشرو است؛ 01 مجاز نیست.
  • پیش نسخه با – جدا می‌شود و می‌تواند حروف، عدد و خط تیره داشته باشد. اجزای پیش نسخه با نقطه از هم جدا می‌شوند.
  • اجزای عددی در پیش نسخه با مقایسه عددی مرتب می‌شوند؛ حروفی ها با مقایسه لغوی.
  • متادیتا با + اضافه می‌شود و در مقایسه تقدم نسخه اثری ندارد.
  • پیش نسخه قدیمی تر از انتشار نهایی همان نسخه است: 1.0.0-beta < 1.0.0.
  • پیشوند v در برچسب گیت رایج است (مثلا v1.2.3)، اما مقدار نسخه در فایل های پروژه باید دقیقا شکل X.Y.Z داشته باشد.

پیش نسخه و متادیتا را چگونه و چه زمانی استفاده کنیم؟

وقتی تغییرات بزرگ دارید اما هنوز پایدار نیستند، از پیش نسخه ها استفاده کنید:

  • -alpha.X: برای نسخه های اولیه که ممکن است ناپایدار باشند.
  • -beta.X: وقتی ویژگی ها تقریبا کامل شده اند اما ممکن است باگ داشته باشند.
  • -rc.X: کاندید انتشار؛ اگر مشکل بحرانی پیدا نشود، همان نسخه بدون پسوند منتشر می‌شود.

متادیتا برای حمل اطلاعات فرایندی مناسب است:

  • 2.0.0-rc.1+build.45 یا 1.5.2+sha.9c1a12d برای ردیابی خط ساخت یا هش کامیت.

دقت کنید متادیتا ترتیب نسخه را تغییر نمی‌دهد؛ 1.5.2+2 و 1.5.2+7 از نظر تقدم برابرند.

نسخه های 0.x چه معنایی دارند؟

در SemVer، بازه 0.y.z به معنی دوره توسعه اولیه است. قرارداد پایدار تضمین نشده و حتی افزایش MINOR می‌تواند ناسازگار باشد. اگر مصرف کننده یک کتابخانه زیر 1.0.0 هستید، بازه های وابستگی را محافظه کارانه انتخاب کنید. اگر نگهدارنده هستید، به محض پایداری API به 1.0.0 مهاجرت کنید تا انتظارات سازگاری روشن شود.

وابستگی ها و محدوده نسخه در مدیرهای پکیج

بسیاری از اکوسیستم ها از SemVer برای تفسیر محدوده ها استفاده می‌کنند. چند نمونه رایج:

npm و Node.js

{
  "name": "my-lib",
  "version": "1.4.2",
  "dependencies": {
    "lodash": "^4.17.21",
    "dayjs": "~1.11.9"
  }
}
  • ^1.4.2: به صورت خودکار PATCH و MINOR را می‌پذیرد تا قبل از 2.0.0.
  • ~1.11.9: فقط PATCH را می‌پذیرد تا قبل از 1.12.0.
  • برای 0.x رفتار محافظه کارانه تر است؛ مثلا ^0.3.4 معمولا فقط PATCH را می‌پذیرد.

Composer و PHP

در Composer نسخه بسته ها معمولا از تگ های گیت خوانده می‌شود. محدوده ها مشابه npm هستند:

{
  "require": {
    "guzzlehttp/guzzle": "^7.0",
    "monolog/monolog": "~2.9"
  }
}

Go Modules

require example.com/lib v1.4.2
  • تگ ها باید به شکل vX.Y.Z باشند.
  • برای نسخه 2 و بالاتر، مسیر ماژول باید با /v2، /v3 و… منطبق شود.

Python

Python از PEP 440 پیروی می‌کند، نه دقیقا SemVer؛ اما بسیاری از پروژه ها با SemVer هماهنگند. نمونه در pyproject.toml:

[project]
name = "my-lib"
version = "1.4.2"
dependencies = [
  "requests>=2.31,<3.0",
]

برای هر اکوسیستم راهنمای رسمی را بخوانید تا جزئیات تفسیر محدوده ها را درست اعمال کنید.

گردش کار پیشنهادی برای انتشار

  1. جمع بندی تغییرات: فهرست Pull Request ها یا کامیت ها را بررسی کنید و تاثیر هر مورد بر API عمومی را بسنجید.
  2. تعیین نوع افزایش: بر اساس معیار MAJOR/MINOR/PATCH تصمیم بگیرید.
  3. به روز کردن نسخه در فایل های پروژه: مانند package.json، pyproject.toml یا فایل های همتا.
  4. به روز کردن CHANGELOG: تغییرات کاربر محور را خلاصه کنید. اگر از الگوی Conventional Commits استفاده می‌کنید، تولید خودکار را در CI تنظیم کنید.
  5. اجرای تست ها و بررسی سازگاری: اجرای تست واحد، ادغام و در صورت نیاز تست های قرارداد.
  6. برچسب گذاری گیت و ساخت بسته:
# تعیین نسخه جدید
export VERSION=1.5.0

# به روز کردن فایل ها و کامیت
git add .
git commit -m "chore(release): $VERSION"

# تگ گذاری و انتشار تگ
git tag "v$VERSION"
git push origin main --tags
  1. انتشار در رجیستری و اعلان: بسته را منتشر و یادداشت انتشار را در دسترس بگذارید.

چگونه درستی نسخه را بررسی کنیم؟

  • اعتبارسنجی با الگو: از یک عبارت منظم سازگار با SemVer 2.0.0 استفاده کنید.
^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)
(?:-(?:0|[1-9A-Za-z-][0-9A-Za-z-]*)
(?:\.(?:0|[1-9A-Za-z-][0-9A-Za-z-]*))*)?
(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$
  • بررسی ترتیب: اطمینان بگیرید نسخه جدید از قبلی بزرگ تر است و از کاهش تصادفی نسخه جلوگیری کنید.
  • اجرای تست سازگاری: اگر کتابخانه منتشر می‌کنید، با پروژه های نمونه یا قراردادهای API سازگاری را بسنجید.

اشتباهات رایج و راه حل

  • بالا بردن MINOR با وجود شکست سازگاری: این کار مصرف کنندگان را غافلگیر می‌کند. اگر تغییر ناسازگار دارید، MAJOR را افزایش دهید یا مسیر مهاجرت فراهم کنید.
  • استفاده از تاریخ یا شمارنده دلخواه برای کتابخانه های عمومی: برای مصرف ماشینی مناسب نیست. اگر نیاز به تاریخ دارید، آن را در متادیتا قرار دهید.
  • گنجاندن v در مقدار نسخه فایل پروژه: مقدار نسخه باید X.Y.Z باشد؛ از v فقط در تگ گیت استفاده کنید.
  • ویرایش نسخه منتشر شده: پس از انتشار نسخه را دوباره استفاده نکنید. اگر مشکل داشتید، نسخه بعدی را منتشر کنید و مستند کنید.
  • بسته بندی پیش نسخه در وابستگی های تولید: تا حد ممکن از پیش نسخه ها در محیط تولیدی اجتناب کنید مگر اینکه آگاهانه مدیریت شوند.
  • نادیده گرفتن 0.x: در 0.x انتظار سازگاری نداشته باشید. اگر API پایدار شد، به 1.0.0 مهاجرت کنید.

چه زمانی SemVer بهترین انتخاب نیست؟

برای پژوهش های سریع، نمونه اولیه یکبار مصرف، یا اپلیکیشن های کاملا داخلی که کاربر خارجی ندارند، ممکن است SemVer ارزش سربار نداشته باشد. همچنین برخی سیستم های بسته بندی سیستم عامل از قواعد نسخه گذاری متفاوتی استفاده می‌کنند که باید رعایت شوند. در چنین مواردی، سیاست نسخه گذاری را با نیاز مصرف کننده واقعی هماهنگ کنید.

یک سناریو عملی تصمیم گیری

فرض کنید کتابخانه ای دارید با نسخه 1.8.2. تغییرات زیر آماده انتشار است:

  • افزوده شدن متد جدید اختیاری برای فیلتر نتایج.
  • رفع باگ حافظه در تابع parse.
  • حذف فیلد deprecated از پاسخ API.

تحلیل:

  • افزودن متد جدید: MINOR.
  • رفع باگ: PATCH.
  • حذف فیلد موجود: شکست ناسازگاری، پس MAJOR.

نتیجه نهایی: 2.0.0 (نه 1.9.0 و نه 1.8.3). اگر بخواهید ابتدا به مشتریان فرصت مهاجرت دهید، می‌توانید موقتا هر دو فیلد قدیم و جدید را نگه دارید و هشدار دهید، سپس در انتشار بعدی MAJOR را اعمال کنید.

گام بعدی چیست؟

یک فایل سیاست نسخه گذاری در مخزن خود اضافه کنید، آستانه های MAJOR/MINOR/PATCH را با مثال مشخص کنید، تولید CHANGELOG و برچسب گذاری گیت را در CI خودکار کنید و از همین انتشار بعدی به SemVer پایبند بمانید. این کار پیش بینی پذیری پروژه شما را بالا می‌برد و هزینه نگهداری مصرف کنندگان را کاهش می‌دهد.