«Если что-то больно делать — делай это чаще» — принцип непрерывной интеграции
CI/CD — практика, при которой каждое изменение кода автоматически проверяется и доставляется на сервер: прогоняются тесты и линтеры, собирается образ, приложение выкатывается в прод — без ручных шагов и «выложу вечером руками». Это часть культуры DevOps (сближение разработки и эксплуатации). Даже одиночному разработчику базовые навыки CI/CD нужны, чтобы автоматически публиковать свои проекты.
- CI (Continuous Integration, непрерывная интеграция) — часто вливать код в общую ветку, и на каждое вливание автоматически гонять сборку, тесты и линтеры. Ломающее изменение ловится сразу, а не через неделю.
- CD расшифровывают двояко:
- Continuous Delivery (доставка) — пайплайн автоматически готовит готовый к выкатке артефакт, но кнопку «в прод» нажимает человек.
- Continuous Deployment (развёртывание) — выкатка в прод тоже автоматическая, без ручного подтверждения.
CI CD
push ─▶ сборка ─▶ тесты ─▶ линт ─▶ [артефакт] ─▶ деплой на сервер
└──────── проверка кода ────────┘ └──── доставка в прод ────┘
Workflow — принятая в команде схема веток и того, что происходит при действиях в них. Самый распространённый — gitflow, где у каждой ветки своя роль и своё окружение:
| Ветка | Версия | Окружение |
|---|---|---|
| main / master | финальная, стабильная | production (боевой сервер) |
| develop | сырая, в активной разработке | dev / тестовый сервер |
| stage | кандидат в релиз | staging (QA и бизнес-тесты) |
| feature/… | отдельная фича | локально, без деплоя |
Смысл в том, что ветка определяет, куда катить: пуш в develop собирается на тестовый сервер, а main всегда соответствует тому, что крутится в проде. Правила «какая ветка → какой пайплайн» задаются в конфиге (ключ branches).
Все они работают по одному принципу: конфигурационный файл описывает, по какому событию (push, pull request, тег) на чистой машине-раннере выполнить набор команд. Отличаются лишь тем, встроены ли в git-хостинг.
| Тип | Инструменты |
|---|---|
| Встроенные в хостинг | GitHub Actions, GitLab CI/CD, Bitbucket Pipelines |
| Внешние, подключаемые | Jenkins, CircleCI, TeamCity, Semaphore CI |
Встроенные не требуют отдельной инфраструктуры — конфиг лежит в репозитории и запускается там же. Внешние (особенно Jenkins) гибче и мощнее, но их надо разворачивать и обслуживать самому. Инфраструктуру, на которую катят, часто готовят через Terraform (IaC) прямо из пайплайна.
Пайплайн (pipeline) — последовательность этапов, через которые прогоняется каждое изменение. Если этап падает — пайплайн останавливается, дальше код не идёт.
checkout ─▶ install ─▶ lint ─▶ test ─▶ build ─▶ deploy
(забрать (поставить (стиль) (тесты) (Docker- (выкатить
код) зависимости) образ) на сервер)
- lint — статическая проверка стиля и ошибок (flake8, ruff);
- test — автотесты (pytest,
manage.py test); - build — сборка артефакта, чаще всего Docker-образа;
- deploy — доставка на сервер (см. Развертывание проекта).
GitHub Actions — встроенный в GitHub CI/CD-инструмент. Он сканирует папку .github/workflows/ и запускает найденные там .yml-скрипты по описанным в них событиям.
Структура workflow-файла:
on— событие-триггер (push,pull_request,schedule) и фильтр по веткам;jobs— задачи (сборка, тесты, деплой); по умолчанию идут параллельно;runs-on— образ раннера, на котором крутится job (ubuntu-latest);steps— последовательность шагов внутри job;uses— подключить готовый экшен (actions/checkout@v4);run— выполнить команду в шелле;name— человекочитаемое имя шага.
Пароли, ключи и токены нельзя класть в .yml (он в репозитории). Их хранят в настройках репозитория: Settings → Secrets and variables → Actions → New repository secret — и подставляют в конфиг через ${{ secrets.ИМЯ }}.
env:
SECRET_KEY: ${{ secrets.SECRET_KEY }}
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}Типичные секреты:
SECRET_KEY,DB_PASSWORD,SSH_HOST,SSH_USER,SSH_KEY. GitHub маскирует их значения в логах, но только если они пришли из secrets, а не напечатаны в коде.
Создаём .github/workflows/ci.yml. Пайплайн: на каждый пуш в main поднимается PostgreSQL, ставятся зависимости, гоняются линтер и тесты, и при успехе код выкатывается на сервер.
name: Тесты и деплой
on:
push:
branches: [main] # запускать только на пуш в main
jobs:
build:
runs-on: ubuntu-latest # раннер — свежая Ubuntu
env: # секреты доступны всем шагам job
SECRET_KEY: ${{ secrets.SECRET_KEY }}
DEBUG: "False"
DB_NAME: ${{ secrets.DB_NAME }}
DB_USER: ${{ secrets.DB_USER }}
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
DB_HOST: localhost
DB_PORT: 5432
services: # сопутствующие контейнеры на время job
postgres:
image: postgres:16
env:
POSTGRES_DB: ${{ secrets.DB_NAME }}
POSTGRES_USER: ${{ secrets.DB_USER }}
POSTGRES_PASSWORD: ${{ secrets.DB_PASSWORD }}
ports: ["5432:5432"]
options: >- # ждать, пока база реально готова принимать соединения
--health-cmd pg_isready
--health-interval 5s
--health-timeout 5s
--health-retries 5
steps:
- name: Забрать код
uses: actions/checkout@v4
- name: Установить Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Установить зависимости
run: pip install -r requirements.txt
- name: Линтинг
run: ruff check .
- name: Тесты
run: python manage.py test
- name: Деплой на сервер
uses: appleboy/ssh-action@v1 # зайти по SSH и выполнить команды
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_KEY }} # приватный ключ, НЕ пароль
script: |
cd /home/deploy/project
git pull origin main
source venv/bin/activate
pip install -r requirements.txt
python manage.py migrate --noinput
python manage.py collectstatic --noinput
sudo systemctl restart gunicornКак это читать. Шаги идут сверху вниз и падение любого останавливает пайплайн: не прошёл линт — до тестов не дойдёт, не прошли тесты — не будет деплоя. services поднимает временный PostgreSQL, чтобы тестам было куда ходить. Последний шаг заходит на сервер по SSH-ключу и выполняет тот же redeploy, что делался бы руками.
Вход по ключу, а не по паролю. В
appleboy/ssh-actionпередавайkey(приватный SSH-ключ из secrets), а неpassword. Подход с паролем и утилитойexpect(скриптыpull.exp, автоматически отвечающие на запрос пароля) хрупок и небезопасен — храни ключ в secrets и забудь про пароли в пайплайне.
Matrix (матрица) — прогнать одну job на нескольких вариантах окружения сразу: разные версии Python, ОС, зависимостей. GitHub размножит job по всем комбинациям и запустит параллельно — библиотеку так проверяют на всех поддерживаемых версиях за один пуш.
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12", "3.13"] # 3 параллельные job
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }} # своя версия в каждой
- run: pip install -r requirements.txt
- run: pytestКаждый запуск ставит зависимости с нуля — на большом проекте это минуты. Кэш сохраняет скачанные пакеты между запусками и переиспользует, пока не изменился файл зависимостей. У setup-python он встроен параметром cache:
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip # кэшировать кэш pip
cache-dependency-path: requirements.txt # ключ = хеш этого файлаКлюч кэша — хеш файла зависимостей: пока
requirements.txtне менялся — берётся готовый кэш, при правке — пересобирается. Так CI не качает одно и то же на каждый пуш.
Другой стиль CD (вместо git pull на сервере) — на релиз собрать Docker-образ и опубликовать его в реестр (Docker Hub, GHCR), откуда сервер его забирает. Триггер — публикация релиза или тег:
on:
release:
types: [published] # запускать при создании релиза
jobs:
docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3 # логин в реестр
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6 # собрать и запушить
with:
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }} # тег = имя релизаТег образа привязывают к версии релиза (github.ref_name) — так в реестре хранится история версий, и откат в прод — это просто запуск предыдущего образа.
Появилось новое применение, которого у пайплайна изначально не было: сдерживать ИИ-агента.
Агент, работающий без присмотра часами и днями, склонен расползаться — придумывать себе задачи, менять то, о чём не просили, постепенно уходить от правил проекта. Просьба в промпте «не выходи за рамки» этому мешает слабо: это уговор, который модель трактует сама.
CI работает иначе — он не советует, а не пропускает. Упавший тест или линтер агент видит как факт и обязан отработать. В этой роли пайплайн называют backpressure (обратное давление).
Чтобы это работало, нужны две вещи:
- Правило прописано в
AGENTS.md(илиCLAUDE.md) — файле-инструкции, который агент читает при входе в репозиторий: какие проверки обязательны и что делать при падении. - Проверка автоматическая и жёсткая. Ручное ревью в этой схеме не помогает: агент делает сотни коммитов, человек не успевает.
Общее правило автономной работы: чем дольше агент работает без человека, тем важнее внешний критерий «получилось». Без него длинная работа неотличима от длинного мусора. Подробнее — CI как обратное давление и Страховки от слопа.
- Развертывание проекта — то, что автоматизирует шаг деплоя (gunicorn, nginx, systemd);
- Работа с сервером — сервер, на который заходит
ssh-action; там же настраиваются SSH-ключи для входа; - Docker — образ, который CI собирает и публикует на этапе build;
- GIT — события в ветках (push, PR) и есть триггеры пайплайна; сам конфиг лежит в репозитории;
- pytest, линтеры — то, что гоняется на этапах test и lint;
- Автономность агента — CI как одно из трёх условий, при которых агента можно отпустить без присмотра.