فرهنگ و ساختار نسخه‌دهی در تیم‌های نرم‌افزاری (بخش اول)

مقدمه:
فرهنگ و ساختار نسخه‌دهی توی تیم‌های نرم‌افزاری، با اینکه پیشینه طولانی داره و نسل اولش به دهه‌های ۶۰ و ۷۰ و میلادی برمی‌گرده و حتی ابزارهای مدرن‌ترش مثل git توی بیست‌سالگی‌شون به سر می‌برن؛ ولی کماکان موضوعی مهم و اثرگذار روی تیم‌هاست. و البته کم نیستن تیم‌هایی که با انتخاب روش اشتباه یا نپرداختن به فرهنگی که بهینه نیست؛ کارآمدی لازم رو ندارن؛ دائما با شلختگی زمانی، یا نارضایتی از کیفیت و… دست‌وپنجه نرم می‌کنن.
این مطلب در مورد انتخاب ساختار، فرایند و چرخه‌ی نسخه‌دهی و ریلیز نرم افزار است. قرار هم نیست نسخه‌ی واحد صادر کنیم؛ اینکه گوگل از مونوریپو پیروی می‌کنه؛ دلیل نمیشه مونوریپو گزینه مناسبی برای همه است. توی این پست قراره ببینیم «چی» در پاسخ به «چه» نیازی به وجود اومده و اینکه گزینه مناسب تیم و شرکت شما چیه؛ یک انتخاب مبتنی بر شناخت و بلوغ فنی خواهد بود.

این مطلب به بهانه سوال یه دوست کانال شروع شد: فرهنگ و ساختار نسخه‌دهی توی تیم‌های بزرگ چطوره؟ مخصوصاً وقتی سیستم monolithic یا microservice داری؟

این نکته هم مهمه که قبل از شروع، منظورمون از چند مفهوم رو با هم مرور کنیم، چون یکی از سوءتفاهم‌های رایج اینه که merge، deploy و release رو یکی می‌گیریم. در تیم‌های بالغ، این سه می‌تونن جدا باشن: کد merge می‌شه، artifact deploy می‌شه، اما feature با flag یا rollout policy بعداً برای کاربران release می‌شه. پس:

– branching strategy یعنی مسیر تغییر کد
– release strategy یعنی تصمیم اینکه چه چیزی، کی، برای چه کسی فعال بشه
– deployment strategy یعنی رسوندن artifact به محیط
versioning strategy یعنی نام‌گذاری و compatibility نسخه‌ها

اینکه فرهنگ و ساختار نسخه‌دهی توی تیم‌های موفق چطوره؟ سوال ساده‌ای به نظر می‌رسه؛ ولی وقتی عمیق‌تر نگاه کنیم، می‌بینیم که زیرلایه‌های متعددی داره. چون «نسخه‌دهی» صرفاً یه تصمیم ابزاری نیست؛ ساختار repository، مدل branching، سیاست merge، چرخه‌ی deploy و حتی فرهنگ تیم، همه با هم در تعامل‌اند و اثرشون رو روی هم می‌ذارن.

توی این مطلب، ابتدا از ساختار repository شروع می‌کنیم؛ یعنی مونوریپو، مالتی‌ریپو و میکروریپو. و می‌بینیم که هر کدوم از کجا اومدن و چه مسئله‌ای رو حل می‌کنن. بعد به مدل‌های branching می‌رسیم؛ یعنی Gitflow، GitHub Flow، GitLab Flow، Trunk-Based Development و اینکه کدوم برای چه زمینه‌ای طراحی شده. در نهایت می‌بینیم که این دو دسته تصمیم چطور باید با هم هم‌راستا بشن؛ چون یه مونوریپو با Gitflow می‌تونه نتیجه‌ای کاملاً متفاوت داشته باشه نسبت به مونوریپو با Trunk-Based.

هدفم از این مطلب اینه که بعد از خوندنش؛ انتخاب‌تون از روی تشخیص؛ یا حداقل متشکل از پرسش‌های دقیق‌تری باشه، نه تقلید.

ساختار Repository؛ از کجا اومدیم، کجا هستیم

قبل از اینکه بپرسیم «کدوم بهتره»، باید بپرسیم «هر کدوم برای حل چه مسئله‌ای به وجود اومد». این سوال رو جدی بگیریم؛ چون اکثر اختلاف‌نظرها در این حوزه از اینجا ناشی میشه که آدم‌ها جواب رو مقایسه می‌کنن، بدون اینکه سوال‌هاشون یکی باشه.


۱: Monorepo: یه خونه، همه چیز زیر یه سقف

خیلی از سازمان‌های بزرگ در گذشته، بخش بزرگی از کدهاشون رو در یک source tree یا depot مرکزی نگهداری می‌کردن، اما monorepo به معنای مدرن امروزی، مخصوصاً با build graph، ownership tooling، affected testing و CI هوشمند، یک انتخاب مهندسی‌شده‌تر است. در نتیجه، مدلی رایج بود که شباهت‌هایی به مونوریپو داشت. وقتی توی دهه‌های ۷۰ و ۸۰ میلادی شرکت‌های بزرگ مثل IBM شروع به نوشتن نرم‌افزار کردن، طبیعی‌ترین کار این بود که همه کد توی یه مخزن واحد باشه. چیزی به اسم «سرویس جداگانه» یا «تیم مستقل» هنوز مفهوم رایجی نبود.

اما جالبه که گوگل، بخش‌هایی از Microsoft و Meta/Facebook historically از مدل‌های نزدیک به monorepo یا large-scale repository استفاده کرده‌اند، اما هر کدوم tooling و فرهنگ متفاوتی دارند.گاهاً با وجود اینکه به خوبی می‌تونستن سوئیچ کنن؛ همچنان به مونوریپو یا پیاده‌سازی‌های مشابهش، در برخی بخش‌ها وفادار موندن. دلیلش نه اینرسی، بلکه یه مزیت واقعی بود که با بزرگ‌شدن تیم بیشتر احساس می‌شه: visibility. وقتی همه کد یه جاست، تغییر در یه کامپوننت بلافاصله در همه‌جا قابل ردیابیه. refactoring سراسری امکان‌پذیره. dependency مخفی وجود نداره.

البته گوگل برای اینکه مونوریپو با این ابعاد کار کنه، مجبور شد ابزار بسازه؛ Blaze که بعداً به شکل Bazel open-source شد. این خودش یه نکته مهمه: مونوریپو در مقیاس بزرگ، هزینه ابزارسازی داره؛ گوگل صدها ابزار ساخته تا بتونه از پس حجم عظیم کدهاش بربیاد و توی مقیاس بزرگ دچار مشکل نشه. یا مایکروسافت برای نگهداری کدبیس عظیم ویندوز؛ یعنی حدود ۳.۵ میلیون فایل و تقریباً ۳۵۰ گیگابایت سورس‌کد، و ۴۰۰۰ مهندس، روی git فایل‌سیستم خودش به نام VFSForGit رو نوشت تا بتونه مشکلات کار روی یک کدبیس عظیم رو حل کنه؛ و…

مزایا:

  • یه جای واحد برای همه کد، تست و پیکربندی
  • امکان refactoring اتمیک: می‌تونی یه interface رو در همه مصرف‌کننده‌ها یکجا عوض کنی
  • امکان versioning ضمنی: همه چیز روی یه commit واحده، دردسر dependency hell کمتره
  • مزیت onboarding ساده‌تر؛ در monorepoهای کوچک و متوسط، onboarding می‌تونه ساده‌تر باشه. اما در مقیاس enterprise، onboarding فقط وقتی ساده می‌مونه که sparse checkout، virtual file system، build graph و مستندات خوب داشته باشین.

معایب:

  • با بزرگ‌شدن، build time و CI time اگر درست مدیریت نشه، مشکلات سر به فلک می‌کشن
  • وضعیت ownership مبهم‌تره؛ مرز تیم‌ها باید از طریق convention و tooling حفظ بشه، نه ساختار فیزیکی
  • برای تیم‌هایی که tempo و چرخه release متفاوتی دارن، چالش‌برانگیزه

یادمون نره توی مونوریپو فقط build tool خوب کافی نیست. این‌ها هم مهم‌ هستن:

  • CODEOWNERS
  • path-based ownership
  • affected test/build
  • dependency graph
  • architectural fitness functions
  • lint rules
  • module boundaries
  • incremental build
  • remote cache
  • selective CI
  • release ownership

بدون این‌ها monorepo واقعاً می‌تونه تبدیل به «انبار آشغال» بشه!


ب: Multirepo : هر تیم، خونه‌ی خودش

مالتی‌ریپو در دوره‌ای پررنگ‌تر شد که تیم‌های مستقل و SOA و بعدتر، microservice داشتن رایج می‌شدن؛ دهه ۲۰۰۰ به بعد. ایده اصلی اینه که autonomy تیم‌ها باید از ساختار repository هم خوندنی باشه. هر سرویس، هر کتابخانه، هر دامنه؛ مخزن مستقل خودش رو داره.

این مدل برای تیم‌هایی که می‌خوان مستقل deploy کنن، مستقل test بزنن و با tempo خودشون کار کنن، جذابه. GitHub و GitLab خودشون روی این مدل کار می‌کنن؛ طنز ماجرا اینه که ابزارهایی که مونوریپو رو آسون‌تر کردن، خودشون مالتی‌ریپو هستن.

در multirepo مشکل فقط coordination (هماهنگی بین تیم‌ها یا ریپوها) نیست. مشکل اصلی در مقیاس پیش میاد که استانداردها خیلی متفاوت می‌شن. و در محیط‌های انترپرایز باید موارد زیر رو خیلی جدی‌تر گرفت:

  • repo templates
  • shared CI templates
  • dependency update automation مثل Renovate/Dependabot
  • service catalog
  • golden paths
  • centralized security scanning
  • automated ownership metadata

مزایا:

  • مرز مالکیت شفاف؛ هر ریپو، یه تیم یا دامنه مشخص
  • آزادی در انتخاب زبان، framework و چرخه release
  • پیاده‌سازی build و CI هر سرویس ایزوله‌ست

معایب:

  • تغییرات cross-repo دردناکه؛ یه breaking change توی یه shared library می‌تونه coordination چندین تیم بخواد
  • باید versioning صریح باشه؛ dependency management پیچیده‌تره
  • نرخ discoverability پایین‌تره؛ فهمیدن اینکه «چی کجاست» برای تازه‌واردها سخت‌تره

ج: Microrepo : مالتی‌ریپو، رادیکال‌تر

اگر مالتی‌ریپو رو خیلی پیش ببریم، به میکروریپو می‌رسیم؛ یه ریپو به ازای هر سرویس، هر function، حتی هر lambda. این مدل در دوران رونق serverless و Function-as-a-Service بیشتر دیده می‌شه.
Microrepo بیشتر یک anti-pattern محتمل یا نتیجه طبیعی granularity افراطیه، مگر اینکه automation، templates، catalog، ownership metadata، dependency update automation و CI standardization قوی وجود داشته باشد.

به بیان ساده‌تر، اکثر تیم‌هایی که این مسیر رو می‌رن، «مالتی‌ریپو» صداش می‌کنن و فقط granularity‌شون بالاست. مشکلات مالتی‌ریپو اینجا تشدید میشه: هماهنگی بین تغییرات، نگهداری هزاران ریپوی کوچیک، و overhead ابزاری که می‌تونه از خود کار بیشتر وقت بگیره.


مقایسه؛ نه برنده، بلکه تناسب

MonorepoMultirepoMicrorepo
مناسب برایتیم‌های تنیده به‌هم، پلتفرم‌های یکپارچهتیم‌های مستقل، microserviceسرویس‌های بسیار کوچک، FaaS
ownershipConvention-basedساختاریساختاری، ولی پراکنده
refactoringاتمیک و سراسریدردناک در cross-repoخیلی دردناک
CI/CDباید هوشمند باشهایزوله و ساده‌ترایزوله ولی پرتعداد
onboardingیه cloneیادگیری نقشه‌ی ریپوهاگیج‌کننده
هزینه اصلیtooling در مقیاسcoordination در تغییرات مشترکoverhead نگهداری

یه نکته مهم قبل از رفتن سراغ branching: انتخاب ساختار ریپو و انتخاب مدل branching دو تصمیم مستقل‌اند، ولی باید با هم هم‌راستا باشن. یه مونوریپو با Gitflow کلاسیک می‌تونه خروجی کاملاً متفاوتی داشته باشه تا همون مونوریپو با Trunk-Based Development. این رو توی بخش بعدی می‌بینیم.

مدل‌های Branching؛ هر کدوم جواب یه سوال متفاوت‌اند

اگر ساختار repository جواب «کد کجا زندگی می‌کنه» رو میده، مدل branching جواب «کد چطور تغییر می‌کنه» رو. و این دومی معمولاً همونجاییه که تیم‌ها بیشتر گیر می‌کنن؛ نه به خاطر پیچیدگی فنی، بلکه به خاطر اینکه مدل اشتباه رو توی زمینه اشتباه به کار می‌برن.


مدل Gitflow؛ مهندسی برای release از پیش برنامه‌ریزی‌شده

مدل Gitflow رو Vincent Driessen در سال ۲۰۱۰ معرفی کرد؛ یه مقاله وبلاگی که به سرعت تبدیل به یه استاندارد غیررسمی شد. زمینه‌ای که توش طراحی شد مهمه: نرم‌افزارهایی که چرخه release مشخص دارن؛ نسخه ۱.۰، نسخه ۱.۱، نسخه ۲.۰؛ و باید چند نسخه رو همزمان نگه دارن.

ساختارش اینه:

  • main: همیشه وضعیت production رو نشون میده
  • develop: پایه یکپارچه‌سازی؛ feature‌ها اینجا merge میشن
  • feature/*: برای هر feature جدید از develop می‌شکنه
  • release/*: وقتی develop آماده release‌ست، این branch برای QA و bugfix نهایی بازه
  • hotfix/*: مستقیم از main برای باگ‌های اورژانسی production

این مدل برای نرم‌افزار desktop، SDK، کتابخونه‌های open-source یا هر چیزی که versioning صریح داره خوب کار می‌کنه. مشکل اینجاست که خیلی از تیم‌ها این مدل رو بدون در نظر گرفتن زمینه‌شون import کردن؛ تیم‌هایی که روزانه deploy می‌کنن و نسخه ۱.۲.۳ براشون معنی نداره.

Driessen خودش سال‌ها بعد یه یادداشت به همون مقاله اضافه کرد: اگر دارید یه web application می‌سازید که continuous delivery دارید، این مدل شاید برای شما نباشه.

Gitflow وقتی ارزش داره که branchهای بلندمدت واقعاً نماینده نسخه‌های پشتیبانی‌شده باشن. اگر release branch فقط تبدیل به صف انتظار QA بشه، احتمالاً داریم complexity رو به جای مدیریت کیفیت استفاده می‌کنیم.

مناسب برای: نرم‌افزارهایی با چرخه release مشخص، چند نسخه موازی، یا نیاز به hotfix مستقل از develop.

نامناسب برای: تیم‌هایی که می‌خوان روزانه یا چند بار در روز deploy کنن.


مدل GitHub Flow؛ ساده، خطی، برای continuous delivery

مدل GitHub Flow در ۲۰۱۱ توسط Scott Chacon از GitHub معرفی شد؛ در واکنش به اینکه Gitflow برای تیم‌های continuous deployment بیش از حد پیچیده‌ست.

قانون‌هاش ساده‌اند:

  • main معمولا deployable‌ است. در حالت ایده‌آل merge به main معمولاً مسیر deploy رو فعال می‌کنه، اما اصل حیاتی اینه که main همیشه deployable بمونه، نه اینکه الزاماً هر merge فوراً برای همه کاربران release بشه.
  • هر تغییر روی یه feature branch با عمر کوتاه‌ اتفاق می‌افته
  • وقتی تغییرات آماده‌ست، Pull Request باز میشه
  • بعد از review و تایید، مستقیم به main merge میشه و deploy میشه

همین. branch دیگه‌ای وجود نداره.

این سادگی، عمدیه. ایده اصلی اینه که اگر main همیشه سالمه و deploy آسونه، نیازی به لایه‌های اضافه‌ی abstraction نداری. تمام هزینه‌ی coordination که Gitflow می‌داد؛ release branch، develop، merge در دو جهت؛ اینجا حذف میشه.

ولی این سادگی یه پیش‌فرض داره: infrastructure خوب. اگر CI/CD محکمی نداری، اگر feature flag نداری، اگر نمی‌تونی سریع rollback کنی، GitHub Flow می‌تونه به جای سادگی، آشوب بیاره.

مناسب برای: تیم‌های web با continuous delivery، feature flag، و CI قوی.

نامناسب برای: نرم‌افزارهایی که باید چند نسخه را همزمان support کنن.


مدل GitLab Flow؛ جایگزین pragmatic برای دو مدل قبلی

مدل GitLab Flow در ۲۰۱۴ توسط تیم GitLab معرفی شد. ایده اینه که GitHub Flow خیلی ساده‌ست و Gitflow خیلی پیچیده؛ و یه رویکرد واقع‌بینانه‌تر باید environment رو هم در نظر بگیره.

دو انشعاب اصلی داره:

مدل Environment-based: به جای release branch، branch به ازای هر محیط دارید؛ مثلاً main، staging، production. کد از main به staging و از staging به production جریان پیدا می‌کنه. merge همیشه یه‌طرفه‌ست؛ downstream؛ که merge conflict رو به حداقل می‌رسونه. environment branch اگر به معنی «کپی‌های جداگانه و واگرا از کد» باشه، خطرناکه! GitLab Flow زمانی سالم‌تره که جریان کد یک‌طرفه، قابل ردیابی، و تا حد ممکن upstream-first بمونه. اگر branch به ازای environment دارید، مراقب باشید environmentها به نسخه‌های متفاوت و غیرقابل ردیابی تبدیل نشن. در مورد خیلی از سیستم‌ها بهتره artifact یک بار build بشه و همون artifact بین environmentها promote بشه.

مدل Release-based: شبیه‌تر به Gitflow ولی بدون develop؛ مستقیم از main برای هر release یه branch می‌زنید و hotfix‌ها هم اول به main میرن، بعد cherry-pick میشن به release branch.

یه اصل کلیدی GitLab Flow اینه: «upstream first»؛ باگ رو اول توی main رفع کن، بعد به branch‌های release یا environment ببر. این از divergence جلوگیری می‌کنه.

مناسب برای: تیم‌هایی که چند environment دارن یا نیاز به release branch دارن ولی نمی‌خوان پیچیدگی کامل Gitflow رو تحمل کنن.


مدل Trunk-Based Development؛ جدی‌ترین رویکرد برای تیم‌های سریع

کار روی mainline یا trunk سابقه طولانی‌تری از خود Git داره، اما اصطلاح و چارچوب امروزی Trunk-Based Development با رشد Continuous Integration و Continuous Delivery دوباره پررنگ شد. پس «مدل» TBD رو به نوعی میشه قدیمی‌ترین مدل از نظر تاریخی دونست؛ در شرکت‌هایی مثل Google و Facebook سال‌هاست که به کار میره، ولی به عنوان یه مدل مستقل و مستند، در دهه ۲۰۱۰ با محبوب شدن مفاهیمی مثل DevOps و continuous delivery بیشتر دیده شد.

ایده اصلی رادیکاله: همه روی یه branch کار می‌کنن یعنی همه روی یک branch اصلی کار می‌کنن: trunk، main یا master. ولی Feature branch‌ها اگر وجود دارن، عمر کوتاهی دارن؛ نهایتاً یکی دو روز، و بعد merge میشن.

این چطور ممکنه؟ با سه ابزار:

  • Feature Flag: کد مربوط به یه feature ناتموم می‌تونه merge بشه، ولی پشت یه flag غیرفعال باشه. deploy مستقل از release میشه.
  • Branch by Abstraction: وقتی می‌خوای یه component بزرگ رو جایگزین کنی، یه abstraction layer می‌ذاری که implementation قدیم و جدید رو جدا نگه داره.
  • CI سریع و قوی: چون همه روی trunk کار می‌کنن، اگر CI کند باشه یا عملکرد ضعیفی داشته باشه، trunk دائماً با شکسته‌ روبرو می‌شه. این مدل، CI رو از یه nice-to-have به یه ضرورت ذاتی تبدیل می‌کنه.

مزیت اصلیش اینه که merge conflict به حداقل می‌رسه، چون branch‌ها عمر کوتاهی دارن، و تیم دائماً یه دید مشترک از وضعیت کد داره.

هزینه‌اش هم مشخصه: نیاز به بلوغ فنی و فرهنگی داره. feature flag باید managed بشه. هر کسی که commit می‌کنه باید مطمئن باشه trunk رو نمی‌شکنه. نیاز به تست جامع و اصولی، CI سریع، feature flag hygiene، discipline جدی است.

مناسب برای: تیم‌های با CI/CD قوی، فرهنگ تست قوی، و نیاز به سرعت deploy بالا.

نامناسب برای: تیم‌هایی که infrastructure آماده ندارن یا نرم‌افزاری با نسخه‌بندی صریح می‌سازن.


مقایسه — یه نگاه کلی

GitflowGitHub FlowGitLab FlowTBD
پیچیدگیبالاپایینمتوسطپایین (ولی با پیش‌نیاز)
سرعت deployکمزیادمتوسطخیلی زیاد
چند نسخه موازیبه‌صورت اصلی نه، مگر با release branches محدود
پیش‌نیاز CI/CDپایینمتوسطمتوسطخیلی بالا
ریسک merge conflictبالامتوسطکمریسک متنی: کم

ریسک معنایی: همچنان وجود داره
مناسب microserviceکمتربلهبلهبله
مناسب monolithicبلهبا احتیاطبلهبا آمادگی

حالا که هر دو بُعد، ساختار ریپو و مدل branching، رو بررسی کردیم، می‌رسیم به بخشی که شاید مهم‌ترین قسمت این مطلب باشه: این دو تصمیم چطور باید با هم ترکیب بشن؛ و اینکه به ازای معمول‌ترین زمینه‌ها، monolith، microservice، چند تیم، یه تیم، کدوم ترکیب منطقی‌تره. در بخش دوم این مطلب؛ به این موضوع می‌پردازم و نکات مهم رو جمع‌بندی می‌کنم…

دیدگاهتان را بنویسید