Skip to main content

Chapter 40: CI/CD with GitHub Actions

Continuous Integration verifies every change. Continuous Deployment ships verified changes to production. This chapter sets up a complete pipeline.

Pipeline Architecture​

Push/PR → Lint → Typecheck → Unit Tests → Build → E2E Tests → Deploy
↓
(main branch only)

Complete Workflow​

# .github/workflows/ci.yml
name: CI

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

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
lint:
name: Lint & Typecheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm lint
- run: pnpm typecheck

test:
name: Unit & Integration Tests
runs-on: ubuntu-latest
needs: lint
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm test:run --coverage
- uses: actions/upload-artifact@v4
with:
name: coverage
path: coverage/

build:
name: Build
runs-on: ubuntu-latest
needs: lint
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm build
- uses: actions/upload-artifact@v4
with:
name: build
path: dist/
retention-days: 7

e2e:
name: E2E Tests
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm playwright install --with-deps chromium
- run: pnpm test:e2e
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
retention-days: 7

deploy:
name: Deploy
runs-on: ubuntu-latest
needs: [test, build, e2e]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
environment: production
steps:
- uses: actions/download-artifact@v4
with:
name: build
path: dist/

# Deploy to your hosting provider
# Example: Deploy to Cloudflare Pages
- name: Deploy to Cloudflare Pages
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy dist/ --project-name=taskforge

security:
name: Security Audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm audit --audit-level=high
continue-on-error: true # Don't block on audit warnings

Key Practices​

pnpm install --frozen-lockfile​

In CI, always use --frozen-lockfile (equivalent to npm ci). This:

  • Installs exact versions from the lockfile
  • Fails if the lockfile is out of date
  • Ensures reproducible builds

Concurrency Controls​

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

If a new push arrives while CI is running on the same branch, the old run is cancelled. No wasted compute.

Caching​

actions/setup-node with cache: "pnpm" caches the pnpm store between runs. First run installs from network; subsequent runs are near-instant.

Environment Protection​

deploy:
environment: production # Requires approval for production deploys

Configure environment protection rules in GitHub Settings → Environments → production → Required reviewers.

Summary​

  • ✅ Pipeline stages: lint → typecheck → test → build → e2e → deploy
  • ✅ Concurrency controls cancel superseded runs
  • ✅ Frozen lockfile ensures reproducible installs
  • ✅ Artifact upload preserves build outputs and failure reports
  • ✅ Conditional deployment — only main branch, only on push
  • ✅ Security audit runs alongside the pipeline
  • ✅ Environment protection for production deployments