Непрерывная интеграция — это автоматические проверки на каждый коммит, которые не пускают неработающий код в основную ветку. Материал разбирает практическую сторону: из чего состоит сценарий GitHub Actions, какой набор проверок закрывает реальные риски, как разнести их по двум дорожкам ради latency обратной связи, чем сократить время полного прогона в разы, когда переводить нагрузку на собственные исполнители и как сформулировать задачу ИИ-агенту, чтобы конфигурация собралась без потери охвата.
- Содержание
- Зачем нужен CI и что он проверяет?
- Из чего состоит сценарий GitHub Actions?
- Почему единый прогон на каждый коммит не работает?
- Как собрать дорожку fast?
- Как собрать дорожку full?
- Как сократить время полного прогона?
- Кэш зависимостей по хешу файлов блокировки
- Кэш слоёв Docker
- Параллельный запуск тестов
- Когда нужны собственные исполнители?
- Три критерия перехода
- Установка исполнителя
- Безопасность: неудаляемое требование
- Масштабирование через Kubernetes
- Что ломается после миграции
- Как включить блокировку слияния?
- Как поручить сборку CI ИИ-агенту?
- 1. Источник истины
- 2. Поимённый список охвата
- 3. Целевая структура
- 4. Жёсткие ограничения
- 5. Отчёт о работе
- 6. Запрос на слияние без слияния
- Шаблон: CI существовал и был удалён
- Шаблон: CI не было никогда
- Как проверить работу агента?
- Частые вопросы
- Как хранить секреты для CI?
- Можно ли ограничить набор допустимых действий?
- Сколько процессов задавать для pytest?
- Что делать с плавающими падениями?
- Чем GitHub Actions отличается от GitLab CI?
- Заключение
Содержание
- Зачем нужен CI и что он проверяет?
- Из чего состоит сценарий GitHub Actions?
- Почему единый прогон на каждый коммит не работает?
- Как собрать дорожку fast?
- Как собрать дорожку full?
- Как сократить время полного прогона?
- Когда нужны собственные исполнители?
- Как включить блокировку слияния?
- Как поручить сборку CI ИИ-агенту?
- Как проверить работу агента?
- Частые вопросы
- Заключение
Зачем нужен CI и что он проверяет?
Экономика CI строится на одном факте: стоимость дефекта растёт по мере продвижения к продакшену. Ошибка в аннотации типов, пойманная статическим анализатором через сорок секунд после коммита, стоит минуту внимания. Та же ошибка на боевом сервере стоит инцидента, отката и разбора.
Кроме раннего обнаружения, конфигурация CI закрывает четыре организационных проблемы:
- Расхождение окружений. Прогон идёт на стерильном исполнителе, поэтому забытые переменные окружения, локальные артефакты и незакоммиченные файлы вскрываются немедленно.
- Споры о стиле. Единственный арбитр — форматтер и линтер. Обсуждение отступов на код-ревью прекращается как класс.
- Страх рефакторинга. Зелёный набор тестов даёт право переписывать внутренние механизмы без ручной регрессии.
- Устаревание документации. YAML-файл — исполняемая инструкция по сборке проекта. Она не расходится с реальностью, потому что при расхождении ломается.
Полезный набор проверок для связки Python + PostgreSQL + React выстраивается от дешёвых к дорогим:
| Проверка | Что ловит | Порядок времени |
|---|---|---|
ruff check . | Неиспользуемые импорты, потенциальные ошибки, нарушения правил | секунды |
ruff format --check . | Расхождение с единым форматированием | секунды |
mypy app | Несовпадение типов, обращение к None, неверные сигнатуры | десятки секунд |
npm run lint + build | Ошибки фронта, несобираемый пакет | десятки секунд |
pytest | Регрессии бизнес-логики, поведенческие ошибки | минуты |
| Обратимость миграций | Миграции, которые невозможно откатить | минуты |
| Сборка образа Docker | Расхождение Dockerfile с реальными зависимостями | минуты |
Последние две строки в типовых конфигурациях отсутствуют чаще всего. Проверка обратимости миграций сводится к прогону схемы вперёд, откату до нуля и повторному накату:
alembic upgrade head
alembic downgrade base
alembic upgrade head Если хотя бы один downgrade написан формально, прогон падает. Минута машинного времени в CI против аварийного отката боевой базы — несопоставимые величины.
CI и CD решают разные задачи. CI отвечает на вопрос «код исправен?», CD — на вопрос «как доставить исправный код на серверы». Порядок внедрения строго такой: без надёжных проверок автоматическая доставка лишь ускоряет распространение дефектов по инфраструктуре.
Из чего состоит сценарий GitHub Actions?
Терминология по смыслу совпадает с GitLab CI, Jenkins и Woodpecker — различается синтаксис.
- Workflow — сценарий целиком, один YAML-файл в каталоге
.github/workflows/. - Триггер (
on:) — событие запуска: отправка коммитов, открытие запроса на слияние, расписание, ручной вызов. - Job — задание на отдельной виртуальной машине со своей файловой системой. Задания по умолчанию выполняются параллельно.
- Step — шаг внутри задания: команда оболочки (
run) либо готовое действие (uses). - Runner — машина-исполнитель, арендованная у GitHub или собственная.
- Service — вспомогательный контейнер рядом с заданием: PostgreSQL, Redis, брокер очередей.
Минимальная рабочая конфигурация занимает двадцать строк:
name: check
on:
push:
pull_request:
permissions:
contents: read
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: "3.13"
- run: pip install ruff
- run: ruff check .
На событие push или pull_request поднимается чистая Ubuntu, в неё выкладывается репозиторий, ставится Python, ставится линтер, запускается проверка. Ненулевой код возврата любой команды красит задание в красный.
Версии действий закрепляются мажорным тегом (@v6), а не веткой main: плавающая ссылка ломает воспроизводимость сборки и открывает вектор атаки на цепочку поставок. В критичной инфраструктуре применяется закрепление по хешу коммита. Блок permissions сужает область действия GITHUB_TOKEN — по умолчанию прав выдаётся больше, чем требуется прогону.
Почему единый прогон на каждый коммит не работает?
Полный набор с тестами, миграциями и сборкой образа занимает пять–пятнадцать минут. За это время разработчик переключается на другую задачу и теряет контекст либо перестаёт смотреть на результат вовсе. Обратная связь, приходящая позже двух минут, перестаёт влиять на процесс разработки.
Оптимизация — разнести проверки по двум сценариям с разными триггерами:
| fast | full | |
|---|---|---|
| Триггер | каждая отправка коммитов | запрос на слияние в main |
| Состав | линтер, форматтер, типы, сборка фронта | всё из fast + тесты, миграции, образ |
| Целевое время | до минуты | до пяти минут |
| Внешние службы | не требуются | PostgreSQL, Redis |
| Блокирует слияние | нет | да |
Дорожка fast закрывает порядка 80% ошибок практически бесплатно и не требует поднятия служб. Дорожка full работает барьером перед основной веткой и запускается на порядок реже.
Как собрать дорожку fast?
Два независимых задания — бэкенд и фронт — идут параллельно, поэтому общее время равно времени более медленного из них:
name: fast
on:
push:
pull_request:
permissions:
contents: read
concurrency:
group: fast-${{ github.ref }}
cancel-in-progress: true
jobs:
backend:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: "3.13"
cache: pip
cache-dependency-path: requirements-dev.txt
- run: pip install -r requirements-dev.txt
- run: ruff check .
- run: ruff format --check .
- run: mypy app
frontend:
runs-on: ubuntu-latest
timeout-minutes: 10
defaults:
run:
working-directory: frontend
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v5
with:
node-version: "24"
cache: npm
cache-dependency-path: frontend/package-lock.json
- run: npm ci
- run: npm run lint
- run: npm run build
Блок concurrency с cancel-in-progress: true отменяет предыдущий прогон той же ветки при новой отправке коммитов — на активной разработке это экономит до половины машинного времени и разгружает очередь исполнителей. Параметр timeout-minutes страхует от зависших заданий, которые иначе выедают квоту вплоть до шестичасового лимита.
Как собрать дорожку full?
Здесь появляются службы, миграции и сборка образа. Три задания независимы и выполняются одновременно:
name: full
on:
pull_request:
branches: [main]
permissions:
contents: read
concurrency:
group: full-${{ github.ref }}
cancel-in-progress: true
jobs:
tests:
runs-on: ubuntu-latest
timeout-minutes: 20
services:
postgres:
image: postgres:18
env:
POSTGRES_USER: app
POSTGRES_PASSWORD: app
POSTGRES_DB: app_test
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U app"
--health-interval 5s
--health-timeout 5s
--health-retries 10
redis:
image: redis:8
ports:
- 6379:6379
env:
DATABASE_URL: postgresql+psycopg://app:app@localhost:5432/app_test
REDIS_URL: redis://localhost:6379/0
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: "3.13"
cache: pip
cache-dependency-path: requirements-dev.txt
- run: pip install -r requirements-dev.txt
- name: Параллельные тесты
run: pytest -n auto --dist loadgroup -q
migrations:
runs-on: ubuntu-latest
timeout-minutes: 15
services:
postgres:
image: postgres:18
env:
POSTGRES_USER: app
POSTGRES_PASSWORD: app
POSTGRES_DB: app_migrations
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U app"
--health-interval 5s
--health-timeout 5s
--health-retries 10
env:
DATABASE_URL: postgresql+psycopg://app:app@localhost:5432/app_migrations
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: "3.13"
cache: pip
cache-dependency-path: requirements-dev.txt
- run: pip install -r requirements-dev.txt
- name: Обратимость миграций
run: |
alembic upgrade head
alembic downgrade base
alembic upgrade head
build:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v6
- uses: docker/setup-buildx-action@v4
- uses: docker/build-push-action@v7
with:
context: .
push: false
tags: app:ci
cache-from: type=gha
cache-to: type=gha,mode=max
Проверка состояния службы через --health-cmd обязательна: без неё шаги стартуют раньше готовности PostgreSQL и падают с ошибкой подключения примерно в одном прогоне из пяти.
Как сократить время полного прогона?
Кэш зависимостей по хешу файлов блокировки
Установка зависимостей — обычно самая длинная часть прогона. Ключ кэша строится по хешу файла блокировки: пока package-lock.json или requirements-dev.txt не изменялись, пакеты берутся из кэша, а не из сети. Экономия — от 40 до 90 секунд на каждое задание.
Для типовых случаев достаточно параметра cache у действий setup-python и setup-node. Для нестандартных путей задаётся явный кэш:
- name: Кэш виртуального окружения
uses: actions/cache@v4
with:
path: .venv
key: venv-${{ runner.os }}-py313-${{ hashFiles('requirements-dev.txt') }}
restore-keys: |
venv-${{ runner.os }}-py313-
Параметр restore-keys включает частичное попадание: при отсутствии точного совпадения подтягивается ближайший подходящий кэш, доустанавливаются только изменившиеся пакеты.
Кэш слоёв Docker
Сборка образа без кэша каждый раз переустанавливает системные пакеты и зависимости. Buildx складывает слои в хранилище GitHub Actions через cache-from: type=gha и cache-to: type=gha,mode=max (см. задание build выше). Режим mode=max сохраняет все промежуточные слои, а не только финальный — на типовом Dockerfile повторная сборка ускоряется в три–пять раз.
Параллельный запуск тестов
Модуль pytest-xdist распределяет тесты по процессам. Стандартный арендованный исполнитель Ubuntu даёт 4 vCPU для публичных репозиториев и 2 vCPU для приватных на базовом тарифе, поэтому фиксированное число процессов заменяется автоопределением:
pytest -n auto --dist loadfile -q Главная ловушка распараллеливания — накопительные (order-dependent) тесты, которые делят состояние базы данных и рассчитывают на строгий порядок выполнения. При случайном распределении по процессам они начинают падать хаотично. Ослабление проверок ради зелёного прогона — прямая потеря охвата. Корректных решений два, и оба сохраняют логику тестов нетронутой.
Отдельная база на процесс. Переменная окружения PYTEST_XDIST_WORKER содержит идентификатор процесса (gw0, gw1…), по которому создаётся изолированная база:
import os
import pytest
from sqlalchemy import create_engine, text
from sqlalchemy.engine.url import make_url
@pytest.fixture(scope="session")
def worker_id() -> str:
"""Идентификатор процесса pytest-xdist; 'master' при последовательном запуске."""
return os.environ.get("PYTEST_XDIST_WORKER", "master")
@pytest.fixture(scope="session")
def database_url(worker_id: str) -> str:
"""Создаёт отдельную базу под текущий процесс и возвращает строку подключения."""
base_url = make_url(os.environ["DATABASE_URL"])
db_name = f"{base_url.database}_{worker_id}"
admin_url = base_url.set(
database="postgres",
drivername="postgresql+psycopg",
)
admin_engine = create_engine(admin_url, isolation_level="AUTOCOMMIT")
with admin_engine.connect() as conn:
conn.execute(text(f'DROP DATABASE IF EXISTS "{db_name}"'))
conn.execute(text(f'CREATE DATABASE "{db_name}"'))
admin_engine.dispose()
return str(base_url.set(database=db_name))
Группировка на один процесс. Режим --dist loadfile отправляет все тесты одного файла в один процесс с сохранением порядка. Если накопительный набор разнесён по нескольким файлам, применяется --dist loadgroup и явная метка:
import pytest
@pytest.mark.xdist_group(name="cumulative_payroll")
def test_payroll_accrual_january() -> None:
...
@pytest.mark.xdist_group(name="cumulative_payroll")
def test_payroll_accrual_february() -> None:
...
pytest -n auto --dist loadgroup -q Тесты с одинаковым именем группы гарантированно попадают в один процесс и выполняются в объявленном порядке. Остальные распределяются свободно.
Когда нужны собственные исполнители?
Арендованный исполнитель поднимается с нуля под каждое задание — это его главное достоинство и главное ограничение. Стерильность гарантирует воспроизводимость, но означает, что кэш каждый раз тянется по сети, а вычислительный ресурс жёстко зафиксирован. Собственный исполнитель (self-hosted runner) снимает оба ограничения ценой ответственности за инфраструктуру.
| Критерий | Арендованный | Собственный |
|---|---|---|
| Ресурсы | 2–4 vCPU по умолчанию, платные классы крупнее | любые, включая NVMe и GPU |
| Стерильность | гарантирована | обеспечивается вручную |
| Кэш | по сети из хранилища Actions | локальный диск, скорость на порядок выше |
| Внутренняя сеть | недоступна | доступна напрямую |
| Стоимость | поминутная тарификация | фиксированная аренда сервера |
| Эксплуатация | на стороне GitHub | обновления, очистка диска, мониторинг |
Три критерия перехода
Переход оправдан, когда выполняется хотя бы одно условие:
- Упор в ресурсы. Прогон ограничен процессором или дисковым вводом-выводом: сборка образов, тяжёлые интеграционные тесты, компиляция. NVMe вместо сетевого диска сокращает время сборки Docker и прогона pytest на 30–60%.
- Доступ во внутреннюю сеть. Тесты требуют внутреннего реестра образов, зеркала пакетов или базы, не выставленной наружу.
- Экономика. Расход минут превышает стоимость выделенного сервера. Порог считается просто: суммарные минуты за месяц умножаются на тариф и сравниваются с арендой машины сопоставимой конфигурации.
Оценить текущий расход можно до всякой миграции:
# суммарное потребление минут по репозиториям организации
gh api /orgs/ORG/settings/billing/actions
# длительность последних прогонов конкретного сценария
gh run list --workflow full.yml --limit 50 \
--json displayTitle,createdAt,updatedAt,conclusion Установка исполнителя
Исполнитель ставится на машину без публичных входящих портов: связь с GitHub идёт исходящими соединениями по HTTPS через длинный опрос. Регистрация выполняется одноразовым токеном:
mkdir -p /opt/actions-runner && cd /opt/actions-runner
curl -o runner.tar.gz -L \
https://github.com/actions/runner/releases/download/v2.330.0/actions-runner-linux-x64-2.330.0.tar.gz
tar xzf runner.tar.gz
./config.sh \
--url https://github.com/OWNER/REPO \
--token <REGISTRATION_TOKEN> \
--name build-node-01 \
--labels self-hosted,linux,x64,nvme \
--work /opt/actions-runner/_work
sudo ./svc.sh install runner-user
sudo ./svc.sh start Скрипт config.sh отказывается работать от имени root — это защита, а не помеха: исполнитель запускается под отдельной непривилегированной учётной записью. Задание направляется на машину по меткам:
jobs:
tests:
runs-on: [self-hosted, linux, x64, nvme]
timeout-minutes: 20 Метки — единственный механизм маршрутизации, поэтому их задают осмысленно: архитектура, тип диска, наличие GPU, окружение. Значение self-hosted добавляется автоматически.
Безопасность: неудаляемое требование
Собственные исполнители не подключают к публичным репозиториям. Запрос на слияние из форка запускает произвольный код автора на машине владельца — с доступом к её файловой системе, локальной сети и остаточным данным предыдущих заданий. Это не теоретический риск, а стандартный сценарий компрометации инфраструктуры сборки.
Для приватных репозиториев базовый набор мер выглядит так:
- Одноразовость. Флаг
--ephemeralзаставляет исполнитель отработать ровно одно задание и сняться с регистрации. Остаточное состояние между прогонами исчезает как класс. - Изоляция. Исполнитель размещается в контейнере или отдельной виртуальной машине, а не на хосте с рабочей нагрузкой.
- Группы исполнителей. На уровне организации доступ к машине ограничивается конкретным перечнем репозиториев.
- Ограничение действий. В политике организации включается допуск только проверенных и явно перечисленных действий вместо произвольных.
- Сетевые правила. Исходящий трафик ограничивается зеркалами пакетов и адресами GitHub, входящие подключения закрываются полностью.
Масштабирование через Kubernetes
Одноразовый исполнитель по определению не совместим с постоянной службой systemd: после выполнения задания процесс завершается и требует новой регистрации. Production-ready решение — контроллер actions-runner-controller, который поднимает подсистему исполнителей по запросу и удаляет их после прогона:
helm install arc \
--namespace arc-systems --create-namespace \
oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controller
helm install arc-runner-set \
--namespace arc-runners --create-namespace \
--set githubConfigUrl="https://github.com/OWNER/REPO" \
--set githubConfigSecret.github_token="<TOKEN>" \
--set maxRunners=8 \
--set minRunners=0 \
oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set Имя набора становится значением runs-on, поэтому маршрутизация не зависит от меток:
jobs:
tests:
runs-on: arc-runner-set Параметр minRunners: 0 отключает простаивающие машины полностью — ресурсы потребляются только во время прогона. Компромисс — холодный старт: первое задание ждёт запуска подсистемы. Значение minRunners: 1 убирает задержку ценой постоянного потребления.
Что ломается после миграции
Часть механизмов, работающих на арендованных машинах автоматически, требует настройки:
- Кэш Actions. Действие
actions/cacheпродолжает ходить в облачное хранилище, поэтому выигрыш от локального диска теряется. На постоянных исполнителях кэш зависимостей размещают в локальном каталоге, на одноразовых — поднимают внутреннее зеркало пакетов. - Кэш слоёв Docker. Драйвер
type=ghaзаменяется наtype=registryс внутренним реестром илиtype=localс общим томом. - Очистка диска. На неодноразовых машинах каталог
_workи образы Docker растут неограниченно. Регламентная очистка обязательна. - Предустановленное окружение. Образ арендованного исполнителя содержит десятки готовых инструментов. На своей машине их ставят явно, иначе сценарии падают на отсутствующих зависимостях.
# регламентная очистка на постоянном исполнителе
docker system prune -af --filter "until=72h"
find /opt/actions-runner/_work -mindepth 1 -maxdepth 1 -mtime +7 -exec rm -rf {} + Рабочая гибридная схема: дорожка fast остаётся на арендованных исполнителях, где важна стерильность и не важна мощность, а тяжёлая дорожка full переезжает на собственные машины с NVMe и локальным кэшем. Разделение задаётся одним значением runs-on и не требует переписывания сценариев.
Как включить блокировку слияния?
Красный прогон сам по себе ничего не запрещает — он рисует крестик. Барьер включается защитой ветки: слияние в main становится технически невозможным, пока обязательные проверки не зелёные. Имена в поле contexts совпадают с именами заданий из сценария:
cat > protection.json <<'EOF'
{
"required_status_checks": {
"strict": true,
"contexts": ["tests", "migrations", "build"]
},
"enforce_admins": true,
"required_pull_request_reviews": {
"required_approving_review_count": 1
},
"restrictions": null
}
EOF
gh api --method PUT \
-H "Accept: application/vnd.github+json" \
repos/OWNER/REPO/branches/main/protection \
--input protection.json Флаг strict: true требует актуальности ветки относительно main перед слиянием и защищает от семантического конфликта: обе ветки зелёные по отдельности, после слияния код ломается. Обратная сторона — на high-load репозитории основную ветку приходится подтягивать постоянно, поэтому там подключают очередь слияний.
Как поручить сборку CI ИИ-агенту?
Конфигурация CI близка к идеальной задаче для агента: результат детерминирован, проверяется запуском, а весь контекст — структура проекта, менеджер пакетов, набор команд — лежит в репозитории. Качество результата целиком определяется постановкой. Формулировка «настрой CI» возвращает шаблонный YAML из обучающей выборки, не связанный с конкретным проектом.
Рабочая постановка держится на шести элементах.
1. Источник истины
Если CI когда-то существовал, писать заново нельзя — часть проверок молча потеряется. Прежний сценарий поднимается из истории git:
# коммиты, затрагивавшие каталог сценариев
git log --oneline --all -- .github/workflows/
# коммит, в котором сценарии были удалены
git log --diff-filter=D --oneline --all -- .github/workflows/
# содержимое файла на момент конкретного коммита
git show <commit>:.github/workflows/ci.yml
# восстановление каталога в рабочее дерево
git checkout <commit> -- .github/workflows/ Если CI не было никогда, источником истины служит описание проекта: pyproject.toml, package.json, Makefile, README, инструкция агенту (CLAUDE.md, AGENTS.md).
2. Поимённый список охвата
Проверки перечисляются явно, к списку добавляется запрет на сокращение. Без этого агент оптимизирует по времени прогона и выбрасывает самое медленное — тесты и миграции.
3. Целевая структура
Две дорожки, состав каждой, целевое время, класс исполнителя для каждой дорожки. Заданную архитектуру агент реализует отлично, изобретает — посредственно.
4. Жёсткие ограничения
Пункт, который пропускают чаще всего. Ускорение конфликтует с корректностью, и конфликт по умолчанию решается в пользу зелёного прогона: падающие тесты помечаются как skip, добавляются повторные попытки, снижается строгость линтера. Формулировка — прямой запрет.
5. Отчёт о работе
Что восстановлено из истории, время прогона до и после, что распараллелено и закэшировано. Отчёт — единственный способ принять работу, не перечитывая YAML построчно.
6. Запрос на слияние без слияния
Агент открывает запрос, но не сливает. Проверка охвата человеком — обязательный шаг: именно здесь ловятся потерянные проверки.
Шаблон: CI существовал и был удалён
CI был удалён — восстанови и сразу ускорь.
1. Сначала подними из истории git прежний сценарий, чтобы ничего не потерять:
найди последний коммит, где каталог .github/workflows/ ещё существовал
(git log --oneline --all -- .github/workflows/), и посмотри его содержимое
(git show :.github/workflows/<файл>.yml). Возьми оттуда точный
список проверок: pytest, ruff check, ruff format, mypy, обратимость
миграций, сборка фронта и образа Docker. Охват не терять.
2. Пересобери CI в две дорожки:
- fast (каждая отправка коммитов): ruff check ., ruff format --check .,
mypy app, фронт npm run lint + build. Цель — до минуты.
- full (запрос на слияние в main): pytest + обратимость миграций + сборка.
3. Ускорь full: кэш pip и npm по хешу файлов блокировки, кэш слоёв Docker,
параллельный pytest через pytest-xdist.
ВАЖНО: не сломай накопительные тесты — они order-dependent и делят
состояние базы. Дай каждому процессу свою базу либо сгруппируй
накопительные наборы на один процесс (--dist loadfile / loadgroup).
Тесты ради параллельности НЕ ослаблять: никаких skip, xfail,
повторных попыток и снижения строгости линтера.
4. Верни защиту ветки: зелёная дорожка full обязательна для слияния в main.
5. Покажи, что восстановил из истории, время прогона и что
распараллелил и закэшировал. Открой запрос на слияние; не сливай —
сначала проверю охват и корректность дорожек. Шаблон: CI не было никогда
В проекте нет CI — собери с нуля.
1. Сначала выведи список команд проверки из самого проекта, не выдумывая:
прочитай pyproject.toml (секции tool.ruff, tool.mypy, tool.pytest),
package.json (scripts), Makefile, README и инструкцию агенту.
Покажи получившийся список команд и версии инструментов
ДО написания YAML. Дождись подтверждения.
2. Определи, что нужно тестам для запуска: базы, кэши, брокеры,
переменные окружения. Возьми это из docker-compose и настроек
приложения, а не из общих соображений.
3. Собери две дорожки:
- fast (каждая отправка коммитов): линтер, форматтер, проверка типов,
сборка фронта. runs-on: ubuntu-latest. Цель — до минуты.
- full (запрос на слияние в main): тесты со службами из пункта 2,
обратимость миграций, сборка образа Docker.
runs-on: [self-hosted, linux, x64, nvme].
4. Оптимизация: кэш зависимостей по хешу файлов блокировки, кэш слоёв
Docker, параллельный pytest через pytest-xdist. На собственных
исполнителях вместо type=gha используй type=registry.
Тесты НЕ ослаблять: никаких skip, xfail, повторных попыток и снижения
строгости линтера. Падает при параллельном запуске — изолируй
состояние или сгруппируй набор на один процесс, но не отключай.
5. Закрепи версии действий мажорными тегами, добавь permissions
с минимальными правами и timeout-minutes каждому заданию.
6. Покажи итоговый список проверок, время каждой дорожки и что
закэшировано. Открой запрос на слияние; не сливай. Схема переносится на любую задачу инфраструктуры: источник истины → явный охват → целевая структура → жёсткие ограничения → отчёт → запрос на слияние без слияния. Меняется только содержимое пунктов.
Как проверить работу агента?
Приёмка занимает несколько минут и состоит из четырёх шагов.
Сверка охвата. Команды из старого и нового сценариев сравниваются построчно:
git show <старый-commit>:.github/workflows/ci.yml | grep -E '^\s+(run|uses):'
grep -rE '^\s+(run|uses):' .github/workflows/ Поиск ослабленных тестов. Диагностический признак — новые пропуски и повторные попытки в тестовом коде:
git diff main --stat -- tests/
git diff main -- tests/ | grep -E '^\+.*(skip|xfail|flaky|rerun|maxfail)' Любое совпадение требует разбора. Правки в тестах при задаче «настрой CI» законны только как расстановка меток xdist_group.
Проверка честности параллельности. Прогон обязан быть зелёным при последовательном запуске и дважды подряд при параллельном:
pytest -q # последовательно, эталон
pytest -n auto --dist loadgroup -q # параллельно
pytest -n auto --dist loadgroup -q # повтор: ловим плавающие падения Число собранных тестов во всех трёх прогонах должно совпадать.
Проверка барьера. Открывается пробный запрос на слияние с заведомо неверным кодом; кнопка слияния должна быть заблокирована.
Частые вопросы
Как хранить секреты для CI?
Значения размещаются в секретах репозитория или окружения и подключаются через ${{ secrets.NAME }}. Для доступа к внешней инфраструктуре вместо долгоживущих ключей используется OIDC-аутентификация с временными учётными данными. Секреты не передаются в прогоны из форков — это штатное ограничение, а не ошибка конфигурации.
Можно ли ограничить набор допустимых действий?
В политике организации или репозитория включается режим, разрешающий только действия самого GitHub, проверенных издателей и явно перечисленные сторонние действия по шаблону owner/repo@*. Мера обязательна для собственных исполнителей: она отсекает загрузку произвольного кода из чужих репозиториев внутрь контура сборки.
Сколько процессов задавать для pytest?
Значение -n auto определяет число доступных ядер и остаётся корректным при смене класса исполнителя. Фиксированное число оправдано, когда узким местом становится не CPU, а PostgreSQL: при большом числе процессов база упирается в лимит подключений.
Что делать с плавающими падениями?
Повторные попытки маскируют причину и постепенно обесценивают весь набор тестов. Нестабильный тест либо чинится через изоляцию состояния и отказ от реального времени и сети, либо выносится в отдельную необязательную дорожку с явной пометкой — но не заглушается в основном прогоне.
Чем GitHub Actions отличается от GitLab CI?
Модель одинакова: сценарий, задания, шаги, кэш, службы, собственные исполнители. Практические различия — в GitLab CI сильнее встроенная работа с окружениями и артефактами, в GitHub Actions шире экосистема готовых действий. При переносе меняется синтаксис и способ описания служб, логика двух дорожек сохраняется без изменений.
Заключение
Ценность CI измеряется не соответствием практикам, а конкретной экономией: дефект, пойманный линтером за сорок секунд, обходится на порядки дешевле того же дефекта в продакшене. Разделение на быструю и полную дорожки удерживает latency обратной связи в пределах минуты без потери строгости перед основной веткой, кэш зависимостей, кэш слоёв Docker и параллельный запуск тестов сокращают полный прогон в разы, а перенос тяжёлой дорожки на собственные исполнители снимает упор в ресурсы — при условии одноразовости, изоляции и полного отказа от подключения таких машин к публичным репозиториям. Сборку конфигурации разумно делегировать ИИ-агенту при трёх обязательных условиях: явный источник истины, поимённый список проверок и прямой запрет ослаблять тесты ради скорости — иначе агент оптимизирует по времени прогона и вынесет из охвата ровно то, ради чего CI и заводился.









