lenec ru

← все посты

Monorepo CI/CD: pipelines для Turborepo и Nx в GitHub Actions

17K

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-минутный.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.