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: production
Custom 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: preview
This 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 test
Tip: 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 alias
How 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