Kurs Next.js · Moduł 8: Deployment i produkcja
CI/CD i automatyzacja wdrożeń
W tej lekcji12
W nowoczesnym procesie rozwoju oprogramowania, Continuous Integration (CI) i Continuous Deployment (CD) są kluczowymi praktykami, które pozwalają na szybsze i bardziej niezawodne dostarczanie aplikacji. Next.js, jako nowoczesny framework, doskonale integruje się z różnymi narzędziami CI/CD, co pozwala zautomatyzować proces testowania, budowania i wdrażania aplikacji.
Czym są CI/CD?
Continuous Integration (CI)
Continuous Integration to praktyka, w której programiści regularnie integrują swoje zmiany w kodzie z główną gałęzią repozytorium. Każda integracja jest automatycznie weryfikowana przez:
- Uruchomienie testów jednostkowych i integracyjnych
- Analizę statyczną kodu (linting)
- Sprawdzanie jakości kodu
- Sprawdzanie typów (w przypadku TypeScript)
CI pozwala na wczesne wykrywanie błędów i zapewnia, że kod w głównej gałęzi pozostaje stabilny.
Continuous Deployment (CD)
Continuous Deployment idzie o krok dalej - po pomyślnym przejściu etapu CI, automatycznie wdraża zmiany na środowisko produkcyjne lub testowe. Proces CD obejmuje:
- Automatyczne budowanie aplikacji
- Wdrażanie na wybrane środowisko (staging, produkcja)
- Automatyczne testy E2E po wdrożeniu
- Monitorowanie wdrożenia
Popularne narzędzia CI/CD dla aplikacji Next.js
Dla aplikacji Next.js możemy wykorzystać różne narzędzia CI/CD:
- GitHub Actions - zintegrowane z GitHub, oferuje doskonałą integrację z repo
- GitLab CI/CD - wbudowane w GitLab, pozwala na tworzenie potoków CI/CD
- CircleCI - popularna platforma CI/CD z prostym konfigurowaniem
- Jenkins - otwarty, rozszerzalny serwer automatyzacji
- Vercel CI/CD - natywne rozwiązanie dla aplikacji Next.js
- Netlify CI/CD - alternatywa z podobnymi funkcjami
- Travis CI - jedna z najstarszych platform CI/CD
Konfiguracja CI/CD z GitHub Actions
Najprostszym sposobem na wdrożenie CI/CD dla projektu Next.js jest użycie GitHub Actions, jeśli twoje repozytorium jest hostowane na GitHub.
1. Konfiguracja workflow testowania
Utwórz plik .github/workflows/ci.yml w swoim repozytorium:
1name: CI
2
3on:
4 push:
5 branches: [main, development]
6 pull_request:
7 branches: [main, development]
8
9jobs:
10 test:
11 runs-on: ubuntu-latest
12
13 strategy:
14 matrix:
15 node-version: [18.x]
16
17 steps:
18 - uses: actions/checkout@v3
19
20 - name: Use Node.js ${{ matrix.node-version }}
21 uses: actions/setup-node@v3
22 with:
23 node-version: ${{ matrix.node-version }}
24 cache: 'npm'
25
26 - name: Install dependencies
27 run: npm ci
28
29 - name: Lint
30 run: npm run lint
31
32 - name: Type check
33 run: npm run type-check
34
35 - name: Run tests
36 run: npm test
37
38 - name: Build
39 run: npm run buildTen workflow uruchamia się przy każdym pushu do gałęzi main i development oraz przy utworzeniu Pull Requestu do tych gałęzi.
2. Konfiguracja workflow wdrażania na Vercel
Utwórz plik .github/workflows/deploy.yml:
1name: Deploy to Production
2
3on:
4 push:
5 branches: [main]
6
7jobs:
8 deploy:
9 runs-on: ubuntu-latest
10
11 steps:
12 - uses: actions/checkout@v3
13
14 - name: Deploy to Vercel
15 uses: amondnet/vercel-action@v20
16 with:
17 vercel-token: ${{ secrets.VERCEL_TOKEN }}
18 vercel-org-id: ${{ secrets.ORG_ID }}
19 vercel-project-id: ${{ secrets.PROJECT_ID }}
20 vercel-args: '--prod'Aby to działało, musisz skonfigurować następujące sekrety w ustawieniach repozytorium:
VERCEL_TOKEN- Token API VercelORG_ID- ID organizacji w VercelPROJECT_ID- ID projektu w Vercel
3. Konfiguracja workflow dla środowiska staging
1name: Deploy to Staging
2
3on:
4 push:
5 branches: [development]
6
7jobs:
8 deploy:
9 runs-on: ubuntu-latest
10
11 steps:
12 - uses: actions/checkout@v3
13
14 - name: Deploy to Vercel (Preview)
15 uses: amondnet/vercel-action@v20
16 with:
17 vercel-token: ${{ secrets.VERCEL_TOKEN }}
18 vercel-org-id: ${{ secrets.ORG_ID }}
19 vercel-project-id: ${{ secrets.PROJECT_ID }}
20 github-comment: trueKonfiguracja CI/CD z GitLab CI
Jeśli korzystasz z GitLab, możesz skonfigurować CI/CD używając .gitlab-ci.yml:
1image: node:18-alpine
2
3stages:
4 - test
5 - build
6 - deploy_staging
7 - deploy_production
8
9cache:
10 key: ${CI_COMMIT_REF_SLUG}
11 paths:
12 - node_modules/
13 - .next/cache/
14
15test:
16 stage: test
17 script:
18 - npm ci
19 - npm run lint
20 - npm run type-check
21 - npm test
22 except:
23 - tags
24
25build:
26 stage: build
27 script:
28 - npm ci
29 - npm run build
30 artifacts:
31 paths:
32 - .next/
33 except:
34 - tags
35
36deploy_staging:
37 stage: deploy_staging
38 script:
39 - npm install -g vercel
40 - vercel --token ${VERCEL_TOKEN} --confirm
41 environment:
42 name: staging
43 url: ${CI_PROJECT_NAME}-git-${CI_COMMIT_REF_NAME}-${CI_PROJECT_NAMESPACE}.vercel.app
44 only:
45 - development
46
47deploy_production:
48 stage: deploy_production
49 script:
50 - npm install -g vercel
51 - vercel --token ${VERCEL_TOKEN} --prod --confirm
52 environment:
53 name: production
54 url: ${CI_PROJECT_NAME}.vercel.app
55 only:
56 - mainKonfiguracja CI/CD dla aplikacji Next.js z Circle CI
Aby skonfigurować CircleCI, utwórz plik .circleci/config.yml:
1version: 2.1
2
3orbs:
4 node: circleci/node@5.0.3
5
6jobs:
7 test:
8 docker:
9 - image: cimg/node:18.15.0
10 steps:
11 - checkout
12 - node/install-packages:
13 pkg-manager: npm
14 - run:
15 name: Run tests
16 command: npm test
17 - run:
18 name: Run linter
19 command: npm run lint
20 - run:
21 name: Check types
22 command: npm run type-check
23
24 build:
25 docker:
26 - image: cimg/node:18.15.0
27 steps:
28 - checkout
29 - node/install-packages:
30 pkg-manager: npm
31 - run:
32 name: Build application
33 command: npm run build
34 - persist_to_workspace:
35 root: .
36 paths:
37 - .next
38 - node_modules
39 - package.json
40
41 deploy-staging:
42 docker:
43 - image: cimg/node:18.15.0
44 steps:
45 - checkout
46 - attach_workspace:
47 at: .
48 - run:
49 name: Install Vercel CLI
50 command: npm install -g vercel
51 - run:
52 name: Deploy to Staging
53 command: vercel --token ${VERCEL_TOKEN} --confirm
54
55 deploy-production:
56 docker:
57 - image: cimg/node:18.15.0
58 steps:
59 - checkout
60 - attach_workspace:
61 at: .
62 - run:
63 name: Install Vercel CLI
64 command: npm install -g vercel
65 - run:
66 name: Deploy to Production
67 command: vercel --token ${VERCEL_TOKEN} --prod --confirm
68
69workflows:
70 version: 2
71 test-build-deploy:
72 jobs:
73 - test
74 - build:
75 requires:
76 - test
77 - deploy-staging:
78 requires:
79 - build
80 filters:
81 branches:
82 only: development
83 - deploy-production:
84 requires:
85 - build
86 filters:
87 branches:
88 only: mainKonfiguracja CI/CD z Jenkins
Jenkins jest potężnym narzędziem CI/CD, ale wymaga większej konfiguracji. Oto przykładowy Jenkinsfile dla potoku CI/CD dla aplikacji Next.js:
1pipeline {
2 agent {
3 docker {
4 image 'node:18-alpine'
5 }
6 }
7
8 stages {
9 stage('Install Dependencies') {
10 steps {
11 sh 'npm ci'
12 }
13 }
14
15 stage('Lint') {
16 steps {
17 sh 'npm run lint'
18 }
19 }
20
21 stage('Type Check') {
22 steps {
23 sh 'npm run type-check'
24 }
25 }
26
27 stage('Test') {
28 steps {
29 sh 'npm test'
30 }
31 }
32
33 stage('Build') {
34 steps {
35 sh 'npm run build'
36 }
37 }
38
39 stage('Deploy to Staging') {
40 when {
41 branch 'development'
42 }
43 steps {
44 sh 'npm install -g vercel'
45 sh 'vercel --token ${VERCEL_TOKEN} --confirm'
46 }
47 }
48
49 stage('Deploy to Production') {
50 when {
51 branch 'main'
52 }
53 steps {
54 sh 'npm install -g vercel'
55 sh 'vercel --token ${VERCEL_TOKEN} --prod --confirm'
56 }
57 }
58 }
59
60 post {
61 always {
62 cleanWs()
63 }
64 }
65}Testowanie w procesie CI/CD
Ważnym elementem procesu CI/CD jest automatyczne testowanie. Dla aplikacji Next.js możemy skonfigurować różne rodzaje testów:
1. Testy jednostkowe z Jest i React Testing Library
1// __tests__/components/Header.test.tsx
2import { render, screen } from '@testing-library/react';
3import Header from '@/components/Header';
4
5describe('Header Component', () => {
6 it('renders the logo', () => {
7 render(<Header />);
8 const logo = screen.getByAltText('Logo');
9 expect(logo).toBeInTheDocument();
10 });
11
12 it('has the correct navigation links', () => {
13 render(<Header />);
14 expect(screen.getByText('Home')).toBeInTheDocument();
15 expect(screen.getByText('About')).toBeInTheDocument();
16 expect(screen.getByText('Contact')).toBeInTheDocument();
17 });
18});2. Testy integracyjne dla API Routes
1// __tests__/api/user.test.ts
2import { createMocks } from 'node-mocks-http';
3import userHandler from '@/app/api/user/route';
4
5describe('/api/user endpoint', () => {
6 it('returns a user when provided with a valid ID', async () => {
7 const { req, res } = createMocks({
8 method: 'GET',
9 query: { id: '123' },
10 });
11
12 await userHandler(req, res);
13
14 expect(res._getStatusCode()).toBe(200);
15 expect(JSON.parse(res._getData())).toEqual(
16 expect.objectContaining({
17 id: '123',
18 name: expect.any(String),
19 })
20 );
21 });
22
23 it('returns 404 for non-existent user', async () => {
24 const { req, res } = createMocks({
25 method: 'GET',
26 query: { id: 'non-existent' },
27 });
28
29 await userHandler(req, res);
30
31 expect(res._getStatusCode()).toBe(404);
32 });
33});3. Testy End-to-End z Cypress
1// cypress/integration/navigation.spec.js
2describe('Navigation', () => {
3 it('should navigate to the about page', () => {
4 cy.visit('/');
5
6 cy.get('a[href*="about"]').click();
7
8 cy.url().should('include', '/about');
9 cy.get('h1').contains('About');
10 });
11
12 it('should navigate to the contact page', () => {
13 cy.visit('/');
14
15 cy.get('a[href*="contact"]').click();
16
17 cy.url().should('include', '/contact');
18 cy.get('h1').contains('Contact');
19 });
20});Dodaj konfigurację Cypress do GitHub Actions:
1# .github/workflows/e2e-tests.yml
2name: E2E Tests
3
4on:
5 deployment_status:
6
7jobs:
8 e2e-tests:
9 if: github.event.deployment_status.state == 'success'
10 runs-on: ubuntu-latest
11 steps:
12 - uses: actions/checkout@v3
13
14 - name: Install dependencies
15 run: npm ci
16
17 - name: Cypress run
18 uses: cypress-io/github-action@v5
19 with:
20 record: true
21 env:
22 CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
23 CYPRESS_BASE_URL: ${{ github.event.deployment_status.target_url }}Automatyzacja tasków deweloperskich
Poza CI/CD, możemy zautomatyzować różne inne zadania deweloperskie:
1. Automatyczne formatowanie kodu z Husky i lint-staged
Zainstaluj potrzebne pakiety:
1npm install --save-dev husky lint-staged prettierZaktualizuj package.json:
1{
2 "scripts": {
3 "prepare": "husky install"
4 },
5 "lint-staged": {
6 "*.{js,jsx,ts,tsx}": [
7 "prettier --write",
8 "eslint --fix"
9 ],
10 "*.{json,md}": [
11 "prettier --write"
12 ]
13 }
14}Utwórz hook pre-commit:
1npx husky add .husky/pre-commit "npx lint-staged"2. Automatyczne wersjonowanie z Semantic Release
Zainstaluj potrzebne pakiety:
1npm install --save-dev semantic-release @semantic-release/git @semantic-release/changelogUtwórz plik .releaserc.json:
1{
2 "branches": ["main"],
3 "plugins": [
4 "@semantic-release/commit-analyzer",
5 "@semantic-release/release-notes-generator",
6 "@semantic-release/changelog",
7 "@semantic-release/npm",
8 ["@semantic-release/git", {
9 "assets": ["package.json", "CHANGELOG.md"],
10 "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
11 }],
12 "@semantic-release/github"
13 ]
14}Dodaj workflow dla semantic-release:
1# .github/workflows/release.yml
2name: Release
3
4on:
5 push:
6 branches: [main]
7
8jobs:
9 release:
10 runs-on: ubuntu-latest
11 steps:
12 - uses: actions/checkout@v3
13 - name: Setup Node.js
14 uses: actions/setup-node@v3
15 with:
16 node-version: '18'
17 cache: 'npm'
18 - name: Install dependencies
19 run: npm ci
20 - name: Release
21 env:
22 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
23 NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
24 run: npx semantic-release3. Automatyczne generowanie dokumentacji
W przypadku projektów z TypeScript, możesz zautomatyzować generowanie dokumentacji:
1# .github/workflows/docs.yml
2name: Generate Documentation
3
4on:
5 push:
6 branches: [main]
7
8jobs:
9 docs:
10 runs-on: ubuntu-latest
11 steps:
12 - uses: actions/checkout@v3
13 - name: Setup Node.js
14 uses: actions/setup-node@v3
15 with:
16 node-version: '18'
17 cache: 'npm'
18 - name: Install dependencies
19 run: npm ci
20 - name: Generate docs
21 run: npx typedoc --out docs src
22 - name: Deploy docs to GitHub Pages
23 uses: peaceiris/actions-gh-pages@v3
24 with:
25 github_token: ${{ secrets.GITHUB_TOKEN }}
26 publish_dir: ./docsStrategie wdrażania i rollbacków
Podczas wdrażania aplikacji warto zaimplementować bezpieczne strategie wdrażania:
1. Blue-Green Deployment
Blue-Green deployment polega na utrzymywaniu dwóch identycznych środowisk produkcyjnych, z których tylko jedno jest aktywne. W przypadku Vercel, możesz skorzystać z ich wbudowanej funkcji "Preview Deployments" i aliasowania domen:
1// scripts/blue-green-deploy.js
2const { execSync } = require('child_process');
3
4// Deploy to new environment (but not live yet)
5const output = execSync('vercel --prod').toString();
6const deploymentUrl = output.match(/https:\/\/[\w.-]+/)[0];
7
8// Run smoke tests against new deployment
9const smokeTestResult = execSync(`npm run test:smoke -- --url=${deploymentUrl}`).toString();
10
11if (smokeTestResult.includes('All tests passed')) {
12 // Switch traffic to new deployment
13 execSync(`vercel alias set ${deploymentUrl} your-production-domain.com`);
14 console.log('Successfully switched to new deployment');
15} else {
16 console.error('Smoke tests failed, aborting deployment');
17 process.exit(1);
18}2. Canary Releases
W strategii Canary, nowa wersja jest udostępniana stopniowo, początkowo tylko dla małego procentu użytkowników. W Next.js możesz to osiągnąć za pomocą flag funkcji i podziału ruchu:
1// lib/feature-flags.ts
2import { useEffect, useState } from 'react';
3
4const CANARY_PERCENTAGE = 10; // 10% użytkowników dostanie nową wersję
5
6export function useCanaryFeature(featureName: string): boolean {
7 const [isEnabled, setIsEnabled] = useState(false);
8
9 useEffect(() => {
10 // Generuj losową liczbę dla użytkownika i zapisz w local storage
11 let userBucket = localStorage.getItem('userBucket');
12 if (!userBucket) {
13 userBucket = Math.floor(Math.random() * 100).toString();
14 localStorage.setItem('userBucket', userBucket);
15 }
16
17 // Sprawdź, czy użytkownik jest w grupie canary
18 const inCanaryGroup = parseInt(userBucket) < CANARY_PERCENTAGE;
19
20 // Sprawdź, czy funkcja jest aktywna dla tego użytkownika
21 fetch('/api/feature-flags')
22 .then(res => res.json())
23 .then(flags => {
24 // Jeśli flaga istnieje i użytkownik jest w grupie canary, włącz funkcję
25 if (flags[featureName] && inCanaryGroup) {
26 setIsEnabled(true);
27 }
28 });
29 }, [featureName]);
30
31 return isEnabled;
32}3. Automatyczny rollback
Wdrożenie automatycznego rollbacku w przypadku problemów jest kluczową praktyką:
1# .github/workflows/deploy-monitor-rollback.yml
2name: Deploy, Monitor & Rollback
3
4on:
5 push:
6 branches: [main]
7
8jobs:
9 deploy:
10 runs-on: ubuntu-latest
11 outputs:
12 deployment_id: ${{ steps.deploy.outputs.deployment_id }}
13 steps:
14 - uses: actions/checkout@v3
15 - name: Deploy to Vercel
16 id: deploy
17 uses: amondnet/vercel-action@v20
18 with:
19 vercel-token: ${{ secrets.VERCEL_TOKEN }}
20 vercel-org-id: ${{ secrets.ORG_ID }}
21 vercel-project-id: ${{ secrets.PROJECT_ID }}
22 vercel-args: '--prod'
23
24
25 monitor:
26 needs: deploy
27 runs-on: ubuntu-latest
28 steps:
29 - name: Wait for 2 minutes
30 run: sleep 120
31
32 - name: Check for errors
33 id: check_errors
34 run: |
35 ERROR_COUNT=$(curl -s "https://api.example.com/monitoring/errors?since=$(date -u -d '2 minutes ago' +%Y-%m-%dT%H:%M:%SZ)" | jq '.count')
36 echo "::set-output name=error_count::$ERROR_COUNT"
37 if [ "$ERROR_COUNT" -gt "10" ]; then
38 echo "Error count too high ($ERROR_COUNT). Failing job."
39 exit 1
40 fi
41
42 - name: Monitor Core Web Vitals
43 id: web_vitals
44 run: |
45 LCP=$(curl -s "https://api.example.com/monitoring/web-vitals" | jq '.lcp')
46 if [ "$LCP" -gt "2500" ]; then
47 echo "LCP too high ($LCP ms). Failing job."
48 exit 1
49 fi
50
51 rollback:
52 needs: [deploy, monitor]
53 runs-on: ubuntu-latest
54 if: ${{ failure() }}
55 steps:
56 - name: Rollback to previous deployment
57 uses: amondnet/vercel-action@v20
58 with:
59 vercel-token: ${{ secrets.VERCEL_TOKEN }}
60 vercel-org-id: ${{ secrets.ORG_ID }}
61 vercel-project-id: ${{ secrets.PROJECT_ID }}
62 vercel-args: '--prod --scope myorg' # Rollback to previous deploymentZarządzanie zmiennymi środowiskowymi
Bezpieczne zarządzanie zmiennymi środowiskowymi jest krytycznym aspektem CI/CD:
1. Przechowywanie zmiennych środowiskowych w systemie CI/CD
Większość systemów CI/CD oferuje bezpieczne przechowywanie sekretów, np. GitHub Secrets:
1# .github/workflows/deploy.yml
2name: Deploy
3
4on:
5 push:
6 branches: [main]
7
8jobs:
9 deploy:
10 runs-on: ubuntu-latest
11 steps:
12 - uses: actions/checkout@v3
13
14 - name: Create .env file
15 run: |
16 cat > .env.production << EOL
17 DATABASE_URL=${{ secrets.DATABASE_URL }}
18 API_KEY=${{ secrets.API_KEY }}
19 NEXT_PUBLIC_ANALYTICS_ID=${{ secrets.NEXT_PUBLIC_ANALYTICS_ID }}
20 EOL
21
22 - name: Deploy
23 run: npm run deploy2. Automatyczna rotacja sekretów
W przypadku krytycznych aplikacji warto zaimplementować automatyczną rotację sekretów:
1# .github/workflows/rotate-secrets.yml
2name: Rotate Secrets
3
4on:
5 schedule:
6 - cron: '0 0 1 * *' # Co miesiąc
7
8jobs:
9 rotate:
10 runs-on: ubuntu-latest
11 steps:
12 - name: Generate new API key
13 id: new_key
14 run: echo "::set-output name=api_key::$(openssl rand -base64 32)"
15
16 - name: Update API service with new key
17 run: |
18 curl -X POST -H "Authorization: Bearer ${{ secrets.ADMIN_TOKEN }}" -d '{"api_key": "${{ steps.new_key.outputs.api_key }}"}' https://api.example.com/admin/update-key
19
20 - name: Update GitHub secret
21 uses: gliech/create-github-secret-action@v1
22 with:
23 name: API_KEY
24 value: ${{ steps.new_key.outputs.api_key }}
25 pa_token: ${{ secrets.PA_TOKEN }}Monitorowanie wdrożeń
Kluczowym elementem procesu CI/CD jest monitorowanie wdrożeń, aby szybko wykryć i zareagować na ewentualne problemy:
1. Integracja z usługami monitorowania
1# .github/workflows/deploy-and-monitor.yml
2name: Deploy and Monitor
3
4on:
5 push:
6 branches: [main]
7
8jobs:
9 deploy:
10 runs-on: ubuntu-latest
11 steps:
12 - uses: actions/checkout@v3
13 - name: Deploy to Vercel
14 uses: amondnet/vercel-action@v20
15 with:
16 vercel-token: ${{ secrets.VERCEL_TOKEN }}
17 vercel-org-id: ${{ secrets.ORG_ID }}
18 vercel-project-id: ${{ secrets.PROJECT_ID }}
19 vercel-args: '--prod'
20
21 - name: Notify Datadog of deployment
22 run: |
23 curl -X POST "https://api.datadoghq.com/api/v1/events" -H "Content-Type: application/json" -H "DD-API-KEY: ${{ secrets.DATADOG_API_KEY }}" -d '{"title":"Deployment to production","text":"Deployed commit ${{ github.sha }} to production","tags":["environment:production","team:frontend"],"alert_type":"info"}'2. Automatyczne testy po wdrożeniu
1# .github/workflows/post-deploy-tests.yml
2name: Post-deployment Tests
3
4on:
5 deployment_status:
6
7jobs:
8 smoke-tests:
9 if: github.event.deployment_status.state == 'success'
10 runs-on: ubuntu-latest
11 steps:
12 - uses: actions/checkout@v3
13
14 - name: Run smoke tests
15 run: |
16 npm ci
17 npm run test:smoke -- --url=${{ github.event.deployment_status.target_url }}
18
19 - name: Post results to Slack
20 if: always()
21 uses: slackapi/slack-github-action@v1
22 with:
23 payload: |
24 {
25 "text": "Smoke test results: ${{ job.status }}\nDeployment URL: ${{ github.event.deployment_status.target_url }}"
26 }
27 env:
28 SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}Podsumowanie
Implementacja CI/CD dla aplikacji Next.js jest kluczowym elementem nowoczesnego procesu rozwoju oprogramowania, który przynosi wiele korzyści:
- Szybsze wdrażanie zmian - automatyzacja procesów skraca czas potrzebny na dostarczenie nowych funkcji
- Wyższa jakość kodu - automatyczne testy i analizy kodu zapewniają wyższą jakość
- Mniejsze ryzyko błędów - automatyzacja eliminuje błędy ludzkie
- Szybszy feedback - deweloperzy otrzymują natychmiastową informację zwrotną o jakości kodu
- Zwiększona produktywność - deweloperzy koncentrują się na rozwoju funkcji, a nie na manualnych procesach
Poprawnie skonfigurowany pipeline CI/CD jest inwestycją, która zwraca się w postaci oszczędności czasu, wyższej jakości kodu i bardziej niezawodnych wdrożeń. Niezależnie od wybranej platformy (GitHub Actions, GitLab CI, CircleCI, Jenkins), kluczowe jest dostosowanie procesu do specyficznych potrzeb projektu i zespołu.
Kod do tej lekcji: App.tsx
1import React, { useState } from 'react';
2
3// Demo: CI/CD i automatyzacja wdrozen
4interface PipelineStage {
5 id: string;
6 name: string;
7 icon: string;
8 status: 'pending' | 'running' | 'success' | 'failed' | 'skipped';
9 duration?: number;
10 steps: string[];
11}
12
13function CICDDemo() {
14 const [stages, setStages] = useState<PipelineStage[]>([
15 { id: 'install', name: 'Install', icon: 'I', status: 'pending', steps: ['npm ci', 'Cache dependencies'] },
16 { id: 'lint', name: 'Lint', icon: 'L', status: 'pending', steps: ['ESLint', 'Prettier check', 'TypeScript check'] },
17 { id: 'test', name: 'Test', icon: 'T', status: 'pending', steps: ['Unit tests', 'Integration tests', 'Coverage report'] },
18 { id: 'build', name: 'Build', icon: 'B', status: 'pending', steps: ['next build', 'Bundle analysis', 'Optimize assets'] },
19 { id: 'deploy', name: 'Deploy', icon: 'D', status: 'pending', steps: ['Deploy to staging', 'Health check', 'Promote to production'] },
20 ]);
21 const [activeProvider, setActiveProvider] = useState<'github' | 'gitlab' | 'vercel'>('github');
22 const [isRunning, setIsRunning] = useState(false);
23 const [failAt, setFailAt] = useState<string>('none');
24
25 const statusColors: Record<string, string> = {
26 pending: '#64748b', running: '#f59e0b', success: '#10b981', failed: '#ef4444', skipped: '#8892b0',
27 };
28
29 const statusIcons: Record<string, string> = {
30 pending: 'o', running: '~', success: 'V', failed: 'X', skipped: '-',
31 };
32
33 const runPipeline = async () => {
34 setIsRunning(true);
35 setStages(prev => prev.map(s => ({ ...s, status: 'pending', duration: undefined })));
36
37 for (let i = 0; i < stages.length; i++) {
38 const stage = stages[i];
39
40 setStages(prev => prev.map((s, idx) => idx === i ? { ...s, status: 'running' } : s));
41
42 const duration = Math.floor(Math.random() * 3000) + 1000;
43 await new Promise(r => setTimeout(r, duration));
44
45 if (failAt === stage.id) {
46 setStages(prev => prev.map((s, idx) =>
47 idx === i ? { ...s, status: 'failed', duration } :
48 idx > i ? { ...s, status: 'skipped' } : s
49 ));
50 setIsRunning(false);
51 return;
52 }
53
54 setStages(prev => prev.map((s, idx) => idx === i ? { ...s, status: 'success', duration } : s));
55 }
56
57 setIsRunning(false);
58 };
59
60 const ciConfigs: Record<string, string> = {
61 github: "# .github/workflows/deploy.yml\nname: Deploy\non:\n push:\n branches: [main]\n\njobs:\n deploy:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-node@v4\n - run: npm ci\n - run: npm run lint\n - run: npm run test\n - run: npm run build\n - run: npx vercel --prod",
62 gitlab: "# .gitlab-ci.yml\nstages:\n - install\n - lint\n - test\n - build\n - deploy\n\ninstall:\n stage: install\n script: npm ci\n cache:\n paths: [node_modules/]\n\ntest:\n stage: test\n script: npm run test\n\nbuild:\n stage: build\n script: npm run build",
63 vercel: "// vercel.json\n{\n \"buildCommand\": \"npm run build\",\n \"devCommand\": \"npm run dev\",\n \"installCommand\": \"npm ci\",\n \"framework\": \"nextjs\",\n \"github\": {\n \"autoAlias\": true,\n \"silent\": true\n }\n}",
64 };
65
66 return (
67 <div style={{ background: '#0f0f23', minHeight: '100vh', padding: '20px', color: '#e0e0e0', fontFamily: 'system-ui' }}>
68 <h1 style={{ color: '#64ffda', marginBottom: '8px' }}>CI/CD Pipeline</h1>
69 <p style={{ color: '#8892b0', marginBottom: '24px' }}>Automatyzacja wdrozen aplikacji Next.js</p>
70
71 <div style={{ display: 'flex', gap: '8px', marginBottom: '20px' }}>
72 {[
73 { id: 'github' as const, label: 'GitHub Actions' },
74 { id: 'gitlab' as const, label: 'GitLab CI' },
75 { id: 'vercel' as const, label: 'Vercel' },
76 ].map(p => (
77 <button key={p.id} onClick={() => setActiveProvider(p.id)} style={{
78 padding: '8px 16px', borderRadius: '8px', border: 'none', cursor: 'pointer',
79 background: activeProvider === p.id ? '#64ffda' : '#1a1a2e', color: activeProvider === p.id ? '#0f0f23' : '#8892b0', fontWeight: 'bold',
80 }}>{p.label}</button>
81 ))}
82 </div>
83
84 <div style={{ background: '#1a1a2e', padding: '20px', borderRadius: '12px', border: '1px solid #333', marginBottom: '24px' }}>
85 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
86 <h3 style={{ color: '#64ffda', margin: 0 }}>Pipeline Stages</h3>
87 <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
88 <select value={failAt} onChange={e => setFailAt(e.target.value)} style={{ padding: '6px 10px', borderRadius: '6px', background: '#0f0f23', color: '#fff', border: '1px solid #333', fontSize: '12px' }}>
89 <option value="none">No failure</option>
90 {stages.map(s => <option key={s.id} value={s.id}>Fail at {s.name}</option>)}
91 </select>
92 <button onClick={runPipeline} disabled={isRunning} style={{ padding: '8px 16px', borderRadius: '6px', background: isRunning ? '#333' : '#64ffda', color: '#0f0f23', border: 'none', cursor: isRunning ? 'default' : 'pointer', fontWeight: 'bold' }}>
93 {isRunning ? 'Running...' : 'Run Pipeline'}
94 </button>
95 </div>
96 </div>
97
98 <div style={{ display: 'flex', gap: '16px', alignItems: 'flex-start', flexWrap: 'wrap' }}>
99 {stages.map((stage, i) => (
100 <React.Fragment key={stage.id}>
101 <div style={{ textAlign: 'center', minWidth: '120px' }}>
102 <div style={{
103 width: '50px', height: '50px', borderRadius: '50%', margin: '0 auto 8px',
104 border: '3px solid ' + statusColors[stage.status],
105 display: 'flex', alignItems: 'center', justifyContent: 'center',
106 background: stage.status === 'running' ? statusColors[stage.status] + '30' : 'transparent',
107 fontSize: '18px', fontWeight: 'bold', color: statusColors[stage.status],
108 }}>
109 {statusIcons[stage.status]}
110 </div>
111 <div style={{ fontWeight: 'bold', fontSize: '14px', color: statusColors[stage.status] }}>{stage.name}</div>
112 {stage.duration && <div style={{ fontSize: '11px', color: '#8892b0' }}>{(stage.duration / 1000).toFixed(1)}s</div>}
113 <div style={{ marginTop: '8px' }}>
114 {stage.steps.map((step, j) => (
115 <div key={j} style={{ fontSize: '11px', color: '#555', padding: '1px 0' }}>{step}</div>
116 ))}
117 </div>
118 </div>
119 {i < stages.length - 1 && <div style={{ paddingTop: '20px', color: '#333', fontSize: '20px' }}>-></div>}
120 </React.Fragment>
121 ))}
122 </div>
123 </div>
124
125 <div style={{ background: '#1a1a2e', padding: '16px', borderRadius: '8px', border: '1px solid #333' }}>
126 <h3 style={{ color: '#f59e0b', fontSize: '14px', marginTop: 0 }}>Konfiguracja ({activeProvider}):</h3>
127 <pre style={{ fontSize: '12px', color: '#e0e0e0', margin: 0, overflow: 'auto', whiteSpace: 'pre-wrap' }}>{ciConfigs[activeProvider]}</pre>
128 </div>
129 </div>
130 );
131}
132
133export default CICDDemo;Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. CI/CD oznacza:
2. Jaka jest różnica między Continuous Delivery a Continuous Deployment?
Zadania praktyczne w grze
- Edytor kodu
Utwórz workflow GitHub Actions uruchamiany przy pushu na gałąź main: checkout, setup-node, instalacja zależności, testy, build, a na końcu deployment na Vercel z tokenem z sekretu VERCEL_TOKEN.
- Układanie w pionie
Ułóż elementy w prawidłowej kolejności: Code push → Run tests → Build app
- Układanie w pionie
Ułóż składnię konfiguracji triggera GitHub Actions workflow:
- Edytor kodu
Utwórz health check API endpoint z database connectivity, external services status i system metrics