Оптимизация пайплайна
Updated Jul 2026
Почему скорость важна
Медленные пайплайны подрывают доверие разработчиков. Когда пайплайн занимает 45 минут, разработчики перестают его ждать и сливают код в любом случае. Когда пайплайн занимает 5 минут, разработчики воспринимают его как надёжную страховочную сеть и действительно смотрят результаты перед слиянием.
Цель: менее 5 минут для цикла обратной связи по PR (линтинг + unit-тесты) и менее 20 минут для полного пайплайна (включая браузерные тесты).
Кэширование
Кэширование — самая эффективная единичная оптимизация. Без кэширования каждый запуск пайплайна скачивает и устанавливает зависимости с нуля — часто тратя 2-5 минут, не добавляющих никакой ценности.
Что кэшировать
| Что | Ключ кэша | Типичная экономия |
|---|---|---|
| Node modules | хэш package-lock.json |
1-3 минуты |
| Python virtualenvs | хэш requirements.txt или poetry.lock |
1-2 минуты |
| Браузеры Playwright | хэш package-lock.json |
2-4 минуты |
| Слои Docker | хэш Dockerfile | 2-10 минут |
| Зависимости Gradle/Maven | хэш build.gradle или pom.xml |
1-5 минут |
| Go-модули | хэш go.sum |
30с-2 минуты |
Кэширование в GitHub Actions
# Cache node_modules (automatic with setup-node)
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
# Cache Playwright browsers (manual)
- uses: actions/cache@v4
id: playwright-cache
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- run: npx playwright install --with-deps
if: steps.playwright-cache.outputs.cache-hit != 'true'
Выход cache-hit позволяет полностью пропустить установку, когда кэш валиден. Это превращает 3-минутную установку браузеров Playwright в 5-секундное восстановление кэша.
Стратегия инвалидации кэша
Ключи кэша должны меняться при изменении зависимостей и оставаться стабильными в остальных случаях:
# Good: Changes only when lockfile changes
key: deps-${{ hashFiles('package-lock.json') }}
# Bad: Changes on every commit (cache is never used)
key: deps-${{ github.sha }}
# Better: Fallback to partial cache match
key: deps-${{ hashFiles('package-lock.json') }}
restore-keys: |
deps-
Запасной вариант restore-keys находит самый свежий кэш, начинающийся с deps-, который может быть немного устаревшим, но значительно быстрее, чем установка с нуля.
Параллелизация
Шардирование тестов
Разделите ваш набор тестов между несколькими раннерами. Каждый раннер выполняет часть тестов, и общее время выполнения примерно равно общее_время / количество_шардов.
# Playwright sharding
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- run: npx playwright test --shard=${{ matrix.shard }}/4
# Jest sharding
strategy:
matrix:
shard: [1, 2, 3]
steps:
- run: npx jest --shard=${{ matrix.shard }}/3
# pytest-xdist (automatic parallelization within a single runner)
steps:
- run: pytest -n auto # Uses all available CPU cores
Выбор правильного количества шардов:
- Начните с 3-4 шардов и измерьте результат
- Каждый шард должен занимать примерно одинаковое время (сбалансированное распределение)
- Слишком много шардов — и накладные расходы на настройку/завершение начинают доминировать
- Слишком мало — и каждый шард по-прежнему медленный
Матричная стратегия для сквозных задач
Комбинируйте шардирование с другими измерениями:
strategy:
fail-fast: false
matrix:
browser: [chromium, firefox]
shard: [1, 2, 3]
# Creates 6 parallel jobs: chromium-1, chromium-2, chromium-3, firefox-1, firefox-2, firefox-3
Параллельные задачи vs параллельные тесты
- Параллельные задачи (матричная стратегия): каждая задача выполняется на отдельном раннере. Хорошо для изоляции и кросс-браузерного тестирования.
- Параллельные тесты (внутри задачи): используйте многоядерные раннеры и инструменты вроде
pytest-xdistили воркеров Jest. Хорошо для CPU-интенсивных unit-тестов.
Для максимальной скорости комбинируйте оба подхода: запустите 4 шарда на 4 раннерах, и внутри каждого шарда запускайте тесты на 2 ядрах CPU.
Стратегия быстрого отказа
Настройка fail-fast контролирует, отменяются ли другие задачи матрицы при сбое одной.
strategy:
fail-fast: true # Cancel all jobs if one fails
fail-fast: false # Let all jobs complete regardless
Когда использовать fail-fast: true:
- Unit-тесты: если один шард падает, остальные, вероятно, тоже сломаны. Отмените их для экономии времени.
- Линтинг/проверка типов: если линтинг не прошёл, нет смысла запускать тесты.
Когда использовать fail-fast: false:
- Браузерные тесты: вы хотите знать полный масштаб сбоев по всем браузерам и шардам. Тест, падающий в Firefox, но проходящий в Chrome — это другой баг, нежели падающий везде.
- Интеграционные тесты с внешними сервисами: сбои могут быть изолированы в конкретных тест-кейсах.
Условное выполнение
Пропускайте дорогую работу, когда она не нужна.
Фильтрация по путям
# Skip tests when only documentation changed
on:
push:
paths-ignore:
- '**.md'
- 'docs/**'
- '.github/ISSUE_TEMPLATE/**'
- 'LICENSE'
# Only run backend tests when backend code changed
on:
push:
paths:
- 'src/api/**'
- 'src/services/**'
- 'tests/integration/**'
Условия на уровне задачи
# Only run browser tests on PRs targeting main
browser-tests:
if: github.event_name == 'pull_request' && github.base_ref == 'main'
# Skip expensive tests for draft PRs
e2e-tests:
if: github.event.pull_request.draft == false
Условия на уровне шага
# Only upload artifacts on failure
- uses: actions/upload-artifact@v4
if: failure()
# Only run deployment on main branch
- run: npm run deploy
if: github.ref == 'refs/heads/main'
Конфигурация пайплайна для производительности тестов
Переменные окружения, влияющие на скорость тестов:
env:
TEST_TIMEOUT: 30000 # 30s timeout per test (prevent hanging tests)
RETRY_COUNT: 2 # Retry failed tests twice (handle infrastructure flakes)
HEADLESS: true # Run browsers in headless mode (faster)
SLOW_MO: 0 # No artificial delay between actions
WORKERS: 4 # Number of parallel test workers
Логика повторных попыток
Повторные попытки должны справляться с инфраструктурными сбоями (таймауты сети, задержки запуска контейнеров), а не с багами тестов. Если тест постоянно требует повторных попыток для прохождения, он нестабильный и его нужно чинить.
# Playwright retry configuration
- run: npx playwright test --retries=2
# Job-level retry (GitHub Actions does not support this natively;
# use reusable workflows or third-party actions)
Настройки таймаутов
Всегда устанавливайте таймауты, чтобы зависшие задачи не занимали раннеры:
jobs:
unit-tests:
runs-on: ubuntu-latest
timeout-minutes: 10 # Kill the job if it takes longer than 10 minutes
browser-tests:
runs-on: ubuntu-latest
timeout-minutes: 30 # Browser tests get more time
Измерение производительности пайплайна
Отслеживайте эти метрики для выявления возможностей оптимизации:
| Метрика | Как измерять | Цель |
|---|---|---|
| Время обратной связи PR | Время от push до результатов unit-тестов | < 5 минут |
| Время полного пайплайна | Время от push до завершения всех проверок | < 20 минут |
| Процент попаданий в кэш | Проверяйте логи cache action | > 90% |
| Процент нестабильных тестов | Тесты, проходящие при повторной попытке | < 2% |
| Время ожидания в очереди | Время ожидания задач свободного раннера | < 1 минуты |
Если время ожидания в очереди высокое, вам нужно больше раннеров или лучшее планирование. Если процент попаданий в кэш низкий, ваши ключи кэша слишком специфичные.
Продвинутые техники оптимизации
Переиспользуемые workflow
Выделяйте общие паттерны в переиспользуемые workflow, чтобы избежать дублирования:
# .github/workflows/reusable-test.yml
on:
workflow_call:
inputs:
test-command:
required: true
type: string
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: ${{ inputs.test-command }}
Оптимизация графа зависимостей
Если ваш репозиторий содержит несколько пакетов (монорепо), тестируйте только пакеты, затронутые изменением:
# Use a tool like Nx or Turborepo to detect affected packages
- run: npx nx affected --target=test --base=origin/main --head=HEAD
Практическое упражнение
- Измерьте текущее время пайплайна от начала до конца. Запишите длительность каждого этапа.
- Добавьте кэширование зависимостей и браузеров Playwright. Измерьте улучшение.
- Добавьте шардирование тестов с 3-4 шардами. Измерьте улучшение.
- Добавьте фильтрацию по путям для пропуска тестов при изменении только документации.
- Установите соответствующие настройки
fail-fastдля каждого типа задачи. - Установите таймауты на все задачи для предотвращения зависших пайплайнов.
- Сравните метрики до и после. Цель: 50% сокращение общего времени пайплайна.