CI/CD Pipelines & Deployment
Multi-Job Pipeline
name: CI/CD Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: testdb
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- run: npm test
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/testdb
build:
needs: test # only runs if test passes
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name == 'push' }}
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy-staging:
needs: build
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: staging # requires approval if configured
steps:
- uses: actions/checkout@v4
- name: Deploy to staging
run: |
echo "Deploying ${{ github.sha }} to staging"
# kubectl set image deployment/app app=ghcr.io/...
deploy-production:
needs: deploy-staging
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment:
name: production
url: https://myapp.com
steps:
- name: Deploy to production
run: echo "Deploy to prod"Reusable Workflows & Composite Actions
# .github/workflows/reusable-deploy.yml
name: Reusable Deploy
on:
workflow_call:
inputs:
environment:
required: true
type: string
secrets:
DEPLOY_KEY:
required: true
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy to ${{ inputs.environment }}
env:
KEY: ${{ secrets.DEPLOY_KEY }}
run: ./deploy.sh ${{ inputs.environment }}
# Call from another workflow
jobs:
deploy:
uses: ./.github/workflows/reusable-deploy.yml
with:
environment: production
secrets:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
# Composite action — .github/actions/setup/action.yml
name: Setup Project
runs:
using: composite
steps:
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- uses: pnpm/action-setup@v3
- run: pnpm install --frozen-lockfile
shell: bash
# Use in workflow
- uses: ./.github/actions/setupSecrets & Security
# Secrets — set in repo/org settings → Actions → Secrets
# Never echo secrets in logs
- name: Use secret
env:
TOKEN: ${{ secrets.MY_TOKEN }}
run: ./script.sh # uses $TOKEN
# GITHUB_TOKEN — auto-provided, scoped to repo
- name: Create release
uses: softprops/action-gh-release@v1
with:
files: dist/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Permissions — principle of least privilege
permissions:
contents: read
packages: write
pull-requests: write
# OIDC — keyless cloud auth (no long-lived secrets)
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/deploy
aws-region: us-east-1
# Requires: permissions: { id-token: write, contents: read }Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free