CI/CD یعنی خودکار کردن مسیر کد از commit تا سرور. CI (یکپارچهسازی مداوم) یعنی هر بار که کسی کد را push میکند، یک سیستم خودکار پروژه را میسازد و تستها را اجرا میکند تا خرابی همان لحظه پیدا شود، نه یک هفته بعد. CD (تحویل یا استقرار مداوم) یعنی کدی که از CI سالم بیرون آمده، با یک قدم مشخص و تکرارپذیر، یا کاملاً خودکار، روی سرور میرود.
GitHub Actions سادهترین راه برای شروع است، چون داخل خود GitHub است و چیزی برای نصب ندارد: یک فایل YAML در پوشهی .github/workflows میگذارید و GitHub روی هر push یا pull request آن را اجرا میکند. در این مقاله یک pipeline واقعی برای پروژهی Laravel میسازیم، نسخهی Python را هم میبینیم و بعد سراغ secrets، محافظت از شاخه و استقرار میرویم.
مفاهیم پایه، بدون اصطلاحبازی
pipeline یک خط تولید است: کد از یک طرف وارد میشود و از چند ایستگاه میگذرد. اگر در هر ایستگاهی مشکلی پیدا شود، خط متوقف میشود و شما خبردار میشوید.
در GitHub Actions این خط از چند جزء ساخته میشود:
| اصطلاح | معنی |
|---|---|
| workflow | یک فایل YAML که کل فرایند را تعریف میکند |
| event | رویدادی که workflow را راه میاندازد؛ مثل push، pull_request یا ساخت تگ |
| job | مجموعهای از قدمها که روی یک ماشین اجرا میشوند؛ jobها بهطور پیشفرض موازیاند |
| step | یک دستور شل یا یک action آماده |
| action | قطعهی قابل استفادهی مجدد، مثل actions/checkout |
| runner | ماشینی که job رویش اجرا میشود؛ مثلاً ubuntu-latest |
ارزش CI در این است که «روی سیستم من کار میکرد» دیگر بهانه نیست. تستها روی یک ماشین تمیز و یکسان اجرا میشوند، هر بار.
اولین workflow برای Laravel
فرض میکنیم پروژه Laravel 13 است و تستها با php artisan test اجرا میشوند. این فایل را بسازید:
name: tests
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
env:
DB_CONNECTION: sqlite
DB_DATABASE: ":memory:"
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: "8.3"
extensions: mbstring, pdo_sqlite, bcmath, intl
coverage: none
- name: Get Composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT"
- name: Cache Composer packages
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: composer-${{ runner.os }}-${{ hashFiles('composer.lock') }}
restore-keys: composer-${{ runner.os }}-
- name: Install dependencies
run: composer install --no-interaction --prefer-dist --no-progress
- name: Prepare environment
run: |
cp .env.example .env
php artisan key:generate
- name: Run tests
run: php artisan test
چند نکته دربارهی این فایل:
on: workflow روی هر push بهmainو روی هر pull request اجرا میشود. یعنی پیش از merge میدانید کد سالم است یا نه.- setup-php: نسخهی PHP و افزونهها را دقیقاً مشخص میکنیم. بهتر است همان نسخهای باشد که روی سرور production دارید.
- کش composer: کلید کش از روی هش
composer.lockساخته میشود. تا وقتی وابستگیها عوض نشدهاند، بستهها از کش میآیند و نصب خیلی سریعتر است. به همین دلیلcomposer.lockباید در مخزن commit شده باشد. - SQLite در حافظه: برای بیشتر تستها کافی و سریع است و سرویس جداگانهای نمیخواهد. متغیرهای
envدر سطح job، مقدارهای.envرا بازنویسی میکنند.
اگر صفحههای شما از @vite استفاده میکنند و تستهای Feature آن صفحهها را رندر میکنند، یا در تستها $this->withoutVite() را صدا بزنید، یا پیش از تستها قدمهای npm ci و npm run build را اضافه کنید؛ وگرنه تستها به خاطر نبودن manifest شکست میخورند.
وقتی تست به MySQL واقعی نیاز دارد
اگر از قابلیتهایی استفاده میکنید که در SQLite رفتار متفاوتی دارند، میتوانید یک سرویس MySQL کنار job بالا بیاورید:
services:
mysql:
image: mysql:8.4
env:
MYSQL_DATABASE: testing
MYSQL_ROOT_PASSWORD: password
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping -h 127.0.0.1"
--health-interval=10s
--health-timeout=5s
--health-retries=5
و متغیرهای job را به DB_CONNECTION: mysql، DB_HOST: 127.0.0.1، DB_DATABASE: testing، DB_USERNAME: root و DB_PASSWORD: password تغییر دهید. این رمز فقط مال یک کانتینر موقت است که بعد از job از بین میرود، پس نوشتنش در فایل اشکالی ندارد. افزونهی pdo_mysql را هم به setup-php اضافه کنید.
همین ایده برای Python
برای پروژهی Python با pytest، ساختار تقریباً یکی است و setup-python کش pip را خودش مدیریت میکند:
name: tests
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- run: pip install -r requirements.txt
- run: pytest
مقایسهی کلی این دو زبان برای بکاند را در PHP یا Python نوشتهایم.
secrets: رمزها جایشان در فایل نیست
هر چیزی که محرمانه است، مثل کلید API یا کلید SSH استقرار، نباید در مخزن باشد. در GitHub از مسیر Settings → Secrets and variables → Actions یک secret بسازید و در workflow اینطور بخوانید:
- name: Call external service
run: ./scripts/notify.sh
env:
API_TOKEN: ${{ secrets.API_TOKEN }}
چند قاعده:
- GitHub مقدار secretها را در لاگ با
***میپوشاند، ولی اگر مقدار را تغییر دهید (مثلاً base64 کنید) و چاپ کنید، این پوشاندن کار نمیکند. secret را هیچوقتechoنکنید. - workflowهایی که از pull requestهای یک fork اجرا میشوند به secretها دسترسی ندارند؛ این رفتار عمدی و درست است.
- برای استقرار، از Environments استفاده کنید: secretهای production را فقط به environmentای به نام
productionبدهید و برایش تأیید دستی بگذارید.
محافظت از شاخهی اصلی
CI وقتی واقعاً ارزش دارد که نشود از رویش رد شد. در Settings → Branches (یا Rulesets) برای شاخهی main قانون بگذارید:
- Require a pull request before merging: کسی مستقیم روی
mainpush نکند. - Require status checks to pass: job به نام
testرا به عنوان check اجباری انتخاب کنید. تا تستها سبز نشوند، دکمهی merge فعال نمیشود. - Require branches to be up to date: شاخه پیش از merge با آخرین
mainتست شده باشد.
اگر با شاخه و pull request هنوز راحت نیستید، دستورهای ضروری git را ببینید.
CD چه شکلی میتواند باشد
سادهترین الگوی استقرار که هم امن است و هم قابل کنترل: استقرار با تگ. هر وقت تصمیم گرفتید نسخهای منتشر شود، یک تگ مثل v1.4.0 میسازید و push میکنید؛ workflow استقرار فقط روی تگها اجرا میشود.
name: deploy
on:
push:
tags: ["v*"]
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- name: Deploy
run: ./scripts/deploy.sh
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
محتوای deploy.sh به زیرساخت شما بستگی دارد، ولی برای یک پروژهی Laravel روی VPS معمولاً این کارها را انجام میدهد: با کلید SSH اختصاصی استقرار به سرور وصل میشود (کلید SSH)، کد تگخورده را میگیرد، composer install --no-dev --optimize-autoloader را اجرا میکند، php artisan migrate --force و کشهای config و route را میسازد و در آخر workerهای صف را با php artisan queue:restart تازه میکند.
چند توصیه برای CD:
- اول CI، بعد CD. استقرار خودکار بدون تست خودکار یعنی رساندن سریعتر باگ به کاربر.
- کلید استقرار جدا بسازید، با کمترین دسترسی لازم، نه کلید شخصی خودتان.
- مسیر برگشت داشته باشید: اگر نسخهی جدید خراب بود، استقرار تگ قبلی باید یک دستور باشد. پیش از migrationهای خطرناک هم پشتیبان بگیرید (قاعدهی ۳-۲-۱).
- اگر برنامه با Docker اجرا میشود، CD میتواند به جای کپی کد، یک image بسازد و سرور آن را بکشد؛ مقدماتش در Docker Compose آمده است.
اشکالهای رایج
- تستها روی سیستم خودتان سبزند ولی در CI قرمز: معمولاً به خاطر وابستگی به فایل
.envمحلی، ترتیب اجرای تستها یا افزونهی PHP که روی runner نصب نیست. - workflow اصلاً اجرا نمیشود: مسیر فایل باید دقیقاً
.github/workflows/باشد و YAML تورفتگی درستی داشته باشد. - کش هیچوقت استفاده نمیشود:
composer.lockدر مخزن نیست یا در هر اجرا تغییر میکند. - نسخهی شناور action:
@v4بهروزرسانیهای جزئی را خودکار میگیرد. اگر امنیت زنجیرهی تأمین برایتان مهم است، action را به هش commit مشخص سنجاق کنید.
جمعبندی
CI یعنی هر push بهطور خودکار تست شود؛ CD یعنی کد سالم با یک قدم تکرارپذیر به سرور برسد. با یک فایل tests.yml شروع کنید، تستها را روی هر pull request اجرا کنید و با قوانین شاخه merge کد قرمز را ناممکن کنید. وقتی به این خط اعتماد پیدا کردید، استقرار با تگ و secretهای محیط production قدم طبیعی بعدی است. مستندات کامل در docs.github.com/actions است.