Skip to content

Backend code quality: Convex query efficiency, Redis TTLs, safer types #408

Backend code quality: Convex query efficiency, Redis TTLs, safer types

Backend code quality: Convex query efficiency, Redis TTLs, safer types #408

name: Preview Deployment
on:
pull_request:
types: [opened, synchronize, reopened]
jobs:
deploy-preview:
name: Deploy Convex Preview
runs-on: ubuntu-latest
# Only run for PRs from the same repo (not forks)
if: github.event.pull_request.head.repo.full_name == github.repository
permissions:
pull-requests: write
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: "1.3.8"
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Deploy Convex Preview
id: convex-deploy
working-directory: apps/server
env:
CONVEX_DEPLOY_KEY: ${{ secrets.CONVEX_PREVIEW_DEPLOY_KEY }}
run: |
PR_NUM=${{ github.event.pull_request.number }}
echo "🚀 Deploying Convex preview for PR #${PR_NUM}..."
# First deploy creates the preview (social providers are optional,
# so this succeeds even without env vars set yet)
OUTPUT_FILE=$(mktemp)
bunx convex deploy --preview-create "pr-${PR_NUM}" 2>&1 | tee "$OUTPUT_FILE"
CONVEX_URL=$(grep -o 'https://[a-z0-9-]*\.convex\.cloud' "$OUTPUT_FILE" | head -1)
if [ -z "$CONVEX_URL" ]; then
echo "❌ Failed to extract Convex URL"
exit 1
fi
CONVEX_SITE_URL=$(echo "$CONVEX_URL" | sed 's/\.convex\.cloud/.convex.site/')
echo "✅ Convex Preview deployed!"
echo " Cloud URL: $CONVEX_URL"
echo " Site URL: $CONVEX_SITE_URL"
echo "convex_url=$CONVEX_URL" >> $GITHUB_OUTPUT
echo "convex_site_url=$CONVEX_SITE_URL" >> $GITHUB_OUTPUT
- name: Set Convex Environment Variables
working-directory: apps/server
env:
CONVEX_DEPLOY_KEY: ${{ secrets.CONVEX_PREVIEW_DEPLOY_KEY }}
BETTER_AUTH_SECRET: ${{ secrets.BETTER_AUTH_SECRET }}
REPO_OWNER: ${{ github.repository_owner }}
BRANCH_REF: ${{ github.event.pull_request.head.ref }}
run: |
set -euo pipefail
PR_NUM=${{ github.event.pull_request.number }}
PREVIEW_NAME="pr-${PR_NUM}"
BRANCH_SLUG=$(printf "%s" "$BRANCH_REF" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9-]+/-/g; s/^-+//; s/-+$//; s/-+/-/g')
PREVIEW_SITE_URL="https://osschat-web-git-${BRANCH_SLUG}-${REPO_OWNER}.vercel.app"
set_if_present() {
local key="$1"
local value="$2"
if [ -n "$value" ]; then
bunx convex env set "$key" "$value" --preview-name "$PREVIEW_NAME"
fi
}
set_if_present "SITE_URL" "$PREVIEW_SITE_URL"
set_if_present "ALLOWED_ORIGINS" "$PREVIEW_SITE_URL"
set_if_present "DEPLOYMENT_TYPE" "preview"
set_if_present "BETTER_AUTH_SECRET" "$BETTER_AUTH_SECRET"
set_if_present "PRODUCTION_CONVEX_SITE_URL" "https://outgoing-setter-201.convex.site"
echo "✅ Convex env vars set for ${PREVIEW_NAME} (${PREVIEW_SITE_URL})"
- name: Set Vercel Branch Env Vars
id: vercel-env
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
CONVEX_URL: ${{ steps.convex-deploy.outputs.convex_url }}
CONVEX_SITE_URL: ${{ steps.convex-deploy.outputs.convex_site_url }}
BRANCH_REF: ${{ github.event.pull_request.head.ref }}
run: |
set -euo pipefail
PROJECT_ID="prj_PydnhiRsYKJ4ASMIhsYZSJY5rqhT"
TEAM_ID="team_HuxyDiDenUAZ2TsD2llXhJZq"
API="https://api.vercel.com/v9/projects/${PROJECT_ID}/env?teamId=${TEAM_ID}"
upsert_env() {
local key="$1"
local value="$2"
local existing_id
existing_id=$(curl -s "https://api.vercel.com/v9/projects/${PROJECT_ID}/env?teamId=${TEAM_ID}&gitBranch=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$BRANCH_REF")" \
-H "Authorization: Bearer ${VERCEL_TOKEN}" | \
python3 -c "import sys,json; envs=json.load(sys.stdin).get('envs',[]); matches=[e['id'] for e in envs if e.get('key')=='$key' and e.get('gitBranch')=='$BRANCH_REF']; print(matches[0] if matches else '')" 2>/dev/null || true)
if [ -n "$existing_id" ]; then
curl -s -X PATCH "https://api.vercel.com/v9/projects/${PROJECT_ID}/env/${existing_id}?teamId=${TEAM_ID}" \
-H "Authorization: Bearer ${VERCEL_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"value\":\"${value}\"}" > /dev/null
echo " Updated ${key} (id=${existing_id})"
else
curl -s -X POST "$API" \
-H "Authorization: Bearer ${VERCEL_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"key\":\"${key}\",\"value\":\"${value}\",\"type\":\"plain\",\"target\":[\"preview\"],\"gitBranch\":\"${BRANCH_REF}\"}" > /dev/null
echo " Created ${key}"
fi
}
echo "Setting branch-scoped Vercel env vars for ${BRANCH_REF}..."
upsert_env "VITE_CONVEX_URL" "${CONVEX_URL}"
upsert_env "VITE_CONVEX_SITE_URL" "${CONVEX_SITE_URL}"
echo "✅ Vercel env vars set"
- name: Trigger Vercel Redeploy
id: vercel-redeploy
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
run: |
set -euo pipefail
PROJECT_ID="prj_PydnhiRsYKJ4ASMIhsYZSJY5rqhT"
TEAM_ID="team_HuxyDiDenUAZ2TsD2llXhJZq"
COMMIT_SHA="${{ github.event.pull_request.head.sha }}"
BRANCH_REF="${{ github.event.pull_request.head.ref }}"
LATEST_DEP=$(curl -s \
"https://api.vercel.com/v6/deployments?teamId=${TEAM_ID}&projectId=${PROJECT_ID}&limit=10" \
-H "Authorization: Bearer ${VERCEL_TOKEN}")
DEP_ID=$(echo "$LATEST_DEP" | python3 -c "
import sys,json
data = json.load(sys.stdin)
for d in data.get('deployments', []):
if d.get('meta',{}).get('githubCommitRef') == '$BRANCH_REF':
print(d['uid'])
break
" 2>/dev/null || true)
if [ -z "$DEP_ID" ]; then
echo "⚠️ No existing Vercel deployment found for branch ${BRANCH_REF}; Vercel will deploy automatically."
exit 0
fi
echo "Redeploying Vercel deployment ${DEP_ID}..."
REDEPLOY=$(curl -s -X POST \
"https://api.vercel.com/v13/deployments?teamId=${TEAM_ID}&forceNew=1" \
-H "Authorization: Bearer ${VERCEL_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"name\":\"osschat-web\",\"deploymentId\":\"${DEP_ID}\"}")
NEW_URL=$(echo "$REDEPLOY" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('url',''))" 2>/dev/null || true)
if [ -n "$NEW_URL" ]; then
echo "✅ Vercel redeploy triggered: https://${NEW_URL}"
echo "vercel_url=https://${NEW_URL}" >> $GITHUB_OUTPUT
else
echo "⚠️ Redeploy response: $(echo "$REDEPLOY" | head -c 500)"
fi
- name: Comment on PR
uses: actions/github-script@v7
with:
script: |
const prNumber = context.payload.pull_request.number;
const convexUrl = '${{ steps.convex-deploy.outputs.convex_url }}';
const convexSiteUrl = '${{ steps.convex-deploy.outputs.convex_site_url }}';
const vercelUrl = '${{ steps.vercel-redeploy.outputs.vercel_url }}';
const body = `## 🚀 Preview Deployment Ready
Vercel is rebuilding the frontend with the new Convex backend URL.
${vercelUrl ? `\n**Latest preview:** ${vercelUrl}` : '\nVercel will post the preview URL automatically.'}
### Convex Preview Backend
- Cloud URL: \`${convexUrl}\`
- Site URL: \`${convexSiteUrl}\`
> ℹ️ Preview deployments support email/password auth only (GitHub/Vercel OAuth disabled).
---
<sub>🤖 Deployed automatically by GitHub Actions</sub>`;
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
});
const botComment = comments.find(comment =>
comment.user.type === 'Bot' &&
comment.body.includes('Preview Deployment Ready')
);
if (botComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: body
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: body
});
}
- name: Summary
if: always()
run: |
PR_NUM=${{ github.event.pull_request.number }}
echo "## Preview Deployment Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Component | Status |" >> $GITHUB_STEP_SUMMARY
echo "|-----------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Convex Preview | ✅ Deployed |" >> $GITHUB_STEP_SUMMARY
echo "| Vercel Env Vars | ✅ Set (branch-scoped) |" >> $GITHUB_STEP_SUMMARY
echo "| Frontend (Vercel) | ✅ Redeploy triggered |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Convex URLs" >> $GITHUB_STEP_SUMMARY
echo "- Cloud: ${{ steps.convex-deploy.outputs.convex_url }}" >> $GITHUB_STEP_SUMMARY
echo "- Site: ${{ steps.convex-deploy.outputs.convex_site_url }}" >> $GITHUB_STEP_SUMMARY