Nathaniel's blog
Back to posts

CI/CD Pipelines with GitHub Actions

Nathaniel LinFebruary 24, 20267 min read2 views
CI/CD Pipelines with GitHub Actions

Continuous Integration and Continuous Deployment (CI/CD) automates the journey from code push to production. GitHub Actions makes this accessible with YAML-based workflows that run on every commit, PR, or schedule.

A Complete Pipeline

Here's a real-world workflow for a TypeScript monorepo:

name: CI
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint-and-typecheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
        with:
          version: 9
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm lint
      - run: pnpm check-types

  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16-alpine
        env:
          POSTGRES_DB: test
          POSTGRES_PASSWORD: test
        ports:
          - 5432:5432
        options: >-
          --health-cmd="pg_isready"
          --health-interval=10s
          --health-timeout=5s
          --health-retries=5
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
        with:
          version: 9
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm test
        env:
          DATABASE_URL: postgres://postgres:test@localhost:5432/test

  deploy:
    needs: [lint-and-typecheck, test]
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to production
        run: |
          echo "Deploying..."
          # Your deploy command here

Key Concepts

Triggers (on): Define when workflows run — pushes, PRs, schedules (cron), manual dispatch, or even webhook events.

Jobs: Independent units of work. They run in parallel by default. Use needs for dependencies.

Services: Sidecar containers (databases, caches) that run alongside your job. Perfect for integration tests.

Caching: The cache option on setup-node caches node_modules. For Docker builds, use actions/cache with build layers.

Monorepo Optimizations

In a Turborepo monorepo, you can skip unchanged packages:


- name: Check affected packages
  run: |
    AFFECTED=$(pnpm turbo run build --filter='...[HEAD^1]' --dry-run=json | jq -r '.packages[]')
    echo "affected=$AFFECTED" >> $GITHUB_OUTPUT

Or use Turbo's remote caching to share build artifacts across CI runs:


- run: pnpm turbo run build lint check-types
  env:
    TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
    TURBO_TEAM: my-team

Security Best Practices

  • Never hardcode secrets: Use ${{ secrets.MY_SECRET }} and set them in repo settings

  • Pin action versions: Use @v4 or full SHA, not @latest

  • Limit permissions: Set permissions: contents: read unless you need more

  • Audit third-party actions: Only use actions from verified publishers

  • Use environments: Gate production deploys with required reviewers

deploy:
  environment:
    name: production
    url: https://myapp.com
  permissions:
    contents: read
    deployments: write

Practical Tips

  • Matrix builds: Test across Node versions with strategy.matrix.node-version: [20, 22]

  • Artifacts: Upload test reports or build outputs with actions/upload-artifact

  • Concurrency: Cancel in-flight runs when a new commit pushes: concurrency: { group: ci-${{ github.ref }}, cancel-in-progress: true }

  • Composite actions: Extract repeated steps into reusable actions in .github/actions/

A well-configured CI/CD pipeline catches errors before they reach production and deploys automatically when checks pass. The 30 minutes you spend setting it up saves hours of manual testing and deployment.

Share this post

Reactions