Preview deployments
Ready-to-copy workflows for Vercel, Netlify, Render, Cloudflare Pages, your own pipeline, or no preview host at all.
QualityGate scans a URL, so every integration comes down to one question: what is the address of this pull request’s preview? This page answers it for common hosts. Each example is a complete GitHub Actions workflow; the same idea applies to GitLab and Bitbucket.
All examples use the keyless GitHub App setup. For an API key, add
api_key: ${{ secrets.QUALITYGATE_API_KEY }} and see
Setup with an API key.
Before you start
The preview must be public to the runner. QualityGate waits for a 2xx
response and loads the page like a visitor. Password protection, Vercel
Deployment Protection, Netlify password protection, or an IP allowlist will make
the run time out waiting for the preview. Turn protection off for previews, or
use Build and serve in the same job.
Add the preview host to the project’s allowed domains. Use the narrowest
pattern your host allows. A *. prefix matches any subdomain:
| Host | Allowed domain to add |
|---|---|
| Your own preview domain | *.preview.example.com |
| Cloudflare Pages | *.your-project.pages.dev |
| Vercel | *.vercel.app |
| Netlify | *.netlify.app |
| Render | *.onrender.com |
| Scanning in the job | localhost |
Shared platform domains such as *.vercel.app are accepted. The allowed list
decides which hosts a run may target, so the narrower it is, the less a leaked
key could spend.
Scan the right commit. Predictable preview addresses keep serving the previous build until the new one is live, so a scan that starts too early can look at the last commit. Where the host tells you the exact deployment URL, the examples below use it. Otherwise, run the scan after the host has finished deploying.
Vercel
Deploy the preview from the workflow with the Vercel CLI, which prints the
exact deployment URL. Create a Vercel token and add the secrets
VERCEL_TOKEN, VERCEL_ORG_ID, and VERCEL_PROJECT_ID (the last two are in
.vercel/project.json after vercel link).
name: Preview and QualityGate
on:
pull_request:
permissions:
contents: read
id-token: write
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
jobs:
preview:
runs-on: ubuntu-latest
outputs:
url: ${{ steps.deploy.outputs.url }}
steps:
- uses: actions/checkout@v5
- run: npm install --global vercel@latest
- run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
- run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
- id: deploy
run: echo "url=$(vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }})" >> "$GITHUB_OUTPUT"
quality:
needs: preview
runs-on: ubuntu-latest
steps:
- run: npx -y playwright@1.61.1 install --with-deps chromium
- uses: qualitygate/scan-action@v1
with:
url: ${{ needs.preview.outputs.url }}
Vercel protects preview deployments by default on many plans. Under Project Settings → Deployment Protection, disable Vercel Authentication for preview deployments, or the scan will wait until it times out.
Netlify
Netlify deploy previews have a predictable address:
https://deploy-preview-<pull request number>--<site name>.netlify.app. The
simplest setup scans it and gives Netlify time to finish building:
name: QualityGate
on:
pull_request:
permissions:
contents: read
id-token: write
jobs:
quality:
runs-on: ubuntu-latest
steps:
- run: npx -y playwright@1.61.1 install --with-deps chromium
- uses: qualitygate/scan-action@v1
with:
url: https://deploy-preview-${{ github.event.pull_request.number }}--your-site.netlify.app
wait_timeout: '600'
On a pull request’s first commit the address returns 404 until the build
finishes, and the action keeps waiting. On later commits the address already
serves the previous build, so the scan can run against it. For an exact match
per commit, deploy from the workflow with the Netlify CLI and scan the URL it
returns. Add the secrets NETLIFY_AUTH_TOKEN and NETLIFY_SITE_ID:
jobs:
preview:
runs-on: ubuntu-latest
outputs:
url: ${{ steps.deploy.outputs.url }}
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: 22
- run: npm ci
- id: deploy
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
run: |
url=$(npx -y netlify-cli deploy --build --json | jq -r '.deploy_url')
echo "url=$url" >> "$GITHUB_OUTPUT"
quality:
needs: preview
runs-on: ubuntu-latest
steps:
- run: npx -y playwright@1.61.1 install --with-deps chromium
- uses: qualitygate/scan-action@v1
with:
url: ${{ needs.preview.outputs.url }}
Render
With pull request previews enabled, Render serves each pull request at
https://<service>-pr-<pull request number>.onrender.com, where <service>
is the subdomain of your service’s normal onrender.com address:
- run: npx -y playwright@1.61.1 install --with-deps chromium
- uses: qualitygate/scan-action@v1
with:
url: https://your-service-pr-${{ github.event.pull_request.number }}.onrender.com
wait_timeout: '900'
Render builds can take several minutes, hence the longer wait. Check the exact preview address on your first pull request and adjust the pattern if needed.
Cloudflare Pages
Deploy with the official Wrangler action and scan the deployment URL it
returns. The preview lives under your project’s pages.dev subdomain, so you
can allow just *.your-project.pages.dev:
jobs:
preview:
runs-on: ubuntu-latest
outputs:
url: ${{ steps.deploy.outputs.deployment-url }}
steps:
- uses: actions/checkout@v5
- run: npm ci && npm run build
- id: deploy
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy dist --project-name=your-project --branch=${{ github.head_ref }}
quality:
needs: preview
runs-on: ubuntu-latest
steps:
- run: npx -y playwright@1.61.1 install --with-deps chromium
- uses: qualitygate/scan-action@v1
with:
url: ${{ needs.preview.outputs.url }}
Your own deployment pipeline
If you deploy previews yourself (Kubernetes, Fly.io, a VM, anything), make the deploy job output the URL and point QualityGate at it. A wildcard preview domain you control keeps the allowed list tight:
jobs:
preview:
runs-on: ubuntu-latest
outputs:
url: ${{ steps.deploy.outputs.url }}
steps:
- uses: actions/checkout@v5
- id: deploy
run: |
./deploy.sh --name "pr-${{ github.event.pull_request.number }}"
echo "url=https://pr-${{ github.event.pull_request.number }}.preview.example.com" >> "$GITHUB_OUTPUT"
quality:
needs: preview
runs-on: ubuntu-latest
steps:
- run: npx -y playwright@1.61.1 install --with-deps chromium
- uses: qualitygate/scan-action@v1
with:
url: ${{ needs.preview.outputs.url }}
Allowed domain: *.preview.example.com.
Build and serve in the same job
No preview host? Build the app in the CI job, start it on the runner, and scan
localhost. Add localhost to the project’s allowed domains. This always scans
exactly the commit under review, and works well for regression mode.
name: QualityGate
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
id-token: write
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run build
# Start the production server in the background. For a static site,
# serve the build output instead: npx -y serve -l 3000 dist &
- run: npm run start -- --port 3000 &
- run: npx -y playwright@1.61.1 install --with-deps chromium
- uses: qualitygate/scan-action@v1
with:
url: http://localhost:3000/
fail_on: regression
The action waits for the server to answer before scanning. Keep in mind that a
local server does not have your hosting platform’s response headers, so the
security bundle is better run against a real deployment.