تغییرات اخیر

در اینجا اطلاعیه‌ها، نسخه‌ها و تغییرات جدید لیارا فهرست می‌شوند.

چگونه کد تولیدی را به یک مونو ریپو منتقل کنیم؟


۳۰ اسفند ۱۴۰۳

در دنیای توسعه نرم‌افزار، مدیریت چندین مخزن جداگانه برای پروژه‌های مختلف می‌تواند چالش برانگیز باشد. هماهنگ‌سازی وابستگی‌ها، به‌روزرسانی‌های هم‌زمان و یکپارچه‌سازی تغییرات در این شرایط زمان‌بر و پیچیده است. به همین دلیل، بسیاری از تیم‌های توسعه به استفاده از مونوریپو روی آورده‌اند؛ روشی که در آن تمام پروژه‌های مرتبط در یک مخزن واحد نگهداری می‌شود.

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

در ادامه خواهید خواند:

  • مونو ریپو چیست؟
  • رویکرد: انتقال به یک مونو ریپو
  • پیش از مهاجرت: آسان‌سازی تغییر
  • مهاجرت: قرار دادن برنامه‌ها در یک مکان مشترک
  • پس از مهاجرت: بهینه‌سازی مونو ریپو
  • جمع بندی

مونو ریپو چیست؟

یک مونو ریپو (Monorepo) مجموعه‌ای از بسته‌های مستقل است که همگی در یک مخزن کد واحد نگهداری می‌شوند. این روش به تیم‌های توسعه اجازه‌ می‌دهند تا کدهای اشتراکی را راحت‌تر مدیریت کنند.این روش باعث می‌شود که تیم‌های توسعه بتوانند کدهای اشتراکی را راحت‌تر مدیریت کنند، درحالی‌که ایمنی و استقلال هر بسته نیز حفظ می‌شود.

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

مزایای استفاده از مونو ریپو

  • مدیریت ساده‌تر وابستگی‌ها: همه بسته‌ها در یکجا قرار دارند. بنابراین به‌روزرسانی و مدیریت وابستگی‌ها راحت‌تر انجام می‌شود.
  • همکاری بهتر بین تیم‌ها: توسعه‌دهندگان می‌توانند کدهای اشتراکی را بهینه کنند، بدون اینکه نیاز به هماهنگی پیچیده بین مخازن متعدد باشد.
  • امکان استقرار مستقل:هر بسته می‌تواند جداگانه اجرا و منتشر شود، که به انعطاف پذیری بیشتر در توسعه نرم‌افزار کمک می‌کند.
  • یکپارچگی بیشتر و هماهنگی در کد: استانداردهای کدنویسی، تست‌ها و CI/CD در کل پروژه یکسان باقی می‌ماند.
مونو ریپو

رویکرد: انتقال به یک مونو ریپو

ما طرفدار نقل قول معروف کت بک در مورد بازسازی کد هستیم،”ابتدا تغییر را آسان کنید (البته این کار ممکن است دشوار باشد)، سپس تغییر را آسان انجام دهید.”

در اصل، یک مونو ریپو به معنای هم‌مکانی کدها (Code Colocation) است. بنابراین، مهاجرت ما به مونو ریپو فقط به این بخش محدود شد؛ یعنی هیچ تغییری در عملکرد اپلیکیشن‌ها ایجاد نشد، بلکه آن‌ها صرفا در کنار یکدیگر قرار گرفتند. هر تغییری که در اپلیکیشنی نیاز بود، قبل از انتقال به مونو ریپو و زمانی که هنوز در مخزن مستقل خود قرار داست، اعمال می‌شد. به این ترتیب، مشکلات مرتبط با هم‌مکانی به حداقل می‌رسید.

اپلیکیشن‌های ما در طی حدود سه سال توسعه یافته‌بودند. در بسیاری از موارد، درس‌هایی که از اپلیکیشن‌های جدیدتر آموخته می‌شد، روی اپلیکیشن‌های قدیمی‌تر اعمال نمی‌شد. این امر باعث ناهماهنگی‌هایی شد که پیچیدگی هم‌مکانی کدها را افزایش داد و نیاز به چرخه‌های بازسازی کد (Refactoring) را ایجاد کرد. در روند انتقال، هر اپلیکیشن باید بتواند:

  • محیط توسعه محلی را اجرا کند.
  • تست‌ها، ابزارهای بررسی کد (Linters) و افزونه‌های IDE را اجرا کند.
  • پایپ‌لاین‌های CI/CD را اجرا کند.
  • در محیط آزمایشی (Staging) مستقر شود.

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

در این مقاله پروژه به سه مرحله تقسیم خواهد شد، البته برخی از اقدامات پیش از مهاجرت در حین انجام پروژه مشخص خواهند شد.

  • پیش از مهاجرت: آسان کردن تغییر
  • مهاجرت: هم‌مکان‌سازی اپلیکیشن‌ها
  • پس از مهاجرت: بهینه‌سازی مونو ریپو

پیش از مهاجرت: آسان‌سازی تغییر

پیش از شروع فرآیند مهاجرت به مونو ریپو، لازم است زیرساخت‌های لازم را برای یک انتقال بدون مشکل فراهم کنیم.

اسکریپت‌نویسی

خودکارسازی را به عنوان اصل راهنما در نظر گرفته شده است؛ هر تغییری باید از طریق یک اسکریپت اجرا شود تا امکان بازتولید آن از ابتدا فراهم باشد. در این فرآیند از zx استفاده شده است تا بتوان هم از Node.js و هم از ابزارهای خط فرمان (CLI) در یک اسکریپت بهره برد. هر بار که مشکلات از طریق بازسازی کد حل شد، اسکریپت و فایل‌های الگو (که ساختار آینده مونو ریپو را شبیه‌سازی می‌کردند) را به‌روزرسانی کرده و مجددا اجرا شده است. این اسکریپت در طی فرآیند توسعه صد باز اجرا شد و در روز مهاجرت نهایی، از بروز خطای انسانی جلوگیری کرد.

این اسکریپت از یک مخزن خارجی اجرا می‌شود تا در اثر فوس پوش‌ها بازنویسی نشود و مراحل زیر را انجام دهد.

  • مقدمه‌سازی گیت: یک مونو ریپوی موقت ایجاد شد.
  • کلون کردن مخازن: هر مخزن در یک پوشه‌ی موقت کلون شد.
  • حذف فایل‌های غیرضروری: فایل‌هایی که پس از مهاجرت بی‌اهمیت می‌شدند، مانند yarn.lock و .nvmrc حذف شدند.
  • ایجاد یک کامیت انتقالی: تمامی فایل‌ها در مسیرهای مناسب در فضای کاری جدید قرار گرفتند.
  • ادغام تاریخچه‌ی مستقل مخازن: تاریخچه‌ی گیت از ریموت‌های محلی ترکیب شد.
  • کپی فایل‌های الگو در مونو ریپو: قالب‌های موردنیاز اضافه شدند.
  • فورس پوش مخزن: در نهایت، مخزن جدید روی ریموت فورس پوش شد.
process.env.FORCE_COLOR = '1';

import { $, path, os, cd, spinner } from 'zx';



const SCRIPT_ROOT = path.resolve(__dirname);

const MONOREPO = path.join(os.tmpdir(), `monorepo-${Date.now()}`);

const REPO_PREFIX = 'git@github.com:username/';

const REPO_SUFFIX = '.git';

// repo names to fetch from Github

const REPOS = ['repo-a', 'repo-b'];



// 1. Initialize git in monorepo

cd(MONOREPO);

await $`mkdir -p ${MONOREPO}/apps/`;

await $`git init`;

await $`git commit --allow-empty -m "Initial commit"`;

cd(SCRIPT_ROOT);



// Merge git histories loop

for await (const repo of REPOS) {

  const repoUrl = `${REPO_PREFIX}${repo}${REPO_SUFFIX}`;

  const tempRepo = path.join(os.tmpdir(), `${repo}-${Date.now()}`);



  // 2. Clone the app into a temporary folder

  await $`mkdir -p ${tempRepo}`;

  await $`git clone ${repoUrl} ${tempRepo}`;

  cd(tempRepo);



  // 3. Remove these files and folders because they're no longer necessary and it speeds up this script

  await $`rm -f .gitignore .gitattributes .github .nvmrc yarn.lock node_modules .yarn build`;

  // try…catch so non-zero exit codes don't stop the script from continuing

  try {

    await $`git add .`;

    await $`git diff --staged --quiet || git commit -m "[${repo}]: Remove conflicting files" --no-verify`;

  } catch {}



  // 4. Create a move commit

  // In order to preserve git history accurately, we need to create a

  // move commit from the root of the sub-repo into a directory that

  // imitates the monorepo ie. from ./ to ./apps/

  const mainBranch = (await $`git branch --show-current`).stdout.trim();

  await $`mkdir -p apps/${repo}`;

  await $`git ls-tree ${mainBranch} --name-only | xargs -I{} git mv {} apps/${repo}`;

  await $`git commit -m "[${repo}]: Move ${repo} to app/${repo}"`;



  cd(MONOREPO);



  // 5. Merge git history using local remote so changes wouldn't break live codebases

  await $`git remote add ${repo} ${tempRepo}`;

  await $`git fetch ${repo}`;

  await $`git merge --allow-unrelated-histories ${repo}/main`;

  await $`git remote rm ${repo}`;

}



// 6. Copy template files

cd(SCRIPT_ROOT);

await $`cp -a monorepo-template/. ${MONOREPO}`;

cd(MONOREPO);



// Create fresh yarn.lock, yarn install exits with non-zero

try {

  await $`yarn install --refresh-lockfile`;

} catch {}



await $`git add .`;

await $`git commit -m "Init monorepo"`;



// 7. Rebuild the monorepo every time

await $`git remote add origin git@github.com:username/your-new-monorepo.git`;

await spinner(() => $`git push -f origin main`);

console.log('🎉 monorepo is live');

جریان‌های کاری در GitHub Actions

یکی از اولین وظایف، به‌روزرسانی پایپ‌لاین‌های CL/CD برای پشتیبانی از اجرای مخازن تک اپلیکیشنی و چند اپلیکیشنی است. تغییر working-directory به اکشن‌های مشترک پاس داده شد تا هر مرحله از اجرای CL/CD از مسیر مربوط به هر اپلیکیشن اجرا شود. نه از ریشه‌ی مخزن. مقدار پیش‌فرض این متغیر، تعیین شد تا با مخازن قبلی سازگار بماند.

در فرآیند دیپلوی، مقادیر سفارشی مانند app_name و service_id به صورت سخت کد در هر مخزن تعریف شده بودند و این مقادیر به یک فایل جداگانه منتقل شدند و مرحله‌ای برای خواندن آن‌ها افزوده شد تا اکشن‌های ما ژنریک (Generic) شوند.

در فایل‌های الگو، اکشنی طراحی شده که فهرست تغییرات در فضای کاری را تشخیص داده و تنها برای بخش‌های تغییر یافته، جاب‌های مرتبط را اجرا کند. این کار زمان پردازش در GitHub Actions را کاهش داده و از اجرای بیهوده‌ی دیپلوی‌ها و تست‌های End-to-End جلوگیری کرد.

مهاجرت در مونو ریپو

ارتقا به Yarn 4

پس از چند روز تلاش برای رفع تعارض‌های وابستگی بین اپلیکیشن‌ها در Yarn 1، تصمیم گرفته شد که ارتقا به Yarn 4 را به دلیل بهبود پشتیبانی از Workspaces در اولویت قرار گیرد. با تنظیم مقدار nmHoistingLimits روی workspaces، هر اپلیکیشن می‌توانست وابستگی‌های متناقض خود را به‌صورت ایزوله‌شده مدیریت کند.

مهاجرت به Yarn 4 در کل فرآیندی بدون مشکل بود و طبق راهنمای رسمی Yarn پیش رفت. این کار را به دو Pull Request برای هر اپلیکیشن تقسیم شد.

  1. ابتدا وابستگی‌های تعریف‌نشده را طبق قوانین Yarn مشخص شد.
  2. سپس، ارتقا به Yarn 4 را تکمیل شد.

برای هر اپلیکیشن، ابتدا Yarn را به‌صورت محلی ارتقا داده و سپس با استفاده از yarn dlx @yarnpkg/doctor و npx depcheck وابستگی‌های نامشخص را شناسایی شد. پس از تهیه‌ی لیست، آن‌ها را روی یک Branch جدید نصب کردیم تا وابستگی‌های تغییر‌یافته از فرآیند ارتقا به Yarn 4 جدا شوند.

در Yarn 4 نحوه‌ی نصب Yarn به‌طور اساسی تغییر کرده است، بنابراین لازم بود در حل مشکلات تیم هنگام ارتقا کمک کند. اغلب مشکلات مربوط به محل نصب اشتباه Yarn بود. برای بررسی مسیرهای نصب، می‌توان از دستورات زیر استفاده کرد.

which node
# should output something like /Users/you/.nvm/versions/node/v20.9.0/bin/node

# if you're using nvm and you get something else run:

# nvm use



which corepack

# should output something like /Users/you/.nvm/versions/node/v20.9.0/bin/corepack

# if you get something else run:

# corepack enable



which yarn

# should output something like /Users/you/.nvm/versions/node/v20.9.0/bin/yarn

# if you get something else run:

# corepack install

مهاجرت: قرار دادن برنامه‌ها در یک مکان مشترک

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

ابتدا، اطمینان از عملکرد صحیح تمام اپلیکیشن‌ها ضروری است. پیش از آغاز مهاجرت، باید تمامی وابستگی‌ها و پیکربندی‌های پروژه بررسی شوند تا هیچ تضادی در ساختار جدید به وجود نیاید. همچنین، تهیه یک برنامه‌ی دقیق برای مهاجرت و اطلاع‌رسانی به تیم توسعه، از ایجاد مشکلات ناگهانی جلوگیری می‌کند.

در طول فرآیند مهاجرت، تمام مراحل به‌طور شفاف مستند می‌شوند تا در صورت بروز مشکل، امکان بررسی و اصلاح سریع وجود داشته باشد. اجرای نهایی اسکریپت‌های مهاجرت، بازنویسی تاریخچه‌ی مخزن، و فعال‌سازی سیستم‌های CI/CD، از مهم‌ترین گام‌هایی هستند که باید با دقت انجام شوند.

پس از انتقال موفقیت‌آمیز، تست کامل پروژه در محیط staging اهمیت زیادی دارد. اجرای دستی PR CI/CD pipeline و بررسی شاخه‌های جدید، تضمین می‌کند که تمام بخش‌ها به درستی ادغام شده‌اند. همچنین، تنظیمات مربوط به مجوزها، حفاظت از شاخه‌ها و بررسی‌های ادغام باید برای جلوگیری از مشکلات آینده پیکربندی شوند.

در نهایت، مخازن قدیمی بایگانی شده و راهنمای لازم برای توسعه‌دهندگان ارائه می‌شود تا بتوانند به‌راحتی در محیط جدید کار کنند. همچنین، برای کاهش مشکلات احتمالی، می‌توان جلسات پرسش و پاسخ برگزار کرد و به بررسی مشکلات ناشی از انتقال پرداخت.

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

# From the archived repo, rebase your PR commits into a single commit, change the sha prefix to fixup

git rebase main -i



# Run the move commit so all files live within an ./apps/ directory like the monorepo

# This only moves changed files to reduce conflicts + commit noise

APP_NAME=REPLACE_THIS_WITH_YOUR_APP_NAME

for file in $(git diff main --name-only --cached); do target_path=$(dirname $file); mkdir -p "apps/$APP_NAME/$target_path"; git mv $file "apps/$APP_NAME/$target_path" -v; done;



# Squash the commit to previous batch of PR commits

git commit --amend --no-edit



# Copy the sha output

SHA=$(git rev-parse --short HEAD)



# In the monorepo

cd monorepo



# Checkout a new branch that matches the original PR name

git checkout -b …



# Assuming the monorepo and original repo are in sibling folders, run

git --git-dir=../${APP_NAME}/.git format-patch -k -1 --stdout ${SHA} | git am -3 -k

# Then open the new PR

پس از مهاجرت: بهینه‌سازی مونو ریپو

بهینه‌سازی یک مونو ریپو پس از مهاجرت، فرآیندی ضروری برای جلوگیری از مشکلات مقیاس‌پذیری و حفظ کارایی پروژه است. در هفته‌های بعد از مهاجرت، انجام تغییرات ساختاری و بهینه‌سازی تنظیمات می‌تواند عملکرد بهتری را در مدیریت وابستگی‌ها، اجرای تست‌ها و پردازش کدها فراهم کند.

یکی از اولین اقدامات پس از مهاجرت، مدیریت وابستگی‌ها و جلوگیری از دسترسی غیرمجاز بین ماژول‌های مختلف است. استفاده از ابزارهایی مانند dependency-cruiser می‌تواند مانع از دسترسی مستقیم اپلیکیشن‌ها به پوشه‌های یکدیگر شود و اطمینان حاصل کند که تمامی ماژول‌ها به‌درستی از طریق import استاندارد بارگیری می‌شوند. این کار از پیچیدگی بی‌رویه در ساختار مونو ریپو جلوگیری کرده و مدیریت کد را ساده‌تر می‌کند.

یکپارچه‌سازی تنظیمات پروژه نیز نقش مهمی در بهینه‌سازی دارد. برای کاهش تکرار و ناسازگاری، تنظیمات مشترکی مانند Prettier و Browserlist از فضای کاری هر اپلیکیشن به دایرکتوری ریشه منتقل شده و استانداردسازی شدند. همچنین، وابستگی‌های توسعه مانند eslint، stylelint، Cypress و Jest در پوشه‌ای مجزا (./packages) قرار گرفتند تا با یک import ساده، قابل استفاده باشند و مدیریت نسخه‌های آن‌ها نیز آسان‌تر شود.

مقیاس‌پذیری اجرای CI/CD در مونو ریپو چالش‌های خاص خود را دارد. بهینه‌سازی GitHub Actions می‌تواند تأثیر قابل‌توجهی بر عملکرد فرآیندهای توسعه و استقرار داشته باشد. برای جلوگیری از زمان‌های طولانی نصب وابستگی‌ها، استفاده از yarn workspaces focus به‌جای yarn install باعث شد که هر اپلیکیشن فقط وابستگی‌های موردنیاز خود را دریافت کند. این روش نه‌تنها سرعت نصب را بهبود می‌بخشد، بلکه از برخورد با محدودیت زمانی اجرای workflowها جلوگیری می‌کند.

همچنین، محدود کردن اجرای موازی و زمان‌بندی درست فرآیندهای تست و استقرار، نقش کلیدی در کاهش بار روی سیستم CI/CD دارد. برای بهینه‌سازی این فرآیند، ابتدا یک build matrix برای شناسایی اپلیکیشن‌های تغییر‌یافته اجرا می‌شود، سپس مراحل ساخت، lint، تست‌های واحد و در نهایت تست‌های end-to-end به‌ترتیب انجام می‌گیرند. این مدل باعث کاهش مصرف منابع پردازشی شده و اجرای فرآیندها را پایدارتر می‌کند.

افزودن run-name به workflowهای مربوط به استقرار، امکان مشاهده سریع‌تر ارتباط بین هر job و اپلیکیشن مربوطه را فراهم می‌کند. علاوه بر این، استفاده از $GITHUB_STEP_SUMMARY برای نمایش گزارش‌های تست و رفع اشکالات، فرآیند اشکال‌زدایی را ساده‌تر کرده و دید بهتری از وضعیت اجرا فراهم می‌آورد.

در نهایت، برای جلوگیری از محدودیت‌های مربوط به تعداد درخواست‌های همزمان، تنظیم max-parallel در برخی از actions باعث کاهش مشکلات ناشی از نرخ درخواست‌های بیش‌ازحد به سرویس‌های خارجی شد. همچنین، استفاده از .git-blame-ignore-revs برای مخفی کردن کامیت‌های گروهی در تاریخچه‌ی گیت، امکان بررسی تغییرات واقعی را فراهم کرده و روند تحلیل تاریخچه‌ی پروژه را بهبود داده است.

با اجرای این تغییرات، نه‌تنها عملکرد مونو ریپو بهینه‌تر شده، بلکه فرآیند توسعه و استقرار نرم‌افزار نیز کارآمدتر و مقیاس‌پذیرتر خواهد بود.

پس از مهاجرت به مونو ریپو

جمع بندی

مهاجرت به مونو ریپو تغییری اساسی است که می‌تواند مدیریت پروژه‌های بزرگ را ساده‌تر کند، اما بدون چالش نیست. تجربه نشان داد است که با انتخاب ابزارهای مناسب، بهینه‌سازی فرآیندهای CI/CD و مدیریت دقیق وابستگی‌ها، می‌توان این تغییر را به شکلی موثر پیاده‌سازی کرد. در نهایت، مونو ریپو نه‌تنها توسعه و همکاری تیمی را تسهیل می‌کند، بلکه مسیر رشد و مقیاس‌پذیری را هموارتر می‌سازد.