CI (Continuous Integration)

CI (Continuous Integration) — практика автоматической проверки изменений в репозитории. После push, Pull Request или Merge Request система CI может установить зависимости, запустить линтеры и тесты, проверить типы, собрать приложение и сохранить результаты.

CI делает проверку изменений воспроизводимой: одинаковые команды выполняются для каждого запуска на подготовленном runner.

commit → push → pipeline → lint → tests → build → artifact
                         └─ ошибка → исправление → новый запуск

Содержание


Концепции CI/CD

CI

Continuous Integration, или непрерывная интеграция, означает регулярное объединение небольших изменений в общий репозиторий с автоматической проверкой.

Типичный CI-процесс:

  1. разработчик создаёт коммит;
  2. отправляет изменения в удалённый репозиторий;
  3. CI запускается по событию push или Pull/Merge Request;
  4. runner получает исходный код и устанавливает зависимости;
  5. запускаются линтеры, тесты и сборка;
  6. результат отображается в интерфейсе репозитория.

CI не гарантирует отсутствие всех ошибок. Он проверяет только те условия, которые описаны в workflow или pipeline.

Continuous Delivery и Continuous Deployment

Continuous Delivery — практика, при которой проект постоянно находится в состоянии, пригодном для выпуска. Публикация обычно требует ручного подтверждения.

Continuous Deployment — автоматическая публикация каждого изменения, прошедшего все проверки.

CI:                  код → тесты → линтеры → сборка
Continuous Delivery: сборка → готовый релиз → ручное подтверждение
Continuous Deployment: сборка → публикация → автоматическое развёртывание
Практика Что автоматизируется Подтверждение
CI Проверка изменений Обычно не требуется
Continuous Delivery Подготовка версии к выпуску Часто требуется перед релизом
Continuous Deployment Выпуск и развёртывание Может не требоваться

Pipeline, workflow, job и step

Термины зависят от системы, но обычно используются следующие понятия:


Структура pipeline

Обычно pipeline делят на этапы:

lint → typecheck → test → build → package → deploy

Не все проекты требуют каждого этапа. Библиотеке может быть достаточно линтеров, тестов и публикации пакета, а веб-приложению — ещё и production-сборки.

Pull Request или Merge Request

При создании запроса на слияние CI обычно должен:

Основная ветка

После слияния в main или master можно дополнительно запускать полный набор тестов, публикацию пакета или образа, создание релизного артефакта и развёртывание в тестовое окружение.

Независимые проверки лучше запускать параллельно. Дорогие проверки можно вынести в отдельные jobs или запускать только для Pull/Merge Request и основной ветки.


GitHub Actions

GitHub Actions хранит workflow в YAML-файлах каталога .github/workflows/.

Минимальный пример:

name: CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - name: Получить исходный код
        uses: actions/checkout@v4

      - name: Установить Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'
          cache: pip

      - name: Установить зависимости
        run: pip install -r requirements.txt

      - name: Запустить линтер
        run: ruff check .

      - name: Запустить тесты
        run: pytest

name

Задаёт название workflow:

name: Python CI

on

Определяет события, запускающие workflow:

on:
  push:
  pull_request:

Ограничение по веткам:

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

Запуск вручную:

on:
  workflow_dispatch:

Запуск по расписанию:

on:
  schedule:
    - cron: '30 2 * * 1'

Время в расписании указывается в UTC.

jobs и runs-on

Раздел jobs содержит задания. Независимые jobs могут выполняться параллельно:

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run lint

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test

runs-on задаёт runner:

runs-on: ubuntu-latest

Также могут использоваться windows-latest и macos-latest, если проект нужно проверять в нескольких ОС.

steps, uses и run

run выполняет команду оболочки:

- name: Запустить тесты
  run: npm test

Многострочная команда записывается через |:

- name: Сборка
  run: |
    npm ci
    npm run build
    npm run package

uses подключает готовое action:

- uses: actions/checkout@v4

Версию action следует указывать явно и регулярно проверять обновления.

Зависимости между jobs

needs запускает job только после успешного завершения другого job:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - run: npm test

  build:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - run: npm run build

Несколько зависимостей:

build:
  needs: [lint, test]
  runs-on: ubuntu-latest
  steps:
    - run: npm run build

Переменные и условия

Переменные можно объявлять на уровне workflow, job или шага:

env:
  NODE_ENV: test

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Режим: $NODE_ENV"

Выражения GitHub Actions записываются в ${{ }}:

- run: echo "Ветка: ${{ github.ref_name }}"

Условие выполнения:

- name: Опубликовать результат
  if: github.ref == 'refs/heads/main'
  run: ./publish.sh

Для сохранения логов независимо от результата используется always():

- name: Сохранить логи
  if: always()
  uses: actions/upload-artifact@v4
  with:
    name: logs
    path: logs/

GitLab CI/CD

GitLab CI/CD обычно описывается в файле .gitlab-ci.yml в корне репозитория.

image: python:3.12

stages:
  - lint
  - test
  - build

lint:
  stage: lint
  script:
    - pip install ruff
    - ruff check .

test:
  stage: test
  script:
    - pip install -r requirements.txt
    - pip install pytest
    - pytest

build:
  stage: build
  script:
    - pip install build
    - python -m build
  artifacts:
    paths:
      - dist/

stages

stages задаёт порядок этапов:

stages:
  - lint
  - test
  - build

Jobs одного stage могут выполняться параллельно. Следующий stage обычно начинается после успешного завершения предыдущего.

Job и script

Job описывается именованным блоком:

unit_tests:
  stage: test
  script:
    - pip install -r requirements.txt
    - pytest

script содержит команды, выполняемые на runner.

image

Для Docker executor можно указать базовый образ:

image: node:22

Для отдельного job:

test:
  image: node:22-alpine
  script:
    - npm ci
    - npm test

before_script и after_script

before_script содержит общие подготовительные команды:

before_script:
  - npm ci

after_script подходит для диагностических действий:

after_script:
  - echo "Job завершён"

rules

rules управляет условиями запуска:

build:
  script:
    - npm run build
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'

needs

needs позволяет запускать job сразу после нужного задания, не дожидаясь всех jobs предыдущего stage:

build:
  stage: build
  needs: [test]
  script:
    - npm run build

Автоматический запуск тестов и линтеров

Назначение проверок

Часто проверки выполняют в таком порядке:

format/lint → typecheck → unit tests → integration tests → build

Node.js: GitHub Actions

name: Node CI

on:
  push:
  pull_request:

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm

      - run: npm ci
      - run: npm run lint
      - run: npm run format:check
      - run: npm run typecheck
      - run: npm test -- --ci
      - run: npm run build

npm ci предназначена для чистой установки по lock-файлу. В CI она обычно предпочтительнее npm install.

Python: GitLab CI

image: python:3.12

stages:
  - check
  - test

lint:
  stage: check
  script:
    - pip install ruff
    - ruff check .
    - ruff format --check .

test:
  stage: test
  script:
    - pip install -r requirements.txt
    - pip install pytest
    - pytest -q

Отчёт о тестах

pytest может создавать JUnit XML:

pytest --junitxml=reports/junit.xml

Файл можно сохранить как артефакт:

- name: Запустить тесты
  run: pytest --junitxml=reports/junit.xml

- name: Загрузить отчёт
  if: always()
  uses: actions/upload-artifact@v4
  with:
    name: test-report
    path: reports/junit.xml

Если тестовая команда завершилась с ненулевым кодом, job должен завершиться ошибкой. Не следует скрывать ошибку без явной причины.


Кеширование зависимостей

Кеш сохраняет данные между запусками и уменьшает время установки зависимостей. Job должен работать и при пустом кеше: cache — это оптимизация, а не источник истины.

Что кешируют

Обычно не стоит без необходимости кешировать готовый node_modules или виртуальное окружение Python: такие каталоги могут быть большими и зависеть от ОС или версии runtime.

GitHub Actions: npm

- uses: actions/setup-node@v4
  with:
    node-version: 22
    cache: npm
    cache-dependency-path: package-lock.json

- run: npm ci

GitHub Actions: Python

- uses: actions/setup-python@v5
  with:
    python-version: '3.12'
    cache: pip
    cache-dependency-path: requirements.txt

- run: pip install -r requirements.txt

Явный cache action

- name: Кешировать pip
  uses: actions/cache@v4
  with:
    path: ~/.cache/pip
    key: pip-${{ runner.os }}-${{ hashFiles('requirements.txt') }}
    restore-keys: |
      pip-${{ runner.os }}-

В ключ обычно включают ОС, версию runtime и хеш lock-файла:

<инструмент>-<ОС>-<версия>-<хеш зависимостей>

GitLab CI

variables:
  PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"

cache:
  key:
    files:
      - requirements.txt
  paths:
    - .cache/pip

Для Node.js:

cache:
  key:
    files:
      - package-lock.json
  paths:
    - .npm/

before_script:
  - npm ci --cache .npm --prefer-offline

Cache и artifacts

Cache предназначен для ускорения следующих запусков и может быть удалён или заменён.

Artifact — результат конкретного job: собранное приложение, отчёт, пакет или архив. Он передаётся другим jobs или скачивается пользователем.


Матрицы сборок

Матрица позволяет запустить одно и то же задание для нескольких комбинаций параметров: версий языка, операционных систем, баз данных или режимов проекта.

Матрица полезна для проверки совместимости, но увеличивает количество запусков. Включайте только действительно поддерживаемые комбинации.

GitHub Actions

name: Compatibility

on:
  push:
  pull_request:

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, windows-latest]
        python-version: ['3.11', '3.12']

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}

      - run: pip install -r requirements.txt
      - run: pytest

Здесь создаются четыре задания:

Ubuntu + Python 3.11
Ubuntu + Python 3.12
Windows + Python 3.11
Windows + Python 3.12

fail-fast: false позволяет дождаться результатов остальных комбинаций, даже если одна завершилась ошибкой.

include и exclude

strategy:
  matrix:
    node-version: [20, 22]
    os: [ubuntu-latest, windows-latest]
    include:
      - node-version: 22
        experimental: true
    exclude:
      - node-version: 20
        os: windows-latest

include добавляет или расширяет комбинации, а exclude исключает ненужные варианты.

GitLab CI

В GitLab аналогичная задача описывается через parallel:matrix:

test:
  image: python:$PYTHON_VERSION
  parallel:
    matrix:
      - PYTHON_VERSION: ['3.11', '3.12']
        DATABASE: ['sqlite', 'postgres']
  script:
    - pip install -r requirements.txt
    - pytest

При использовании матриц важно, чтобы переменные действительно использовались в job. Иначе pipeline создаст несколько одинаковых заданий.


Артефакты сборки

Артефакт — файл или каталог, который job сохраняет после выполнения. Это может быть:

GitHub Actions: загрузка

- name: Собрать приложение
  run: npm run build

- name: Загрузить артефакт
  uses: actions/upload-artifact@v4
  with:
    name: web-dist
    path: dist/
    retention-days: 7

Артефакты диагностики можно загрузить даже при ошибке:

- name: Загрузить логи
  if: always()
  uses: actions/upload-artifact@v4
  with:
    name: test-logs
    path: logs/

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

Job выполняется на отдельном runner, поэтому файлы из предыдущего job не появляются автоматически. Для передачи используйте artifact:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: web-dist
          path: dist/

  package:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: web-dist
          path: dist/
      - run: tar -czf web-dist.tar.gz dist/

GitLab CI

build:
  stage: build
  script:
    - npm ci
    - npm run build
  artifacts:
    name: "web-dist-$CI_COMMIT_SHORT_SHA"
    paths:
      - dist/
    expire_in: 7 days

Передача артефакта в следующий job:

package:
  stage: package
  needs:
    - job: build
      artifacts: true
  script:
    - tar -czf web-dist.tar.gz dist/

Артефакты и публикация

Сохранить артефакт в CI и опубликовать его пользователям — разные операции. Артефакт может быть временным результатом pipeline, а публикация отправляет пакет в registry, release или объектное хранилище.

Не следует помещать в артефакты пароли, токены, приватные ключи, .env с секретами и диагностические дампы с конфиденциальными данными.


Секреты и переменные

Обычные настройки можно хранить в переменных pipeline:

env:
  APP_ENV: test

Секреты хранятся в GitHub Secrets, GitLab CI/CD Variables или внешнем secret manager.

GitHub Actions:

- name: Опубликовать пакет
  env:
    REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
  run: ./publish.sh

Секреты нельзя выводить в лог:

# Плохо
- run: echo "Token: ${{ secrets.TOKEN }}"

Даже если CI маскирует значения, не следует передавать секреты через команды, артефакты или диагностические файлы без необходимости.


Практические рекомендации

Фиксируйте версии

Используйте lock-файлы и фиксируйте версии runtime:

python-version: '3.12'
node-version: 22

Для actions и внешних компонентов выбирайте контролируемые версии. Обновления выполняйте отдельно и проверяйте в review.

Делайте job воспроизводимыми

Job не должен зависеть от состояния предыдущего запуска. Он должен сам получить исходный код и установить зависимости либо получить явно переданный артефакт.

Проверяйте проект до сборки

Lint, typecheck и тесты должны завершаться до публикации артефакта. Не публикуйте результат, если обязательные проверки не пройдены.

Разделяйте быстрые и долгие проверки

Быстрые проверки дают обратную связь раньше. Долгие e2e-тесты и тесты совместимости можно запускать параллельно или отдельным job.

Сохраняйте диагностику

Отчёты и логи полезно загружать с условием always():

- name: Сохранить логи
  if: always()
  uses: actions/upload-artifact@v4
  with:
    name: logs
    path: logs/

Ограничивайте права workflow

GitHub Actions может явно ограничить разрешения:

permissions:
  contents: read

Права расширяйте только для операций, которым они нужны.

Проверяйте изменения CI

Файлы CI являются исполняемой частью проекта. В review проверяйте внешние actions, команды публикации, права, условия запуска и работу с секретами.


Общий пример

Ниже workflow GitHub Actions для Node.js-проекта. Он запускает линтеры и тесты, использует кеш npm, проверяет проект на двух версиях Node.js, собирает приложение и сохраняет каталог dist как артефакт.

Файл .github/workflows/ci.yml:

name: Node CI

on:
  push:
    branches: [main, develop]
  pull_request:

permissions:
  contents: read

jobs:
  check:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        node-version: [20, 22]

    steps:
      - name: Получить исходный код
        uses: actions/checkout@v4

      - name: Установить Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: npm

      - name: Установить зависимости
        run: npm ci

      - name: Проверить стиль
        run: npm run lint

      - name: Проверить типы
        run: npm run typecheck

      - name: Запустить тесты
        run: npm test -- --ci

      - name: Собрать приложение
        run: npm run build

      - name: Загрузить артефакт
        if: matrix.node-version == 22
        uses: actions/upload-artifact@v4
        with:
          name: web-dist
          path: dist/
          retention-days: 7

В этом workflow:


Краткая шпаргалка

GitHub Actions

name: CI

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test
strategy:
  matrix:
    node-version: [20, 22]
- uses: actions/upload-artifact@v4
  with:
    name: build
    path: dist/

GitLab CI/CD

stages:
  - test
  - build

test:
  stage: test
  script:
    - npm ci
    - npm test

build:
  stage: build
  script:
    - npm run build
  artifacts:
    paths:
      - dist/

Основные понятия

Понятие Назначение
Workflow/pipeline Весь автоматизированный процесс
Job Отдельное задание на runner
Step Команда или действие внутри job
Runner Среда выполнения job
Cache Ускорение повторных запусков
Artifact Результат, передаваемый между jobs или сохраняемый после запуска
Matrix Запуск job для набора комбинаций
Secret Защищённое значение, например токен

Чек-лист CI

Перед использованием pipeline проверьте: