CI/CD با GitHub Actions قدمبهقدم؛ از تست خودکار تا استقرار
آموزش CI/CD با GitHub Actions قدمبهقدم: اجرای خودکار تستها، ساخت و انتشار ایمیج Docker در GHCR و استقرار امن روی سرور، با فایلهای workflow آماده.

فهرست مطالب
CI/CD با GitHub Actions سادهترین راه برای اینکه تیم شما دیگر نگران «یادم رفت تستها را اجرا کنم» یا «کدام نسخه روی سرور است؟» نباشد. GitHub Actions سرویس اتوماسیون داخلی GitHub است که با هر push یا Pull Request، کارهایی را که تعریف کردهاید روی ماشینهای ابری اجرا میکند.
در این آموزش یک خط لوله کامل میسازیم: اجرای تستها، ساخت و انتشار ایمیج Docker و در نهایت استقرار روی سرور.
مفاهیم پایه GitHub Actions
| مفهوم | توضیح |
|---|---|
| Workflow | فایل YAML در پوشه .github/workflows که کل فرایند را تعریف میکند |
| Event | رویدادی که workflow را اجرا میکند؛ مثل push یا pull_request |
| Job | مجموعهای از مراحل که روی یک ماشین اجرا میشوند؛ jobها بهطور پیشفرض موازیاند |
| Step | یک دستور شل (run) یا یک اکشن آماده (uses) |
| Runner | ماشینی که job روی آن اجرا میشود؛ مثل ubuntu-latest |
پروژه نمونه
از همان API ساده Node.js در آموزش Docker برای توسعهدهندگان استفاده میکنیم، با این تفاوت که برای تستپذیری، ساخت اپلیکیشن را از اجرای سرور جدا میکنیم. فایل app.js:
const express = require("express");
const app = express();
app.get("/health", (req, res) => {
res.send("ok");
});
module.exports = app;
و فایل server.js:
const app = require("./app");
const port = process.env.PORT || 3000;
app.listen(port, () => console.log(`API listening on port ${port}`));
برای تست از اجراکننده تست داخلی Node.js استفاده میکنیم تا به هیچ کتابخانه اضافهای نیاز نباشد. فایل test/health.test.js:
const { test } = require("node:test");
const assert = require("node:assert/strict");
const app = require("../app");
test("GET /health returns ok", async () => {
const server = app.listen(0);
await new Promise((resolve) => server.once("listening", resolve));
const { port } = server.address();
const res = await fetch(`http://localhost:${port}/health`);
assert.equal(res.status, 200);
assert.equal(await res.text(), "ok");
await new Promise((resolve) => server.close(resolve));
});
در package.json اسکریپت تست را اضافه کنید:
{
"scripts": {
"start": "node server.js",
"test": "node --test"
}
}
با npm test مطمئن شوید تست روی سیستم خودتان سبز است.
مرحله اول CI/CD با GitHub Actions: اجرای خودکار تستها
فایل .github/workflows/ci-cd.yml را بسازید:
name: CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [22, 24]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node-version }}
cache: npm
- run: npm ci
- run: npm test
بیایید بخشهای مهمش را مرور کنیم:
on: این workflow روی هر push بهmainو هر Pull Request به سمتmainاجرا میشود.permissions: دسترسی توکن خودکارGITHUB_TOKENرا به حداقل، یعنی فقط خواندن محتوا، محدود کردهایم.concurrency: اگر پشت سر هم push کنید، اجرای قدیمیتر لغو میشود تا منابع هدر نرود.matrix: تستها همزمان روی Node.js 22 و 24 اجرا میشوند.cache: npm: پوشه کش npm بین اجراها نگه داشته میشود و نصب وابستگیها سریعتر است.
فایل را کامیت و push کنید و به تب Actions مخزن بروید. اگر با شاخه و Pull Request آشنا نیستید، اول آموزش Git و GitHub از صفر را بخوانید.
تست را اجباری کنید
در تنظیمات مخزن، بخش Rules یا Branch protection، برای شاخه main قانونی بسازید که ادغام Pull Request را به قبولی job تست مشروط کند. از این لحظه هیچ کد قرمزی وارد main نمیشود.
مرحله دوم: ساخت و انتشار ایمیج Docker
وقتی تستها روی main سبز شدند، ایمیج Docker را میسازیم و در GitHub Container Registry (GHCR) منتشر میکنیم. این job را زیر jobs اضافه کنید:
docker:
needs: test
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v7
- uses: docker/setup-buildx-action@v4
- uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v6
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=sha
type=raw,value=latest,enable={{is_default_branch}}
- uses: docker/build-push-action@v7
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
نکتههای کلیدی:
needs: testیعنی این job فقط بعد از موفقیت همه نسخههای ماتریس تست اجرا میشود.ifمانع میشود که Pull Requestها ایمیج منتشر کنند.packages: writeفقط برای همین job داده شده، نه کل workflow.metadata-actionبرچسبها را خودکار میسازد: یک برچسب بر اساس SHA کامیت برای ردیابی دقیق وlatestبرای شاخه اصلی.cache-from/cache-toلایههای Docker را در کش GitHub Actions نگه میدارد تا ساختهای بعدی سریعتر شوند.
بعد از اجرای موفق، ایمیج در بخش Packages حساب یا سازمان شما دیده میشود. نام ایمیج در رجیستری باید با حروف کوچک باشد؛ اگر نام کاربری یا مخزن شما حروف بزرگ دارد، مقدار images را دستی و با حروف کوچک بنویسید.
مرحله سوم: استقرار روی سرور
برای استقرار، از Environment استفاده میکنیم. در تنظیمات مخزن بخش Environments یک محیط به نام production بسازید، در صورت نیاز تأییدکننده اجباری (Required reviewers) تعیین کنید و این Secretها را در آن تعریف کنید:
DEPLOY_HOST: آدرس سرورDEPLOY_SSH_KEY: کلید خصوصی SSH مخصوص استقرارDEPLOY_KNOWN_HOSTS: خروجیssh-keyscanسرور که یک بار و از مسیر مطمئن گرفتهاید
فرض میکنیم روی سرور، پروژه با Docker Compose و ایمیج GHCR اجرا میشود. job استقرار:
deploy:
needs: docker
runs-on: ubuntu-latest
environment: production
steps:
- name: Deploy over SSH
env:
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
DEPLOY_KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
run: |
mkdir -p ~/.ssh
echo "$DEPLOY_SSH_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
echo "$DEPLOY_KNOWN_HOSTS" > ~/.ssh/known_hosts
ssh deploy@"$DEPLOY_HOST" \
"cd /srv/devna-api && docker compose pull && docker compose up -d"
اگر پکیج شما خصوصی است، سرور هم باید یک بار با توکنی که دسترسی read:packages دارد وارد ghcr.io شود. اگر برای محیط production تأییدکننده تعیین کرده باشید، این job منتظر تأیید میماند و بعد اجرا میشود. تاریخچه همه استقرارها هم در صفحه مخزن ثبت میشود.
اجرای دستی و عیبیابی workflow
گاهی لازم است workflow را بدون push اجرا کنید؛ مثلاً برای استقرار دوباره. کافی است رویداد workflow_dispatch را به بخش on اضافه کنید تا دکمه Run workflow در تب Actions ظاهر شود:
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
وقتی یک job شکست میخورد، این مسیر عیبیابی معمولاً جواب میدهد:
- لاگ همان مرحله را کامل بخوانید. هر step در تب Actions لاگ جداگانه دارد و خطای واقعی معمولاً چند خط بالاتر از آخرین خط است.
- لاگ اشکالزدایی را روشن کنید. با گزینه Re-run jobs و فعال کردن Enable debug logging، جزئیات بیشتری از اجرای هر مرحله میبینید.
- محیط را بازسازی کنید. دستورهای
runرا به همان ترتیب روی سیستم خودتان یا داخل یک کانتینر لینوکسی اجرا کنید؛ بسیاری از خطاها ناشی از تفاوت سیستمعامل یا متغیرهای محیطیاند. - به فایل قفل دقت کنید.
npm ciاگرpackage-lock.jsonباpackage.jsonهماهنگ نباشد، شکست میخورد؛ فایل قفل را همیشه کامیت کنید.
امنیت خط لوله را جدی بگیرید
خط لوله CI/CD به کد، رازها و سرورهای شما دسترسی دارد؛ پس خودش هدف جذابی برای حمله است. چند اصل ضروری:
- کمترین دسترسی: مثل مثال بالا،
permissionsرا در سطح workflow روی حداقل بگذارید و فقط برای job لازم بازش کنید. - پین کردن اکشنها: در پروژههای حساس، اکشنهای شخص ثالث را بهجای برچسب با هش کامل کامیت ارجاع دهید.
- مراقب
pull_request_targetباشید: این رویداد با دسترسی مخزن اصلی اجرا میشود و اجرای کد Pull Requestهای فورک در آن خطرناک است. - رازها فقط در Secrets: هرگز رمز یا توکن را در فایل workflow ننویسید.
- بهروز نگه داشتن وابستگیها: با Dependabot نسخه اکشنها و بستههای npm را خودکار بهروز کنید تا وصلههای امنیتی بهصورت Pull Request به دستتان برسد. فایل
.github/dependabot.yml:
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
اهمیت بهروز ماندن وابستگیها را ماجرای اخیر بهروزرسانی امنیتی Next.js بهخوبی نشان داد.
جمعبندی
حالا یک خط لوله کامل دارید: هر Pull Request تست میشود، هر ادغام در main یک ایمیج Docker با برچسب دقیق میسازد و استقرار با تأیید و بهشکلی قابل ردیابی انجام میشود. قدم بعدی میتواند افزودن لینتر، اسکن امنیتی ایمیج یا استقرار روی Kubernetes باشد؛ تازههای Kubernetes 1.37 را هم ببینید.



