Skip to content

Ask AI

Ask anything about Destesi — setup, products, APIs.

Powered by Claude. Answers may be wrong — always verify against the docs.

GitHub Actions workflow

A preview per pull request, driven from your repo’s CI. No webhooks to configure — pull_request events do the work.

  • A workspace to run previews in: dst workspace switch <slug>
  • A GitHub connection on the workspace, added in Connect at connect.destesi.io — the connection’s id is what --third-party-app takes
  • A scoped CI key (dst api-key create --preview-runner)

Use a preview-runner key, not your personal token. It’s scoped to preview:write on one workspace — a leaked key can’t touch your database branches, other workspaces, or the rest of your account.

Terminal window
dst api-key create \
--preview-runner \
--workspace my-workspace \
--label github-actions \
-o token \
| xargs -I {} gh secret set DESTESI_API_KEY --body {}

Also set repo-level GitHub Actions vars (not secrets) for DESTESI_WORKSPACE and DESTESI_GITHUB_APP (the connected GitHub account’s id).

Drop this into .github/workflows/preview.yml. Two jobs — preview on open/synchronize, teardown on closed.

name: Preview environment
on:
pull_request:
types: [opened, synchronize, reopened, closed]
concurrency:
group: preview-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
preview:
if: github.event.action != 'closed'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dst
run: curl -fsSL https://get.destesi.io/install.sh | sh
- name: Upsert preview
env:
DESTESI_API_KEY: ${{ secrets.DESTESI_API_KEY }}
DESTESI_WORKSPACE: ${{ vars.DESTESI_WORKSPACE }}
PR_BODY: ${{ github.event.pull_request.body }}
run: |
printf '%s' "$PR_BODY" > pr-body.md
dst preview upsert \
--repo ${{ github.repository }} \
--branch ${{ github.head_ref }} \
--commit ${{ github.event.pull_request.head.sha }} \
--third-party-app ${{ vars.DESTESI_GITHUB_APP }} \
--pr-number ${{ github.event.pull_request.number }} \
--pr-title "${{ github.event.pull_request.title }}" \
--pr-body-file pr-body.md \
--pr-url ${{ github.event.pull_request.html_url }} \
-o id > preview-id.txt
dst preview wait "$(cat preview-id.txt)" --timeout 10m
- name: Comment on PR
if: success()
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DESTESI_API_KEY: ${{ secrets.DESTESI_API_KEY }}
DESTESI_WORKSPACE: ${{ vars.DESTESI_WORKSPACE }}
run: |
URL=$(dst preview show --by-pr ${{ github.event.pull_request.number }} -o url)
gh pr comment ${{ github.event.pull_request.number }} \
--body "Preview ready: $URL"
teardown:
if: github.event.action == 'closed'
runs-on: ubuntu-latest
steps:
- name: Install dst
run: curl -fsSL https://get.destesi.io/install.sh | sh
- name: Delete preview
env:
DESTESI_API_KEY: ${{ secrets.DESTESI_API_KEY }}
DESTESI_WORKSPACE: ${{ vars.DESTESI_WORKSPACE }}
run: |
dst preview delete --by-pr ${{ github.event.pull_request.number }}

dst preview upsert is keyed by (workspace, pr_number) — so every synchronize event redeploys the same preview in place instead of accumulating new ones. The concurrency block on the job cancels in-flight runs for the same PR so you don’t race on a deploy.

  • --repo / --branch / --commit / --third-party-app — the source. --third-party-app takes your workspace’s Connect GitHub connection id. The tarball is fetched with a token Preview reads from Connect for the workspace at fetch time, so rotating or reconnecting in Connect takes effect without touching this workflow.
  • --pr-number / --pr-title / --pr-url — used as the idempotency key and for UI display.
  • Env overrides (declared in the workflow or in the web UI) pass through to the container and are resolved against the forked branch’s endpoints. See env templates.
  • --from-remote <name|id> (or --from-snap <id>) — the database source. Name it explicitly. A preview does not inherit a workspace default: with neither flag the preview still builds and runs, but with no database — the fork is skipped and the preview’s event timeline records skipped snap fork (no source configured). That is a preview whose app boots and then can’t find its data, which reads as an app bug rather than a missing flag.