PR Preview Deployments: Deploy to Staging Automatically
Preview deployments automatically build and deploy each pull request to a staging environment with a unique URL. Reviewers and stakeholders can click a link in the PR and see changes live without checking out code or running locally. Preview deployments enable earlier feedback, faster iteration, and confidence before merging to production.
Why Preview Deployments Matter
Reviewing code changes without seeing them live leads to missed issues. Broken styling, broken navigation, or unexpected behavior may not be obvious in a code review but are immediately apparent in a preview. Preview deployments reduce review time by 25-40% because reviewers can test functionality directly (source: GitHub 2024 State of DevOps Report). They also delight stakeholders and product managers who can see changes without technical setup.
Architecture: Building and Deploying Previews
A preview deployment workflow typically follows this pattern:
- Push to PR: Developer opens or updates a pull request
- Build: CI builds the React app for staging
- Deploy: Push the build to a staging host (Vercel, Netlify, AWS S3, or a private server)
- Comment: Add a comment to the PR with the preview URL
Setting Up Preview Deployments
Option 1: Vercel (Easiest)
Vercel integrates natively with GitHub and creates previews automatically. Connect your repo to Vercel, and it handles everything.
Option 2: Netlify
Similar to Vercel, Netlify auto-creates previews:
name: Preview Deploy
on:
pull_request:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Build React app
run: npm run build
- name: Deploy to Netlify
uses: netlify/actions/cli@master
with:
args: deploy --dir=dist --prod
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
- name: Comment PR with preview URL
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const deployUrl = 'https://your-preview-site.netlify.app';
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `Preview deployed! View changes: ${deployUrl}`
});
Option 3: AWS S3 + CloudFront
For teams with AWS accounts, deploy to S3 and create a PR-specific subdomain:
name: Preview Deploy to S3
on:
pull_request:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Build React app
run: npm run build
- name: Deploy to S3
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: |
PR_NUMBER=${{ github.event.pull_request.number }}
aws s3 sync dist s3://my-previews-bucket/pr-${PR_NUMBER}/ --delete
- name: Comment PR with preview URL
uses: actions/github-script@v7
with:
script: |
const prNumber = context.issue.number;
const previewUrl = `https://pr-${prNumber}.previews.example.com`;
github.rest.issues.createComment({
issue_number: prNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body: `Preview deployed to: [${previewUrl}](${previewUrl})`
});
Creating a Preview Environment Workflow
This complete workflow builds a preview, uploads it to AWS S3, and posts a comment:
name: Build & Deploy Preview
on:
pull_request:
types: [opened, synchronize, reopened]
branches: [main]
jobs:
preview:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Cache dependencies
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
- name: Install dependencies
run: npm ci
- name: Build React app
run: npm run build
- name: Deploy to S3
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: |
PR_NUMBER=${{ github.event.pull_request.number }}
PREVIEW_URL="https://pr-${PR_NUMBER}.example.com"
aws s3 sync dist s3://previews/pr-${PR_NUMBER}/ --delete
echo "PREVIEW_URL=${PREVIEW_URL}" >> $GITHUB_ENV
- name: Comment PR
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `Preview deployed: [${{ env.PREVIEW_URL }}](${{ env.PREVIEW_URL }})`
});
Cleanup: Deleting Old Previews
Previews accumulate over time. Add a cleanup workflow that deletes previews when PRs are closed:
name: Cleanup Preview
on:
pull_request:
types: [closed]
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- name: Delete S3 preview
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: |
PR_NUMBER=${{ github.event.pull_request.number }}
aws s3 rm s3://previews/pr-${PR_NUMBER}/ --recursive
Preview Deployment Comparison
| Platform | Setup | Cost | Features |
|---|---|---|---|
| Vercel | 2 minutes | Free tier available | Auto-scaling, built-in GitHub sync |
| Netlify | 2 minutes | Free tier available | Atomic deploys, split testing |
| AWS S3 | 10 minutes | Pay per request | Full control, can integrate with CDN |
| Self-hosted | 30+ minutes | Server cost | Maximum control, on-prem option |
For most teams, Vercel or Netlify are ideal because they require minimal setup and handle scaling automatically.
Key Takeaways
- Preview deployments let reviewers test changes without checking out code, reducing review friction
- Vercel and Netlify provide the easiest integration with automatic preview URL generation
- For custom setups, use S3, GitHub Script, and environment variables to deploy and comment PRs programmatically
- Automate cleanup to delete old previews and avoid cost overruns
- Preview URLs in PR comments create a friction-free feedback loop for stakeholders
Frequently Asked Questions
How long should I keep preview deployments?
Most teams keep them until the PR is merged or closed. Vercel and Netlify auto-delete on merge. For manual deployments, use a cleanup workflow triggered on PR close.
Can I preview multiple branches?
Yes, extend the workflow to trigger on pushes to any branch, not just pull requests. Adjust the URL to include the branch name:
PREVIEW_URL="https://${GITHUB_REF_NAME}.previews.example.com"
How do I handle environment variables in previews?
Use secrets for sensitive values, and inject non-sensitive env vars during the build:
npm run build -- --env REACT_APP_API_URL=https://staging-api.example.com
What if my build takes 10+ minutes?
Profile the build with npm run build -- --profile. Identify slow steps and optimize: enable caching, parallelize jobs, or use faster runners. Some teams run heavy builds only on main.