Next.js CI/CD Pipeline
In modern web development, automating the testing and deployment of your applications is essential for maintaining quality and rapid delivery. In this tutorial, you'll learn how to set up a Continuous Integration and Continuous Deployment (CI/CD) pipeline for your Next.js applications.
What is CI/CD?
Continuous Integration (CI) is the practice of automatically integrating code changes from multiple contributors into a shared repository, where automated builds and tests verify each integration.
Continuous Deployment (CD) automatically deploys all code changes to a testing and/or production environment after the build stage.
Together, CI/CD creates an automated pipeline that improves developer productivity and helps catch bugs early.
Why implement CI/CD for Next.js applications?
- Consistency: Every code change goes through the same testing process
- Early bug detection: Automated tests catch issues before they reach production
- Faster releases: Automate tedious manual deployment steps
- Better collaboration: Team members can integrate their changes frequently
Setting up a CI/CD Pipeline for Next.js
We'll cover how to set up a CI/CD pipeline using GitHub Actions, which is free for public repositories and includes limited free minutes for private repositories.
Prerequisites
- A Next.js application
- A GitHub repository for your project
- Basic understanding of Git
- A deployment platform (we'll use Vercel in this tutorial)
Creating a Basic GitHub Actions Workflow
Let's start by creating a basic CI workflow that runs tests whenever code is pushed to the repository.
- Create a directory structure in your project:
mkdir -p .github/workflows
- Create a new file
.github/workflows/ci.yml
:
name: Next.js CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: '18.x'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Build
run: npm run build
- name: Test
run: npm test
This workflow does the following:
- Triggers on pushes to the
main
branch and pull requests targetingmain
- Uses an Ubuntu environment
- Sets up Node.js 18
- Installs dependencies with
npm ci
(faster and more reliable thannpm install
) - Runs linting, building, and testing scripts
Understanding the workflow file
name
: A name for your workflowon
: Defines when the workflow runsjobs
: Groups the jobs to runbuild
: The job IDruns-on
: The type of runner to usesteps
: The sequence of tasks to execute
Adding Automated Testing
Testing is a crucial component of CI. Let's set up Jest for testing our Next.js components.
- Install Jest and testing libraries:
npm install --save-dev jest @testing-library/react @testing-library/jest-dom jest-environment-jsdom
- Create a
jest.config.js
file:
const nextJest = require('next/jest')
const createJestConfig = nextJest({
// Provide the path to your Next.js app to load next.config.js and .env files
dir: './',
})
// Add any custom config to be passed to Jest
const customJestConfig = {
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
moduleNameMapper: {
'^@/components/(.*)$': '<rootDir>/components/$1',
'^@/pages/(.*)$': '<rootDir>/pages/$1',
},
testEnvironment: 'jest-environment-jsdom',
}
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
module.exports = createJestConfig(customJestConfig)
- Create a
jest.setup.js
file:
import '@testing-library/jest-dom/extend-expect'
- Add test scripts to your
package.json
:
"scripts": {
"test": "jest",
"test:watch": "jest --watch"
}
- Create a simple test file
__tests__/Home.test.js
:
import { render, screen } from '@testing-library/react'
import Home from '../pages/index'
describe('Home page', () => {
it('renders without crashing', () => {
render(<Home />)
expect(screen.getByRole('heading')).toBeInTheDocument()
})
})
Setting up Continuous Deployment with Vercel
Vercel is a deployment platform that works excellently with Next.js (since Next.js is developed by Vercel). Let's set up CD with Vercel:
- Sign up for a Vercel account at vercel.com
- Install the Vercel CLI:
npm install -g vercel
- Log in to Vercel from the CLI:
vercel login
- Create a
.github/workflows/cd.yml
file:
name: Next.js CD
on:
push:
branches: [ main ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Deploy to Vercel
uses: amondnet/vercel-action@v20
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.ORG_ID }}
vercel-project-id: ${{ secrets.PROJECT_ID }}
vercel-args: '--prod'
- Generate your Vercel token and project details:
vercel whoami
vercel projects
- Add the secrets to your GitHub repository:
- Go to your GitHub repository
- Navigate to Settings > Secrets and variables > Actions
- Add the following secrets:
VERCEL_TOKEN
: Your Vercel tokenORG_ID
: Your Vercel organization IDPROJECT_ID
: Your Vercel project ID
Creating a Complete CI/CD Pipeline
Now, let's integrate both CI and CD into a single workflow file:
name: Next.js CI/CD
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: '18.x'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Build
run: npm run build
- name: Test
run: npm test
deploy:
needs: build-and-test
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Deploy to Vercel
uses: amondnet/vercel-action@v20
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.ORG_ID }}
vercel-project-id: ${{ secrets.PROJECT_ID }}
vercel-args: '--prod'
Key points in this workflow:
- The
deploy
job depends on successful completion ofbuild-and-test
(needs: build-and-test
) - Deployment only happens for pushes to the
main
branch, not pull requests (if: github.ref == 'refs/heads/main' && github.event_name == 'push'
)
Adding Environment-Specific Deployments
For a more advanced setup, you might want different environments:
name: Next.js CI/CD Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
# Same as before
deploy-preview:
needs: build-and-test
if: github.ref == 'refs/heads/develop' && github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Deploy to Preview
uses: amondnet/vercel-action@v20
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.ORG_ID }}
vercel-project-id: ${{ secrets.PROJECT_ID }}
deploy-production:
needs: build-and-test
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Deploy to Production
uses: amondnet/vercel-action@v20
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.ORG_ID }}
vercel-project-id: ${{ secrets.PROJECT_ID }}
vercel-args: '--prod'
Real-World Example: Integrating Cache and Performance Checks
Here's a more comprehensive workflow with cache optimization and web performance checks:
name: Production CI/CD
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build-test-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: '18.x'
cache: 'npm'
- name: Get npm cache directory
id: npm-cache-dir
run: echo "::set-output name=dir::$(npm config get cache)"
- name: Cache node modules
uses: actions/cache@v3
with:
path: ${{ steps.npm-cache-dir.outputs.dir }}
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Build
run: npm run build
- name: Test
run: npm test
- name: Run Lighthouse CI
uses: treosh/lighthouse-ci-action@v9
with:
uploadArtifacts: true
temporaryPublicStorage: true
runs: 3
configPath: './.github/workflows/lighthouserc.json'
- name: Deploy to Vercel
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
uses: amondnet/vercel-action@v20
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.ORG_ID }}
vercel-project-id: ${{ secrets.PROJECT_ID }}
vercel-args: '--prod'
And create a .github/workflows/lighthouserc.json
file:
{
"ci": {
"collect": {
"staticDistDir": "./out",
"url": ["http://localhost:3000"]
},
"assert": {
"assertions": {
"categories:performance": ["error", {"minScore": 0.8}],
"categories:accessibility": ["warn", {"minScore": 0.9}]
}
}
}
}
Summary
You've now learned how to create a CI/CD pipeline for your Next.js applications using GitHub Actions and Vercel. This setup will:
- Automatically run tests when code is pushed or pull requests are created
- Deploy to preview environments for development branches
- Deploy to production for the main branch
- Include performance and accessibility checks
By implementing a CI/CD pipeline, you'll save time, catch issues early, and maintain a high-quality codebase.
Additional Resources
- GitHub Actions Documentation
- Vercel Documentation for Next.js
- Jest Testing Framework
- React Testing Library
- Lighthouse CI
Exercises
- Set up a basic CI workflow for your Next.js project that runs linting and tests
- Add a step to your workflow that checks for bundle size increases
- Configure preview deployments for feature branches
- Implement end-to-end tests using Cypress and add them to your CI pipeline
- Set up notifications (Slack, email, etc.) for failed deployments
These exercises will help you become more familiar with CI/CD pipelines and ensure your Next.js applications are always production-ready.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)