Next.js course Β· Module 8: Deployment and Production
CI/CD and deployment automation
In this lesson12
In modern software development, Continuous Integration (CI) and Continuous Deployment (CD) are key practices that allow for faster and more reliable application delivery. Next.js, as a modern framework, integrates perfectly with various CI/CD tools, which allows automating the process of testing, building, and deploying applications.
What are CI/CD?
Continuous Integration (CI)
Continuous Integration is a practice where developers regularly integrate their code changes into the main branch of the repository. Each integration is automatically verified by:
- Running unit and integration tests
- Static code analysis (linting)
- Checking code quality
- Type checking (in the case of TypeScript)
CI allows early detection of errors and ensures that the code in the main branch remains stable.
Continuous Deployment (CD)
Continuous Deployment goes one step further - after successfully passing the CI stage, it automatically deploys changes to the production or test environment. The CD process includes:
- Automatic application building
- Deployment to a selected environment (staging, production)
- Automatic E2E tests after deployment
- Deployment monitoring
Popular CI/CD tools for Next.js applications
For Next.js applications, we can use various CI/CD tools:
- GitHub Actions - integrated with GitHub, offers excellent integration with the repo
- GitLab CI/CD - built into GitLab, allows creating CI/CD pipelines
- CircleCI - a popular CI/CD platform with simple configuration
- Jenkins - an open, extensible automation server
- Vercel CI/CD - native solution for Next.js applications
- Netlify CI/CD - an alternative with similar features
- Travis CI - one of the oldest CI/CD platforms
CI/CD configuration with GitHub Actions
The easiest way to implement CI/CD for a Next.js project is to use GitHub Actions if your repository is hosted on GitHub.
1. Configuring the testing workflow
Create the file .github/workflows/ci.yml in your repository:
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 buildThis workflow runs on every push to the main and development branches and when a Pull Request is created to these branches.
2. Vercel deployment workflow configuration
Create the file .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'For this to work, you need to configure the following secrets in your repository settings:
VERCEL_TOKEN- Vercel API tokenORG_ID- Vercel organization IDPROJECT_ID- Vercel project ID
3. Workflow configuration for the staging environment
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: trueCI/CD configuration with GitLab CI
If you use GitLab, you can configure CI/CD using .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 - mainCI/CD configuration for a Next.js application with CircleCI
To configure CircleCI, create the file .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: mainCI/CD configuration with Jenkins
Jenkins is a powerful CI/CD tool, but requires more configuration. Here is an example Jenkinsfile for a CI/CD pipeline for a Next.js application:
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}Testing in the CI/CD process
An important element of the CI/CD process is automatic testing. For Next.js applications, we can configure different types of tests:
1. Unit tests with Jest and 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. Integration tests for 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. End-to-End tests with 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});Add Cypress configuration to 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 }}Automation of development tasks
Beyond CI/CD, we can automate various other development tasks:
1. Automatic code formatting with Husky and lint-staged
Install the required packages:
1npm install --save-dev husky lint-staged prettierUpdate 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}Create a pre-commit hook:
1npx husky add .husky/pre-commit "npx lint-staged"2. Automatic versioning with Semantic Release
Install the required packages:
1npm install --save-dev semantic-release @semantic-release/git @semantic-release/changelogCreate the file .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}Add a workflow for 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. Automatic documentation generation
For TypeScript projects, you can automate documentation generation:
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: ./docsDeployment and rollback strategies
When deploying applications, it's worth implementing safe deployment strategies:
1. Blue-Green Deployment
Blue-Green deployment involves maintaining two identical production environments, of which only one is active. In the case of Vercel, you can use their built-in "Preview Deployments" feature and domain aliasing:
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
In the Canary strategy, a new version is gradually released, initially only for a small percentage of users. In Next.js, you can achieve this using feature flags and traffic splitting:
1// lib/feature-flags.ts
2import { useEffect, useState } from 'react';
3
4const CANARY_PERCENTAGE = 10; // 10% of users will get the new version
5
6export function useCanaryFeature(featureName: string): boolean {
7 const [isEnabled, setIsEnabled] = useState(false);
8
9 useEffect(() => {
10 // Generate a random number for the user and save it in 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 // Check if the user is in the canary group
18 const inCanaryGroup = parseInt(userBucket) < CANARY_PERCENTAGE;
19
20 // Check if the feature is active for this user
21 fetch('/api/feature-flags')
22 .then(res => res.json())
23 .then(flags => {
24 // If the flag exists and the user is in the canary group, enable the feature
25 if (flags[featureName] && inCanaryGroup) {
26 setIsEnabled(true);
27 }
28 });
29 }, [featureName]);
30
31 return isEnabled;
32}3. Automatic rollback
Automatic rollback deployment when issues arise is a key practice:
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 deploymentManaging environment variables
Secure management of environment variables is a critical aspect of CI/CD:
1. Storing environment variables in the CI/CD system
Most CI/CD systems offer secure storage of secrets, e.g., 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. Automatic rotation of secrets
For critical applications, it's worth implementing automatic rotation of secrets:
1# .github/workflows/rotate-secrets.yml
2name: Rotate Secrets
3
4on:
5 schedule:
6 - cron: '0 0 1 * *' # Every month
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 }}Deployment monitoring
A key element of the CI/CD process is deployment monitoring to quickly detect and respond to potential problems:
1. Integration with monitoring services
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. Automatic post-deployment tests
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 }}Summary
Implementing CI/CD for Next.js applications is a key element of the modern software development process that brings many benefits:
- Faster deployment of changes - process automation reduces the time needed to deliver new features
- Higher code quality - automatic tests and code analysis ensure higher quality
- Lower risk of errors - automation eliminates human errors
- Faster feedback - developers receive immediate feedback on code quality
- Increased productivity - developers focus on feature development, not on manual processes
A properly configured CI/CD pipeline is an investment that pays off in time savings, higher code quality, and more reliable deployments. Regardless of the chosen platform (GitHub Actions, GitLab CI, CircleCI, Jenkins), the key is to adapt the process to the specific needs of the project and team.
Code for this lesson: App.tsx
1import React, { useState } from 'react';
2
3// Demo: CI/CD and deployment automation
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' }}>Automation of Next.js application deployments</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 }}>Configuration ({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;Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. CI/CD stands for:
2. What is the difference between Continuous Delivery and Continuous Deployment?
Hands-on tasks in the game
- Code editor
Create a GitHub Actions workflow that runs on push to the main branch: checkout, setup-node, install the dependencies, run the tests, build, and finally deploy to Vercel with the token from the VERCEL_TOKEN secret.
- Vertical ordering
Arrange the elements in the correct order: Code push β Run tests β Build app
- Vertical ordering
Arrange the GitHub Actions workflow trigger configuration syntax:
- Code editor
Create a health check API endpoint with database connectivity, external services status, and system metrics