CI/CD Pipelines and DevOps Automation with GitHub Actions
CI/CD pipelines are essential for modern software development. Automated testing, building, and deployment save time and reduce human errors. Let's build a production-ready pipeline for a full-stack application using GitHub Actions.
Why CI/CD Matters
CI/CD provides significant benefits:
- Speed - Automate repetitive tasks, deploy faster
- Reliability - Consistent deployments reduce errors
- Quality - Automated testing catches issues early
- Collaboration - Better feedback for team members
- Monitoring - Track build history and failures
- Rollback - Quickly revert problematic deployments
GitHub Actions Basics
GitHub Actions is a built-in CI/CD platform integrated with GitHub.
Key Concepts
- Workflows - YAML files in .github/workflows/
- Jobs - Individual tasks in a workflow
- Steps - Commands within a job
- Actions - Reusable units of code
- Triggers - Events that start workflows (push, pull_request, etc.)
Basic Workflow Structure
yaml
# .github/workflows/ci.yml
name: CI
# Trigger on push to main and pull requests
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
env:
NODE_VERSION: "20"
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x, 20.x]
steps:
# Check out code
- uses: actions/checkout@v4
# Setup Node.js
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: "npm"
# Install dependencies
- name: Install dependencies
run: npm ci
# Lint code
- name: Lint
run: npm run lint
# Run tests
- name: Run tests
run: npm run test:ci
# Build project
- name: Build
run: npm run build
# Upload coverage
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage/coverage-final.json
Full-Stack CI/CD Pipeline
Comprehensive pipeline for Next.js full-stack app.
yaml
# .github/workflows/full-stack-ci-cd.yml
name: Full-Stack CI/CD
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
workflow_dispatch:
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
permissions:
contents: read
packages: write
jobs:
# Quality checks
quality:
name: Code Quality
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- run: npm ci
- name: Lint TypeScript
run: npm run lint
- name: Type checking
run: npm run type-check
- name: Format check
run: npm run format:check
# Unit & Integration Tests
test:
name: Tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: test_db
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
redis:
image: redis:7-alpine
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- run: npm ci
- name: Setup database
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db
run: |
npm run db:migrate
npm run db:seed
- name: Run unit tests
run: npm run test:unit
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db
REDIS_URL: redis://localhost:6379
- name: Run integration tests
run: npm run test:integration
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db
REDIS_URL: redis://localhost:6379
- name: Run e2e tests
run: npm run test:e2e
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db
BASE_URL: http://localhost:3000
- name: Upload coverage
uses: codecov/codecov-action@v3
if: always()
with:
flags: unittests
name: codecov-umbrella
# Build Docker image
build:
name: Build Docker Image
needs: [quality, test]
runs-on: ubuntu-latest
outputs:
image: ${{ steps.image.outputs.image }}
tags: ${{ steps.meta.outputs.tags }}
digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@v4
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha
- name: Build and push
id: build
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Set image output
id: image
run: echo "image=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" >> $GITHUB_OUTPUT
# Security scanning
security:
name: Security Scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- run: npm ci
- name: Run npm audit
run: npm audit --audit-level=moderate
continue-on-error: true
- name: Dependency scanning
uses: github/super-linter@v5
env:
DEFAULT_BRANCH: main
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Deploy to staging
deploy-staging:
name: Deploy to Staging
needs: [build, security]
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/develop'
environment:
name: staging
url: https://staging.akkaya.dev
steps:
- uses: actions/checkout@v4
- name: Deploy to staging
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID_STAGING }}
run: |
npm i -g vercel
vercel pull --yes --environment=preview
vercel build --prod
vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }}
- name: Notify deployment
uses: 8398a7/action-slack@v3
if: always()
with:
status: ${{ job.status }}
text: "Staging deployment ${{ job.status }}"
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
# Deploy to production
deploy-production:
name: Deploy to Production
needs: [build, security]
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
environment:
name: production
url: https://akkaya.dev
steps:
- uses: actions/checkout@v4
- name: Create deployment
uses: actions/github-script@v7
id: deployment
with:
script: |
const deployment = await github.rest.repos.createDeployment({
owner: context.repo.owner,
repo: context.repo.repo,
ref: context.sha,
environment: 'production',
required_contexts: [],
auto_merge: false
})
return deployment.data.id
- name: Deploy to production
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
run: |
npm i -g vercel
vercel pull --yes --environment=production
vercel build --prod
vercel deploy --prod --token=${{ secrets.VERCEL_TOKEN }}
- name: Update deployment status
uses: actions/github-script@v7
if: always()
with:
script: |
github.rest.repos.createDeploymentStatus({
owner: context.repo.owner,
repo: context.repo.repo,
deployment_id: ${{ steps.deployment.outputs.result }},
state: '${{ job.status }}',
environment_url: 'https://akkaya.dev'
})
- name: Notify Slack
uses: 8398a7/action-slack@v3
if: always()
with:
status: ${{ job.status }}
text: "Production deployment ${{ job.status }}"
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
Dockerfile for Next.js
Optimized multi-stage Docker build.
dockerfile
# Dockerfile
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine AS runtime
WORKDIR /app
ENV NODE_ENV production
RUN addgroup -g 1001 -S nodejs && \
adduser -S nextjs -u 1001
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
CMD ["node", "server.js"]
Environment Secrets Management
Securely manage secrets in GitHub Actions.
yaml
# .github/workflows/secure-deployment.yml
name: Secure Deployment
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: production
url: https://akkaya.dev
steps:
- uses: actions/checkout@v4
- name: Deploy with secrets
env:
# Organization secrets (available to all repos)
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
# Repository secrets
DATABASE_URL: ${{ secrets.DATABASE_URL }}
JWT_SECRET: ${{ secrets.JWT_SECRET }}
# Environment secrets (per environment)
API_KEY: ${{ secrets.API_KEY }}
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
run: |
npm run build
npm run deploy
- name: Create issue on failure
if: failure()
uses: actions/github-script@v7
with:
script: |
github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: Deployment failed on ${new Date().toISOString()},
body: Workflow: ${context.workflow}\nRun: ${context.runId}
})
Matrix Testing
Test across multiple configurations.
yaml
# .github/workflows/matrix-testing.yml
name: Matrix Testing
on: [push, pull_request]
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node-version: [18.x, 20.x]
database: [postgres, mysql, mariadb]
exclude:
# Don't test MySQL on macOS
- os: macos-latest
database: mysql
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Run tests on ${{ matrix.os }}
run: npm run test
env:
DB_TYPE: ${{ matrix.database }}
Caching Dependencies
Speed up workflows with caching.
yaml
name: Fast CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
# Automatically handles npm caching
cache: "npm"
# Cache Prisma generated files
- uses: actions/cache@v3
with:
path: |
node_modules/.prisma/client
key: ${{ runner.os }}-prisma-${{ hashFiles('**/package-lock.json') }}
# Cache build output
- uses: actions/cache@v3
with:
path: .next/cache
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}
- run: npm ci
- run: npm run build
Performance Monitoring
Track workflow performance.
yaml
name: Performance Monitoring
on: [push]
jobs:
monitor:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Run Lighthouse CI
uses: treosh/lighthouse-ci-action@v9
with:
configPath: "./lighthouserc.json"
uploadArtifacts: true
- name: Performance report
run: npm run analyze:build
- name: Upload bundle analysis
uses: actions/upload-artifact@v3
with:
name: bundle-analysis
path: .next/analyze
Release Management
Automate versioning and releases.
yaml
# .github/workflows/release.yml
name: Release
on:
push:
branches: [main]
permissions:
contents: write
packages: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: "20"
- name: Create release
uses: changesets/action@v1
with:
publish: npm run release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
Notification Integration
Send notifications to Slack, Discord, etc.
yaml
# .github/workflows/notifications.yml
name: Notifications
on: [push, pull_request]
jobs:
notify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Slack notification
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
text: |
Commit: ${{ github.event.head_commit.message }}
Author: ${{ github.event.head_commit.author.name }}
Status: ${{ job.status }}
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
- name: Discord notification
if: failure()
uses: sarisia/actions-status-discord@v1
with:
webhook_url: ${{ secrets.DISCORD_WEBHOOK }}
status: ${{ job.status }}
Best Practices
1. Keep Secrets Secure - Never hardcode secrets
2. Use Matrix Testing - Test across configurations
3. Cache Dependencies - Speed up workflows
4. Fail Fast - Run quick checks first
5. Parallel Jobs - Run independent jobs concurrently
6. Clear Naming - Make workflow purposes obvious
7. Documentation - Document why workflow exists
8. Monitor Costs - Watch for slow runners
9. Regular Reviews - Update actions regularly
10. Automate Everything - More automation = fewer errors
Common Pitfalls
❌ Don't:
- Commit .env files with secrets
- Use always() condition everywhere
- Have extremely long workflows
- Run expensive operations unnecessarily
- Ignore workflow failures
✅ Do:
- Use GitHub secrets for sensitive data
- Use if: failure() for cleanup steps
- Break workflows into jobs
- Use matrix for variations
- Monitor workflow execution time
Conclusion
GitHub Actions provides a powerful, free CI/CD platform integrated with your repository. Start with basic testing and linting, then gradually add deployment automation. A well-designed CI/CD pipeline catches bugs early, speeds up releases, and gives confidence in code quality.
Happy automating! 🚀