Generic selectors
Exact matches only
Search in title
Search in content
Post Type Selectors

CI на GitHub Actions в 2026: настройка, ускорение, задача агенту

Linux и DevOps

Непрерывная интеграция — это автоматические проверки на каждый коммит, которые не пускают неработающий код в основную ветку. Материал разбирает практическую сторону: из чего состоит сценарий GitHub Actions, какой набор проверок закрывает реальные риски, как разнести их по двум дорожкам ради latency обратной связи, чем сократить время полного прогона в разы, когда переводить нагрузку на собственные исполнители и как сформулировать задачу ИИ-агенту, чтобы конфигурация собралась без потери охвата.

Содержание
  1. Содержание
  2. Зачем нужен CI и что он проверяет?
  3. Из чего состоит сценарий GitHub Actions?
  4. Почему единый прогон на каждый коммит не работает?
  5. Как собрать дорожку fast?
  6. Как собрать дорожку full?
  7. Как сократить время полного прогона?
  8. Кэш зависимостей по хешу файлов блокировки
  9. Кэш слоёв Docker
  10. Параллельный запуск тестов
  11. Когда нужны собственные исполнители?
  12. Три критерия перехода
  13. Установка исполнителя
  14. Безопасность: неудаляемое требование
  15. Масштабирование через Kubernetes
  16. Что ломается после миграции
  17. Как включить блокировку слияния?
  18. Как поручить сборку CI ИИ-агенту?
  19. 1. Источник истины
  20. 2. Поимённый список охвата
  21. 3. Целевая структура
  22. 4. Жёсткие ограничения
  23. 5. Отчёт о работе
  24. 6. Запрос на слияние без слияния
  25. Шаблон: CI существовал и был удалён
  26. Шаблон: CI не было никогда
  27. Как проверить работу агента?
  28. Частые вопросы
  29. Как хранить секреты для CI?
  30. Можно ли ограничить набор допустимых действий?
  31. Сколько процессов задавать для pytest?
  32. Что делать с плавающими падениями?
  33. Чем GitHub Actions отличается от GitLab CI?
  34. Заключение

Содержание

Зачем нужен 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 и заводился.

Оцените статью
ctrllife.ru
Подписаться
Уведомить о
guest
0 комментариев
Старые
Новые Популярные
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x