وراثت و چندریختی در جاوا (Inheritance و Polymorphism)
وراثت در جاوا یعنی یک کلاس با کلمه extends ویژگیها و رفتار کلاس دیگری را به ارث ببرد و فقط …
CI/CD یعنی خودکار کردن ساخت، تست و انتشار نرمافزار تا هر تغییر کوچک سریع و بیخطر بررسی و منتشر شود. در این آموزش اول CI، تحویل مداوم و استقرار مداوم را به زبان ساده توضیح میدهیم، بعد برای یک پروژه پایتون یک پایپلاین واقعی با GitHub Actions و GitLab CI میسازیم که تستها را اجرا میکند و ایمیج داکر میسازد.
آخرین بهروزرسانی: ۲۱ آبان ۱۴۰۵ — منابع بررسیشده در این روز: راهنمای شروع، مرجع نحو workflow، راهنمای Secrets و Environmentها و صفحه هزینه GitHub Actions، صفحه انتشار actions/checkout و actions/setup-python، آموزش اولین پایپلاین و مرجع .gitlab-ci.yml و ساخت ایمیج داکر در GitLab، صفحه قیمت GitLab و مقاله مارتین فاولر.
⚡ پاسخ کوتاه
CI (یکپارچهسازی مداوم) یعنی هر push یا Pull Request بهطور خودکار ساخته و تست شود. CD یعنی کد سالم بهطور خودکار برای انتشار آماده شود (تحویل مداوم) یا مستقیم روی سرور برود (استقرار مداوم). در GitHub Actions پایپلاین را در فایل .github/workflows/ci.yml و در GitLab در فایل .gitlab-ci.yml تعریف میکنید. پایپلاین GitHub این آموزش را با act و کار تست GitLab را با gitlab-ci-local اجرا کردهایم.
📑 فهرست مطالب
تصور کنید تیمی پنجنفره روی یک فروشگاه اینترنتی کار میکند. هر کس چند روز روی شاخه خودش کد مینویسد و آخر هفته همه کدها را با هم ادغام میکنند. حالا دهها تعارض و خطا یکجا پیدا میشود و کسی نمیداند کدام تغییر باعث کدام مشکل شده است. بعد هم یک نفر باید با دست به سرور وصل شود، کد تازه را بکشد، دستورها را یکییکی اجرا کند و امیدوار باشد چیزی را جا نیندازد. CI/CD برای پایان دادن به همین وضعیت است. ایده اصلی ساده است: کارهای تکراری ساخت، تست و انتشار را به ماشین بسپارید تا هر تغییر کوچک، سریع و یکجور بررسی و منتشر شود. مارتین فاولر در مقاله معروفش درباره یکپارچهسازی مداوم توضیح میدهد که وقتی اعضای تیم کارشان را مرتب و زود ادغام کنند و هر ادغام خودکار تست شود، مشکلات خیلی زودتر و ارزانتر پیدا میشوند. در این آموزش اول مفهوم CI و CD را روشن میکنیم، بعد یک پایپلاین واقعی برای یک پروژه پایتون با GitHub Actions و GitLab CI میسازیم. پایپلاین GitHub Actions را با ابزار act روی سیستم خودمان اجرا کردیم و کار تست GitLab را با gitlab-ci-local؛ هر دو موفق بودند. اگر هنوز با گیت راحت نیستید، اول آموزش گیت و گیتهاب برای مبتدیها را بخوانید، چون CI/CD روی گیت سوار است.
CI مخفف Continuous Integration است. یعنی هر بار که کسی کد را push میکند یا Pull Request میفرستد، یک سرور بهطور خودکار کد را میگیرد، وابستگیها را نصب میکند، تستها را اجرا میکند و نتیجه را گزارش میدهد. اگر تستی شکست بخورد، همه همان لحظه میفهمند و کد خراب وارد شاخه اصلی نمیشود.
حرف D در CD دو معنای نزدیک به هم دارد. تحویل مداوم (Continuous Delivery) یعنی بعد از هر تغییر سالم، نسخهای آماده انتشار ساخته میشود، اما انتشار نهایی روی سرور با یک تأیید انسانی انجام میشود. استقرار مداوم (Continuous Deployment) یک قدم جلوتر است: اگر همه تستها سبز باشند، کد بدون دخالت انسان روی سرور اصلی میرود. توضیح Atlassian درباره تفاوت این سه مفهوم منبع خوبی برای مقایسه است.
| مفهوم | یعنی چه؟ | دخالت انسان |
|---|---|---|
| یکپارچهسازی مداوم (CI) | ساخت و تست خودکار هر تغییر | بررسی نتیجه تستها |
| تحویل مداوم (Continuous Delivery) | آماده شدن خودکار نسخه قابل انتشار | تأیید دستی انتشار نهایی |
| استقرار مداوم (Continuous Deployment) | انتشار خودکار روی سرور اصلی | ندارد؛ تستها تصمیم میگیرند |

اولین سود، پیدا کردن زودهنگام خطاست. خطایی که پنج دقیقه بعد از نوشتن کد پیدا شود، با چند خط درست میشود؛ همان خطا اگر یک ماه بعد روی سایت اصلی پیدا شود، ممکن است فروش و اعتبار شما را به خطر بیندازد. دومین سود، انتشارهای کوچک و کمخطر است. وقتی انتشار خودکار و آسان باشد، تیم بهجای یک انتشار بزرگ و پراسترس در ماه، تغییرات کوچک را چند بار در هفته منتشر میکند. سود سوم، تکرارپذیری است. انسان خسته ممکن است دستور collectstatic یا migrate را فراموش کند؛ پایپلاین هیچوقت فراموش نمیکند. این همان چیزی است که فاکتور ساخت، انتشار و اجرا از مجموعه دوازده فاکتور میخواهد: مرحله ساخت، انتشار و اجرا باید جدا و خودکار باشند. سود چهارم هم امنیت است؛ میشود بررسی امنیتی کد و وابستگیها را هم در پایپلاین گذاشت تا هیچ کدی بدون بررسی وارد نشود. این موضوع وقتی کد را با کمک هوش مصنوعی مینویسید مهمتر است؛ ریسکهایش را در امنیت کد تولیدشده با هوش مصنوعی بررسی کردهایم.
پایپلاین (Pipeline) زنجیرهای از مراحل است که با یک رویداد شروع میشود. ساختار تقریباً همه ابزارها شبیه هم است، فقط اسمها کمی فرق میکند. رویداد (Trigger) چیزی است که پایپلاین را راه میاندازد: push به یک شاخه، باز شدن Pull Request، زمانبندی شبانه یا اجرای دستی. هر پایپلاین چند کار (Job) دارد، مثل test و build. هر کار هم چند قدم (Step) دارد که دستورهای واقعیاند. کارها روی ماشینی به اسم Runner اجرا میشوند؛ میتواند ماشین آماده خود سرویس باشد یا سروری که خودتان راه انداختهاید. مراحل رایج یک پایپلاین به ترتیب اینها هستند: دریافت کد، نصب وابستگیها، بررسی سبک کد (Lint)، اجرای تستها، ساخت خروجی مثل ایمیج داکر، و در نهایت انتشار روی محیط تست یا تولید. اصل مهم این است که مرحله بعد فقط وقتی اجرا شود که مرحله قبل موفق بوده باشد.
| مرحله | کار | نمونه ابزار |
|---|---|---|
| دریافت کد | گرفتن آخرین نسخه مخزن | actions/checkout یا کلون خودکار GitLab |
| نصب وابستگی | نصب کتابخانهها با نسخه مشخص | pip، npm، Maven |
| بررسی سبک کد | پیدا کردن خطاهای ساده و ناهماهنگی | Ruff، ESLint |
| تست | اجرای تستهای خودکار | pytest، JUnit |
| ساخت | تولید خروجی قابل انتشار | docker build |
| انتشار | بردن خروجی به محیط تست یا تولید | SSH، رجیستری و کانتینر |
برای اینکه پایپلاین چیزی برای تست داشته باشد، یک پروژه کوچک پایتون میسازیم. یک تابع که قیمت نهایی را با درصد تخفیف حساب میکند و سه تست برای آن. ساختار پروژه این است:
shop-ci/
├── shop/
│ ├── __init__.py
│ └── pricing.py
├── tests/
│ └── test_pricing.py
├── pyproject.toml
├── requirements-dev.txt
├── Dockerfile
├── .github/workflows/ci.yml
└── .gitlab-ci.yml
کد تابع در فایل shop/pricing.py:
def final_price(price: int, discount_percent: int = 0) -> int:
if price < 0:
raise ValueError("price must not be negative")
if not 0 <= discount_percent <= 100:
raise ValueError("discount must be between 0 and 100")
return price * (100 - discount_percent) // 100
و تستها با pytest در فایل tests/test_pricing.py:
import pytest
from shop.pricing import final_price
def test_no_discount():
assert final_price(250_000) == 250_000
def test_twenty_percent_discount():
assert final_price(250_000, 20) == 200_000
def test_invalid_discount():
with pytest.raises(ValueError):
final_price(100_000, 120)
فایل requirements-dev.txt فقط یک خط دارد: pytest==9.2.0. در pyproject.toml هم این دو خط را گذاشتیم تا pytest پوشه پروژه را در مسیر import ببیند:
[tool.pytest.ini_options]
pythonpath = ["."]
یک نکته از آزمایش خودمان: بدون این تنظیم، pytest در کانتینر خطای ModuleNotFoundError: No module named 'shop' داد. این یکی از خطاهای رایج اولین پایپلاین است؛ چیزی که روی سیستم شما به خاطر تنظیمات محلی کار میکند، در محیط تمیز CI شکست میخورد. اتفاقاً همین یکی از فایدههای CI است. با این تنظیم خروجی 3 passed شد.
GitHub Actions سیستم CI/CD خود گیتهاب است. لازم نیست چیزی نصب کنید؛ کافی است یک فایل YAML در پوشه .github/workflows مخزن بگذارید. طبق راهنمای شروع سریع GitHub Actions، گیتهاب هر فایل workflow در این پوشه را میشناسد و با رویدادهای تعریفشده اجرا میکند.
این فایل .github/workflows/ci.yml پروژه ماست:
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.14", "3.15"]
steps:
- uses: actions/checkout@v7
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python-version }}
cache: pip
- name: Install dependencies
run: pip install -r requirements-dev.txt
- name: Run tests
run: pytest -q
build:
needs: test
if: github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Build Docker image
run: docker build -t shop:${{ github.sha }} .
- name: Smoke test
run: docker run --rm shop:${{ github.sha }}
بخش on میگوید پایپلاین با push به main و با هر Pull Request به سمت main اجرا شود. بخش jobs دو کار دارد. کار test روی ماشین ubuntu-latest اجرا میشود و با strategy.matrix دو بار، یک بار با پایتون 3.14 و یک بار با 3.15. به این ترتیب مطمئن میشوید کد روی هر دو نسخه کار میکند. قدم uses: actions/checkout@v7 کد مخزن را روی Runner میآورد. actions/setup-python@v7 پایتون را نصب میکند و با cache: pip بستههای دانلودشده را برای اجرای بعدی نگه میدارد. ما شماره نسخهها را از صفحه انتشار رسمی خود این اکشنها گرفتیم: در ۲۱ آبان ۱۴۰۵ آخرین نسخه checkout، v7.0.1 و آخرین نسخه setup-python، v7.1.0 است. اگر در مستندات قدیمیتر v4 یا v5 دیدید، نسخه تازهتر را بررسی کنید. بعد دو قدم run دستورهای معمولی ترمینالاند. کار build با needs: test فقط وقتی اجرا میشود که هر دو اجرای test موفق باشند. شرط if هم باعث میشود ساخت ایمیج فقط هنگام push انجام شود، نه برای هر Pull Request. این کار ایمیج داکر را میسازد و یک بار اجرا میکند تا مطمئن شود ایمیج واقعاً بالا میآید. Dockerfile این پروژه چهار خط ساده دارد؛ اگر با ساخت ایمیج آشنا نیستید، آموزش مقدماتی داکر را ببینید.
ما این workflow را اول با ابزار actionlint بررسی کردیم که خطایی گزارش نکرد. بعد با ابزار act که workflowهای GitHub Actions را روی داکر محلی اجرا میکند، آن را اجرا کردیم. هر دو اجرای test با پیام 3 passed تمام شدند و کار build ایمیج را ساخت و خروجی 200000 را چاپ کرد؛ یعنی قیمت ۲۵۰ هزار تومانی با ۲۰ درصد تخفیف. روی خود گیتهاب هم نتیجه هر اجرا در زبانه Actions مخزن و کنار هر Pull Request با تیک سبز یا ضربدر قرمز دیده میشود.

پایپلاین برای انتشار معمولاً به رمز نیاز دارد: کلید SSH سرور، توکن رجیستری یا کلید API. هرگز اینها را داخل فایل YAML ننویسید. طبق راهنمای استفاده از Secrets در GitHub Actions، رمزها را در تنظیمات مخزن یا Environment ذخیره کنید و در workflow با عبارتی مثل ${{ secrets.DEPLOY_KEY }} به آنها دسترسی بگیرید. گیتهاب تلاش میکند مقدار این رمزها را در لاگها پنهان کند، اما باز هم مراقب باشید دستوری رمز را چاپ نکند. همان راهنما یک نکته امنیتی مهم دارد: وقتی workflow از یک Fork اجرا شود، بهجز GITHUB_TOKEN هیچ Secret دیگری به Runner داده نمیشود.
طبق صفحه هزینه GitHub Actions، اجرای کار روی Runnerهای استاندارد برای مخزنهای عمومی رایگان است. برای مخزنهای خصوصی، هر پلن یک سهم ماهانه به دلار دارد: در پلن رایگان GitHub Free این سهم ۱۲ دلار است و هر دقیقه Runner استاندارد لینوکس ۰٫۰۰۶ دلار حساب میشود؛ یعنی تقریباً ۲۰۰۰ دقیقه لینوکس در ماه. Runnerهایی که خودتان راه بیندازید (Self-hosted) هم از این سهم کم نمیکنند.
در GitLab، پایپلاین با فایلی به اسم .gitlab-ci.yml در ریشه مخزن تعریف میشود. طبق آموزش ساخت اولین پایپلاین GitLab، روی GitLab.com لازم نیست Runner راه بیندازید، چون Runnerهای آماده (Instance runners) در دسترساند. فقط فایل را commit کنید تا اولین پایپلاین اجرا شود.
stages:
- test
- build
test:
stage: test
image: python:3.15-slim
script:
- pip install -r requirements-dev.txt
- pytest -q
build:
stage: build
image: docker:29-cli
services:
- docker:29-dind
variables:
DOCKER_TLS_CERTDIR: "/certs"
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
- docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" .
- docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
در GitLab، بهجای Job و Step، مرحلهها (stages) و کارها داریم. stages ترتیب را تعیین میکند: همه کارهای مرحله test باید موفق شوند تا مرحله build شروع شود. هر کار با image مشخص میکند در کدام ایمیج داکر اجرا شود؛ اینجا کار test مستقیم در ایمیج رسمی پایتون اجرا میشود و نیازی به قدم نصب پایتون نیست. بخش script هم دستورهای اجرایی است. کار build از روش Docker-in-Docker استفاده میکند که مستندات ساخت ایمیج داکر در GitLab توضیح داده است. rules میگوید این کار فقط روی شاخه پیشفرض اجرا شود. متغیرهایی مثل CI_REGISTRY_IMAGE و CI_COMMIT_SHORT_SHA را خود GitLab آماده میکند و ایمیج در رجیستری داخلی همان پروژه ذخیره میشود.
ما کار test این فایل را با ابزار gitlab-ci-local و همان ایمیج python:3.15-slim اجرا کردیم و با 3 passed موفق شد. ساختار فایل هم در همان ابزار با شِمای رسمی GitLab سنجیده شد. کار build را به دلیل نیاز به رجیستری واقعی GitLab و حالت privileged اجرا نکردیم؛ آن را طبق الگوی مستندات رسمی نوشتهایم. درباره هزینه، طبق صفحه قیمت GitLab پلن رایگان در ۲۱ آبان ۱۴۰۵ تا ۵ کاربر در هر گروه و ۴۰۰ دقیقه محاسبه در ماه دارد.
| ویژگی | GitHub Actions | GitLab CI |
|---|---|---|
| فایل تنظیمات | .github/workflows/*.yml | .gitlab-ci.yml در ریشه مخزن |
| واحد کار | Job و Step | Stage و Job |
| محیط اجرا | ماشین Runner مثل ubuntu-latest | ایمیج داکر هر Job |
| قطعههای آماده | بازار اکشنها (uses) | قالبها و include |
| رایگان | Runner استاندارد برای مخزن عمومی؛ سهم ۱۲ دلاری برای خصوصی در پلن Free | ۴۰۰ دقیقه محاسبه در ماه در پلن Free |
| نصب روی سرور خود | Self-hosted runner | GitLab Self-Managed و Runner |

تا اینجا پایپلاین ما تست میکند و ایمیج میسازد. قدم بعد انتشار است. چند روش رایج وجود دارد. سادهترین روش این است که پایپلاین با کلید SSH ذخیرهشده در Secrets به سرور وصل شود و همان دستورهای بهروزرسانی را اجرا کند که در آموزش دیپلوی جنگو با Gunicorn و Nginx با دست اجرا کردیم: دریافت کد، نصب وابستگیها، migrate، collectstatic و reload سرویس. روش دوم، که با داکر جور است، این است که پایپلاین ایمیج را با برچسب شناسه commit در رجیستری بگذارد و سرور همان ایمیج را بکشد و کانتینر را عوض کند. این کار باعث میشود دقیقاً همان چیزی که تست شده، روی سرور برود. اگر نسخه تازه مشکل داشت، برگشت به نسخه قبلی یعنی اجرای ایمیج با برچسب قبلی. برای شروع، تحویل مداوم را انتخاب کنید نه استقرار کاملاً خودکار. در گیتهاب میشود برای Environment تولید، بازبین اجباری (Required reviewers) تعریف کرد تا انتشار نهایی فقط با تأیید یک نفر انجام شود. طبق مستندات Environmentها، در پلنهای Free، Pro و Team این قانون فقط برای مخزنهای عمومی در دسترس است؛ برای مخزن خصوصی میشود همین کار را با اجرای دستی workflow (رویداد workflow_dispatch) یا تأیید Pull Request انجام داد. وقتی به تستهایتان اعتماد کردید، میتوانید این تأیید را بردارید.
پایپلاین سریع نگه دارید. اگر تستها نیم ساعت طول بکشند، افراد منتظر نتیجه نمیمانند و کمکم آن را نادیده میگیرند. از کش وابستگیها استفاده کنید و تستهای کند را جدا کنید. شاخه اصلی را محافظت کنید؛ در گیتهاب و GitLab میشود تعیین کرد که هیچ Pull Requestی بدون سبز شدن پایپلاین ادغام نشود. نسخه ابزارها و اکشنها را مشخص کنید. نوشتن latest همه جا راحت است، اما یک روز ابزار عوض میشود و پایپلاین شما بیدلیل خراب میشود. اکشنهای شخص ثالث را هم با دقت انتخاب کنید، چون به کد و گاهی به رمزهای شما دسترسی دارند. اشتباه رایج دیگر، پایپلاینی است که همیشه قرمز است و تیم به آن عادت کرده. پایپلاین قرمز باید اولویت اول باشد. اگر یک تست گاهی شکست میخورد و گاهی نه، آن را درست یا موقتاً غیرفعال کنید؛ تست غیرقابلاعتماد از نبودن تست بدتر است. نوشتن تست خوب هم مهارت جداگانهای است؛ برای نمونه در آموزش Django REST Framework آزمون API با APITestCase را دیدهایم.
| اشتباه | نتیجه | راه درست |
|---|---|---|
| نوشتن رمز داخل فایل YAML | رمز در تاریخچه گیت میماند | Secrets در تنظیمات مخزن |
| پایپلاین کند و بدون کش | تیم منتظر نمیماند و نتیجه را نادیده میگیرد | کش وابستگیها و جدا کردن تستهای کند |
| استفاده از نسخه شناور اکشنها و ایمیجها | خرابی ناگهانی بعد از بهروزرسانی ابزار | نسخه مشخص و بهروزرسانی آگاهانه |
| ادغام PR بدون سبز شدن پایپلاین | کد خراب وارد main میشود | محافظت از شاخه اصلی |
| عادت به پایپلاین قرمز | هشدارهای واقعی گم میشوند | درست کردن فوری یا غیرفعال کردن تست ناپایدار |
| استقرار کاملاً خودکار از روز اول | انتشار خطا روی سایت اصلی | شروع با تحویل مداوم و تأیید دستی |
نه. حتی اگر تنها کار میکنید، پایپلاینی که با هر push تستها را اجرا کند، جلوی خیلی از اشتباهها را میگیرد. برای مخزنهای عمومی در گیتهاب هم هزینهای ندارد.
هر دو کامل و قابلاعتمادند. اگر کدتان روی گیتهاب است، GitHub Actions طبیعیترین انتخاب است و بازار اکشنهای آماده بزرگی دارد. اگر از GitLab استفاده میکنید یا میخواهید همه چیز روی سرور خودتان باشد، GitLab CI انتخاب بهتری است.
بله، Jenkins ابزار متنباز قدیمی و پرطرفداری است که روی سرور خودتان نصب میشود و انعطاف زیادی دارد. اما راهاندازی و نگهداری آن کار بیشتری میخواهد. برای شروع، ابزار داخلی گیتهاب یا GitLab سادهتر است.
برای شروع نه؛ پایپلاین GitHub Actions این آموزش بخش تستش را بدون داکر انجام میدهد. اما بیشتر پایپلاینهای حرفهای در مرحله ساخت و انتشار از داکر استفاده میکنند، پس یادگیری آن قدم بعدی خوبی است.
عدد ثابتی وجود ندارد، اما هر چه سریعتر بهتر. اگر نتیجه تستها در چند دقیقه برسد، برنامهنویس هنوز ذهنش روی همان تغییر است و راحتتر خطا را درست میکند.
نظرات 0
برای ارسال نظر وارد حساب کاربری شوید.
هنوز نظری ثبت نشده؛ اولین نفر باشید.