Test private previews in CI and with agents
When only your team can open a project’s previews (see who can open previews), a script or a headless browser gets the sign-in page instead of your site. Ask the API for short-lived access to that one preview and pass it along. You get two things:
- A request header for scripted fetches: curl,
fetch, link checkers, load tests - A one-time link for a fresh headless browser: open it once and the browser holds a session for the rest of the run
Check what you need
Section titled “Check what you need”- An API key in a variable such as
$DEPLOYBASE_API_KEY, or a dashboard session. Any role can ask, Viewer and up - The project ID, in
$PROJECT_ID - A preview that exists. A branch shows up once its first preview build has finished
An AI agent connected over MCP needs none of this: see ask your agent.
Find the preview ID
Section titled “Find the preview ID”List the project’s previews and pick the one for your branch:
curl https://api.deploybase.eu/api/v1/projects/$PROJECT_ID/previews \ -H "Authorization: Bearer $DEPLOYBASE_API_KEY"Each entry in data.previews has an id and the branch it was built from. In a script, select it with jq:
PREVIEW_ID=$(curl -sS --fail https://api.deploybase.eu/api/v1/projects/$PROJECT_ID/previews \ -H "Authorization: Bearer $DEPLOYBASE_API_KEY" \ | jq -r --arg b "$BRANCH" '.data.previews[] | select(.branch == $b) | .id')The branch name is matched exactly, slashes and capitals included: feat/login, not feat-login.
Ask for access
Section titled “Ask for access”curl -X POST https://api.deploybase.eu/api/v1/projects/$PROJECT_ID/previews/$PREVIEW_ID/access \ -H "Authorization: Bearer $DEPLOYBASE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"ttl_seconds": 900}'ttl_seconds is how long the access lasts: 60 to 3600 seconds. Leave the body out for the default of 900 (15 minutes). Anything outside that range answers 422 with the code INVALID_TTL.
{ "data": { "protected": true, "host": "feat-login-my-blog.previews.deploybase.eu", "expires_at": "2026-10-23T12:15:00Z", "header": { "name": "x-deploybase-access", "value": "eyJhbGciOiJFZERTQSJ9.0123456789abcdef.0123456789abcdef" }, "url": "https://feat-login-my-blog.previews.deploybase.eu/.deploybase/gate/callback?code=eyJhbGciOiJFZERTQSJ9.fedcba9876543210.fedcba9876543210" }}Both header.value and url stop working at expires_at. Treat them like the API key itself: never print them, log them or commit them.
If the preview is open to anyone with the link, the answer has protected: false, an empty header.value, and url is the plain preview address. The scripts on this page work either way, so they need no special case for public previews.
A 404 with the code PREVIEW_NOT_FOUND means the preview ID is wrong, belongs to another project, or the preview has no address yet.
Fetch pages from a script
Section titled “Fetch pages from a script”Send the header on every request to the preview host:
curl -sS -H "x-deploybase-access: $PREVIEW_TOKEN" \ "https://$PREVIEW_HOST/pricing"$PREVIEW_TOKEN is data.header.value and $PREVIEW_HOST is data.host. A request whose header was not accepted, because it expired or belongs to another preview, gets a 302 to the sign-in page at app.deploybase.eu rather than an error. Do not pass -L if you want to see that: with it, curl follows the redirect and reports a 200 for the sign-in page.
In Node or Deno, fetch takes the same header:
const res = await fetch(`https://${host}/pricing`, { headers: { [header.name]: header.value }, redirect: "manual",});Tools with an extra-headers option, such as Lighthouse (--extra-headers), work the same way. A tool that drives a real browser sends that header to every address the page loads from, third-party fonts and scripts included. For those, open the link instead.
Open a preview in a headless browser
Section titled “Open a preview in a headless browser”Open url once in a fresh browser. The preview sets a session cookie for its own host and redirects to /. From then on the browser can open any page of that preview until expires_at, with nothing extra on its requests.
The link itself works for 60 seconds after you asked for it, so ask for access right before the browser starts.
In Playwright, open the link in a global setup and save the session for every test:
import { chromium } from "@playwright/test";
export default async function globalSetup() { const browser = await chromium.launch(); const page = await browser.newPage(); await page.goto(process.env.PREVIEW_ACCESS_URL!); await page.context().storageState({ path: "preview-session.json" }); await browser.close();}import { defineConfig } from "@playwright/test";
export default defineConfig({ globalSetup: "./global-setup.ts", use: { baseURL: `https://${process.env.PREVIEW_HOST}`, storageState: "preview-session.json", },});import { test, expect } from "@playwright/test";
test("pricing page loads", async ({ page }) => { await page.goto("/pricing"); await expect(page.getByRole("heading", { level: 1 })).toBeVisible();});preview-session.json holds the session. Add it to .gitignore and keep it out of uploaded test artifacts. Do not put the header in Playwright’s extraHTTPHeaders: Playwright sends those to every address the page loads from.
Run it in GitHub Actions
Section titled “Run it in GitHub Actions”Store the API key and the project ID as repository secrets, then mint access in a step before the tests. ::add-mask:: hides the values in the job log, and $GITHUB_ENV hands them to later steps without printing them:
name: Preview testson: pull_request
jobs: e2e: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm ci && npx playwright install --with-deps chromium
- name: Get preview access env: DEPLOYBASE_API_KEY: ${{ secrets.DEPLOYBASE_API_KEY }} PROJECT_ID: ${{ secrets.DEPLOYBASE_PROJECT_ID }} BRANCH: ${{ github.head_ref }} run: | set -euo pipefail API=https://api.deploybase.eu/api/v1/projects/$PROJECT_ID/previews PREVIEW_ID=$(curl -sS --fail "$API" -H "Authorization: Bearer $DEPLOYBASE_API_KEY" \ | jq -r --arg b "$BRANCH" '.data.previews[] | select(.branch == $b) | .id') test -n "$PREVIEW_ID" || { echo "No preview for $BRANCH yet"; exit 1; } ACCESS=$(curl -sS --fail -X POST "$API/$PREVIEW_ID/access" \ -H "Authorization: Bearer $DEPLOYBASE_API_KEY" \ -H "Content-Type: application/json" -d '{"ttl_seconds": 1800}') TOKEN=$(jq -r '.data.header.value' <<<"$ACCESS") LINK=$(jq -r '.data.url' <<<"$ACCESS") [ -n "$TOKEN" ] && echo "::add-mask::$TOKEN" echo "::add-mask::$LINK" { echo "PREVIEW_HOST=$(jq -r '.data.host' <<<"$ACCESS")" echo "PREVIEW_TOKEN=$TOKEN" echo "PREVIEW_ACCESS_URL=$LINK" } >> "$GITHUB_ENV"
- run: npx playwright testThe browser is installed before the access step on purpose: the link expires 60 seconds after that step, so keep it right before playwright test.
Run it in Forgejo Actions
Section titled “Run it in Forgejo Actions”Forgejo Actions reads the same workflow syntax, secrets and ::add-mask::. Put the file in .forgejo/workflows/, set runs-on to your runner’s label, and keep the steps above. On a push rather than a pull request, take the branch from github.ref_name:
on: push: branches-ignore: [main]
jobs: e2e: runs-on: docker steps: - uses: actions/checkout@v4 - name: Get preview access env: DEPLOYBASE_API_KEY: ${{ secrets.DEPLOYBASE_API_KEY }} PROJECT_ID: ${{ secrets.DEPLOYBASE_PROJECT_ID }} BRANCH: ${{ github.ref_name }} run: | # same script as the GitHub Actions exampleA job that starts right after a push can run before deploybase has finished building that commit. The preview then still serves the previous build. If the tests must see this commit, poll the previews list until the preview’s deployment_id is the deployment built from it, then ask for access.
Let an AI agent open a preview
Section titled “Let an AI agent open a preview”An agent connected over MCP has the tool get_preview_access. Ask in plain words, for example “open the preview of feat/login and check the pricing page”. The agent finds the preview by its exact branch name or its ID, gets access for 60 seconds to an hour (15 minutes by default), and uses the header or the link itself. The tool tells the agent not to repeat the values back or write them anywhere.
Know the limits
Section titled “Know the limits”- Access lasts at most one hour. There is no way to extend it: ask again for a new one.
- The link works for 60 seconds, and anyone who has it in that minute gets in. It is not single-use. Keep it out of chat messages, issue comments and logs.
- Ending it early is all or nothing. Sign everyone out, changing or removing the password, or moving away from Your team and people with the password ends every access handed out for the project’s previews, along with every person’s session. Revoking the API key that asked for access does not end access already handed out; it runs until
expires_at. - One preview host only. Access to
feat-login-my-blog.previews.deploybase.eudoes not open another branch’s preview. - Production and function addresses are not covered. The access does nothing on your production site, your custom domains or
*.fn.deploybase.eu.
Live b001be8d built