# How can I use GitHub Actions with Vercel?

**Author:** Vercel

---

## Key takeaways

- Vercel's Git integration already handles preview URLs, immutable deploys, and instant rollback, so most teams need no pipeline at all.
  
- Add GitHub Actions only for tests, security scans, performance budgets, or approval gates, the four things Vercel does not run.
  
- Building in Actions without the `--prebuilt` flag makes Vercel rebuild the same artifact, doubling CI minutes and slowing feedback.
  
- Vercel can trigger Actions on `deployment_status` and `repository_dispatch` events, so end-to-end tests run against a live preview with no polling.
  
- Skew Protection, instant rollback, Fluid compute, and 126 PoPs behave identically whether you deploy through Git integration or the CLI.
  

Teams often stand up a GitHub Actions pipeline on top of Vercel, then spend a week wondering why every commit builds twice and feedback got slower instead of faster. The integration is useful when you need it. Most of the work teams reach for Actions to do, though, is work Vercel already runs for them.

This guide covers three things: the CLI pattern that connects Actions and Vercel, the reverse direction where Vercel triggers Actions, and the one flag that stops you from paying for the same build twice.

## Stop adding a pipeline Vercel already runs for you

Before you write a line of YAML, confirm what you're adding. Vercel's built-in Git integration already does the bulk of what teams build CI/CD pipelines for:

- Every pull request gets a preview URL with no configuration.
  
- Framework detection picks up your build settings without a YAML file.
  
- Every deployment is immutable and retained indefinitely.
  
- Instant rollback to any previous production deployment is a pointer update, not a rebuild.
  

If your workflow isn't running tests, scanning for vulnerabilities, enforcing a performance budget, or holding a deploy for human approval, you don't need GitHub Actions on top of Vercel, and adding it makes your pipeline worse. The reason is mechanical. The moment you put Actions in front of a deploy, you've created a second system that thinks it owns the build, and now you're maintaining two things that have to agree with each other. That's a cost you take on willingly when you need it and accidentally when you don't.

You need GitHub Actions when a workflow has to do one of four things, none of which Vercel does on its own:

- Run a test suite. Vercel builds and deploys, but it does not execute your tests.
  
- Run security scans such as software bill of materials (SBOM) generation or vulnerability detection.
  
- Enforce a performance budget through Lighthouse or Core Web Vitals gates.
  
- Require a human approval step for compliance.
  

If your workflow falls into one of those, read on. If it doesn't, let the native Git integration do its job.

## Know what changes when you switch from Git integration to the CLI

Once you've decided you need Actions, the practical question is how different the CLI path is. The differences cluster in two places:

1. The build moves into your Actions runner.
   
2. Branch-specific URL generation changes, because CLI deployments don't carry the same Git provider metadata.
   

Everything else that makes Vercel reliable in production behaves identically across both paths. Compare the two before committing to either one.

| Dimension                                                  | Native Git integration                                  | GitHub Actions + `--prebuilt`                                              |
| ---------------------------------------------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------- |
| Where the build runs                                       | Vercel's build infrastructure                           | Your GitHub Actions runner                                                 |
| Build caching                                              | Vercel's managed cache, automatic                       | The Actions environment, configured manually                               |
| Branch URLs and Git metadata                               | Full gitSource metadata, branch-specific URLs generated | Branch and commit shown, but not treated as a full Git provider deployment |
| PR comments                                                | Vercel bot posts preview URLs automatically             | Requires a custom step to post URLs                                        |
| Test, scan, and approval gates                             | Not run by Vercel's build                               | Available, the reason you're here                                          |
| Skew Protection, instant rollback, Fluid compute, 126 PoPs | Identical                                               | Identical                                                                  |

Read that last row first. The reliability features are the reason you deploy on Vercel, and they don't change based on how the deployment was triggered. Moving to the CLI path trades automatic build caching and automatic PR comments for the ability to gate a deploy on your own checks. That's a good trade when you have checks to gate on, and a bad one when you don't.

## Build once in Actions, deploy the artifact to Vercel

The whole integration runs on three CLI commands: `vercel pull`, `vercel build`, and `vercel deploy --prebuilt`. Vercel's documented pattern uses the CLI directly, which gives you explicit control over each step. That control is what you want when you're here to insert checks between build and deploy.

Before any of it works, you need three secrets stored as GitHub repository secrets:

- `VERCEL_TOKEN` is the API authentication token, created from your account's tokens page or with `vercel tokens add`.
  
- `VERCEL_ORG_ID` and `VERCEL_PROJECT_ID` both live in the `.vercel/project.json` file that `vercel login` and `vercel link` generate locally.
  

Reference them as `${{ secrets.VERCEL_TOKEN }}` and never hardcode them in a workflow file. A token committed to a YAML file is a token you'll be rotating under pressure later.

The preview workflow runs on pushes to any branch other than `main`:

`name: Vercel Preview Deployment env: VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} on: push: branches-ignore: - main jobs: Deploy-Preview: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install Vercel CLI run: npm install --global vercel@latest - name: Pull Vercel Environment Information run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }} - name: Build Project Artifacts run: vercel build --token=${{ secrets.VERCEL_TOKEN }} - name: Deploy Project Artifacts to Vercel run: vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }}`

The production workflow is the same structure with two changes: the `--prod` flag on both `vercel build` and `vercel deploy`, and the trigger restricted to pushes on `main`:

`name: Vercel Production Deployment env: VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} on: push: branches: - main jobs: Deploy-Production: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install Vercel CLI run: npm install --global vercel@latest - name: Pull Vercel Environment Information run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }} - name: Build Project Artifacts run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }} - name: Deploy Project Artifacts to Vercel run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}`

Here's where the duplicate-build failure comes from. If your workflow lints, type-checks, and builds, then triggers a Vercel deployment without `--prebuilt`, Vercel runs the build a second time. Two builds per commit, doubled CI minutes, slower feedback. The `--prebuilt` flag is the fix, and understanding what it separates matters more than treating it as a magic word.

`--prebuilt` decouples where the build runs from where the deployment is hosted. The build runs in your GitHub Actions runner, only the compiled `.vercel/output` folder gets uploaded to Vercel, and Vercel's global network hosts it. That separation buys you two things beyond avoiding the double build: source code privacy when you need it, because only the output ships, and the ability to gate deploys on test results, because build and deploy are now discrete steps you can put a check between.

Two operational differences come with the flag:

- CLI deployments don't send `gitSource` metadata the way the built-in Git integration does. Branch name and commit information still show up in the Vercel dashboard, but the deployment isn't treated as a full Git provider deployment, and branch-specific URLs aren't generated the same way.
  
- System Environment Variables aren't available at build time with `--prebuilt`, because the build happens outside Vercel. If your framework relies on them during the build, set the values yourself in the Actions environment or use Git-based deployments.
  

## Confirm the deploy and gate on it before it goes live

The integration runs in both directions, and the reverse direction is where teams get the most value with the least YAML. Vercel can trigger GitHub Actions in response to deployment events, which means you can run your checks against a real, live deployment instead of guessing whether it's ready.

When a project is connected through the Vercel for GitHub integration, Vercel sends `repository_dispatch` events (such as `vercel.deployment.ready` and `vercel.deployment.error`) to the connected repository as deployment status changes. Those events carry the preview URL, deployment status, environment, and full context as JSON. GitHub Actions can also trigger directly on `deployment_status` events. The payoff is that end-to-end tests run only after Vercel confirms the deployment is live, with no polling, no `sleep` commands, and no parsing PR comments to scrape a URL:

`name: E2E Tests on Vercel Preview on: deployment_status: types: [success] jobs: e2e-tests: if: github.event.deployment_status.state == 'success' && github.event.deployment_status.environment == 'Preview' runs-on: ubuntu-latest timeout-minutes: 30 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' - run: npm ci - run: npx playwright install --with-deps - name: Run Playwright tests run: npx playwright test env: BASE_URL: ${{ github.event.deployment_status.target_url }} VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }} - uses: actions/upload-artifact@v4 if: always() with: name: playwright-report path: playwright-report/` The `target_url` field hands you the unique preview URL, so the test runner always points at the deployment that just went live. If Deployment Protection is enabled on the project, you have two ways to let the runner through: - `VERCEL_AUTOMATION_BYPASS_SECRET`, a static secret shared with the workflow, shown in the example above.    - Trusted Sources, which authorizes GitHub Actions as an external service using short-lived OIDC (OpenID Connect) tokens instead of a shared secret. Configure it under **Settings** > **Deployment Protection** > **Trusted Sources**, where a guided form lets you scope the rule to a GitHub account, repository, and branch.    Prefer Trusted Sources for new setups. A short-lived token scoped to a repository is a smaller liability than a static secret stored in two places. ### Block production promotion until your checks pass Running tests against a preview is useful. Blocking a bad deploy from reaching production is the part that protects users. Vercel's Deployment Checks hold a deployment back from production promotion until selected checks complete successfully. Checks come from three sources: - Native Deployment Checks, which Vercel runs directly, including script-based lint and typecheck jobs from your `package.json`.    - GitHub Checks, where Vercel reads commit statuses and GitHub Actions check run results to decide whether a deployment promotes.    - Integration Checks from Vercel Marketplace integrations for testing, monitoring, and observability.    To gate on GitHub Actions, link your project to a GitHub repository, turn on automatic aliasing in the production environment settings, then open the project's Deployment Checks settings, select **Add Checks**, choose **GitHub** as the provider, and select which workflows must pass. The division of labor in this configuration is clean. Vercel handles the build and the preview deployment, GitHub Actions runs the quality checks against the live preview URL, and production promotion waits until those checks pass. No duplicate build runs, and you don't spend CI minutes rebuilding an artifact that already exists. ## Run the same pattern in a Turborepo monorepo Monorepos are where the duplicate-build problem compounds, because you're not rebuilding one app twice, you're rebuilding every package on every commit. Turborepo and the `--prebuilt` pattern solve it together: remote caching skips the packages that didn't change, and `--prebuilt` keeps the deploy from re-running the build that just happened. `name: CI/CD with Turborepo and Vercel on: push: branches: ["main", "develop"] pull_request: types: [opened, synchronize] env: TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ vars.TURBO_TEAM }} VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} jobs: build-and-test: name: Build and Test timeout-minutes: 15 runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 2 - uses: pnpm/action-setup@v3 with: version: 8 - uses: actions/setup-node@v4 with: node-version: 20 cache: 'pnpm' - run: pnpm install - run: pnpm build - run: pnpm test - run: pnpm lint deploy-preview: name: Deploy Preview needs: build-and-test if: github.event_name == 'pull_request' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm install --global vercel@latest - run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }} - run: vercel build --token=${{ secrets.VERCEL_TOKEN }} - run: vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }} deploy-production: name: Deploy Production needs: build-and-test if: github.ref == 'refs/heads/main' && github.event_name == 'push' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm install --global vercel@latest - run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }} - run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }} - run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}` The `needs: build-and-test` dependency holds every deploy job until the build and test job finishes successfully, so nothing ships on a red build. `TURBO_TOKEN` and `TURBO_TEAM` turn on Turborepo remote caching, which lets unchanged packages skip their build steps on later runs. One caveat trips people up. Turborepo remote caching is configured automatically when builds run on Vercel through the built-in Git integration. The moment you move builds into GitHub Actions runners, those tokens become your responsibility to set manually. The automation moved out of the system that used to handle it for you. ## Avoid the common failure modes before they cost you Most of the trouble with this integration comes from a short list of predictable failures. Naming them up front is faster than debugging them in production. - **Duplicate builds.** Covered above, and the most common one. Build in Actions without `--prebuilt`, and Vercel rebuilds the same artifact. Two builds, double the CI minutes.    - **Both systems deploying on the same push.** If you leave the native Git integration enabled while running an Actions deploy workflow, both fire on the same commit. Set `"git": { "deploymentEnabled": false }` in `vercel.json` to hand deployment to Actions cleanly.    - **Missing branch URLs and Git metadata.** CLI deployments don't carry full `gitSource` metadata, so don't expect branch-specific URLs to generate the way they do under the Git integration. Plan around it.    - **Missing System Environment Variables at build time.** With `--prebuilt`, the build runs outside Vercel, so Vercel's System Environment Variables aren't injected. Frameworks that rely on them at build time may not function correctly unless you set the values in the Actions environment.    - **No OIDC for deploy authentication, yet.** Vercel doesn't currently support OIDC for authenticating the GitHub Actions deployment itself. A community feature request for token-less deployments exists, but until it ships, you're on static `VERCEL_TOKEN` secrets, which means rotating tokens when team members leave. OIDC works in the other directions: Vercel Functions authenticate to AWS or GCP through OIDC federation, and GitHub Actions can bypass Deployment Protection through Trusted Sources. The deploy auth step itself still requires a static token.    On the security side, do the hygiene before you need it. Scope access tokens to teams or projects so a CI token only reaches what it should, using the token creation page or `vercel tokens add` with the `--project` flag. Use GitHub's environment protection rules to add a required-reviewer approval step on production deploys, since secrets stored in a GitHub environment are only available to jobs that reference it. If you have audit requirements, Vercel audit logs track team activity and security-relevant events, and Enterprise teams can stream them in real time to Datadog, Splunk, Amazon S3, Google Cloud Storage, or a custom HTTP endpoint. ## Rely on the same production features across both paths Everything Vercel does to keep production stable behaves the same whether the deployment came from the Git integration or a `--prebuilt` CLI deploy. Both paths produce identical artifacts on identical infrastructure. Three features carry the most weight, and each one is independent of how the deployment was triggered. | Feature          | How it behaves on `--prebuilt` deploys                                                                                                                                                | What to set                                        | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | | Skew Protection  | Version-locks the client and server to the same deployment, so once an app instance starts talking to a deployment, its later requests keep routing there. Max age is best-effort.    | A custom deployment ID (Next.js), per the CLI docs | | Instant rollback | A pointer update at the domain alias level. The domain points back to the deployment you selected, with no rebuild, no redeploy, and no scale-down event. The CDN cache stays intact. | Nothing, it works as-is                            | | Fluid compute    | Concurrent request handling, minimal cold starts, and active-CPU-only billing, on both paths. Default for new projects since April 2025.                                              | Nothing, on by default                             | Global distribution across 126 PoPs behaves identically too. None of these change based on whether the build ran in your Actions runner or on Vercel. The right division of labor was never "GitHub Actions or Vercel." Each system owns the half it's good at: - Vercel owns the deployment lifecycle: building, distributing across 126 PoPs, version-locking with Skew Protection, and rolling back instantly.    - GitHub Actions owns the checks Vercel doesn't run: tests, security scans, performance budgets, and approvals.    The `--prebuilt` flag connects the two so the build happens once and the deployment still inherits every platform feature. To set it up, start from a [Vercel template](https://vercel.com/templates) or [deploy a new project](https://vercel.com/new), then layer your Actions checks on top once the deployment flow is working.

## Frequently asked questions about GitHub Actions with Vercel

### Do I need to disable Vercel's built-in Git integration when using GitHub Actions?

Yes. Set `"git": { "deploymentEnabled": false }` in `vercel.json` to prevent duplicate deployments. Without it, both Vercel's integration and your GitHub Actions workflow trigger on the same push, and you're back to two deploys per commit.

### Can I deploy multiple Vercel projects from a single repository using GitHub Actions?

Yes. Create separate project ID secrets in GitHub, such as `VERCEL_PROJECT_ID_APP` and `VERCEL_PROJECT_ID_DOCS`, and reference each one in its own workflow file or job. Run `vercel link` against each project to retrieve its ID. Each project deploys independently.

### Do preview deployment comments and the Vercel Toolbar work with GitHub Actions deployments?

The Vercel Toolbar and DOM comments work identically regardless of how the deployment was triggered, because they're deployment-level features. The Vercel bot's automatic PR comments appear with the built-in Git integration and supported Git providers including GitHub, GitLab, and Bitbucket. When you deploy through GitHub Actions, you need a custom step to post preview URLs as PR comments.

### What happens to build caching when I move builds to GitHub Actions?

Build caching shifts from Vercel's managed cache to the GitHub Actions environment. For monorepos, configure Turborepo remote caching with `TURBO_TOKEN` and `TURBO_TEAM` to share cache across runs. For dependency caching, use `actions/setup-node@v4` with the `cache` option.

### Can I trigger production deployments from tags instead of branch pushes?

Yes. Replace the `on.push.branches` trigger with `on.push.tags: ['*']` in the production workflow. The rest of the workflow, including the `--prod` flags on both `vercel build` and `vercel deploy --prebuilt`, stays the same.

---

[View full KB sitemap](/kb/sitemap.md)
