Теория CI/CD

CI - Continuous Integration - непрерывная интеграция. CD - Continuous Delivery / Deployment - непрерывная доставка и развёртывание.

Пайплайн выглядит следующим образом:

  • Планирование
  • Разработка
  • Сборка
  • Тестирование
  • Релиз
  • Развёртывание
  • Управление
  • Мониторинг

Пример:

  • Менеджер присылает задачу
  • Создаём ветку для создания новой фичи проекта
  • Пишем код
  • Далее создаём pull/merge request и отдаём код на ревью
  • Если всё ок, то код можно заливать в мастер

Однако так же нужно было прогнать все проверки и тесты - линтеры, юнит-тесты, e2e и так далее. Это процесс долгий, и выполнять его каждый раз самостоятельно - трудная задача.

И тут в дело вступает CI. Она позволяет автоматизировать проведение всех проверок перед тем, как залить определённые изменения в ветку.

CD же представляет из себя merge всех изменений с основной веткой, сборку приложения и деплой этой сборки.

CI зачастую реализуется через сервисы по типу GitLab CI, Jenkins, BitBucket Pipelines, GitHub Actions. Далее рассмотрим GitHub Actions подробно.

GitHub Actions - основы

Workflow описывается в YAML-файле в директории .github/workflows/. При push или pull request в указанную ветку GitHub запускает jobs по описанной стратегии.

Каждый шаг указывается в steps. Пример простого CI-пайплайна:

.github/workflows/ci.yml

name: CI Pipeline
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
jobs:
  build-and-test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [20.x]
    steps:
      - uses: actions/checkout@v4
      - name: Setup Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'
      - name: Install dependencies
        run: npm ci
      - name: Lint
        run: npm run lint
      - name: Unit tests
        run: npm run test:unit
      - name: Build
        run: npm run build

Если workflow словит ошибку, GitHub покажет её в интерфейсе:

Если коммит пройдёт все проверки, рядом с ним появится зелёная галочка:

GitHub Actions - продвинутые темы

Triggers - события запуска

Поле on определяет, когда workflow запускается. Основные триггеры:

on:
  # Push в указанные ветки
  push:
    branches: [main, develop]
    paths:
      - 'src/**'
      - 'package.json'
    tags:
      - 'v*'
 
  # Pull request в указанные ветки
  pull_request:
    branches: [main]
    types: [opened, synchronize, reopened]
 
  # Ручной запуск из интерфейса GitHub
  workflow_dispatch:
    inputs:
      environment:
        description: 'Target environment'
        required: true
        type: choice
        options: [staging, production]
      dry_run:
        description: 'Dry run mode'
        type: boolean
        default: false
 
  # Запуск по расписанию (cron, UTC)
  schedule:
    - cron: '0 3 * * 1'  # каждый понедельник в 03:00 UTC
 
  # Запуск через API
  repository_dispatch:
    types: [deploy-trigger]

Фильтрация по paths

Фильтр paths позволяет запускать workflow только при изменении определённых файлов. Это экономит минуты CI, когда меняются только docs или конфиги.

Jobs и steps - зависимости и условия

Jobs запускаются параллельно по умолчанию. Для последовательного выполнения используется needs:

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run lint
 
  test:
    runs-on: ubuntu-latest
    needs: lint  # запустится только после lint
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test
 
  build:
    runs-on: ubuntu-latest
    needs: [lint, test]  # ждёт завершения обоих
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run build
 
  deploy:
    runs-on: ubuntu-latest
    needs: build
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    steps:
      - run: echo "Deploying..."

Условия if можно применять и на уровне шагов:

steps:
  - name: Run e2e tests
    run: npm run test:e2e
    continue-on-error: true  # не ломает весь job
 
  - name: Notify on failure
    if: failure()
    run: echo "Previous step failed"
 
  - name: Always cleanup
    if: always()
    run: rm -rf tmp/
 
  - name: Only on PR
    if: github.event_name == 'pull_request'
    run: echo "This is a PR"

Matrix strategy - кросс-платформенное тестирование

Matrix создаёт комбинации параметров и запускает job для каждой:

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false  # не останавливать другие при ошибке одного
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        node-version: [18, 20, 22]
        include:
          - os: ubuntu-latest
            node-version: 22
            coverage: true  # дополнительный параметр для одной комбинации
        exclude:
          - os: windows-latest
            node-version: 18  # исключить эту комбинацию
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm ci
      - run: npm test
      - name: Upload coverage
        if: matrix.coverage
        run: npm run test:coverage

fail-fast

По умолчанию fail-fast: true - при ошибке в одной комбинации остальные отменяются. Для полного отчёта по совместимости выставляй false.

Reusable workflows - переиспользуемые пайплайны

Reusable workflow определяется через workflow_call и вызывается из других workflow. Это позволяет стандартизировать пайплайны между репозиториями.

Определение переиспользуемого workflow:

.github/workflows/reusable-deploy.yml

name: Reusable Deploy
on:
  workflow_call:
    inputs:
      environment:
        required: true
        type: string
      image_tag:
        required: true
        type: string
    secrets:
      KUBE_CONFIG:
        required: true
    outputs:
      deploy_url:
        description: 'Deployed URL'
        value: ${{ jobs.deploy.outputs.url }}
 
jobs:
  deploy:
    runs-on: ubuntu-latest
    outputs:
      url: ${{ steps.deploy.outputs.url }}
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to ${{ inputs.environment }}
        id: deploy
        run: |
          echo "Deploying ${{ inputs.image_tag }} to ${{ inputs.environment }}"
          echo "url=https://${{ inputs.environment }}.example.com" >> "$GITHUB_OUTPUT"

Вызов из другого workflow:

jobs:
  deploy-staging:
    uses: ./.github/workflows/reusable-deploy.yml
    with:
      environment: staging
      image_tag: ${{ github.sha }}
    secrets:
      KUBE_CONFIG: ${{ secrets.KUBE_CONFIG_STAGING }}

Composite actions - собственные actions

Composite action объединяет несколько шагов в переиспользуемое действие. Размещается в отдельной директории с action.yml:

.github/actions/setup-and-build/action.yml

name: 'Setup and Build'
description: 'Install deps, lint, test, build'
inputs:
  node-version:
    description: 'Node.js version'
    default: '20'
runs:
  using: 'composite'
  steps:
    - uses: actions/setup-node@v4
      with:
        node-version: ${{ inputs.node-version }}
        cache: 'npm'
    - run: npm ci
      shell: bash
    - run: npm run lint
      shell: bash
    - run: npm test
      shell: bash
    - run: npm run build
      shell: bash

Использование:

steps:
  - uses: actions/checkout@v4
  - uses: ./.github/actions/setup-and-build
    with:
      node-version: '22'

Environments - окружения и approval

Environments позволяют настроить правила защиты и ручное подтверждение для деплоя:

jobs:
  deploy-production:
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://example.com
    steps:
      - run: echo "Deploying to production"

В настройках репозитория для environment можно задать: required reviewers, wait timer, branch restrictions и environment-specific secrets.

OIDC - аутентификация без секретов

OIDC позволяет получить короткоживущие токены для облачных провайдеров без хранения долгосрочных credentials в секретах:

jobs:
  deploy-aws:
    runs-on: ubuntu-latest
    permissions:
      id-token: write   # обязательно для OIDC
      contents: read
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789:role/github-actions
          aws-region: eu-west-1
      - run: aws s3 ls  # работает без AWS_ACCESS_KEY_ID

Преимущества OIDC

  • Нет долгоживущих секретов для ротации
  • Токены живут минуты, а не месяцы
  • Можно ограничить доступ по repo, branch, environment
  • Поддерживается AWS, GCP, Azure, HashiCorp Vault

Caching - кэширование зависимостей

Кэширование ускоряет workflow, сохраняя зависимости между запусками:

steps:
  - uses: actions/checkout@v4
 
  # Встроенный cache в setup-node
  - uses: actions/setup-node@v4
    with:
      node-version: '20'
      cache: 'npm'
 
  # Или ручной cache для более сложных случаев
  - uses: actions/cache@v4
    with:
      path: |
        ~/.npm
        node_modules
      key: deps-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
      restore-keys: |
        deps-${{ runner.os }}-
 
  - run: npm ci

Ключи кэша для разных экосистем: Go - hashFiles('**/go.sum') + путь ~/go/pkg/mod, Python - hashFiles('**/requirements.txt') + путь ~/.cache/pip, Docker - cache-from: type=gha в docker/build-push-action.

Artifacts - передача данных между jobs

Artifacts позволяют сохранить файлы из одного job и использовать в другом:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: build-output
          path: dist/
          retention-days: 5
 
  deploy:
    runs-on: ubuntu-latest
    needs: build
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: build-output
          path: dist/
      - run: ls -la dist/

Self-hosted runners

Self-hosted runners запускаются на собственной инфраструктуре. Это полезно для доступа к приватным сетям, специфическому железу или экономии минут.

jobs:
  build:
    runs-on: [self-hosted, linux, x64, gpu]  # labels для выбора runner
    steps:
      - uses: actions/checkout@v4
      - run: nvidia-smi  # доступ к GPU

Важно: не используй self-hosted runners в публичных репозиториях - любой PR может выполнить произвольный код. Изолируй runners в VM или контейнеры, очищай рабочую директорию между запусками.

Concurrency - управление параллельными запусками

Concurrency предотвращает одновременные деплои и экономит ресурсы при быстрых последовательных пушах:

# На уровне всего workflow
concurrency:
  group: deploy-${{ github.ref }}
  cancel-in-progress: true  # отменить предыдущий при новом запуске
 
# Или на уровне job
jobs:
  deploy:
    concurrency:
      group: production-deploy
      cancel-in-progress: false  # для production - дождаться завершения

Типичная стратегия

Для CI-проверок - cancel-in-progress: true - экономит ресурсы, отменяя устаревшие запуски. Для production deploy - cancel-in-progress: false - гарантирует завершение текущего деплоя.

Secrets и variables

GitHub поддерживает три уровня секретов и переменных:

  • Repository - доступны всем workflows в репозитории
  • Environment - доступны только в конкретном environment
  • Organization - доступны всем репозиториям организации
jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - name: Deploy
        env:
          # Секреты - зашифрованы, маскируются в логах
          API_KEY: ${{ secrets.API_KEY }}
          # Переменные - не зашифрованы, видны в логах
          API_URL: ${{ vars.API_URL }}
        run: curl -H "Authorization: $API_KEY" "$API_URL/deploy"

Правила работы с секретами

  • Секреты не передаются в workflows из форков
  • Секреты маскируются в логах автоматически
  • Environment secrets имеют приоритет над repository secrets
  • Для ротации используй GitHub API или CLI

Container jobs и services

Jobs можно запускать внутри Docker-контейнеров. Services поднимают зависимости - базы данных, кэши:

jobs:
  integration-test:
    runs-on: ubuntu-latest
    container:
      image: node:20-slim
 
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_DB: testdb
          POSTGRES_USER: test
          POSTGRES_PASSWORD: test
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432
 
      redis:
        image: redis:7
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 6379:6379
 
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - name: Run integration tests
        env:
          DATABASE_URL: postgresql://test:test@postgres:5432/testdb
          REDIS_URL: redis://redis:6379
        run: npm run test:integration

Сетевые имена сервисов

Когда job запускается в контейнере, сервисы доступны по имени - postgres, redis. Без контейнера используй localhost и проброшенные порты.

Workflow dispatch - ручной запуск с параметрами

workflow_dispatch позволяет запускать workflow вручную с параметрами из UI, CLI или API. Поддерживаемые типы inputs: string, choice, boolean, number, environment.

on:
  workflow_dispatch:
    inputs:
      version:
        description: 'Release version'
        required: true
        type: string
      environment:
        description: 'Deploy target'
        required: true
        type: choice
        options: [staging, production]
      skip_tests:
        type: boolean
        default: false
 
jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm version ${{ inputs.version }} --no-git-tag-version
      - if: ${{ !inputs.skip_tests }}
        run: npm test
      - run: echo "Deploying ${{ inputs.version }} to ${{ inputs.environment }}"

Запуск через CLI: gh workflow run release.yml -f version=1.2.3 -f environment=staging

Production pipeline - полный пример

Пример production-ready пайплайна с lint, тестами, сборкой образа, security-сканированием и деплоем через environments:

name: Production Pipeline
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
 
concurrency:
  group: pipeline-${{ github.ref }}
  cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
 
env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}
 
jobs:
  # ── Lint и статический анализ ──
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - run: npm run lint
      - run: npx tsc --noEmit
 
  # ── Тесты ──
  test:
    runs-on: ubuntu-latest
    needs: lint
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_DB: testdb
          POSTGRES_USER: ci
          POSTGRES_PASSWORD: ci
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - name: Unit tests
        run: npm run test:unit -- --coverage
      - name: Integration tests
        env:
          DATABASE_URL: postgresql://ci:ci@localhost:5432/testdb
        run: npm run test:integration
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: coverage-report
          path: coverage/
          retention-days: 7
 
  # ── Сборка Docker-образа ──
  build:
    runs-on: ubuntu-latest
    needs: test
    permissions:
      contents: read
      packages: write
    outputs:
      image_tag: ${{ steps.meta.outputs.tags }}
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=sha,prefix={{branch}}-
            type=ref,event=branch
            type=semver,pattern={{version}}
      - uses: docker/build-push-action@v5
        with:
          context: .
          push: ${{ github.event_name == 'push' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
 
  # ── Security scan ──
  security:
    runs-on: ubuntu-latest
    needs: build
    if: github.event_name == 'push'
    steps:
      - uses: actions/checkout@v4
      - name: Trivy - scan filesystem
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: fs
          scan-ref: .
          severity: CRITICAL,HIGH
          exit-code: '1'
      - name: Gitleaks - scan for secrets
        uses: gitleaks/gitleaks-action@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 
  # ── Deploy to staging ──
  deploy-staging:
    runs-on: ubuntu-latest
    needs: [build, security]
    if: github.ref == 'refs/heads/develop'
    environment:
      name: staging
      url: https://staging.example.com
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to staging
        env:
          IMAGE_TAG: ${{ needs.build.outputs.image_tag }}
        run: |
          echo "Deploying $IMAGE_TAG to staging"
          # kubectl set image deployment/app app=$IMAGE_TAG -n staging
          # kubectl rollout status deployment/app -n staging --timeout=5m
      - name: Smoke test
        run: curl -f https://staging.example.com/health || exit 1
 
  # ── Deploy to production (с approval) ──
  deploy-production:
    runs-on: ubuntu-latest
    needs: [build, security]
    if: github.ref == 'refs/heads/main'
    environment:
      name: production  # reviewers настраиваются в Settings > Environments
      url: https://example.com
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to production
        env:
          IMAGE_TAG: ${{ needs.build.outputs.image_tag }}
        run: |
          echo "Deploying $IMAGE_TAG to production"
          # kubectl set image deployment/app app=$IMAGE_TAG -n production
          # kubectl rollout status deployment/app -n production --timeout=5m
      - name: Verify deployment
        run: curl -f https://example.com/health || exit 1
      - name: Notify team
        if: always()
        uses: 8398a7/action-slack@v3
        with:
          status: ${{ job.status }}
          text: 'Production deploy: ${{ job.status }}'
          webhook_url: ${{ secrets.SLACK_WEBHOOK }}

Настраиваем CD - деплой через Netlify

Задеплоить фронтенд-приложение можно через Netlify.

Добавляем новый проект с нашего GitHub и подключаем его в Netlify:

Netlify берёт доступ к проекту на GitHub и отслеживает его изменения, чтобы деплоить проект автоматически.

И по ссылке можно просмотреть приложение:

Jenkins

Jenkins - один из старейших CI/CD инструментов с открытым исходным кодом. Работает по принципу master-agent: master управляет пайплайнами, agents выполняют задачи.

Jenkinsfile - пайплайн как код

Declarative pipeline описывается в Jenkinsfile в корне репозитория:

pipeline {
    agent any
 
    environment {
        REGISTRY = 'registry.example.com'
        IMAGE = "${REGISTRY}/myapp:${BUILD_NUMBER}"
    }
 
    options {
        timeout(time: 30, unit: 'MINUTES')
        disableConcurrentBuilds()
        buildDiscarder(logRotator(numToKeepStr: '10'))
    }
 
    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }
 
        stage('Install') {
            steps {
                sh 'npm ci'
            }
        }
 
        stage('Lint & Test') {
            parallel {
                stage('Lint') {
                    steps {
                        sh 'npm run lint'
                    }
                }
                stage('Unit Tests') {
                    steps {
                        sh 'npm run test:unit'
                        junit 'reports/**/*.xml'
                    }
                }
            }
        }
 
        stage('Build Docker') {
            steps {
                sh "docker build -t ${IMAGE} ."
            }
        }
 
        stage('Deploy to Staging') {
            when {
                branch 'develop'
            }
            steps {
                sh "docker push ${IMAGE}"
                sh "./scripts/deploy.sh staging ${IMAGE}"
            }
        }
 
        stage('Deploy to Production') {
            when {
                branch 'main'
            }
            input {
                message 'Deploy to production?'
                ok 'Deploy'
            }
            steps {
                sh "docker push ${IMAGE}"
                sh "./scripts/deploy.sh production ${IMAGE}"
            }
        }
    }
 
    post {
        failure {
            slackSend channel: '#ci-alerts',
                      message: "Build FAILED: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
        }
        success {
            slackSend channel: '#ci-alerts',
                      message: "Build SUCCESS: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
        }
        always {
            cleanWs()
        }
    }
}

Shared Libraries

Shared Libraries позволяют переиспользовать логику между Jenkinsfile разных проектов. Библиотека хранится в отдельном git-репозитории со структурой vars/ для глобальных функций и src/ для классов.

Использование в Jenkinsfile проекта:

@Library('my-shared-lib') _
buildApp(image: 'registry.example.com/myapp')

Jenkins в современном стеке

Jenkins мощный, но требует значительных усилий на поддержку - обновление плагинов, управление agents, бэкапы. Для новых проектов GitHub Actions или GitLab CI часто проще в эксплуатации. Jenkins выбирают при сложных on-premise требованиях или когда уже есть развитая инфраструктура.

Сравнение CI/CD платформ

КритерийGitHub ActionsGitLab CIJenkins
ХостингSaaS + self-hosted runnersSaaS + self-hosted runnersТолько self-hosted
КонфигурацияYAML в .github/workflows/YAML в .gitlab-ci.ymlJenkinsfile (Groovy)
Начало работыМгновенно для GitHub-проектовМгновенно для GitLab-проектовТребует установки и настройки
Marketplace20000+ готовых actionsШаблоны CI/CD1800+ плагинов
Container registryGitHub Packages (GHCR)ВстроенныйЧерез плагин
EnvironmentsВстроенные с approvalsВстроенные с approvalsЧерез плагины
OIDCВстроенныйВстроенныйЧерез плагин
СекретыRepository / Env / OrgProject / Group / InstanceCredentials plugin
СтоимостьБесплатно для public repos, лимиты для privateБесплатно 400 мин/месБесплатный, но нужен сервер
МасштабированиеАвто, managed runnersАвто, managed runnersРучное управление agents
Review appsЧерез actionsВстроенныеЧерез плагины
Кривая обученияНизкаяНизкаяСредняя-высокая
ГибкостьВысокаяВысокаяМаксимальная

Как выбрать

  • GitHub Actions - проект на GitHub, команда до 50 человек, стандартные пайплайны
  • GitLab CI - нужен self-hosted git, review apps, встроенный registry, DevSecOps из коробки
  • Jenkins - сложные on-premise требования, нестандартные интеграции, уже существующая инфраструктура Jenkins