> ## Documentation Index
> Fetch the complete documentation index at: https://docs.peepsai.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Test Vercel preview deployments

> Run each pull request's tests against that pull request's own Vercel preview, get through Vercel's Deployment Protection without storing a secret, and test production on your default branch.

When your app deploys to Vercel, every pull request gets its own preview URL. Peeps can run the pull request's tests against that preview, so a failure means this change broke it, not something else on a shared environment.

This takes three things: Vercel tells GitHub about each deployment, your Peeps workflow waits for the pull request's preview and tests it, and Vercel lets the workflow past its login.

> [!NOTE]
>
> Peeps code review needs none of this. It reviews every pull request on a repository where code review is on, whether or not you deploy previews.

## 1. Let Vercel tell GitHub about deployments

In Vercel, open your project, then **Settings** > **Git**, and turn on **deployment\_status Events**.

New Vercel projects leave this off. Without it, Vercel posts only a commit status that links to the Vercel dashboard, and nothing in GitHub says where the preview lives. With it on, Vercel records a GitHub deployment for each commit, with the preview's URL.

## 2. Test the pull request's preview

Add `preview: vercel` to the Peeps step and run the workflow on `pull_request`. On a pull request the [Peeps action](https://github.com/Peeps-Labs/peeps-action) waits for the preview of the pull request's head commit and sets `BASE_URL` to it before your tests start.

Start the job from `pull_request`, not from Vercel's `deployment_status` event: Peeps counts a job as the pull request's tests only when it runs on the pull request. See [Pull request tests](#pull-request-tests) below.

```yaml .github/workflows/peeps.yml theme={null}
name: Peeps
on:
  workflow_dispatch:
    inputs:
      mode: { type: string, required: true }
      sessionId: { type: string, required: false }
      ref: { type: string, required: false }
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read
  deployments: read # read the preview Vercel recorded
  statuses: read # tell "deployment events off" apart from "still building"
  id-token: write # OIDC: to Peeps, and to Vercel's Trusted Sources

jobs:
  peeps:
    name: "peeps ${{ inputs.mode || 'report' }} ${{ inputs.sessionId || '' }}"
    runs-on: ubuntu-latest
    container: mcr.microsoft.com/playwright:v1.59.1-jammy
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v6
        with:
          ref: ${{ inputs.ref || github.ref }}
      - run: npm ci
      - name: Test production
        if: github.event_name == 'push'
        env:
          PRODUCTION_URL: ${{ vars.PRODUCTION_URL }}
        run: echo "BASE_URL=$PRODUCTION_URL" >> "$GITHUB_ENV"
      - uses: Peeps-Labs/peeps-action@v1
        with:
          mode: ${{ inputs.mode || 'ci' }}
          session-id: ${{ inputs.sessionId }}
          preview: vercel
```

What `preview: vercel` does:

* It finds the GitHub deployment Vercel recorded for the pull request's head commit, in the `Preview` environment, and waits until it succeeds: up to 10 minutes, or `preview-timeout` seconds.
* When the repository deploys several Vercel projects, name the one to test with `preview-project`, for example `preview-project: web`.
* When Vercel deployed the commit but recorded no GitHub deployment, it stops at once and tells you to turn on **deployment\_status Events** (step 1).
* A `BASE_URL` your workflow sets itself wins.
* When Peeps starts the workflow to fix a failed test, the fix runs on the preview of the commit it checked out, so a pull request's failure is fixed against that pull request's own preview.

On your default branch, test your production domain (`PRODUCTION_URL` above, an Actions variable). A production deployment's own URL is behind Deployment Protection too; the domain is not.

Point your Playwright config at `BASE_URL`:

```ts playwright.config.ts theme={null}
use: { baseURL: process.env.BASE_URL || "http://localhost:3000" },
```

## 3. Get through Deployment Protection

Vercel's Standard Protection sends every visitor of a preview to **Log in to Vercel**. Your tests are visitors too. When they hit it, every test fails on the login page, and Peeps' failure analysis says the preview's Deployment Protection stopped the test.

Let your CI through with a Trusted Source. Nothing is stored: each job proves which repository it runs in with its own short-lived GitHub token.

**1. Add the trusted source**

In Vercel, open your project, then **Settings** > **Deployment Protection** > **Trusted Sources** > **Add trusted source** > **External Service**. Choose **GitHub Actions**, your GitHub account, and the repository whose workflow runs the tests. Keep the default audience, `https://github.com/<your account>`, and the **Preview** environment. Click **Add**.

**2. Send the token from your tests**

A job with `id-token: write` can request its own GitHub token at any time. Each one lasts five minutes, and Vercel sets no cookie for it, so request it in the test process, send it as the `x-vercel-trusted-oidc-idp-token` header on every request to the deployment under test, and renew it before it expires:

```ts tests/fixtures.ts theme={null}
import { test as base } from "@playwright/test";

const requestUrl = process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
let cached: { token: string; expiresAt: number } | null = null;

async function trustedSourceToken(): Promise<string | null> {
  if (!requestUrl || !requestToken) return null; // not in GitHub Actions
  if (cached && Date.now() < cached.expiresAt - 60_000) return cached.token;
  const res = await fetch(requestUrl, {
    headers: { authorization: `Bearer ${requestToken}` },
  });
  if (!res.ok) throw new Error(`GitHub OIDC token request failed: ${res.status}`);
  const { value } = (await res.json()) as { value: string };
  const { exp } = JSON.parse(
    Buffer.from(value.split(".")[1]!, "base64url").toString(),
  ) as { exp: number };
  cached = { token: value, expiresAt: exp * 1000 };
  return value;
}

export const test = base.extend({
  context: async ({ context, baseURL }, use) => {
    if (baseURL && requestUrl && requestToken) {
      const origin = new URL(baseURL).origin;
      await context.route(
        (url) => url.origin === origin,
        async (route) => {
          const token = await trustedSourceToken();
          await route.continue({
            headers: {
              ...route.request().headers(),
              ...(token ? { "x-vercel-trusted-oidc-idp-token": token } : {}),
            },
          });
        },
      );
    }
    await use(context);
  },
});

export { expect } from "@playwright/test";
```

Import `test` and `expect` from this file in your specs instead of from `@playwright/test`. The token goes only to the deployment under test, never to another site the page loads from.

> [!TIP]
>
> The repository in the trusted source must be the one the workflow runs in. A source for another repository, even your organization's main one, does not let these jobs through.

**Or use a bypass secret.** Under **Protection Bypass for Automation**, click **Add Secret**, save the value as the repository secret `VERCEL_AUTOMATION_BYPASS_SECRET`, and send it as the `x-vercel-protection-bypass` header in the same fixture. Unlike the trusted source's token it does not expire, and Playwright traces record request headers, so set `trace: "off"` while it is set.

## Pull request tests

On the project's **Repository** settings page, under **Pull request tests**:

1. Choose the **Smoke tests**: a selection of the tests every pull request runs. Tests the pull request adds run too.
2. Under **On a pull request, your tests run against**, choose **That pull request's own build**.

Peeps then records each run against the URL your workflow tested, so a run's page shows which preview it ran on.

Once a pull request has run, the checklist there also shows two Vercel rows:

* **Vercel tells GitHub where each preview is**: ticked when Vercel recorded a GitHub deployment for the pull request's commit. If Vercel built it without one, the row tells you to turn on **deployment\_status Events** (step 1).
* **Pull requests test their own Vercel preview**: ticked when the pull request's tests ran against a Vercel deployment. If not, the row tells you to add `preview: vercel` (step 2).

When a run's analysis finds the test stopped on Vercel's login, it says so and doesn't suggest changing the test: fix the trusted source (step 3) and run it again.

## Troubleshooting

| What you see | What it means | What to do |
| - | - | - |
| Every test fails on **Log in to Vercel** | Deployment Protection stopped the tests. | Add the trusted source and send its token (step 3). |
| `Vercel deployed <sha> but recorded no GitHub deployment` | **deployment\_status Events** is off. | Turn it on (step 1) and push again. |
| `No successful Vercel preview of <sha> after 600s` | The preview is still building, failed, or the project doesn't deploy this branch. | Check the deployment in Vercel. |
| `This commit has Vercel previews of … Name the one to test with preview-project.` | The repository deploys several Vercel projects. | Add `preview-project: <name>` to the Peeps step. |
| `GitHub refused … Give the job deployments: read and statuses: read` | The workflow's `permissions` leave those out. | Add them, as in step 2. |
| The preview loads but the run counts as a plain run | The job started from `deployment_status`, not `pull_request`. | Run the workflow on `pull_request` (step 2). |

## Known limits

* **The fix assistant in Peeps can't open a protected preview.** It runs in Peeps, not in your CI, so your workflow's trusted-source token doesn't reach it, and Peeps sends your project's HTTP headers only to your environments' URLs. It can still read the run's evidence; to look at the preview itself, open it in your browser.
* **A fork's pull request is not tested on its preview.** `preview: vercel` refuses pull requests from forks and `pull_request_target`, because a fork's preview runs the fork's code.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.