Comprehensive guide to the GitHub Actions CI/CD pipeline that ensures quality deployments and prevents broken code from reaching production.
Why GitHub Actions? The CI/CD pipeline acts as your deployment guardian, running automated tests, validating builds, and executing database migrations before any code reaches production.
The SaaS boilerplate uses two main GitHub Actions workflows that control all deployments:
File: production.yml
main branchProduction Safety First! This workflow only runs on the main branch and uses strict quality gates to prevent broken deployments.
File: .github/workflows/production.yml
name: Production Deployment
on:
push:
branches: [main] # Only triggers on main branch pushes
jobs:
production:
runs-on: ubuntu-latest
environment: Production # Uses Production environment secrets
The workflow uses GitHub's Production environment, which can be configured with protection rules like required reviewers and deployment delays.
The production pipeline follows a fail-fast approach - if any step fails, the entire deployment stops immediately.
- name: Checkout code
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: latest
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '23' # Latest Node.js version
cache: 'pnpm' # Cache pnpm dependencies for speed
Node.js and pnpm caching dramatically speeds up builds by reusing dependencies from previous runs.
- name: Install dependencies
run: pnpm install --frozen-lockfile # Ensures exact dependency versions
The --frozen-lockfile flag ensures production uses the exact same dependency versions as your local development.
- name: Lint
run: pnpm lint # Must pass or deployment blocks
ESLint catches code style issues, unused imports, and potential bugs before they reach production.
- name: Build
run: pnpm build
env:
# Complete production environment variables
DATABASE_URL: ${{ secrets.DATABASE_URL }}
BETTER_AUTH_SECRET: ${{ secrets.BETTER_AUTH_SECRET }}
BETTER_AUTH_URL: https://prod.example.com
# ... all required variables
NODE_ENV: production
Critical Quality Gate: This step validates that your app can build successfully with production environment variables. If the build fails here, deployment is immediately blocked.
Build Requirements:
- name: Test
run: pnpm test
env:
# Same environment variables as build
DATABASE_URL: ${{ secrets.DATABASE_URL }}
# ... all variables repeated for tests
All unit tests, integration tests, and service tests must pass to ensure code reliability.
- name: Install Drizzle Kit
run: npm install -g drizzle-kit
- name: Run database migrations
run: pnpm db:migrate
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
Database migrations run automatically to keep your production schema in sync with code changes.
Critical Migration Point:
pnpm db:migrate with production databaseFile: .github/workflows/preview.yml
Trigger:
on:
push:
branches: [dev] # Dev branch pushes
pull_request:
branches: [dev] # PRs to dev branch
Environment Configuration:
environment: Preview # Uses Preview environment secrets
# Different URLs in environment variables:
BETTER_AUTH_URL: https://preview.example.com
NEXT_PUBLIC_APP_URL: https://preview.example.com
Enhanced Testing:
- name: Test
run: pnpm test --reporter=verbose # More detailed test output
Same Quality Gates:
Required GitHub Secrets (accessed via ${{ secrets.SECRET_NAME }}):
DATABASE_URL: ${{ secrets.DATABASE_URL }}
# Example: "postgres://user:pass@host:5432/db?sslmode=require"
BETTER_AUTH_SECRET: ${{ secrets.BETTER_AUTH_SECRET }}
# Must be 32+ character secure random string
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
# Format: "re_xxxxxxxxxxxx"
SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }}
# Supabase anonymous/public key for storage access
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY }}
URLs and Domains:
# Production
BETTER_AUTH_URL: https://prod.example.com
NEXT_PUBLIC_APP_URL: https://prod.example.com
# Preview
BETTER_AUTH_URL: https://preview.example.com
NEXT_PUBLIC_APP_URL: https://preview.example.com
Service Configuration:
# Email settings
EMAIL_FROM: noreply@example.com
EMAIL_TO: admin@example.com
# File storage
SUPABASE_URL: https://xxx.supabase.co
SUPABASE_BUCKET: BUCKET-NAME
NEXT_PUBLIC_MAX_FILE_SIZE: 5242880
ALLOWED_MIME_TYPES: image/jpeg,image/png,image/webp
STORAGE_TYPE: supabase
# Application settings
NODE_ENV: production
NEXT_PUBLIC_STRIPE_CHECKOUT_TYPE: EmbededForm
NEXT_PUBLIC_BILLING_MODE: organization
LOG_LEVEL: info
Each step must pass or deployment is blocked:
- name: Lint
run: pnpm lint
Blocks deployment if:
- name: Build
run: pnpm build
Blocks deployment if:
- name: Test
run: pnpm test
Blocks deployment if:
- name: Run database migrations
run: pnpm db:migrate
Blocks deployment if:
Repository Configuration:
Database:
Name: DATABASE_URL
Value: postgres://user:pass@host:5432/db?sslmode=require
Authentication:
Name: BETTER_AUTH_SECRET
Value: [32+ character random string]
Email Service:
Name: RESEND_API_KEY
Value: re_xxxxxxxxxxxx
File Storage:
Name: SUPABASE_ANON_KEY
Value: [Supabase anonymous key]
Payment Processing:
Name: STRIPE_SECRET_KEY
Value: sk_live_xxxxxxxxxxxx (production) or sk_test_xxxxxxxxxxxx (testing)
Name: STRIPE_WEBHOOK_SECRET
Value: whsec_xxxxxxxxxxxx
Name: NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
Value: pk_live_xxxxxxxxxxxx (production) or pk_test_xxxxxxxxxxxx (testing)
Production Environment:
Preview Environment:
Enforce CI/CD pipeline with branch protection:
Repository Settings → Branches → Add rule:
Branch name pattern: main
✓ Require a pull request before merging
✓ Require approvals: 1
✓ Dismiss stale PR approvals when new commits are pushed
✓ Require review from code owners
✓ Require status checks to pass before merging
✓ Require branches to be up to date before merging
✓ Status checks that are required:
- Production Deployment (your workflow name)
✓ Restrict pushes that create new commits
✓ Restrict pushes to matching branches
✓ Do not allow bypassing the above settings
Branch name pattern: dev
✓ Require status checks to pass before merging
✓ Status checks that are required:
- Preview Deployment
Benefits:
Modify workflows for your specific needs:
In workflow files, change:
# From:
BETTER_AUTH_URL: https://prod.example.com
NEXT_PUBLIC_APP_URL: https://prod.example.com
# To:
BETTER_AUTH_URL: https://yourdomain.com
NEXT_PUBLIC_APP_URL: https://yourdomain.com
Example: Add E2E tests:
- name: Install Playwright
run: npx playwright install
- name: Run E2E tests
run: pnpm test:e2e
env:
# Same environment variables as other steps
Add build optimizations:
- name: Build with optimizations
run: pnpm build
env:
ANALYZE: true # Bundle analyzer
OPTIMIZE: true # Custom optimizations
# ... other environment variables
Slack notifications:
- name: Notify deployment success
if: success()
run: |
curl -X POST -H 'Content-type: application/json' \
--data '{"text":"✅ Production deployment successful!"}' \
${{ secrets.SLACK_WEBHOOK_URL }}
- name: Notify deployment failure
if: failure()
run: |
curl -X POST -H 'Content-type: application/json' \
--data '{"text":"❌ Production deployment failed!"}' \
${{ secrets.SLACK_WEBHOOK_URL }}
Track your deployments:
Repository → Actions tab:
GitHub Actions Workflow Interface:
Add to README.md:

Repository → Insights → Actions:
Troubleshooting GitHub Actions failures:
# Problem: Secret not found
# Error: "Warning: Unexpected input(s) 'secret', valid inputs are..."
# Solution: Check secret name spelling in repository settings
# Problem: Secret value incorrect
# Error: Database connection failed, Invalid API key
# Solution: Verify secret values are correct in GitHub settings# Problem: Missing environment variables
# Error: "Environment variable X is not defined"
# Solution: Add missing variables to workflow file or secrets
# Problem: TypeScript errors
# Error: "Type 'string | undefined' is not assignable to type 'string'"
# Solution: Fix type issues or add proper environment validation# Problem: Tests pass locally but fail in CI
# Solution: Check environment variables match local setup
# Ensure test database is properly configured
# Problem: Tests timeout in CI
# Solution: Increase timeout values or optimize test performance# Problem: Migration fails in production
# Error: "relation already exists" or "column does not exist"
# Solution: Review migration files for conflicts
# Ensure migration order is correctYour GitHub Actions CI/CD pipeline is now configured to ensure quality deployments. Next, set up Vercel Integration to complete the automated deployment process.