CI/CD Fundamentals
CI/CD automates the path from code commit to production. CI catches bugs early by running tests on every push. CD ensures the artifact is always deployable — or deployed automatically.
GitHub Actions — Complete Workflow
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
NODE_VERSION: '20'
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
lint-and-test:
name: Lint & Test
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
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
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm' # cache node_modules
- name: Install dependencies
run: npm ci # clean install from lockfile
- name: Lint
run: npm run lint
- name: Type check
run: npm run typecheck
- name: Run tests
run: npm run test -- --coverage
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/testdb
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage/lcov.infoBuild & Docker in CI
build-and-push:
name: Build & Push Docker Image
runs-on: ubuntu-latest
needs: lint-and-test # only run if tests pass
if: github.ref == 'refs/heads/main'
permissions:
contents: read
packages: write # push to GitHub Container Registry
steps:
- uses: actions/checkout@v4
- name: Log in to Container Registry
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=sha,prefix=,suffix=,format=short
type=ref,event=branch
type=semver,pattern={{version}}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha # GitHub Actions cache
cache-to: type=gha,mode=max
build-args: |
APP_VERSION=${{ github.sha }}Secrets, Environments & Artifacts
deploy-staging:
name: Deploy to Staging
runs-on: ubuntu-latest
needs: build-and-push
environment:
name: staging
url: https://staging.myapp.com
steps:
- name: Deploy to staging
run: |
curl -X POST ${{ secrets.DEPLOY_WEBHOOK_STAGING }} \
-H 'Authorization: Bearer ${{ secrets.DEPLOY_TOKEN }}' \
-d '{"image": "${{ env.IMAGE_NAME }}:${{ github.sha }}"}'
# Upload/download build artifacts
build:
steps:
- name: Build
run: npm run build
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
retention-days: 7
deploy:
needs: build
steps:
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- name: Deploy to Vercel
run: npx vercel --prod --token ${{ secrets.VERCEL_TOKEN }}Matrix Builds & Reusable Workflows
# Matrix strategy — test across multiple versions
jobs:
test:
strategy:
matrix:
node: [18, 20, 22]
os: [ubuntu-latest, windows-latest]
fail-fast: false # don't cancel others if one fails
runs-on: ${{ matrix.os }}
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm ci && npm test
# Reusable workflow — .github/workflows/deploy.yml
on:
workflow_call:
inputs:
environment:
required: true
type: string
secrets:
DEPLOY_TOKEN:
required: true
jobs:
deploy:
environment: ${{ inputs.environment }}
runs-on: ubuntu-latest
steps:
- run: echo 'Deploying to ${{ inputs.environment }}'
# Caller workflow
jobs:
deploy-prod:
uses: ./.github/workflows/deploy.yml
with:
environment: production
secrets:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free