Оптимизация GitHub Actions: −50% времени CI (реальные примеры)
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 смотреть цифры. Этот человек всегда забывает.
Шесть шагов по порядку. Сначала измерьте, чтобы резать реально медленное.
Типичные ошибки
| Ошибка | Чем вредит | Как чинить |
|---|---|---|
| Кешировать 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
-
concurrencyccancel-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, платформой и тем, кто её сломал последним. Назначьте одного человека, дайте ему две недели, замерьте до/после. Готово.
