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.
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:
CI pozwala na wczesne wykrywanie błędów i zapewnia, że kod w głównej gałęzi pozostaje stabilny.
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:
Dla aplikacji Next.js możemy wykorzystać różne narzędzia CI/CD:
Najprostszym sposobem na wdrożenie CI/CD dla projektu Next.js jest użycie GitHub Actions, jeśli twoje repozytorium jest hostowane na GitHub.
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.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 Vercel1name: 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: trueJeś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 - mainAby 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: mainJenkins 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}Ważnym elementem procesu CI/CD jest automatyczne testowanie. Dla aplikacji Next.js możemy skonfigurować różne rodzaje testów:
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});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});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 }}Poza CI/CD, możemy zautomatyzować różne inne zadania deweloperskie:
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"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]
11
12${nextRelease.notes}"
13 }],
14 "@semantic-release/github"
15 ]
16}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-releaseW 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: ./docsPodczas wdrażania aplikacji warto zaimplementować bezpieczne strategie wdrażania:
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}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}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 deploymentBezpieczne zarządzanie zmiennymi środowiskowymi jest krytycznym aspektem 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 deployW 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 }}Kluczowym elementem procesu CI/CD jest monitorowanie wdrożeń, aby szybko wykryć i zareagować na ewentualne problemy:
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"}'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 }}
26Deployment URL: ${{ github.event.deployment_status.target_url }}"
27 }
28 env:
29 SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}Implementacja CI/CD dla aplikacji Next.js jest kluczowym elementem nowoczesnego procesu rozwoju oprogramowania, który przynosi wiele korzyści:
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.