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.
Workflow Overview
The SaaS boilerplate uses two main GitHub Actions workflows that control all deployments:File:
production.yml- Trigger: Push to
mainbranch - Environment: Production
- Purpose: Deploy to live production environment
- Quality Gates: All steps must pass before deployment
Production Workflow Analysis
Production Safety First! This workflow only runs on the
main branch and uses strict quality gates to prevent broken deployments..github/workflows/production.yml
Trigger Configuration
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.Step-by-Step Pipeline
The production pipeline follows a fail-fast approach - if any step fails, the entire deployment stops immediately.1
Environment Setup
- 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.
2
Dependency Installation
- 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.3
Code Quality Check
- name: Lint
run: pnpm lint # Must pass or deployment blocks
ESLint catches code style issues, unused imports, and potential bugs before they reach production.
4
Build Validation (Critical Step)
- 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.
- All production environment variables must be set
- Database connection must be valid
- External service API keys must work
- TypeScript must compile without errors
5
Test Execution
- 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.
6
Database Migration
- 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.
- Runs
pnpm db:migratewith production database - Must complete successfully before Vercel deployment
- Schema changes applied automatically
- If migration fails, entire deployment is blocked
Preview Workflow Analysis
File:.github/workflows/preview.yml
Differences from Production
Trigger:on:
push:
branches: [dev] # Dev branch pushes
pull_request:
branches: [dev] # PRs to dev branch
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
- name: Test
run: pnpm test --reporter=verbose # More detailed test output
- Lint must pass
- Build must succeed
- All tests must pass
- Database migrations must complete
Environment Variables in Workflows
Required GitHub Secrets (accessed via${{ secrets.SECRET_NAME }}):
Database Configuration
DATABASE_URL: ${{ secrets.DATABASE_URL }}
# Example: "postgres://user:pass@host:5432/db?sslmode=require"
Authentication (Better Auth)
BETTER_AUTH_SECRET: ${{ secrets.BETTER_AUTH_SECRET }}
# Must be 32+ character secure random string
Email Service (Resend)
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
# Format: "re_xxxxxxxxxxxx"
File Storage (Supabase)
SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }}
# Supabase anonymous/public key for storage access
Payment Processing (Stripe)
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 }}
Hardcoded Configuration Values
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
# 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
Quality Gates and Blocking Conditions
Each step must pass or deployment is blocked:1. Linting Gate
- name: Lint
run: pnpm lint
- ESLint errors found
- Code style violations
- Import/export issues
- TypeScript type errors
2. Build Gate
- name: Build
run: pnpm build
- Missing environment variables
- Database connection fails
- API key validation fails
- TypeScript compilation errors
- Next.js build errors
3. Testing Gate
- name: Test
run: pnpm test
- Unit tests fail
- Integration tests fail
- Service tests fail
- Test environment setup fails
4. Database Migration Gate
- name: Run database migrations
run: pnpm db:migrate
- Database connection fails
- Migration scripts have errors
- Schema conflicts occur
- Drizzle Kit installation fails
Setting Up GitHub Secrets
Repository Configuration:1. Access Repository Secrets
- Go to your GitHub repository
- Settings → Secrets and variables → Actions
- Click "New repository secret"
2. Add Required Secrets
Database:Name: DATABASE_URL
Value: postgres://user:pass@host:5432/db?sslmode=require
Name: BETTER_AUTH_SECRET
Value: [32+ character random string]
Name: RESEND_API_KEY
Value: re_xxxxxxxxxxxx
Name: SUPABASE_ANON_KEY
Value: [Supabase anonymous key]
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)
3. Environment-Specific Secrets
Production Environment:- Repository Settings → Environments
- Create "Production" environment
- Add production-specific secrets
- Configure deployment protection rules
- Create "Preview" environment
- Use same secrets (can be test keys)
- Less restrictive protection rules
Branch Protection Setup
Enforce CI/CD pipeline with branch protection:1. Protect Main Branch
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
2. Protect Dev Branch (Optional)
Branch name pattern: dev
✓ Require status checks to pass before merging
✓ Status checks that are required:
- Preview Deployment
- Prevents direct commits to protected branches
- Ensures all changes go through CI/CD pipeline
- Maintains code quality and deployment safety
- Provides audit trail of all changes
Workflow Customization
Modify workflows for your specific needs:1. Update Environment URLs
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
2. Add Additional Steps
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
3. Customize Build Process
Add build optimizations:- name: Build with optimizations
run: pnpm build
env:
ANALYZE: true # Bundle analyzer
OPTIMIZE: true # Custom optimizations
# ... other environment variables
4. Add Deployment Notifications
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 }}
Monitoring Workflow Execution
Track your deployments:1. GitHub Actions Dashboard
Repository → Actions tab:- View workflow runs
- Check step-by-step execution
- Download logs for debugging
- Monitor execution time
2. Status Badges
Add to README.md:
3. Workflow Insights
Repository → Insights → Actions:- Workflow run statistics
- Success/failure rates
- Average execution times
- Resource usage metrics
Common Workflow Issues
Troubleshooting GitHub Actions failures:Secret-Related Issues
# 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 settingsBuild Failures
# 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 validationTest Failures
# 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 performanceMigration Failures
# Problem: Migration fails in production
# Error: "relation already exists" or "column does not exist"
# Solution: Review migration files for conflicts
# Ensure migration order is correct