A reusable GitHub Action for deploying preview and production environments to Vercel with automatic PR comments and full build control.
Basic Usage
name: Deploy Preview
on:
pull_request:
types: [opened, synchronize, reopened]
permissions:
contents: read
pull-requests: write
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: W3Dev/vercel-deploy@main
with:
vercel_token: ${{ secrets.VERCEL_TOKEN }}
vercel_org_id: 'team_xxxxx'
vercel_project_id: 'prj_xxxxx'Preview Environment
Deploy with the preview environment explicitly set. This is the default behavior, shown here for clarity.
- uses: W3Dev/vercel-deploy@main
with:
vercel_token: ${{ secrets.VERCEL_TOKEN }}
vercel_org_id: 'team_xxxxx'
vercel_project_id: 'prj_xxxxx'
environment: preview
alias_prefix: 'myapp'This creates preview deployments with stable aliases like pr-123--myapp.vercel.app.
Production Deployment (on merge to main)
name: Deploy Production
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: W3Dev/vercel-deploy@main
with:
vercel_token: ${{ secrets.VERCEL_TOKEN }}
vercel_org_id: 'team_xxxxx'
vercel_project_id: 'prj_xxxxx'
environment: productionCustom Alias Domain
Use a custom domain for preview aliases instead of the default .vercel.app.
In PR deployments, alias_domain replaces the .vercel.app suffix:
- uses: W3Dev/vercel-deploy@main
with:
vercel_token: ${{ secrets.VERCEL_TOKEN }}
vercel_org_id: 'team_xxxxx'
vercel_project_id: 'prj_xxxxx'
alias_prefix: 'myapp'
alias_domain: 'preview.example.com'This creates aliases like pr-123--myapp.preview.example.com instead of pr-123--myapp.vercel.app.
In PR deployments you can also set alias_domain without alias_prefix -- the repo name will be used as the prefix automatically:
- uses: W3Dev/vercel-deploy@main
with:
vercel_token: ${{ secrets.VERCEL_TOKEN }}
vercel_org_id: 'team_xxxxx'
vercel_project_id: 'prj_xxxxx'
alias_domain: 'preview.example.com'This creates aliases like pr-123--my-repo.preview.example.com.
Outside PR context (e.g., push to main), alias_domain is used directly as the full alias target:
name: Deploy to custom domain
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: W3Dev/vercel-deploy@main
with:
vercel_token: ${{ secrets.VERCEL_TOKEN }}
vercel_org_id: 'team_xxxxx'
vercel_project_id: 'prj_xxxxx'
alias_domain: 'dev.example.vercel.app'
environment: previewThis aliases the deployment directly to dev.example.vercel.app.
Note: The custom domain must be configured in your Vercel project settings before use.
Monorepo Usage
- uses: W3Dev/vercel-deploy@main
with:
vercel_token: ${{ secrets.VERCEL_TOKEN }}
vercel_org_id: 'team_xxxxx'
vercel_project_id: 'prj_xxxxx'
working_directory: 'apps/dashboard'
alias_prefix: 'myapp-dashboard'With Prebuild Script
- uses: W3Dev/vercel-deploy@main
with:
vercel_token: ${{ secrets.VERCEL_TOKEN }}
vercel_org_id: 'team_xxxxx'
vercel_project_id: 'prj_xxxxx'
prebuild_script: |
git config --global user.email "ci@example.com"
git config --global user.name "CI Bot"Production with Pre-deploy Script
The predeploy_script runs after vercel build but before vercel deploy --prebuilt. Use it to run checks or steps that require the built output to exist, or to gate the actual upload to Vercel.
- uses: W3Dev/vercel-deploy@main
with:
vercel_token: ${{ secrets.VERCEL_TOKEN }}
vercel_org_id: 'team_xxxxx'
vercel_project_id: 'prj_xxxxx'
environment: production
predeploy_script: |
echo "Running post-build checks before deploying..."
npm run testTip: To run scripts before the build (e.g., code generation or configuring git), use
prebuild_scriptinstead.
Using Yarn
- uses: W3Dev/vercel-deploy@main
with:
vercel_token: ${{ secrets.VERCEL_TOKEN }}
vercel_org_id: 'team_xxxxx'
vercel_project_id: 'prj_xxxxx'
package_manager: 'yarn'Using pnpm
- uses: W3Dev/vercel-deploy@main
with:
vercel_token: ${{ secrets.VERCEL_TOKEN }}
vercel_org_id: 'team_xxxxx'
vercel_project_id: 'prj_xxxxx'
package_manager: 'pnpm'Pinning the Vercel CLI
The action installs a pinned Vercel CLI by default for reproducible deploys. Override vercel_cli_version when you want to test or roll forward to a different CLI release.
- uses: W3Dev/vercel-deploy@main
with:
vercel_token: ${{ secrets.VERCEL_TOKEN }}
vercel_org_id: 'team_xxxxx'
vercel_project_id: 'prj_xxxxx'
vercel_cli_version: '54.6.1'Passing Additional Deploy Arguments
Pass deploy_args as a single-line argument string such as --archive=tgz --meta key=value.
Quoted values are supported when needed, for example --meta "description=My App".
- uses: W3Dev/vercel-deploy@main
with:
vercel_token: ${{ secrets.VERCEL_TOKEN }}
vercel_org_id: 'team_xxxxx'
vercel_project_id: 'prj_xxxxx'
deploy_args: '--archive=tgz'Teardown on PR Close
Clean up preview deployments automatically when a PR is closed (merged or abandoned):
name: Deploy Preview
on:
pull_request:
types: [opened, synchronize, reopened, closed] # Include 'closed' for teardown
permissions:
contents: read
pull-requests: write
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: W3Dev/vercel-deploy@main
with:
vercel_token: ${{ secrets.VERCEL_TOKEN }}
vercel_org_id: 'team_xxxxx'
vercel_project_id: 'prj_xxxxx'
alias_prefix: 'myapp'
teardown: 'true'
auto_clean_deployment: 'true' # Also delete deployment, not just aliasHow it works:
- When
teardown: 'true'and the PR is closed, the action removes the preview alias - With
auto_clean_deployment: 'true', it also deletes the deployment itself from Vercel - The PR comment is updated to reflect the cleanup
- When the PR is open (opened/synchronize/reopened), normal deployment occurs
Teardown modes:
| Mode | teardown |
auto_clean_deployment |
Behavior |
|---|---|---|---|
| No cleanup | false |
- | Alias remains after PR close |
| Alias only | true |
false |
Removes alias, keeps deployment |
| Full cleanup | true |
true |
Removes alias AND deletes deployment |
Full Workflow Example
name: Dashboard Preview
on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- 'apps/dashboard/**'
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to deploy'
required: false
permissions:
contents: read
pull-requests: write
jobs:
deploy:
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: W3Dev/vercel-deploy@main
id: deploy
with:
vercel_token: ${{ secrets.VERCEL_TOKEN }}
vercel_org_id: 'team_xxxxx'
vercel_project_id: 'prj_xxxxx'
package_manager: 'bun'
working_directory: 'apps/dashboard'
alias_prefix: 'myapp'
- name: Use deployment URL
run: echo "Deployed to ${{ steps.deploy.outputs.deployment_url }}"While Vercel provides native GitHub integration, this action gives you control over where and how your builds happen:
Build in Your GitHub Actions Environment
- Builds run on your GitHub Actions runners, not Vercel's infrastructure
- Perfect for self-hosted runners with better caching, specific tooling, or compliance requirements
- Use
vercel build+vercel deploy --prebuiltfor full control
Advanced Build Customization
- Run prebuild scripts (configure git, generate code, set up credentials)
- Override install commands for complex dependency requirements
- Pass additional deploy flags such as
--archive=tgz - Native Bun, Yarn, npm, and pnpm support
Stable Preview URLs
- Get predictable PR-specific URLs:
pr-123--myapp.vercel.app - Use custom domains for aliases:
pr-123--myapp.preview.example.com - Vercel's native integration generates random deployment URLs
- Easier to share and reference in testing workflows
Monorepo-Friendly
- Explicit
working_directorysupport for monorepos - Fine-grained control over which parts of your repo to deploy
Smart PR Comments
- Updates the same comment on subsequent pushes (no PR spam)
- Vercel's native integration can create multiple comments per PR
Workflow Flexibility
- Integrate with larger workflows (run tests first, conditional deploys, etc.)
- Manual deployment triggers with
workflow_dispatch
Use Vercel's native integration if you:
- Want zero configuration
- Don't need custom build steps or environment control
- Prefer Vercel to manage the entire build environment
- Build in GitHub Actions, deploy to Vercel
- Support for both preview and production deployments
- Stable preview aliases (
pr-123--myapp.vercel.app) - Custom alias domains (
pr-123--myapp.preview.example.com) - Smart PR comments that update instead of spam
- Support for Bun, Yarn, npm, and pnpm
- Prebuild and predeploy scripts for custom workflows
- Monorepo support with
working_directory - GitHub Actions summary
- Automatic teardown of preview deployments on PR close
| Input | Description | Required | Default |
|---|---|---|---|
vercel_token |
Vercel API token | Yes | - |
vercel_org_id |
Vercel Organization/Team ID | Yes | - |
vercel_project_id |
Vercel Project ID | Yes | - |
vercel_project_name |
Project name for linking | No | repo name |
package_manager |
bun, yarn, npm, or pnpm |
No | bun |
vercel_cli_version |
Vercel CLI version to install for reproducible deploys | No | 54.6.1 |
node_version |
Node.js version | No | 22 |
working_directory |
Build directory | No | . |
alias_prefix |
Prefix for preview alias (e.g., myapp creates pr-123--myapp.vercel.app) |
No | - |
alias_domain |
Custom domain for preview alias. In PR context, replaces .vercel.app suffix. Outside PR context, used directly as the full alias target |
No | - |
prebuild_script |
Script to run before vercel build |
No | - |
predeploy_script |
Script to run after vercel build but before vercel deploy --prebuilt |
No | - |
install_command |
Custom install command | No | - |
deploy_args |
Space-separated extra arguments for vercel deploy --prebuilt |
No | - |
environment |
Deployment environment (preview or production) |
No | preview |
github_token |
Token for PR comments | No | github.token |
teardown |
Remove preview alias when PR is closed | No | false |
auto_clean_deployment |
Also delete the deployment (not just alias) on teardown | No | false |
| Output | Description |
|---|---|
deployment_url |
The Vercel deployment URL |
alias_url |
The aliased preview URL |
pr_number |
The PR number (if applicable) |
pnpm/action-setup@v4is pinned stale. Itsv4tag is an annotated, signed tag the maintainer has not moved since 2026-03-11 (b906affc), which predates that project's own pnpm-11 support added onmainon 2026-03-21 (58e6119f). That older commit only adds$PNPM_HOMEtoPATH, not$PNPM_HOME/bin— pnpm 11 changed its global-bin-dir default to the latter, sopnpm install -gon that pinnedaction-setuprefuses with "configured global bin directory ... is not in PATH". Overriding vianpm_config_global_bin_dirdoes not fix it either — pnpm 11 still derives the directory straight from$PNPM_HOMEfor this particular check, ignoring the config override. That's why the "Install Vercel CLI" step'spnpmbranch installs the globalvercelbinary vianpminstead ofpnpm(see the comment at that step) — pnpm is still used for the project's own dependency install a few steps earlier, this only swaps the tool for the one-off global install. Ifpnpm/action-setup@v4is ever re-pinned to a SHA past the March 2026 rewrite, switching that branch back topnpm install -gshould work again, but there's no urgency — the npm route is deterministic either way.- The
pnpmbranch'snpm install -guses its own--prefix, not the runner's default global path. The first cut of the npm workaround above (installing to npm's default global prefix,/opt/hostedtoolcache/.../lib/node_modules) worked in isolation but failed intermittently in real traffic: these are persistent self-hosted runners, and every pnpm-based app in a monorepo consumer hits this step at the same moment on every push, all installing the identical pinnedvercelversion into that one shared, mutable path. npm's global-install update does an atomic rename-swap of the existing package directory, so concurrent installs race and one fails withENOTEMPTYon the rename. Giving the install its own$RUNNER_TEMP-scoped--prefix(mirroring theBUN_INSTALLisolation on the bun branch above) avoids touching shared state entirely, so it can't race with a sibling job no matter how many run concurrently. The yarn/npm branch and the teardown path further down still use the shared default prefix — they haven't been observed to race in practice (fewer consumers hit them, and not from many apps at once), but the same fix would apply there if that ever changes.
MIT