GitHub Actions
02 / 02

CI/CD Pipelines & Deployment

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/setup

Secrets & 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