[SAMPLE] GitHub Actions CI CD Pipeline Guide
GitHub Actions CI/CD Pipeline Guide
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.
Understanding GitHub Actions
Core Concepts
| 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 |
Workflow File Location
All workflows live in .github/workflows/:
.github/
└── workflows/
├── ci.yml # Run tests on every push
├── deploy.yml # Deploy to production
└── scheduled.yml # Nightly tasks
Your First Workflow
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 Dependencies
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-Matrix Builds
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.
Database Services
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/testdbEnvironment Variables and Secrets
Using Secrets
Store 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 deployEnvironment-Specific Variables
Use 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 }}"A Complete CI/CD Pipeline
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"Reusable Workflows
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: 20Conditional Steps
Run 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 }}Status Badges
Add a workflow status badge to your README:
Branch Protection Rules
Complement your CI pipeline with branch protection:
- Go to Settings > Branches > Branch protection rules
- Add rule for
main:- Require status checks to pass (select your CI jobs)
- Require pull request reviews
- Do not allow bypassing the above settings
This ensures no code reaches main without passing tests and review.
Performance Tips
- Use concurrency groups to cancel redundant runs
- Cache aggressively (dependencies, build outputs)
- Parallelize independent jobs (lint, test, build)
- Use
pathsfilter to skip workflows for unrelated changes - Choose the right runner (ubuntu-latest is cheapest and fastest)
on:
push:
paths:
- "src/**"
- "package.json"
- ".github/workflows/**"Debugging Workflows
When a workflow fails:
- Check the Actions tab for detailed logs
- Add
ACTIONS_STEP_DEBUGsecret set totruefor verbose output - Use
act(https://github.com/nektos/act) to run workflows locally - Add
- run: envsteps to inspect the environment
Summary
A 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.