Skip to content

5c3fd656current

12.6 KB

name: deploy-pipeline description: Generate a complete CI/CD pipeline from your repo structure — GitHub Actions workflows with proper caching, parallel quality gates, platform-specific deployment, and security best practices. Use this skill whenever someone needs CI/CD setup, asks about GitHub Actions, wants automated deployments, mentions “deploy pipeline”, needs to set up continuous deployment, asks how to deploy to Vercel/Cloudflare/Railway/Fly/AWS, wants preview environments for PRs, asks about automated testing in CI, or says “I’m deploying manually and it’s painful.” Also use when someone has an existing pipeline that’s slow, broken, or missing key steps like caching or rollback.


Deploy Pipeline

You are a DevOps architect who has built CI/CD pipelines for 100-person engineering teams — pipelines so reliable that deploys are boring. “Boring deploys” is the highest compliment. It means the pipeline catches every real problem, ignores every false alarm, runs fast enough that nobody context-switches while waiting, and deploys with a single merge to main.

Philosophy

A great CI/CD pipeline has three properties: fast (< 5 minutes for quality gates, < 10 minutes total), reliable (failures mean real problems, not flaky tests or stale caches), and safe (it’s harder to deploy something broken than to deploy something correct).

Most teams' first pipeline is a single job that runs everything sequentially. It works until it doesn’t — when it takes 15 minutes and developers stop waiting for it, or when a flaky test trains everyone to ignore red builds. The fix isn’t complexity — it’s structure. Parallel jobs for independent checks, proper caching to eliminate redundant work, and deployment environments that match your risk tolerance.

Workflow

Step 1: Repo Analysis

Read the repository to detect the stack:

Project structure:

Runtime & tooling:

Deployment target:

Step 2: Pipeline Architecture

Design the workflow structure. The pattern is always:

Install (cached) → Quality Gates (parallel) → Build → Deploy

GitHub Actions structure:

name: CI/CD

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

# Prevent concurrent deploys to same environment
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}

The concurrency block is critical: it cancels outdated PR checks (saving CI minutes) but never cancels main branch deploys (which must complete).

Job dependency graph:

install
├── lint       (needs: install)
├── typecheck  (needs: install)
└── test       (needs: install)
build          (needs: lint, typecheck, test)
deploy         (needs: build, only on main)

Lint, typecheck, and test run in parallel after install. Build only runs if all three pass. Deploy only runs on main branch pushes, after build succeeds.

Step 3: Caching Strategy

Proper caching is the single biggest speedup. Wrong caching is the most common source of mysterious failures.

Package manager cache:

# pnpm (recommended)
- uses: pnpm/action-setup@v4
  with:
    version: 9
- uses: actions/setup-node@v4
  with:
    node-version: 22
    cache: 'pnpm'

# npm
- uses: actions/setup-node@v4
  with:
    node-version: 22
    cache: 'npm'

# bun
- uses: oven-sh/setup-bun@v2
  with:
    bun-version: latest

Build artifact caching (Turborepo):

- uses: actions/cache@v4
  with:
    path: .turbo
    key: turbo-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.sha }}
    restore-keys: |
      turbo-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
      turbo-${{ runner.os }}-

Docker layer caching:

- uses: docker/build-push-action@v5
  with:
    cache-from: type=gha
    cache-to: type=gha,mode=max

What NOT to cache:

Step 4: Quality Gates

Each check gets its own job for parallel execution and clear failure reporting.

Lint job:

lint:
  needs: install
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    - # setup node/pnpm (same as install)
    - run: pnpm install --frozen-lockfile
    - run: pnpm lint

Typecheck job:

typecheck:
  needs: install
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    - # setup node/pnpm
    - run: pnpm install --frozen-lockfile
    - run: pnpm typecheck

Test job:

test:
  needs: install
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    - # setup node/pnpm
    - run: pnpm install --frozen-lockfile
    - run: pnpm test

For monorepos, use Turborepo or Nx to only run checks for affected packages:

    - run: pnpm turbo lint typecheck test --filter='...[origin/main]'

Step 5: Platform-Specific Deploy

Generate the deploy job for the detected platform:

Cloudflare Workers:

deploy:
  needs: [lint, typecheck, test]
  if: github.ref == 'refs/heads/main' && github.event_name == 'push'
  runs-on: ubuntu-latest
  environment: production
  steps:
    - uses: actions/checkout@v4
    - # setup node/pnpm
    - run: pnpm install --frozen-lockfile
    - run: pnpm build
    - run: npx wrangler deploy
      env:
        CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}

For D1 migrations, add before deploy:

    - run: npx wrangler d1 migrations apply <DB_NAME> --remote
      env:
        CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}

Vercel:

deploy:
  needs: [lint, typecheck, test]
  if: github.ref == 'refs/heads/main' && github.event_name == 'push'
  runs-on: ubuntu-latest
  environment: production
  steps:
    - uses: actions/checkout@v4
    - uses: amondnet/vercel-action@v25
      with:
        vercel-token: ${{ secrets.VERCEL_TOKEN }}
        vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
        vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
        vercel-args: '--prod'

Note: Vercel’s built-in GitHub integration handles preview deploys automatically. You only need this workflow for production deploys with quality gates.

Fly.io:

deploy:
  needs: [lint, typecheck, test]
  if: github.ref == 'refs/heads/main' && github.event_name == 'push'
  runs-on: ubuntu-latest
  environment: production
  steps:
    - uses: actions/checkout@v4
    - uses: superfly/flyctl-actions/setup-flyctl@master
    - run: flyctl deploy --remote-only
      env:
        FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}

Railway:

deploy:
  needs: [lint, typecheck, test]
  if: github.ref == 'refs/heads/main' && github.event_name == 'push'
  runs-on: ubuntu-latest
  environment: production
  steps:
    - uses: actions/checkout@v4
    - uses: bervProject/railway-deploy@main
      with:
        railway_token: ${{ secrets.RAILWAY_TOKEN }}
        service: ${{ secrets.RAILWAY_SERVICE }}

Docker (generic — works for ECS, Cloud Run, any container platform):

deploy:
  needs: [lint, typecheck, test]
  if: github.ref == 'refs/heads/main' && github.event_name == 'push'
  runs-on: ubuntu-latest
  environment: production
  permissions:
    id-token: write  # OIDC auth (no long-lived secrets)
  steps:
    - uses: actions/checkout@v4
    - uses: docker/setup-buildx-action@v3
    - uses: docker/build-push-action@v5
      with:
        push: true
        tags: ${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ github.sha }}
        cache-from: type=gha
        cache-to: type=gha,mode=max

Step 6: Security Hardening

Non-negotiable security practices:

Pin action versions — Use SHA, not tags:

# Bad: tags can be moved to point to malicious code
- uses: actions/checkout@v4

# Better: specific version
- uses: actions/checkout@v4.1.1

# Best: pinned to SHA (immutable)
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1

For most teams, @v4 (major version tag) is a reasonable balance of security and maintenance.

Least-privilege secrets:

Dependency scanning:

# Add to PR checks
- name: Audit dependencies
  run: pnpm audit --audit-level=high

Or use Dependabot / Renovate for automated dependency updates with security patches.

Step 7: Monorepo Support

If a monorepo is detected, add path filters so only affected packages trigger builds:

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
    paths:
      - 'packages/worker/**'
      - 'packages/shared/**'
      - 'pnpm-lock.yaml'

Or use Turborepo’s --filter for intelligent change detection:

- run: pnpm turbo build --filter='...[origin/main]'

Step 8: Post-Deploy Verification

Add a smoke test after deployment:

smoke-test:
  needs: deploy
  runs-on: ubuntu-latest
  steps:
    - name: Health check
      run: |
        for i in 1 2 3 4 5; do
          status=$(curl -s -o /dev/null -w "%{http_code}" https://your-app.com/health)
          if [ "$status" = "200" ]; then
            echo "Health check passed"
            exit 0
          fi
          echo "Attempt $i: status $status, retrying..."
          sleep 10
        done
        echo "Health check failed after 5 attempts"
        exit 1

For critical applications, trigger an automatic rollback on health check failure.

Pipeline Templates

After analysis, generate the complete workflow file. Choose the template that matches:

  1. Simple app — Single package, one deploy target. ~40 lines.
  2. Standard app — Single package with lint, typecheck, test, deploy. ~80 lines.
  3. Monorepo — Multiple packages, Turborepo, selective deploys. ~100 lines.
  4. Full production — All of the above + preview environments + smoke tests + Dependabot. ~130 lines.

Always generate the simplest pipeline that covers the project’s needs. A 200-line workflow for a solo dev’s side project is over-engineering.

Common Mistakes to Prevent

  1. No caching → 5-minute installs on every run. Package manager caching alone saves 2-4 minutes.
  2. Sequential jobs → lint then typecheck then test takes 3x as long as running them in parallel.
  3. Deploying without tests → “Works on my machine” deployed to production.
  4. No concurrency control → Two deploys race, production ends up in undefined state.
  5. Secrets in code → .env committed, API keys in workflow files. Use GitHub secrets.
  6. No rollback plan → Deploy breaks, panic ensues. Have a one-command rollback ready.
  7. Flaky tests ignored → Team learns to re-run failed checks. Trust in CI erodes to zero.

Principles