Monorepo CI/CD: pipelines для Turborepo и Nx в GitHub Actions
Monorepo с 20+ пакетами — это удобно для разработки, но кошмар для CI. Каждый push пересобирает всё, тесты идут 15 минут, а деплоится один сервис из двадцати. Turborepo и Nx решают эту проблему через граф зависимостей и инкрементальные билды. Разберём, как настроить быстрый CI/CD для monorepo в GitHub Actions.
Проблема: всё пересобирается при каждом коммите
Наивный подход к CI в monorepo:
# ПЛОХО: пересобираем всё
- run: npm ci
- run: npm run build --workspaces
- run: npm run test --workspaces
- run: npm run lint --workspaces
Изменили одну строку в packages/utils — пересобрались все 20 пакетов, прогнались все тесты. На большом monorepo это 10–20 минут вместо 1–2. Деньги на раннеры горят, разработчики ждут.
Решение — собирать только то, что изменилось (affected), и кэшировать результаты задач, которые не менялись.
Turborepo: --filter и affected packages
Turborepo строит граф зависимостей между пакетами и выполняет только затронутые задачи:
# Собрать только пакеты, изменённые с main
turbo run build --filter=...[origin/main]
# Собрать конкретный пакет и его зависимости
turbo run build --filter=@myorg/api...
# Тесты только для затронутых
turbo run test --filter=...[origin/main]
Конфигурация в turbo.json:
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"test": {
"dependsOn": ["build"],
"outputs": []
},
"lint": {
"outputs": []
}
}
}
Ключевое: "dependsOn": ["^build"] означает «сначала собери зависимости этого пакета». Turborepo автоматически определяет порядок и параллелизм.
Nx: affected и граф зависимостей
Nx использует аналогичный подход, но с более мощным анализом графа:
# Собрать только затронутые проекты
nx affected --target=build --base=origin/main
# Тесты для затронутых
nx affected --target=test --base=origin/main --parallel=4
# Визуализация графа
nx graph --affected
Конфигурация в nx.json:
{
"targetDefaults": {
"build": {
"dependsOn": ["^build"],
"cache": true
},
"test": {
"dependsOn": ["build"],
"cache": true
}
},
"affected": {
"defaultBase": "main"
}
}
Nx дополнительно анализирует импорты на уровне файлов — если изменился файл, который никто не импортирует, пересборка не запустится даже внутри пакета.
Кэширование: remote cache vs local
Local cache — результаты задач хранятся на диске раннера. Работает только если кэш восстановлен между запусками:
- name: Cache Turborepo
uses: actions/cache@v4
with:
path: .turbo
key: turbo-${{ runner.os }}-${{ github.sha }}
restore-keys: |
turbo-${{ runner.os }}-
Remote cache (Turborepo) — результаты хранятся в облаке Vercel и доступны всем разработчикам и CI:
- name: Build affected
run: turbo run build --filter=...[origin/main]
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
Remote cache (Nx Cloud) — аналогично, но через Nx Cloud:
- name: Build affected
run: nx affected --target=build --base=origin/main
env:
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_TOKEN }}
Remote cache даёт максимальный эффект: если коллега уже собрал пакет с тем же хэшем входных файлов — CI скачает результат за секунды вместо пересборки.
Параллелизм: matrix strategy + affected list
Для больших monorepo можно распараллелить CI по пакетам через matrix:
jobs:
detect:
runs-on: ubuntu-latest
outputs:
packages: ${{ steps.affected.outputs.packages }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- id: affected
run: |
PACKAGES=$(npx turbo run build --filter=...[origin/main] --dry-run=json \
| jq -c '[.packages[] | select(. != "//")]')
echo "packages=$PACKAGES" >> $GITHUB_OUTPUT
build:
needs: detect
if: needs.detect.outputs.packages != '[]'
strategy:
matrix:
package: ${{ fromJson(needs.detect.outputs.packages) }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: turbo run build test --filter=${{ matrix.package }}
Каждый затронутый пакет собирается в отдельном job — максимальный параллелизм. Но учтите лимит concurrent jobs (20 для GitHub Free, 60 для Pro).
Практика: полный workflow с кэшем и деплоем
name: CI/CD Monorepo
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: myorg
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- name: Build affected
run: turbo run build --filter=...[origin/main]
- name: Test affected
run: turbo run test --filter=...[origin/main]
- name: Lint affected
run: turbo run lint --filter=...[origin/main]
deploy-api:
needs: ci
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check if API changed
id: check
run: |
CHANGED=$(npx turbo run build --filter=@myorg/api...[HEAD~1] --dry-run=json \
| jq '.packages | length')
echo "deploy=$([[ $CHANGED -gt 0 ]] && echo true || echo false)" >> $GITHUB_OUTPUT
- name: Deploy API
if: steps.check.outputs.deploy == 'true'
run: |
docker build -t ghcr.io/myorg/api:${{ github.sha }} ./apps/api
docker push ghcr.io/myorg/api:${{ github.sha }}
kubectl set image deploy/api app=ghcr.io/myorg/api:${{ github.sha }}
Ключевые моменты:
fetch-depth: 0— нужен для сравнения с main (иначе git history обрезана).- Деплой только если пакет реально изменился — проверяем через
--dry-run=json. - Remote cache через
TURBO_TOKEN— повторные билды мгновенны. - Отдельный deploy-job на каждый деплоимый сервис с условием.
Вывод
Monorepo CI/CD без инструментов вроде Turborepo или Nx — это пересборка всего при каждом коммите. С ними — только affected пакеты, кэш результатов и параллельные job-ы. Turborepo проще в настройке и хорошо интегрирован с Vercel. Nx мощнее в анализе графа и подходит для enterprise-проектов. Оба превращают 15-минутный CI в 2-минутный.