توسعه فرانت‌اند

CI/CD برای پروژه‌های فرانت‌اند

طراحی pipeline با نصب مبتنی بر lock، GitHub Actions، مجوز کم، تفاوت cache و artifact و نمونه کامل CI با کد و تست آموزشی.

1404/12/02 4 دقیقه مطالعه 738 بازدید

هر تغییر باید مسیر بررسی مشخصی داشته باشد

یک تغییر در فرم ممکن است build را خراب نکند، اما مقدار اشتباه به بخش بعدی بفرستد. تغییر dependency هم شاید روی دستگاه نویسنده کار کند و در نصب تازه شکست بخورد. CI کمک می‌کند بررسی‌های مشخص روی نسخهٔ تازهٔ کد و محیط تعریف‌شده اجرا شوند. هدف این است که نتیجهٔ بررسی به حافظهٔ اعضای تیم و وضعیت لپ‌تاپ آن‌ها وابسته نباشد. Pipeline باید بگوید چه چیزی بررسی شده؛ سبز شدن یک مرحلهٔ build، صحت تمام رفتار محصول را ثابت نمی‌کند.

CI یا Continuous Integration معمولاً بررسی تغییرها هنگام ادغام مداوم کد است. Continuous Delivery خروجی آمادهٔ انتشار فراهم می‌کند و ممکن است انتشار نهایی تصمیم انسانی بخواهد. Continuous Deployment انتشار خودکار تغییر پذیرفته‌شده را هم انجام می‌دهد. در گفتگوها هر دو با CD خلاصه می‌شوند، بنابراین سیاست تیم را صریح بنویسید. نمونهٔ این مقاله فقط CI دارد و چیزی را روی میزبان عمومی منتشر نمی‌کند. افزودن یک دستور deploy بدون مشخص کردن مقصد و مجوز، pipeline کامل ایجاد نمی‌کند.

مرحله‌ها را بر اساس خطاهای واقعی انتخاب کنید

Lint قواعد کد را بررسی می‌کند، type check سازگاری نوع‌ها را می‌سنجد، test یک رفتار تعریف‌شده را بررسی می‌کند و build امکان تولید خروجی را نشان می‌دهد. هر پروژه الزاماً همهٔ این مرحله‌ها را با همان ابزار ندارد. برنامهٔ JavaScript کوچک ممکن است type checker نداشته باشد؛ ادعای بررسی نوع بدون script واقعی درست نیست. ترتیب مرحله‌های ارزان پیش از build سنگین معمولاً بازخورد را زودتر می‌دهد، ولی هزینهٔ نصب و ساخت در هر پروژه باید جدا سنجیده شود.

اگر تستی فقط تابع را صدا می‌زند و خروجی را بدون assertion رها می‌کند، سبز شدن آن اطلاعات کمی دارد. اگر lint تمام پوشهٔ generated را می‌خواند، زمان بررسی بی‌دلیل زیاد می‌شود. برای هر مرحله، ورودی، فرمان و معیار شکست را معلوم کنید. نام واضح step به خواندن log کمک می‌کند. در شروع رسمی GitHub Actions ساختار workflow، رویداد و job توضیح داده شده است؛ خود این ساختار جای انتخاب بررسی مناسب را نمی‌گیرد.

Lock و نسخهٔ ابزار، قرارداد نصب هستند

نصب dependency از یک محدودهٔ نسخه بدون lock می‌تواند در دو روز نتیجهٔ متفاوت داشته باشد. package-lock.json حاصل نصب واقعی را همراه package.json نگه دارید. npm ci برای محیط تازه از lock استفاده می‌کند و اختلاف قرارداد نصب را پنهان نمی‌کند. نسخهٔ Node هم باید با نیاز پروژه سازگار باشد. نمونهٔ این نوشته Node 24 دارد و شمارهٔ اصلی را در workflow مشخص می‌کند. اگر تکرار دقیق نسخهٔ patch لازم است، همان نسخه را با سیاست به‌روزرسانی روشن ثبت کنید.

Cache مدیر بسته به کم کردن دانلود کمک می‌کند، اما جای نصب درست را نمی‌گیرد. setup-node در حالت cache npm، cache مربوط به مدیر بسته را مدیریت می‌کند و نباید آن را تضمین وجود node_modules آماده دانست. فرمان npm ci همچنان اجرا می‌شود. مستندات setup-node دربارهٔ نسخه و cache توضیح می‌دهد. در Monorepo محل lock و مسیر cache هم اهمیت دارد؛ مقالهٔ Turborepo و گراف task این بخش را ادامه می‌دهد.

نمونهٔ کامل با یک قرارداد کوچک

نمونه عدد صحیح بین صفر و ۹۹ را می‌پذیرد و مقدار نامعتبر را به صفر تبدیل می‌کند. این سیاست آموزشی است و الزام عمومی همهٔ فرم‌ها نیست. در پوشهٔ خالی package.json و index.html را در ریشه، count.js و count.test.js را داخل src، و ci.yaml را با نام .github/workflows/ci.yaml ذخیره کنید. Node 24 و npm لازم‌اند. ابتدا npm install را محلی اجرا کنید تا lock واقعی تولید شود؛ سپس فایل lock را همراه کد در مخزن نگه دارید. فایل نمونهٔ lock ساختگی ارائه نمی‌شود.

{"name":"frontend-ci-demo","private":true,"version":"1.0.0","type":"module","scripts":{"check":"node --check src/count.js","test":"node --test src/count.test.js","build":"vite build","dev":"vite"},"devDependencies":{"vite":"^8.0.0"}}
export function normalizeCount(value) {
  const number = Number(value);
  return Number.isInteger(number) && number >= 0 && number <= 99 ? number : 0;
}
import test from 'node:test';
import assert from 'node:assert/strict';
import { normalizeCount } from './count.js';
test('accepts integer within range', () => assert.equal(normalizeCount('12'), 12));
test('rejects negative and fractional input', () => {
  assert.equal(normalizeCount(-2), 0);
  assert.equal(normalizeCount('1.5'), 0);
});
test('rejects invalid or excessive value', () => {
  assert.equal(normalizeCount('hello'), 0);
  assert.equal(normalizeCount(100), 0);
});
<!doctype html><html lang="fa" dir="rtl"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>تعداد معتبر</title><style>body{font-family:system-ui,sans-serif;padding:2rem;background:#edf3f4;color:#19303a}main{max-width:36rem;margin:auto;background:white;padding:1.5rem;border-radius:1rem}input,button{font:inherit;padding:.6rem}button{background:#075c55;color:white;border:0}form{display:grid;gap:.7rem}:focus-visible{outline:3px solid #b66700;outline-offset:3px}</style></head><body><main><h1>تعداد معتبر</h1><form id="form"><label for="count">عدد صحیح از صفر تا ۹۹</label><input id="count" name="count" type="number" min="0" max="99" step="1" required value="0"><button>نمایش</button></form><p id="result" role="status"></p></main><script type="module">import {normalizeCount} from '/src/count.js';document.querySelector('#form').addEventListener('submit',event=>{event.preventDefault();document.querySelector('#result').textContent=`تعداد: ${normalizeCount(document.querySelector('#count').value).toLocaleString('fa-IR')}`;});</script></body></html>
name: Frontend CI
on:
  pull_request:
  push:
    branches: [main]
permissions:
  contents: read
concurrency:
  group: ci-${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true
jobs:
  verify:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v7
        with:
          persist-credentials: false
      - uses: actions/setup-node@v7
        with:
          node-version: '24'
          cache: npm
      - run: npm ci
      - run: npm run check
      - run: npm test
      - run: npm run build

برای دیدن رابط npm run dev را اجرا کنید. npm run check فقط syntax تابع را می‌خواند؛ نام آن عمداً lint یا type check نیست. npm test تست‌های Node را یک بار اجرا می‌کند و npm run build خروجی Vite می‌سازد. همین فرمان‌ها در workflow وجود دارند. تست‌ها تابع خالص را بررسی می‌کنند و جای تست واقعی تعامل فرم در مرورگر را نمی‌گیرند. در این مقاله فایل‌ها و روش اجرا ارائه شده‌اند و اجرای تست یا workflow گزارش نشده است.

در GitHub، workflow با pull request و push روی main فعال می‌شود؛ اگر شاخهٔ اصلی پروژه نام دیگری دارد، مقدار main را مطابق آن تغییر دهید. job یک runner میزبان‌شدهٔ Ubuntu می‌خواهد. نام ubuntu-latest نسخهٔ ثابت سیستم‌عامل نیست؛ برای نیاز دقیق‌تر باید image مناسب را صریح انتخاب کنید. نتیجهٔ job را از log هر step بخوانید. وجود فایل YAML به تنهایی check اجباری برای merge نمی‌سازد؛ سیاست شاخه یا ruleset باید check مورد نیاز را جدا مشخص کند.

مجوز کم، خواندن log را هم ساده‌تر می‌کند

Workflow بررسی به مجوز نوشتن مخزن نیاز ندارد؛ contents: read برای مسیر نمونه کافی است. persist-credentials در checkout غیرفعال شده تا اعتبارنامه برای استفادهٔ بعدی نگه داشته نشود. مثال از نسخهٔ اصلی v7 actionهای رسمی مطابق راهنمای فعلی مخزن آن‌ها استفاده می‌کند. برای تثبیت کامل کد action می‌توان SHA کامل و بررسی‌شده را ثبت کرد؛ تگ اصلی قابل تغییر است. مستندات checkout جزئیات دسترسی و گزینه‌ها را مشخص می‌کند.

کد pull request را غیرقابل اعتماد در نظر بگیرید، به‌ویژه وقتی از fork می‌آید. اجرای آن کنار secret انتشار یا token دارای مجوز زیاد، تصمیم جداگانه‌ای است. تغییر event به pull_request_target برای حل سریع دسترسی secret می‌تواند مرز اعتماد را عوض کند. راهنمای امنیت workflow دربارهٔ کد ورودی، action بیرونی و تثبیت SHA توضیح می‌دهد. Secret باید از منطق بررسی کد جدا بماند و در فرمان یا log نمایش داده نشود.

هم‌زمانی بررسی با هم‌زمانی انتشار فرق دارد

در مثال، اجرای قدیمی همان ref با رسیدن تغییر تازه لغو می‌شود. برای CI این رفتار جلوی مصرف وقت روی کدی را می‌گیرد که بازخوردش دیگر لازم نیست. در استقرار، لغو وسط کار ممکن است وضعیت نیمه‌کامل ایجاد کند؛ همان تنظیم را بدون شناخت روش انتشار کپی نکنید. timeout هم مانع اجرای نامحدود می‌شود، ولی مقدارش باید برای هزینهٔ واقعی کار مناسب باشد. job کند و job متوقف‌شده دو مسئلهٔ متفاوت‌اند و log باید علت را نشان دهد.

Artifact با cache متفاوت است

Cache برای بازاستفادهٔ دادهٔ قابل محاسبه است؛ artifact خروجی مشخص یک اجرای workflow است که می‌توان برای بررسی یا مرحلهٔ بعد نگه داشت. اگر build را برای انتشار می‌سازید، بهتر است معلوم باشد کدام commit و کدام ورودی محیطی آن را تولید کرده‌اند. راهنمای artifact روش دریافت خروجی اجرای مشخص را توضیح می‌دهد. نمونهٔ کوتاه ما فقط امکان ساخت را بررسی می‌کند و مرحلهٔ بارگذاری artifact ندارد؛ نیاز پروژه تعیین می‌کند آن مرحله اضافه شود.

ساخت دوباره در job انتشار ممکن است خروجی متفاوتی تولید کند، به‌خصوص اگر متغیرهای محیطی یا منابع بیرونی فرق کنند. اگر تصمیم تیم انتشار همان artifact بررسی‌شده است، زنجیرهٔ انتقال و اعتبار آن را روشن نگه دارید. فایل bundle مرورگر هم جای secret نیست. نوشتهٔ ساخت فرانت‌اند با Docker همین موضوع را در محیط container بررسی می‌کند. نام محیط production به تنهایی به خروجی اعتماد یا محرمانگی اضافه نمی‌کند.

انتشار باید مقصد و راه بازگشت داشته باشد

برای افزودن CD، آدرس مقصد، نوع احراز هویت، محدودیت branch و سیاست تأیید را تعیین کنید. Environment می‌تواند secrets و قواعد حفاظت مخصوص مقصد داشته باشد؛ امکاناتش به تنظیم و طرح سرویس وابسته است. راهنمای environment در GitHub این قرارداد را توضیح می‌دهد. رایگان بودن هم ادعای عمومی برای هر مخزن و هر میزان مصرف نیست؛ شرایط استفاده را از حساب و مستندات مربوط بررسی کنید.

در پایان طراحی pipeline، از یک تغییر ناموفق فرضی مسیر را دنبال کنید: کدام step شکست را پیدا می‌کند، چه کسی نتیجه را می‌خواند و آیا merge یا انتشار متوقف می‌شود؟ اگر نسخهٔ منتشرشده مشکل داشت، باید خروجی قبلی یا روش بازگشت قابل شناسایی باشد. افزودن مرحله‌های زیاد بدون مالک و هدف، فقط log بیشتری تولید می‌کند. Pipeline مفید همان بررسی‌ها و تصمیم‌هایی را ثبت می‌کند که تیم برای تحویل یک تغییر قابل اعتماد نیاز دارد.

واکنش به این مقاله

اشتراک‌گذاری این مقاله:
دیدگاه‌ها 0
۰ / ۲۰۰۰

هنوز دیدگاهی ثبت نشده

اولین نفری باشید که نظر می‌دهد!