Tools: CI/CD Pipeline Design: From Push to Production in Under 10 Minutes (2026)
What a Good CI/CD Pipeline Actually Does
The Base Pipeline
Parallel Jobs
Production Deployment with Approval
Caching Dependencies
Deploy Script Pattern
Slack Notifications
Secrets Management
The 10-Minute Goal Not just "runs tests before deploy." A real pipeline: Here's how to build one with GitHub Actions. In GitHub Settings → Environments → production, you can require reviewers before the job runs. Proper caching cuts CI time by 40-60%. Pipeline phases and target times: Anything over 10 minutes is friction that causes engineers to merge without waiting for CI. Fast pipelines get used. Slow ones get bypassed. GitHub Actions workflows for test, build, and deploy pre-configured: Whoff Agents AI SaaS Starter Kit includes CI/CD templates ready to drop in. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse
# .github/workflows/ci.yml
name: CI on: push: branches: [main, develop] pull_request: branches: [main] jobs: validate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' cache: '-weight: 500;">npm' - run: -weight: 500;">npm ci - name: Type check run: npx tsc --noEmit - name: Lint run: -weight: 500;">npm run lint - name: Test run: -weight: 500;">npm test -- --coverage - name: Upload coverage uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }}
# .github/workflows/ci.yml
name: CI on: push: branches: [main, develop] pull_request: branches: [main] jobs: validate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' cache: '-weight: 500;">npm' - run: -weight: 500;">npm ci - name: Type check run: npx tsc --noEmit - name: Lint run: -weight: 500;">npm run lint - name: Test run: -weight: 500;">npm test -- --coverage - name: Upload coverage uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }}
# .github/workflows/ci.yml
name: CI on: push: branches: [main, develop] pull_request: branches: [main] jobs: validate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' cache: '-weight: 500;">npm' - run: -weight: 500;">npm ci - name: Type check run: npx tsc --noEmit - name: Lint run: -weight: 500;">npm run lint - name: Test run: -weight: 500;">npm test -- --coverage - name: Upload coverage uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }}
jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: '20', cache: '-weight: 500;">npm' } - run: -weight: 500;">npm ci && -weight: 500;">npm run lint typecheck: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: '20', cache: '-weight: 500;">npm' } - run: -weight: 500;">npm ci && npx tsc --noEmit test: runs-on: ubuntu-latest services: postgres: image: postgres:16 env: POSTGRES_PASSWORD: testpass POSTGRES_DB: testdb options: >- --health-cmd pg_isready --health-interval 10s env: DATABASE_URL: postgresql://postgres:testpass@localhost:5432/testdb steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: '20', cache: '-weight: 500;">npm' } - run: -weight: 500;">npm ci - run: npx prisma migrate deploy - run: -weight: 500;">npm test deploy-staging: needs: [lint, typecheck, test] # runs after all pass if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: ./scripts/deploy.sh staging env: DEPLOY_KEY: ${{ secrets.STAGING_DEPLOY_KEY }}
jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: '20', cache: '-weight: 500;">npm' } - run: -weight: 500;">npm ci && -weight: 500;">npm run lint typecheck: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: '20', cache: '-weight: 500;">npm' } - run: -weight: 500;">npm ci && npx tsc --noEmit test: runs-on: ubuntu-latest services: postgres: image: postgres:16 env: POSTGRES_PASSWORD: testpass POSTGRES_DB: testdb options: >- --health-cmd pg_isready --health-interval 10s env: DATABASE_URL: postgresql://postgres:testpass@localhost:5432/testdb steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: '20', cache: '-weight: 500;">npm' } - run: -weight: 500;">npm ci - run: npx prisma migrate deploy - run: -weight: 500;">npm test deploy-staging: needs: [lint, typecheck, test] # runs after all pass if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: ./scripts/deploy.sh staging env: DEPLOY_KEY: ${{ secrets.STAGING_DEPLOY_KEY }}
jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: '20', cache: '-weight: 500;">npm' } - run: -weight: 500;">npm ci && -weight: 500;">npm run lint typecheck: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: '20', cache: '-weight: 500;">npm' } - run: -weight: 500;">npm ci && npx tsc --noEmit test: runs-on: ubuntu-latest services: postgres: image: postgres:16 env: POSTGRES_PASSWORD: testpass POSTGRES_DB: testdb options: >- --health-cmd pg_isready --health-interval 10s env: DATABASE_URL: postgresql://postgres:testpass@localhost:5432/testdb steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: '20', cache: '-weight: 500;">npm' } - run: -weight: 500;">npm ci - run: npx prisma migrate deploy - run: -weight: 500;">npm test deploy-staging: needs: [lint, typecheck, test] # runs after all pass if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: ./scripts/deploy.sh staging env: DEPLOY_KEY: ${{ secrets.STAGING_DEPLOY_KEY }}
deploy-production: needs: deploy-staging runs-on: ubuntu-latest environment: production # requires approval in GitHub settings steps: - uses: actions/checkout@v4 - run: ./scripts/deploy.sh production env: DEPLOY_KEY: ${{ secrets.PROD_DEPLOY_KEY }}
deploy-production: needs: deploy-staging runs-on: ubuntu-latest environment: production # requires approval in GitHub settings steps: - uses: actions/checkout@v4 - run: ./scripts/deploy.sh production env: DEPLOY_KEY: ${{ secrets.PROD_DEPLOY_KEY }}
deploy-production: needs: deploy-staging runs-on: ubuntu-latest environment: production # requires approval in GitHub settings steps: - uses: actions/checkout@v4 - run: ./scripts/deploy.sh production env: DEPLOY_KEY: ${{ secrets.PROD_DEPLOY_KEY }}
- uses: actions/cache@v3 with: path: | ~/.-weight: 500;">npm ${{ github.workspace }}/.next/cache key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }} restore-keys: | ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
- uses: actions/cache@v3 with: path: | ~/.-weight: 500;">npm ${{ github.workspace }}/.next/cache key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }} restore-keys: | ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
- uses: actions/cache@v3 with: path: | ~/.-weight: 500;">npm ${{ github.workspace }}/.next/cache key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }} restore-keys: | ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
#!/bin/bash
# scripts/deploy.sh
set -euo pipefail ENV=${1:-staging}
echo "Deploying to $ENV..." # Build
-weight: 500;">npm run build # Run migrations before deploying new code
if [ "$ENV" = "production" ]; then npx prisma migrate deploy
fi # Deploy (Vercel example)
vercel deploy --prod --token=$VERCEL_TOKEN # Smoke test
sleep 10
-weight: 500;">curl -f https://app.example.com/api/health || { echo "Health check failed, rolling back" vercel rollback --token=$VERCEL_TOKEN exit 1
} echo "Deploy complete"
#!/bin/bash
# scripts/deploy.sh
set -euo pipefail ENV=${1:-staging}
echo "Deploying to $ENV..." # Build
-weight: 500;">npm run build # Run migrations before deploying new code
if [ "$ENV" = "production" ]; then npx prisma migrate deploy
fi # Deploy (Vercel example)
vercel deploy --prod --token=$VERCEL_TOKEN # Smoke test
sleep 10
-weight: 500;">curl -f https://app.example.com/api/health || { echo "Health check failed, rolling back" vercel rollback --token=$VERCEL_TOKEN exit 1
} echo "Deploy complete"
#!/bin/bash
# scripts/deploy.sh
set -euo pipefail ENV=${1:-staging}
echo "Deploying to $ENV..." # Build
-weight: 500;">npm run build # Run migrations before deploying new code
if [ "$ENV" = "production" ]; then npx prisma migrate deploy
fi # Deploy (Vercel example)
vercel deploy --prod --token=$VERCEL_TOKEN # Smoke test
sleep 10
-weight: 500;">curl -f https://app.example.com/api/health || { echo "Health check failed, rolling back" vercel rollback --token=$VERCEL_TOKEN exit 1
} echo "Deploy complete"
notify: needs: deploy-production if: always() runs-on: ubuntu-latest steps: - name: Notify Slack uses: slackapi/slack-github-action@v1.24.0 with: payload: | { "text": "${{ needs.deploy-production.result == 'success' && '✅' || '❌' }} Production deploy ${{ needs.deploy-production.result }}", "attachments": [{ "text": "${{ github.event.head_commit.message }}", "footer": "by ${{ github.actor }}" }] } env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
notify: needs: deploy-production if: always() runs-on: ubuntu-latest steps: - name: Notify Slack uses: slackapi/slack-github-action@v1.24.0 with: payload: | { "text": "${{ needs.deploy-production.result == 'success' && '✅' || '❌' }} Production deploy ${{ needs.deploy-production.result }}", "attachments": [{ "text": "${{ github.event.head_commit.message }}", "footer": "by ${{ github.actor }}" }] } env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
notify: needs: deploy-production if: always() runs-on: ubuntu-latest steps: - name: Notify Slack uses: slackapi/slack-github-action@v1.24.0 with: payload: | { "text": "${{ needs.deploy-production.result == 'success' && '✅' || '❌' }} Production deploy ${{ needs.deploy-production.result }}", "attachments": [{ "text": "${{ github.event.head_commit.message }}", "footer": "by ${{ github.actor }}" }] } env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
# Never hardcode secrets
# Bad:
env: API_KEY: sk-abc123 # Good:
env: API_KEY: ${{ secrets.API_KEY }} # Per-environment secrets
# GitHub Settings → Environments → staging/production
# Different values per environment, automatic scoping
# Never hardcode secrets
# Bad:
env: API_KEY: sk-abc123 # Good:
env: API_KEY: ${{ secrets.API_KEY }} # Per-environment secrets
# GitHub Settings → Environments → staging/production
# Different values per environment, automatic scoping
# Never hardcode secrets
# Bad:
env: API_KEY: sk-abc123 # Good:
env: API_KEY: ${{ secrets.API_KEY }} # Per-environment secrets
# GitHub Settings → Environments → staging/production
# Different values per environment, automatic scoping - Validates code quality (lint, types, tests)
- Builds artifacts
- Deploys to staging automatically
- Gates production on approval or scheduled windows
- Rolls back automatically on failure - Lint + typecheck: 2 min (parallel)
- Tests with DB: 3 min
- Build: 2 min
- Deploy: 1 min
- Smoke test: 30s