CI/CD для React/Vue приложения: сборка, тесты, деплой на CDN.
CI/CD для React/Vue приложения: тесты, линтинг, сборка через Vite/Webpack, деплой на Vercel/Netlify или S3 + CloudFront.
В этом кейсе мы настроим полный CI/CD пайплайн для frontend-приложения на React или Vue. Пайплайн будет включать:
project/
├── .github/
│ └── workflows/
│ ├── ci.yml # CI: тесты, линты, типизация
│ └── deploy.yml # CD: сборка и деплой
├── src/
│ ├── App.tsx # Главный компонент
│ ├── main.tsx # Точка входа
│ ├── components/ # React/Vue компоненты
│ ├── hooks/ # Custom hooks
│ ├── utils/ # Утилиты
│ └── styles/ # Стили
├── tests/
│ ├── App.test.tsx # Unit тесты
│ └── setup.ts # Настройка тестового окружения
├── public/
│ └── index.html
├── .eslintrc.js
├── .prettierrc
├── tsconfig.json
├── vite.config.ts # или webpack.config.js
├── package.json
├── package-lock.json
└── README.md
{
"name": "my-frontend-app",
"version": "1.0.0",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"lint": "eslint src --ext .ts,.tsx",
"lint:fix": "eslint src --ext .ts,.tsx --fix",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@testing-library/react": "^14.0.0",
"@types/react": "^18.2.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@vitejs/plugin-react": "^4.0.0",
"eslint": "^8.45.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-react": "^7.33.0",
"eslint-plugin-react-hooks": "^4.6.0",
"prettier": "^3.0.0",
"typescript": "^5.1.0",
"vite": "^5.0.0",
"vitest": "^1.0.0"
}
}# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run ESLint
run: npm run lint
- name: Run TypeScript check
run: npm run typecheck
- name: Run Prettier check
run: npx prettier --check src/
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests with coverage
run: npm run test:coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
files: ./coverage/lcov.info
fail_ci_if_error: false
build:
runs-on: ubuntu-latest
needs: [quality, test]
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
retention-days: 7- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' # Автоматическое кэширование по package-lock.jsonGitHub Actions автоматически кэширует node_modules на основе хэша package-lock.json. Это ускоряет установку зависимостей с 2-3 минут до 20-30 секунд.
# npm install — может обновить lock файл
npm install
# npm ci — чистая установка по lock файлу (быстрее, детерминировано)
npm cinpm ci:
npm install на 20-30%// .eslintrc.js
module.exports = {
root: true,
env: {
browser: true,
es2021: true,
node: true,
},
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 'latest',
sourceType: 'module',
},
plugins: ['react', 'react-hooks', '@typescript-eslint'],
rules: {
'react/react-in-jsx-scope': 'off',
'react/prop-types': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
},
settings: {
react: {
version: 'detect',
},
},
};// .prettierrc
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"bracketSpacing": true,
"arrowParens": "always"
}// .prettierignore
node_modules
dist
build
coverage
*.min.jsimport { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
build: {
outDir: 'dist',
sourcemap: true,
minify: 'terser',
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
utils: ['lodash', 'axios'],
},
},
},
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
},
},
},
server: {
port: 3000,
open: true,
},
});Vite автоматически разделяет код на чанки:
dist/
├── index.html
├── assets/
│ ├── index-abc123.js # Main bundle
│ ├── index-def456.css # Main styles
│ ├── vendor-xyz789.js # React, React-DOM
│ └── utils-uvw012.js # lodash, axios
Vercel автоматически деплоит при push в репозиторий:
{
"buildCommand": "npm run build",
"outputDirectory": "dist",
"devCommand": "npm run dev",
"installCommand": "npm install",
"framework": "vite",
"rewrites": [
{
"source": "/api/(.*)",
"destination": "https://api.example.com/$1"
}
],
"headers": [
{
"source": "/assets/(.*)",
"headers": [
{
"key": "Cache-Control",
"value": "public, max-age=31536000, immutable"
}
]
}
]
}Настройте в Vercel Dashboard:
| Variable | Development | Preview | Production |
|---|---|---|---|
| API_URL | http://localhost:8000 | https://staging-api.example.com | https://api.example.com |
| SENTRY_DSN | - | https://...@sentry.io/123 | https://...@sentry.io/456 |
# .github/workflows/deploy-vercel.yml
name: Deploy to Vercel
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Vercel CLI
run: npm install --global vercel@latest
- name: Pull Vercel Environment Information
run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
- name: Build Project Artifacts
run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
- name: Deploy Project Artifacts
run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}[build]
command = "npm run build"
publish = "dist"
[build.environment]
NODE_VERSION = "20"
[[redirects]]
from = "/api/*"
to = "https://api.example.com/:splat"
status = 200
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
[[headers]]
for = "/assets/*"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"Netlify автоматически создаёт preview для каждого PR:
https://deploy-preview-42--myapp.netlify.app
# .github/workflows/deploy-s3.yml
name: Deploy to S3
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Deploy to S3
run: |
aws s3 sync dist/ s3://myapp-bucket \
--delete \
--cache-control "public, max-age=300" \
--exclude "*" \
--include "*.js" \
--include "*.css" \
--cache-control "public, max-age=31536000, immutable"
- name: Invalidate CloudFront cache
run: |
aws cloudfront create-invalidation \
--distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} \
--paths "/*"Вместо полной инвалидации используйте хэши в именах файлов:
- name: Deploy to S3 with selective invalidation
run: |
# Инвалидировать только index.html (остальные файлы имеют хэши)
aws cloudfront create-invalidation \
--distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} \
--paths "/index.html"Vite автоматически добавляет хэши к именам файлов:
index.abc123.js — не требует инвалидацииindex.html — требует инвалидации при каждом деплое# .env.development
VITE_API_URL=http://localhost:8000
VITE_SENTRY_DSN=
VITE_ENVIRONMENT=development
# .env.staging
VITE_API_URL=https://staging-api.example.com
VITE_SENTRY_DSN=https://...@sentry.io/123
VITE_ENVIRONMENT=staging
# .env.production
VITE_API_URL=https://api.example.com
VITE_SENTRY_DSN=https://...@sentry.io/456
VITE_ENVIRONMENT=production// src/config.ts
export const config = {
apiUrl: import.meta.env.VITE_API_URL,
sentryDsn: import.meta.env.VITE_SENTRY_DSN,
environment: import.meta.env.VITE_ENVIRONMENT,
};- name: Build for production
run: |
cp .env.production .env
npm run build- name: Run Lighthouse
uses: treosh/lighthouse-ci-action@v11
with:
urls: |
http://localhost:3000/
http://localhost:3000/about
uploadArtifacts: true
temporaryPublicStorage: true- name: Bundle size check
uses: preactjs/compressed-size-action@v2
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
pattern: './dist/**/*.js'// vite.config.ts
export default defineConfig({
build: {
// Minification
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
},
},
// Source maps
sourcemap: false, // Отключить для production
// Chunk size warning
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
},
},
},
},
// CSS options
css: {
modules: {
localsConvention: 'camelCase',
},
},
});Vercel автоматически создаёт preview для каждого PR:
https://pr-42-myapp.vercel.app
Netlify также создаёт preview:
https://deploy-preview-42--myapp.netlify.app
# .github/workflows/preview.yml
name: Deploy Preview
on:
pull_request:
branches: [main]
jobs:
preview:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./dist
destination_dir: pr-${{ github.event.number }}Полный CI/CD пайплайн для frontend включает:
CI (Continuous Integration)
Сборка
CD (Continuous Deployment)
Производительность
Preview окружения
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.