GitHub Action
A disposable preview database for every pull request - idempotent create, masked outputs, TTL backstop, schema diffs and generated types.
What it does
capydatabase/github-action@v1 creates a CapyDB preview database for a pull request, waits for it to be ready, and exports the connection strings into the job environment. It talks to the API with curl + jq (preinstalled on GitHub runners) - nothing to install, nothing to cache.
Usage
name: preview-database
on:
pull_request:
types: [opened, synchronize, reopened, closed]
jobs:
preview:
runs-on: ubuntu-latest
steps:
- name: Create preview database
if: github.event.action != 'closed'
id: capydb
uses: capydatabase/github-action@v1
with:
api-key: ${{ secrets.CAPYDB_API_KEY }}
project-id: ${{ vars.CAPYDB_PROJECT_ID }}
- name: Run migrations + tests against the preview
if: github.event.action != 'closed'
run: |
pnpm db:migrate
pnpm test
# DATABASE_URL / DATABASE_URL_UNPOOLED are already exported by the
# action (write-env defaults to true).
- name: Delete preview database
if: github.event.action == 'closed'
uses: capydatabase/github-action@v1
with:
api-key: ${{ secrets.CAPYDB_API_KEY }}
project-id: ${{ vars.CAPYDB_PROJECT_ID }}
command: deleteInputs
| Input | Default | Description |
|---|---|---|
api-key | - (required) | CapyDB API key. Use a project-scoped key stored in repo secrets. |
project-id | - (required) | The CapyDB project the preview belongs to. |
command | create | create (idempotent), reset, delete, schema-diff, or generate-types. |
github-token | - | Token for schema-diff / generate-types to post and update their PR comment (secrets.GITHUB_TOKEN with pull-requests: write). |
name | pr-<number> / branch slug | Preview name; re-running with the same name reuses the database. |
mode | clone | clone copies production data; empty starts blank. |
ttl-hours | 72 | Auto-delete after this many hours (1–168). |
api-url | https://capydb.dev/api/capydb | Override for self-hosted control planes. |
wait-timeout | 600 | Seconds to wait for the preview to become ready. |
write-env | true | Export DATABASE_URL / DATABASE_URL_UNPOOLED into the job env. |
types-language | typescript | generate-types: typescript, zod, or drizzle. |
types-style | API default (capydb) | generate-types, TypeScript only: capydb or supabase. |
types-path | generator's file name | generate-types: file to write, e.g. src/db/database.types.ts. |
types-source | preview | generate-types: preview (this PR's preview) or project (production). |
types-commit | false | generate-types: commit and push the file when it changed. |
Outputs
| Output | Description |
|---|---|
preview-id | ID of the preview database. |
preview-name | Resolved preview name. |
database-url | Pooled connection string (masked). |
database-url-unpooled | Direct connection string (masked). |
schema-changed | schema-diff only: true when the preview's schema differs from production. |
types-path | generate-types only: the file that was written. |
types-changed | generate-types only: true when the generated file differs from the one on disk (or is new). |
types-committed | generate-types only: true when the file was committed and pushed. |
Schema diff on pull requests
After your migrations run against the preview, a schema-diff step compares the preview's schema to production and keeps one PR comment updated with the unified diff, plus a step summary and the schema-changed output. Connection strings never appear in it.
permissions:
pull-requests: write
steps:
- uses: capydatabase/github-action@v1
with:
api-key: ${{ secrets.CAPYDB_API_KEY }}
project-id: ${{ vars.CAPYDB_PROJECT_ID }}
- run: pnpm drizzle-kit migrate # or your migration tool, against DATABASE_URL_UNPOOLED
- uses: capydatabase/github-action@v1
with:
api-key: ${{ secrets.CAPYDB_API_KEY }}
project-id: ${{ vars.CAPYDB_PROJECT_ID }}
command: schema-diff
github-token: ${{ secrets.GITHUB_TOKEN }}Generated types on pull requests
generate-types calls the same generator as capydb generate and writes TypeScript interfaces, Zod schemas, or a Drizzle schema into the workspace - generated from the preview's schema after your migrations ran, or from production with types-source: project. It reports whether the file changed as the types-changed output, in the step summary, and - with a github-token - in its own PR comment, with the diff against the file on the branch.
Fail the job when the committed types lag the schema:
- uses: capydatabase/github-action@v1
id: types
with:
api-key: ${{ secrets.CAPYDB_API_KEY }}
project-id: ${{ vars.CAPYDB_PROJECT_ID }}
command: generate-types
types-path: src/db/database.types.ts
github-token: ${{ secrets.GITHUB_TOKEN }}
- if: steps.types.outputs.types-changed == 'true'
run: |
echo "::error::src/db/database.types.ts is out of date; see the PR comment"
exit 1Or commit the regenerated file to the PR branch with types-commit: true. That needs contents: write and a branch checkout: on pull_request events actions/checkout checks out a detached merge commit by default, and the action refuses to commit on top of it.
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.head_ref }}
# ... create the preview and run your migrations ...
- uses: capydatabase/github-action@v1
with:
api-key: ${{ secrets.CAPYDB_API_KEY }}
project-id: ${{ vars.CAPYDB_PROJECT_ID }}
command: generate-types
types-path: src/db/database.types.ts
types-commit: true
github-token: ${{ secrets.GITHUB_TOKEN }}The commit contains only the generated file and is authored by github-actions[bot]. A push made with GITHUB_TOKEN starts no new workflow run, so it cannot loop; it also cannot push to a fork, so leave types-commit off for PRs from forks. Both schema-diff and generate-types need an API key with schema:read (or projects:read, which project-scoped keys carry by default).
Behavior worth knowing
createis idempotent: pushing more commits to the same PR reuses the existing preview and extends its TTL instead of recreating it. Usecommand: resetif you want a fresh clone on every push.- Masked outputs: connection strings are registered with
::add-mask::so they never appear in logs. - TTL backstop: previews expire via their TTL even if the
closedjob never runs (force-pushed branch, cancelled workflow), so a missed cleanup cannot leak databases forever. - Key scoping: create the API key in the dashboard under Settings → API keys with a
project_id, scoped to the single project the workflow uses. A leaked project-scoped key cannot touch sibling projects or the org. See Authentication. - Ordering: unlike the Vercel/Netlify deploy-webhook flow, the Action is synchronous - database first, then your steps. Use it when build steps need the database to exist.