Kurs Next.js · Moduł 8: Deployment i produkcja

CI/CD i automatyzacja wdrożeń

13 min czytania
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:

  1. GitHub Actions - zintegrowane z GitHub, oferuje doskonałą integrację z repo
  2. GitLab CI/CD - wbudowane w GitLab, pozwala na tworzenie potoków CI/CD
  3. CircleCI - popularna platforma CI/CD z prostym konfigurowaniem
  4. Jenkins - otwarty, rozszerzalny serwer automatyzacji
  5. Vercel CI/CD - natywne rozwiązanie dla aplikacji Next.js
  6. Netlify CI/CD - alternatywa z podobnymi funkcjami
  7. 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 build

Ten 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 Vercel
  • ORG_ID - ID organizacji w Vercel
  • PROJECT_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: true

Konfiguracja 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    - main

Konfiguracja 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: main

Konfiguracja 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 prettier

Zaktualizuj 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/changelog

Utwó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-release

3. 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: ./docs

Strategie 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 deployment

Zarzą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 deploy

2. 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:

  1. Szybsze wdrażanie zmian - automatyzacja procesów skraca czas potrzebny na dostarczenie nowych funkcji
  2. Wyższa jakość kodu - automatyczne testy i analizy kodu zapewniają wyższą jakość
  3. Mniejsze ryzyko błędów - automatyzacja eliminuje błędy ludzkie
  4. Szybszy feedback - deweloperzy otrzymują natychmiastową informację zwrotną o jakości kodu
  5. 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' }}>-&gt;</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. 1. CI/CD oznacza:

  2. 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

Przydatne artykuły