۱۴۰۵ مهر ۶, دوشنبه
دلار آمریکا۲۴۳٬۴۱۵▲ ۳٫۵۸٪یورو۲۷۷٬۰۰۰▲ ۳٫۳۹٪درهم امارات۶۶٬۳۰۵▲ ۳٫۶۱٪سکه امامی۲۴۴٬۵۰۵٬۰۰۰▲ ۱٫۶۶٪طلای ۱۸ عیار (گرم)۲۴٬۳۱۹٬۰۰۰▲ ۱٫۷۵٪انس طلا۴٬۱۵۰ $▼ ۳٫۲۶٪تتر۲۴۴٬۴۵۶▲ ۳٫۲۴٪بیت‌کوین۸۲٬۶۵۳ $▼ ۲٫۴۱٪اتریوم۲٬۶۴۰ $▼ ۱٫۸۹٪سولانا۱۱۷٫۸۴ $▼ ۴٫۳۴٪اپل۳۴۱٫۰۷ $▲ ۱٫۵۳٪انویدیا۲۲۵٫۰۷ $▲ ۰٫۲۲٪مایکروسافت۵۱۶٫۱۷ $▲ ۳٫۶۶٪آلفابت (گوگل)۳۴۳٫۹۲ $▲ ۰٫۴۶٪تسلا۳۷۲٫۱۱ $▼ ۱٫۵۴٪شاخص نزدک۲۷٬۰۶۹ $▲ ۰٫۴۸٪
نرخ ارز

CI/CD با GitHub Actions قدم‌به‌قدم؛ از تست خودکار تا استقرار

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

دواپس و کلاد۵ دقیقه مطالعه
CI/CD با GitHub Actions قدم‌به‌قدم؛ از تست خودکار تا استقرار
فهرست مطالب
  1. مفاهیم پایه GitHub Actions
  2. پروژه نمونه
  3. مرحله اول CI/CD با GitHub Actions: اجرای خودکار تست‌ها
  4. مرحله دوم: ساخت و انتشار ایمیج Docker
  5. مرحله سوم: استقرار روی سرور
  6. اجرای دستی و عیب‌یابی workflow
  7. امنیت خط لوله را جدی بگیرید
  8. جمع‌بندی

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 شکست می‌خورد، این مسیر عیب‌یابی معمولاً جواب می‌دهد:

  1. لاگ همان مرحله را کامل بخوانید. هر step در تب Actions لاگ جداگانه دارد و خطای واقعی معمولاً چند خط بالاتر از آخرین خط است.
  2. لاگ اشکال‌زدایی را روشن کنید. با گزینه Re-run jobs و فعال کردن Enable debug logging، جزئیات بیشتری از اجرای هر مرحله می‌بینید.
  3. محیط را بازسازی کنید. دستورهای run را به همان ترتیب روی سیستم خودتان یا داخل یک کانتینر لینوکسی اجرا کنید؛ بسیاری از خطاها ناشی از تفاوت سیستم‌عامل یا متغیرهای محیطی‌اند.
  4. به فایل قفل دقت کنید. npm ci اگر package-lock.json با package.json هماهنگ نباشد، شکست می‌خورد؛ فایل قفل را همیشه کامیت کنید.

امنیت خط لوله را جدی بگیرید

خط لوله CI/CD به کد، رازها و سرورهای شما دسترسی دارد؛ پس خودش هدف جذابی برای حمله است. چند اصل ضروری:

  1. کمترین دسترسی: مثل مثال بالا، permissions را در سطح workflow روی حداقل بگذارید و فقط برای job لازم بازش کنید.
  2. پین کردن اکشن‌ها: در پروژه‌های حساس، اکشن‌های شخص ثالث را به‌جای برچسب با هش کامل کامیت ارجاع دهید.
  3. مراقب pull_request_target باشید: این رویداد با دسترسی مخزن اصلی اجرا می‌شود و اجرای کد Pull Requestهای فورک در آن خطرناک است.
  4. رازها فقط در Secrets: هرگز رمز یا توکن را در فایل workflow ننویسید.
  5. به‌روز نگه داشتن وابستگی‌ها: با 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 را هم ببینید.

مطالب مرتبط