Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions .github/workflows/neon-preview-branch.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
name: Neon preview branch

# Creates (or reuses) a Neon branch for each PR, applies pending Drizzle
# migrations to it, and points the Vercel preview deployment for this git
# branch at it. Teardown lives in neon-preview-cleanup.yml.

on:
pull_request:
types: [opened, reopened, synchronize]

concurrency:
group: neon-preview-${{ github.event.pull_request.number }}
cancel-in-progress: false

permissions:
contents: read

jobs:
preview-db:
# Forked PRs cannot read secrets, so every API call below would fail.
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
env:
NEON_BRANCH: preview/pr-${{ github.event.pull_request.number }}
GIT_BRANCH: ${{ github.event.pull_request.head.ref }}
VERCEL_API: https://api.vercel.com
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_PROJECT_ID: ${{ vars.VERCEL_PROJECT_ID }}
VERCEL_ORG_ID: ${{ vars.VERCEL_ORG_ID }}

steps:
- uses: actions/checkout@v4

- name: Create Neon branch
id: neon
uses: neondatabase/create-branch-action@v6.4.0
with:
project_id: ${{ vars.NEON_PROJECT_ID }}
branch_name: ${{ env.NEON_BRANCH }}
api_key: ${{ secrets.NEON_API_KEY }}
# database/role default to neondb/neondb_owner, which match production.

# NOTE: do not add an "::add-mask::" step here. Actions echoes a run
# block's script into the log before executing it, so masking a literal
# prints the very secret it is protecting. create-branch-action already
# registers its connection strings as masked values.

- uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Apply migrations to the preview branch
# Direct (unpooled) endpoint — DDL should not go through the pooler.
env:
DATABASE_URL: ${{ steps.neon.outputs.db_url }}
run: bun db:migrate

- name: Point Vercel preview at the Neon branch
id: vercel_env
env:
DB_URL_POOLED: ${{ steps.neon.outputs.db_url_pooled }}
run: |
set -euo pipefail

existing=$(curl -sS -f \
-H "Authorization: Bearer $VERCEL_TOKEN" \
"$VERCEL_API/v10/projects/$VERCEL_PROJECT_ID/env?teamId=$VERCEL_ORG_ID&decrypt=true&gitBranch=$GIT_BRANCH" \
| jq -r --arg b "$GIT_BRANCH" \
'first(.envs[]? | select(.key == "DATABASE_URL" and .gitBranch == $b) | .value) // ""')

if [ "$existing" = "$DB_URL_POOLED" ]; then
echo "DATABASE_URL for $GIT_BRANCH is already correct; skipping redeploy."
echo "changed=false" >> "$GITHUB_OUTPUT"
exit 0
fi

jq -n \
--arg value "$DB_URL_POOLED" \
--arg branch "$GIT_BRANCH" \
--arg comment "Neon branch $NEON_BRANCH (managed by neon-preview-branch.yml)" \
'{key: "DATABASE_URL", value: $value, type: "encrypted",
target: ["preview"], gitBranch: $branch, comment: $comment}' \
| curl -sS -f -X POST \
-H "Authorization: Bearer $VERCEL_TOKEN" \
-H "Content-Type: application/json" \
--data @- \
"$VERCEL_API/v10/projects/$VERCEL_PROJECT_ID/env?teamId=$VERCEL_ORG_ID&upsert=true" \
> /dev/null

echo "Set DATABASE_URL for preview branch $GIT_BRANCH."
echo "changed=true" >> "$GITHUB_OUTPUT"

- name: Redeploy preview with the new DATABASE_URL
# Vercel's own build for this push already started with the previous
# (project-level) DATABASE_URL. Env changes only take effect on a new
# build, so force one. Only needed when the value actually changed —
# i.e. the first run for this PR.
if: steps.vercel_env.outputs.changed == 'true'
run: |
set -euo pipefail

jq -n \
--arg name "${{ github.event.repository.name }}" \
--arg ref "$GIT_BRANCH" \
--arg sha "${{ github.event.pull_request.head.sha }}" \
--argjson repoId '${{ github.event.repository.id }}' \
'{name: $name,
gitSource: {type: "github", repoId: $repoId, ref: $ref, sha: $sha}}' \
| curl -sS -f -X POST \
-H "Authorization: Bearer $VERCEL_TOKEN" \
-H "Content-Type: application/json" \
--data @- \
"$VERCEL_API/v13/deployments?teamId=$VERCEL_ORG_ID&forceNew=1&skipAutoDetectionConfirmation=1" \
| jq -r '"Triggered preview deployment: https://" + .url'
61 changes: 61 additions & 0 deletions .github/workflows/neon-preview-cleanup.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: Neon preview cleanup

# Tears down everything neon-preview-branch.yml created for a PR: the
# branch-scoped Vercel env var and the Neon branch itself.

on:
pull_request:
types: [closed]

concurrency:
group: neon-preview-${{ github.event.pull_request.number }}
cancel-in-progress: false

permissions:
contents: read

jobs:
cleanup:
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
env:
NEON_BRANCH: preview/pr-${{ github.event.pull_request.number }}
GIT_BRANCH: ${{ github.event.pull_request.head.ref }}
VERCEL_API: https://api.vercel.com
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_PROJECT_ID: ${{ vars.VERCEL_PROJECT_ID }}
VERCEL_ORG_ID: ${{ vars.VERCEL_ORG_ID }}

steps:
- name: Remove the branch-scoped Vercel env var
run: |
set -euo pipefail

ids=$(curl -sS -f \
-H "Authorization: Bearer $VERCEL_TOKEN" \
"$VERCEL_API/v10/projects/$VERCEL_PROJECT_ID/env?teamId=$VERCEL_ORG_ID&gitBranch=$GIT_BRANCH" \
| jq -r --arg b "$GIT_BRANCH" \
'.envs[]? | select(.key == "DATABASE_URL" and .gitBranch == $b) | .id')

if [ -z "$ids" ]; then
echo "No branch-scoped DATABASE_URL found for $GIT_BRANCH."
exit 0
fi

for id in $ids; do
curl -sS -f -X DELETE \
-H "Authorization: Bearer $VERCEL_TOKEN" \
"$VERCEL_API/v9/projects/$VERCEL_PROJECT_ID/env/$id?teamId=$VERCEL_ORG_ID" \
> /dev/null
echo "Deleted env var $id."
done

- name: Delete the Neon branch
# Best-effort teardown: a failure removing the Vercel env var must not
# strand the Neon branch, since that is the piece that costs storage.
if: always()
uses: neondatabase/delete-branch-action@v3.2.1
with:
project_id: ${{ vars.NEON_PROJECT_ID }}
branch: ${{ env.NEON_BRANCH }}
api_key: ${{ secrets.NEON_API_KEY }}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,6 @@ next-env.d.ts

# clerk configuration (can include secrets)
/.clerk/

# local claude code settings
/.claude/
Loading