В Enterprise-разработке концепция Quality Gates (шлюзов качества) представляет собой автоматизированный набор детерминированных проверок, нарушение хотя бы одной из которых безусловно блокирует слияние кода в релизную ветку. По мере роста кодовой базы последовательное выполнение статического анализа, линтинга, модульных и сквозных (End-to-End) тестов начинает занимать от 30 до 60 минут, что неприемлемо для непрерывной поставки.
Решение этой проблемы строится на архитектурном паттерне Fan-out / Fan-in:
strategy.matrix): Задачи разбиваются на независимые векторные пространства (Fan-out). Раннеры выполняют изоляцию окружения, запускаясь одновременно на свободных ядрах инфраструктуры. Параметр fail-fast: false гарантирует, что падение одной проверки (например, опечатка в типах) не прервет выполнение длительных E2E-тестов, предоставляя разработчику полный отчет по всем дефектам за один прогон.tsc --noEmit): Компиляторы Next.js 15+ (SWC и Turbopack) спроектированы для максимальной скорости сборки, поэтому они намеренно транспилируют код без проверки типов (Type Stripping). Проверка типов выделяется в обособленный Quality Gate. Использование флага --incremental с сохранением .tsbuildinfo в кэше ускоряет последующие проверки в 4–7 раз.eslint.config.mjs). Новая архитектура избавлена от каскадного разрешения плагинов (.eslintrc), нативно поддерживает параллельный анализ файлов воркерами и эффективно кэширует абстрактные синтаксические деревья (AST) через .eslintcache.--shard=x/y. Набор из 500 тестов делится между $N$ параллельными виртуальными машинами. Каждый шард генерирует бинарный отчет (blob-report), которые затем скачиваются и объединяются единым процессом через npx playwright merge-reports (Fan-in).ci-gatekeeper, зависящая от всех параллельных проверок через needs: [...] и оценивающая их статусы через выражение if: always().Паттерн Fan-out / Fan-in разделяет проверки на параллельные независимые потоки с последующим сведением в финальную контрольную точку:
[ Push / Pull Request Event ]
|
+-------------------+-------------------+
| (Fan-out: Параллельный запуск раннеров) |
v v v
+-----------------+ +-----------------+ +-----------------+
| Job: Typecheck | | Job: ESLint v9 | | Job: Vitest |
| (tsc --noEmit) | | (.eslintcache) | | (>90% Coverage) |
+-----------------+ +-----------------+ +-----------------+
| | |
+-------------------+-------------------+
|
v
+-----------------------------------+
| Job: Playwright Sharding (4 VMs) |
| ├── Shard 1/4 -> Blob 1 |
| ├── Shard 2/4 -> Blob 2 |
| ├── Shard 3/4 -> Blob 3 |
| └── Shard 4/4 -> Blob 4 |
+-----------------------------------+
|
v
+-----------------------------------+
| Step: Playwright Merge Reports |
| (Слияние Blob 1..4 -> HTML Report)|
+-----------------------------------+
|
v
+-----------------------------------+
| (Fan-in: Агрегатор Quality Gates) |
| Job: ci-gatekeeper |
| (Оценка needs.*.result == 0) |
+-----------------------------------+
|
+-----------+-----------+
| |
v v
[ Все проверки GREEN ] [ Любой FAIL / ERROR ]
| |
v v
(Merge Разрешен) (Merge Заблокирован)Изоляция typecheck от компиляции Next.js. Настройка инкрементального флага tsc --incremental и кэширования .tsbuildinfo.
Конвертация устаревших .eslintrc в eslint.config.mjs с поддержкой нативного кэширования абстрактных синтаксических деревьев.
Внедрение V8 coverage провайдера с лимитами 90% Statements и 85% Branches в конфигурацию тестирования.
Настройка strategy.matrix с аргументом --shard, кэшированием бинарных файлов Chromium и объединением через merge-reports.
Создание финальной джобы ci-gatekeeper и ее фиксация как единственной обязательной проверки в GitHub Branch Protection Rulesets.
С какой целью в пайплайнах GitHub Actions используется паттерн Fan-out / Fan-in с единой финальной джобой ci-gatekeeper вместо привязки Branch Protection к отдельным матричным джобам?
vitest --coverage блокирует любые изменения в расчетном ядре, если разработчик не покрыл тестами граничные сценарии.shard: 1/8 ... 8/8) в матрице GitHub Actions сократило время прохождения до 7 минут.noImplicitAny, exactOptionalPropertyTypes). Quality Gates выполняются параллельно, гарантируя 100% валидацию типов интерфейсов компонентов перед публикацией в общий npm-реестр.main от поломки общих контрактов между командами.Для создания надежной системы проверок требуется точная конфигурация каждого инструмента и их координация на уровне GitHub Actions.
В конфигурации Vitest задаются минимально допустимые проценты покрытия. При нарушении хотя бы одного критерия процесс завершается с ошибкой Process completed with exit code 1.
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
plugins: [react(), tsconfigPaths()],
test: {
environment: 'happy-dom',
globals: true,
coverage: {
provider: 'v8',
reporter: ['text', 'json-summary', 'html', 'lcov'],
reportsDirectory: './coverage',
// Строгие корпоративные пороги Quality Gate
thresholds: {
lines: 90,
statements: 90,
functions: 85,
branches: 85,
},
exclude: [
'node_modules/**',
'.next/**',
'**/*.d.ts',
'**/*.config.*',
'**/test-utils/**',
],
},
},
});Новый формат конфигурации eslint.config.mjs обеспечивает декларативную настройку без использования устаревших .eslintignore.
import nextPlugin from '@next/eslint-plugin-next';
import reactPlugin from 'eslint-plugin-react';
import hooksPlugin from 'eslint-plugin-react-hooks';
import tsParser from '@typescript-eslint/parser';
import tsPlugin from '@typescript-eslint/eslint-plugin';
export default [
{
ignores: ['.next/**', 'dist/**', 'coverage/**', 'node_modules/**'],
},
{
files: ['**/*.{ts,tsx}'],
plugins: {
'@typescript-eslint': tsPlugin,
'react': reactPlugin,
'react-hooks': hooksPlugin,
'@next/next': nextPlugin,
},
languageOptions: {
parser: tsParser,
parserOptions: {
project: true,
},
},
rules: {
...tsPlugin.configs['recommended-type-checked'].rules,
...hooksPlugin.configs.recommended.rules,
...nextPlugin.configs.recommended.rules,
...nextPlugin.configs['core-web-vitals'].rules,
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-floating-promises': 'error',
'no-console': ['warn', { allow: ['warn', 'error'] }],
},
},
];Конфигурация Playwright адаптируется для вывода артефактов в формате blob:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 2 : undefined,
// В CI каждый шард пишет уникальный blob-отчет
reporter: process.env.CI ? [['blob', { outputDir: 'blob-report' }]] : [['html']],
use: {
baseURL: process.env.BASE_URL || 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
});В CI раннере запускается команда:
npx playwright test --shard=${{ matrix.shard }}/${{ strategy.job-total }}
После завершения всех шардов запускается шаг слияния:
npx playwright merge-reports --reporter=html ./all-blob-reports
GitHub Actions запускает независимые раннеры для задач Typecheck, ESLint, Vitest и 4 шардов Playwright.
Выполнение tsc --noEmit, ESLint 9 и расчет покрытия Vitest (проверка лимита 90% Statements).
Шарды 1/4, 2/4, 3/4 и 4/4 прогоняют распределенные E2E-сценарии и загружают blob-отчеты в артефакты.
Загрузка всех blob-файлов и формирование консолидированного HTML-дашборда сквозных тестов.
Джоба ci-gatekeeper проверяет статус всех этапов. При отсутствии падений выдается зеленый статус для мерджа.
Ниже представлена готовая production-конфигурация GitHub Actions, включающая параллельные джобы, кэширование браузеров Playwright, шардинг, объединение отчетов и отказоустойчивый Gatekeeper.
name: CI Quality Gates
on:
push:
branches:
- main
- develop
pull_request:
types:
- opened
- synchronize
- reopened
concurrency:
group: quality-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
checks: write
pull-requests: write
jobs:
# -------------------------------------------------------------
# Джоба 1: Проверка типов TypeScript (Isolated Typecheck)
# -------------------------------------------------------------
typecheck:
name: TypeScript Check
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node & pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.0
run_install: false
- uses: actions/setup-node@v4
with:
node-version: 22.x
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Восстановление кэша .tsbuildinfo
uses: actions/cache@v4
with:
path: '**/.tsbuildinfo'
key: ${{ runner.os }}-tsbuildinfo-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-tsbuildinfo-${{ hashFiles('**/pnpm-lock.yaml') }}-
- name: Run tsc --noEmit
run: pnpm tsc --noEmit --incremental
# -------------------------------------------------------------
# Джоба 2: Статический анализ кода (ESLint 9 Flat Config)
# -------------------------------------------------------------
lint:
name: ESLint v9 Analysis
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node & pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.0
run_install: false
- uses: actions/setup-node@v4
with:
node-version: 22.x
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Восстановление кэша ESLint
uses: actions/cache@v4
with:
path: .eslintcache
key: ${{ runner.os }}-eslint-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-eslint-${{ hashFiles('**/pnpm-lock.yaml') }}-
- name: Run ESLint
run: pnpm eslint . --cache --max-warnings 0
# -------------------------------------------------------------
# Джоба 3: Модульные и интеграционные тесты с Coverage
# -------------------------------------------------------------
unit-tests:
name: Vitest & Coverage Thresholds
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node & pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.0
run_install: false
- uses: actions/setup-node@v4
with:
node-version: 22.x
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run Vitest with Threshold Enforcement
run: pnpm vitest run --coverage
# -------------------------------------------------------------
# Джоба 4: E2E Playwright с распределенным шардингом
# -------------------------------------------------------------
playwright-e2e:
name: Playwright Shard ${{ matrix.shard }}/${{ strategy.job-total }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node & pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.0
run_install: false
- uses: actions/setup-node@v4
with:
node-version: 22.x
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Получение установленной версии Playwright
id: playwright-version
shell: bash
run: |
echo "version=$(pnpm list --depth 0 @playwright/test | grep @playwright/test | awk '{print $2}')" >> $GITHUB_OUTPUT
- name: Восстановление кэша браузеров Playwright
uses: actions/cache@v4
id: playwright-cache
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.version }}
- name: Установка браузера Chromium с системными зависимостями
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: pnpm exec playwright install --with-deps chromium
- name: Установка только системных зависимостей (при Cache HIT)
if: steps.playwright-cache.outputs.cache-hit == 'true'
run: pnpm exec playwright install-deps chromium
- name: Сборка Next.js приложения
run: pnpm build
env:
NEXT_TELEMETRY_DISABLED: 1
- name: Запуск E2E тестов для шарда
run: pnpm exec playwright test --shard=${{ matrix.shard }}/${{ strategy.job-total }}
env:
CI: true
- name: Загрузка Blob-отчета шарда в артефакты
if: always()
uses: actions/upload-artifact@v4
with:
name: blob-report-${{ matrix.shard }}
path: blob-report
retention-days: 1
# -------------------------------------------------------------
# Джоба 5: Слияние отчетов Playwright (Fan-in Consolidation)
# -------------------------------------------------------------
merge-e2e-reports:
name: Merge Playwright Reports
needs: [playwright-e2e]
if: always()
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node & pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.0
run_install: false
- uses: actions/setup-node@v4
with:
node-version: 22.x
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Скачивание всех blob-отчетов
uses: actions/download-artifact@v4
with:
path: all-blob-reports
pattern: blob-report-*
merge-multiple: true
- name: Объединение отчетов в единый HTML дашборд
run: npx playwright merge-reports --reporter=html ./all-blob-reports
- name: Публикация объединенного отчета
uses: actions/upload-artifact@v4
with:
name: merged-playwright-html-report
path: playwright-report
retention-days: 7
# -------------------------------------------------------------
# Джоба 6: Итоговый Gatekeeper (Точка защиты веток)
# -------------------------------------------------------------
ci-gatekeeper:
name: Quality Gatekeeper Check
needs:
- typecheck
- lint
- unit-tests
- playwright-e2e
- merge-e2e-reports
if: always()
runs-on: ubuntu-latest
steps:
- name: Оценка статуса всех Quality Gates
shell: bash
run: |
TYPECHECK="${{ needs.typecheck.result }}"
LINT="${{ needs.lint.result }}"
UNIT="${{ needs.unit-tests.result }}"
E2E="${{ needs.playwright-e2e.result }}"
echo "Результаты проверок:"
echo "Typecheck: $TYPECHECK"
echo "ESLint: $LINT"
echo "Unit Tests: $UNIT"
echo "Playwright E2E: $E2E"
if [ "$TYPECHECK" != "success" ] || [ "$LINT" != "success" ] || [ "$UNIT" != "success" ] || [ "$E2E" != "success" ]; then
echo "❌ Ошибка: Один или несколько Quality Gates не прошли проверку!"
exit 1
fi
echo "✅ Все Quality Gates успешно пройдены. Слияние разрешено."Почему в матричных джобах распределенного тестирования Playwright необходимо явно указывать fail-fast: false?
По умолчанию в GitHub Actions включена опция strategy.fail-fast: true. Если шард 1/4 падает из-за опечатки в селекторе на второй секунде запуска, GitHub немедленно отменяет выполнение шардов 2, 3 и 4. Разработчик исправляет найденную опечатку, отправляет коммит, и следующий запуск падает уже на шарде 3. Это порождает мучительные циклы правок «по одной ошибке за раз».
# НЕПРАВИЛЬНО: Отмена всех остальных шардов при первой ошибке
strategy:
matrix:
shard: [1, 2, 3, 4]
# ПРАВИЛЬНО: Полный прогон всех проверок для предоставления исчерпывающего отчета
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]Выполнение команды npx playwright install --with-deps на каждом шаге матрицы. Утилита скачивает бинарники Chromium, Firefox и WebKit суммарным объемом свыше 1 ГБ на каждую виртуальную машину. При 4 шардах это приводит к 4 ГБ бессмысленного трафика и потере 2–3 минут времени на раннер.
# НЕПРАВИЛЬНО: Скачивание всех браузеров вслепую
- run: npx playwright install --with-deps
# ПРАВИЛЬНО: Кэширование ~/.cache/ms-playwright и выборочная установка одного браузера
- uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }}
- if: steps.cache.outputs.cache-hit != 'true'
run: npx playwright install --with-deps chromiumУказание в настройках GitHub Branch Protection конкретных проверок вида Playwright Shard (1/4), Playwright Shard (2/4). Если со временем количество E2E-тестов вырастет и лид команды изменит матрицу на shard: [1, 2, 3, 4, 5, 6], статус-чек Playwright Shard (1/4) перестанет существовать в системе, и слияние любых PR в ветку main окажется заблокировано на уровне репозитория.
НЕПРАВИЛЬНО: Branch Protection -> Required Status Checks -> Playwright Shard (1/4)
ПРАВИЛЬНО: Branch Protection -> Required Status Checks -> Quality Gatekeeper Check (единая джоба)Попытка искусственно удовлетворить порог 90% покрытия путем добавления сложных сервисов в секцию exclude конфигурационного файла. Это создает иллюзию безопасности («зеленый пайплайн»), в то время как наиболее уязвимая финансовая или бизнес-логика попадает в продакшн без единого автоматизированного теста.