Loading...
Continuous Integration and Continuous Deployment (CI/CD) automates the process of testing, building, and deploying your code. GitHub Actions makes this accessible directly within your repository. This guide walks you through building a production-ready pipeline for a modern web application.
| Concept | Description |
|---|---|
| Workflow | An automated process defined in a YAML file |
| Event | A trigger that starts a workflow (push, PR, schedule) |
| Job | A set of steps that run on the same runner |
| Step | An individual task (run a command or use an action) |
| Action | A reusable unit of code (from GitHub Marketplace or custom) |
| Runner | A server that executes your workflow |
All workflows live in .github/workflows/:
.github/
└── workflows/
├── ci.yml # Run tests on every push
├── deploy.yml # Deploy to production
└── scheduled.yml # Nightly tasks
Create .github/workflows/ci.yml:
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run tests
run: npm testThis workflow runs on every push and pull request, checking out your code, installing dependencies, linting, and running tests.
Caching dramatically speeds up workflows by reusing downloaded dependencies:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"The cache: "npm" option automatically caches the npm global cache directory. For more control:
- name: Cache node_modules
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-Test across multiple Node.js versions or operating systems:
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
node-version: [18, 20, 22]
fail-fast: false
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm testfail-fast: false ensures all combinations run even if one fails, giving you a complete picture.
Many applications need a database for integration tests. Use service containers:
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm test
env:
DATABASE_URL: postgres://test:test@localhost:5432/testdbStore sensitive values in repository settings under Settings > Secrets:
steps:
- name: Deploy
env:
API_KEY: ${{ secrets.API_KEY }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: npm run deployUse environments for staging/production separation:
jobs:
deploy-staging:
runs-on: ubuntu-latest
environment: staging
steps:
- run: echo "Deploying to ${{ vars.DEPLOY_URL }}"
deploy-production:
runs-on: ubuntu-latest
environment: production
needs: deploy-staging
steps:
- run: echo "Deploying to ${{ vars.DEPLOY_URL }}"Here is a production-ready pipeline for a Next.js application:
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
# Job 1: Lint and Type Check
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"
- run: npm ci
- run: npm run lint
- run: npm run type-check
# Job 2: Unit and Integration Tests
test:
runs-on: ubuntu-latest
needs: quality
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"
- run: npm ci
- run: npm test -- --coverage
env:
DATABASE_URL: postgres://test:test@localhost:5432/testdb
- name: Upload coverage
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
# Job 3: Build
build:
runs-on: ubuntu-latest
needs: quality
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"
- run: npm ci
- run: npm run build
- name: Upload build
uses: actions/upload-artifact@v4
with:
name: build-output
path: .next/
# Job 4: Deploy to Staging
deploy-staging:
runs-on: ubuntu-latest
needs: [test, build]
if: github.ref == 'refs/heads/develop'
environment: staging
steps:
- uses: actions/checkout@v4
- name: Deploy to Vercel (Preview)
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
# Job 5: Deploy to Production
deploy-production:
runs-on: ubuntu-latest
needs: [test, build]
if: github.ref == 'refs/heads/main'
environment: production
steps:
- uses: actions/checkout@v4
- name: Deploy to Vercel (Production)
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
vercel-args: "--prod"Avoid duplication by creating reusable workflows:
# .github/workflows/reusable-test.yml
name: Reusable Test Workflow
on:
workflow_call:
inputs:
node-version:
required: false
type: number
default: 20
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- run: npm ci
- run: npm testCall it from another workflow:
# .github/workflows/ci.yml
jobs:
test:
uses: ./.github/workflows/reusable-test.yml
with:
node-version: 20Run steps based on conditions:
steps:
- name: Run E2E tests (PRs only)
if: github.event_name == 'pull_request'
run: npm run test:e2e
- name: Notify on failure
if: failure()
uses: slackapi/slack-github-action@v1
with:
payload: '{"text": "CI failed on ${{ github.ref }}"}'
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}Add a workflow status badge to your README:
Complement your CI pipeline with branch protection:
main:
This ensures no code reaches main without passing tests and review.
paths filter to skip workflows for unrelated changeson:
push:
paths:
- "src/**"
- "package.json"
- ".github/workflows/**"When a workflow fails:
ACTIONS_STEP_DEBUG secret set to true for verbose outputact (https://github.com/nektos/act) to run workflows locally- run: env steps to inspect the environmentA well-configured CI/CD pipeline catches bugs early, enforces code quality, and automates deployment. Start with a simple test workflow, then incrementally add linting, builds, and deployment as your project matures. The pipeline in this guide covers the most common patterns for modern web applications.
Comments