В 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
(Злиття Дозволено) (Злиття Заблоковано)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 успішно пройдено. Злиття дозволено."За замовчуванням у 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-${{ steps.playwright-version.outputs.version }}
- if: steps.playwright-cache.outputs.cache-hit != 'true'
run: pnpm exec playwright install --with-deps chromium
- if: steps.playwright-cache.outputs.cache-hit == 'true'
run: pnpm exec playwright install-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% покриття шляхом додавання складних сервісів у секцію coverage.exclude конфігураційного файлу. Це створює ілюзію безпеки («зелений пайплайн»), тоді як найбільш уразлива фінансова або бізнес-логіка потрапляє в продакшн без жодного автоматизованого тесту.
Ізоляція 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.