GitHub Actions workflow
GitHub Actions
Section titled “GitHub Actions”A preview per pull request, driven from your repo’s CI. No webhooks to configure — pull_request events do the work.
Prerequisites
Section titled “Prerequisites”- 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-apptakes - A scoped CI key (
dst api-key create --preview-runner)
1. Mint a scoped CI key
Section titled “1. Mint a scoped CI key”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.
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).
2. Add the workflow
Section titled “2. Add the workflow”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 }}Why upsert over create?
Section titled “Why upsert over create?”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.
What you pass, what gets resolved
Section titled “What you pass, what gets resolved”--repo / --branch / --commit / --third-party-app— the source.--third-party-apptakes 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 recordsskipped 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.