Перейти к основному содержимому

Оптимизация GitHub Actions: −50% времени CI (реальные примеры)

· 8 мин. чтения
Artur Pan
CTO & Co-Founder at PanDev

14-минутный CI-пайплайн — это не просто 14 минут ожидания. GitHub Octoverse 2024 отчитался: медианный enterprise-репозиторий прогоняет pull request через CI 4.2 раза перед merge — ретраи, пуши после ревью, починка flaky-тестов. Это почти час компьюта на один PR. В команде, шипящей 200 PR в неделю, CI-бюджет вам ничего не приносит, а context-switch налог стоит вам четверга senior-разработчика.

Это how-to. Шесть шагов, которые стабильно режут время GitHub Actions CI на 50%+ на реальных репо, которые мы помогали оптимизировать. Без теории; у каждого шага есть патч, который можно адаптировать.

{/* truncate */}

Проблема

Большинство CI-пайплайнов растут наслоениями. Джун добавляет test-workflow. Senior добавляет линт. Release-инженер добавляет security-скан. Никто не держит бюджет на общее время CI, так что оно лезет вверх, пока кто-то на on-call не пожалуется на очередь в четверг днём.

Опрос CNCF DevOps 2024: 32% команд тратят больше 20 минут на один CI-прогон на монорепо. Работа Gloria Mark из UC Irvine про внимание показывает: разработчик, ждущий CI 15+ минут регулярно, теряет способность вернуться в фокус после ожидания — это совпадает с нашими данными: PR, отбрасываемые CI больше 2 раз, показывают cycle time на 23% длиннее, чем first-pass PR, даже после нормализации по размеру.

Стоимость компаундится:

РасходТипичный масштаб
GitHub Actions минуты (платные планы)$0.008/мин × тысячи прогонов = реальные доллары
Время ожидания разработчика10–20 мин × 4 retry × 200 PR/неделя = 130+ dev-часов/неделю
Налог на context switchПо Gloria Mark — до 23 минут на полное возвращение в фокус
Атрибуция flakinessИнженеры перестают доверять CI; «ретраить до зелёного» становится паттерном

CI медленнее 5 минут на среднем репо — это проблема продуктивности, притворяющаяся проблемой инфры.

Фреймворк: 6 шагов

Шаг 1 — Замерьте бейзлайн

Прежде чем резать, узнайте, что медленное. GitHub Actions уже показывает длительность по job и step. Вытащите последние 30 дней вашего основного CI workflow и ранжируйте job по медианной длительности.

# GitHub CLI для дампа workflow-прогонов за 30 дней
gh run list --workflow=ci.yml --limit 200 --json databaseId,displayTitle,conclusion,createdAt,updatedAt > runs.json

Три цифры до Шага 2:

  • Медианная end-to-end длительность workflow
  • p95 длительность workflow (важнее медианы для боли разработчика)
  • Топ-3 job по потраченному времени

Без этих цифр вы будете резать не то. Одна команда, с которой мы работали, потратила неделю на параллелизацию тестов — которые не были узким местом. Узким был docker build.

Шаг 2 — Кешируйте агрессивно (самый большой одиночный выигрыш)

Dependency install и build-артефакты обычно 40–60% времени CI. GitHub actions/cache@v4 — это изменение с самой быстрой ROI.

- name: Cache node_modules
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-

- name: Cache Maven dependencies
uses: actions/cache@v4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}

- name: Cache Docker layers
uses: docker/build-push-action@v5
with:
cache-from: type=gha
cache-to: type=gha,mode=max

Ошибка большинства команд: кешировать output-директорию (dist/, target/) вместо dependency-директории. Вы хотите кешировать то, что дорого выкачивать, а не то, что дёшево пересобрать из исходников.

Выигрыш, который мы обычно видим: 30–50% на JS-heavy репо, 25–40% на Maven/Gradle, 40–60% на Docker-сборках.

Шаг 3 — Параллелизуйте через matrix

Последовательные job — это ленивый CI. Если у вас 4 тестовых сьюта, не зависящих друг от друга — запускайте параллельно.

jobs:
test:
strategy:
matrix:
suite: [unit, integration, e2e, contract]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm test -- --suite=${{ matrix.suite }}

Две ловушки:

  • Не пере-параллелизуйте. Matrix из 20 даёт вам 20 старт-ап расходов ранеров (~30 секунд каждый). Свыше 8 matrix-entry, старт-ап съедает выигрыш.
  • Flaky-тесты амплифицируются. 1% flaky-rate на сьют превращается в ~4% flake-rate на matrix из 4, если любой failure валит PR. Чините flake'и до параллелизации.

Шаг 4 — Дробите тяжёлые job; дешёвые чеки сначала

Не блокируйте падение линта за 10-минутным тест-сьютом. Гейтите дорогие job через дешёвые.

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

test:
needs: lint # запускается только если lint прошёл
strategy:
matrix:
suite: [unit, integration]
runs-on: ubuntu-latest

Dev-цикл: push → lint падает за 30 секунд → починка → push. Это 2-минутный цикл вместо 12 минут ожидания, чтобы узнать, что вы оставили console.log.

Шаг 5 — Урезайте прогоны через change detection

Монорепо получают здесь гигантскую выгоду. Если PR трогает только services/billing/** — не надо запускать тесты services/auth.

jobs:
changes:
runs-on: ubuntu-latest
outputs:
billing: ${{ steps.filter.outputs.billing }}
auth: ${{ steps.filter.outputs.auth }}
steps:
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
billing:
- 'services/billing/**'
auth:
- 'services/auth/**'

test-billing:
needs: changes
if: needs.changes.outputs.billing == 'true'
runs-on: ubuntu-latest
steps: [...]

Осторожно: path-фильтры, скипающие полный regression-сьют перед merge в main, частый источник «зелёный PR, сломанный main». Гейтите path-filter skip на PR, но запускайте полный сьют на push'ах в main. Это безопасный паттерн.

Шаг 6 — Мониторьте и держите линию

Самый сложный шаг. Оптимизации деградируют — добавляются новые workflow, растут зависимости, копятся тесты. Без бюджета через 6 месяцев вы снова на 14 минутах.

Поставьте hard ceiling. Пример: CI p95 должен оставаться под 8 минутами. Добавьте workflow, падающий, если бюджет превышается две недели подряд.

Команды на engineering-metrics платформах получают это бесплатно — PanDev Metrics трекает длительности CI job наравне с другими delivery-метриками и подсвечивает тренд в еженедельных дэшбордах. Без этого кому-то надо ставить месячный календарный reminder смотреть цифры. Этот человек всегда забывает.

Flow-плейбук оптимизации CI: measure, cache, parallelize, split, prune, monitor Шесть шагов по порядку. Сначала измерьте, чтобы резать реально медленное.

Типичные ошибки

ОшибкаЧем вредитКак чинить
Кешировать output вместо зависимостейCache miss на каждом PR; зависимости качаются каждый разКешируйте ~/.npm, ~/.m2, node_modules, не dist/
npm install вместо npm ciНедетерминированно, медленнее, пишет в lockfileВсегда npm ci в CI
Matrix из 20+Доминирует стоимость старта ранеровДержите matrix ≤8, бейте на отдельные workflow, если больше
Полный тест-сьют на каждом PR в монорепо80% тестов не релевантны diff'уPath filters, плюс полный сьют на main
Нет timeout на jobЗалипшие job тихо жгут billing-минутыtimeout-minutes: 15 на каждой job
ubuntu-latest для простых скриптовТянет тяжёлый образ для 2-строчной задачиМеньшие ранеры или более быстрые образы, где возможно
Игнор workflow concurrencyМножественные прогоны одной ветки копятсяconcurrency group с cancel-in-progress: true

Последнее — concurrency — самый ленивый выигрыш, который пропускает большинство команд.

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

Одна строка. Отменяет in-flight прогон, когда приходит новый push. На активных PR экономит 20–30% compute-минут.

Чек-лист (копируйте и используйте)

  • Бейзлайн замерен: медиана + p95 длительность + топ-3 job по времени
  • Dependency-кеши настроены (npm, maven, docker layers)
  • Тесты параллелизованы через matrix (макс 8)
  • Дешёвые чеки (lint, format) гейтят дорогие (тесты, сборка)
  • Path filters используются для монорепо; полный сьют на main
  • concurrency c cancel-in-progress: true на PR-workflow
  • timeout-minutes стоит на каждой job
  • Flaky-тесты починены, а не ретраются (ретраи их прячут)
  • Бюджет длительности CI опубликован; алерт на p95 > бюджета

Как мерить успех

Четыре цифры, каждую неделю:

  • Медиана длительности CI. Таргет: −50% за 4 недели после применения чек-листа.
  • p95 длительность CI. Таргет: −40%. p95 важнее медианы для боли разработчика.
  • CI retry rate на PR. Таргет: меньше 1.5x. Выше 2x — flake'и или неправильное гейтирование.
  • Compute-минут в неделю. Таргет: −30% (вы скешируете много пере-скачиваний).

Две из этих четырёх — медиана длительности и retry rate — коррелируют напрямую с cycle time разработчика. Команды, срезавшие время CI на 50%, обычно видят падение cycle time на 10–15% за квартал, потому что loop становится плотнее.

Когда этот фреймворк не подходит

Два случая:

  • Matrix-зависимое тестирование, которое по своей природе последовательно. Некоторые integration-сьюты не параллелятся, потому что делят тест-БД. Сначала почините инфраструктуру тестов (ephemeral DB, test containers), потом возвращайтесь.
  • Очень маленькие репо (CI под 5 минут). Не оптимизируйте ниже 3 минут — усилия того не стоят. Инженеро-часы лучше потратить на другое.

Контринтуитивный вывод

Команды, инвестирующие 2 недели в оптимизацию CI, отбивают эту инвестицию за 6–8 недель сэкономленного dev-времени, по нашим данным с 40+ клиентскими командами. Это самая высоко-ROI неделя, которую можно потратить не на шиппинг фич. Большинство команд этого никогда не делают, потому что нет единого владельца — CI живёт между DevOps, платформой и тем, кто её сломал последним. Назначьте одного человека, дайте ему две недели, замерьте до/после. Готово.

Связанное чтение

Попробуйте сами — бесплатно

Подключите IDE-плагин за 2 минуты и увидьте свои реальные метрики. Без карты, без обязательств.

Попробовать бесплатно