Skip to content

chore(sandbox): promote snapshot 3336016022d8 #21

chore(sandbox): promote snapshot 3336016022d8

chore(sandbox): promote snapshot 3336016022d8 #21

Workflow file for this run

name: Production Release
on:
pull_request:
paths:
- infra/supabase/migrations/**
- packages/db/src/schema/**
- packages/db/drizzle/**
- scripts/migrate.ts
- scripts/migration-drizzle.ts
- scripts/database-operation-safety.ts
- scripts/archive-audit-log.ts
- scripts/audit-archive-options.ts
- scripts/audit-archive-storage.ts
- packages/db/src/drizzle-migrations.ts
- packages/db/src/supabase-target.ts
- packages/env/src/migrate.ts
- .github/workflows/db-migrate.yml
- .github/workflows/deploy-workers.yml
- apps/web/vercel.json
workflow_dispatch:
inputs:
confirmation:
description: Type RELEASE_PRODUCTION to release the exact main-branch commit.
required: true
type: string
permissions:
actions: read
contents: read
# One lock covers the schema apply and every dependent deployment, so production
# releases cannot overlap.
concurrency:
group: ${{ github.event_name == 'workflow_dispatch' && 'production-release' || format('db-migrate-pr-{0}', github.event.pull_request.number) }}
cancel-in-progress: false
jobs:
diff:
if: github.event_name == 'pull_request'
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 10.34.5
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
# drizzle.config.ts imports @cheatcode/env/migrate from its built output.
- run: pnpm turbo build --filter=@cheatcode/db^...
# Generation never connects. The placeholder only satisfies eager env parsing.
- run: pnpm --filter @cheatcode/db db:generate
env:
SUPABASE_MIGRATION_URL: postgres://placeholder@localhost:5432/placeholder
- name: Fail if Drizzle migrations drifted
run: git diff --exit-code packages/db/drizzle
preflight:
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-24.04
timeout-minutes: 10
environment: production
steps:
- name: Authorize production release
env:
CONFIRMATION: ${{ inputs.confirmation }}
run: |
if [ "$GITHUB_REF" != "refs/heads/main" ]; then
echo "Production releases must be dispatched from main." >&2
exit 1
fi
if [ "$CONFIRMATION" != "RELEASE_PRODUCTION" ]; then
echo "Confirmation must exactly match RELEASE_PRODUCTION." >&2
exit 1
fi
- name: Preflight production configuration
env:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
SUPABASE_MIGRATION_EXPECTED_DATABASE: ${{ vars.SUPABASE_MIGRATION_EXPECTED_DATABASE }}
SUPABASE_MIGRATION_EXPECTED_HOST: ${{ vars.SUPABASE_MIGRATION_EXPECTED_HOST }}
SUPABASE_MIGRATION_EXPECTED_ROLE: ${{ vars.SUPABASE_MIGRATION_EXPECTED_ROLE }}
SUPABASE_MIGRATION_EXPECTED_SYSTEM_IDENTIFIER: ${{ vars.SUPABASE_MIGRATION_EXPECTED_SYSTEM_IDENTIFIER }}
SUPABASE_MIGRATION_URL: ${{ secrets.SUPABASE_MIGRATION_URL }}
VERCEL_ORG_ID: ${{ vars.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ vars.VERCEL_PROJECT_ID }}
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
run: |
required=(
CLOUDFLARE_ACCOUNT_ID
CLOUDFLARE_API_TOKEN
SUPABASE_MIGRATION_EXPECTED_DATABASE
SUPABASE_MIGRATION_EXPECTED_HOST
SUPABASE_MIGRATION_EXPECTED_ROLE
SUPABASE_MIGRATION_EXPECTED_SYSTEM_IDENTIFIER
SUPABASE_MIGRATION_URL
VERCEL_ORG_ID
VERCEL_PROJECT_ID
VERCEL_TOKEN
)
missing=()
for name in "${required[@]}"; do
if [ -z "${!name}" ]; then
missing+=("$name")
fi
done
if [ "${#missing[@]}" -gt 0 ]; then
printf 'Missing production configuration: %s\n' "${missing[*]}" >&2
exit 1
fi
- name: Require successful static checks for this commit
env:
GH_TOKEN: ${{ github.token }}
run: |
successful_runs="$(gh api --method GET \
"repos/${GITHUB_REPOSITORY}/actions/workflows/static-checks.yml/runs" \
--field branch=main \
--field event=push \
--field head_sha="$GITHUB_SHA" \
--field per_page=1 \
--field status=success \
--jq '.total_count')"
if [ "$successful_runs" -lt 1 ]; then
echo "Static Checks has not succeeded for $GITHUB_SHA on main." >&2
exit 1
fi
prepare-frontend:
needs: preflight
if: needs.preflight.result == 'success'
runs-on: ubuntu-24.04
timeout-minutes: 45
environment: production
outputs:
deployment_url: ${{ steps.deploy.outputs.url }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ github.sha }}
- name: Verify release commit
env:
RELEASE_SHA: ${{ github.sha }}
run: test "$(git rev-parse HEAD)" = "$RELEASE_SHA"
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 10.34.5
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Pull Vercel production configuration
working-directory: apps/web
env:
VERCEL_ORG_ID: ${{ vars.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ vars.VERCEL_PROJECT_ID }}
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
run: |
test -n "$VERCEL_ORG_ID"
test -n "$VERCEL_PROJECT_ID"
test -n "$VERCEL_TOKEN"
pnpm exec vercel pull --yes --environment=production --token="$VERCEL_TOKEN"
- name: Build immutable Vercel artifact
working-directory: apps/web
env:
NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA: ${{ github.sha }}
VERCEL_ORG_ID: ${{ vars.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ vars.VERCEL_PROJECT_ID }}
run: pnpm exec vercel build --prod
# Create a production-target deployment without assigning production
# domains. This proves the exact artifact before any database or backend
# mutation; the later promote step is the only frontend traffic cutover.
- name: Stage exact frontend release
id: deploy
working-directory: apps/web
env:
VERCEL_ORG_ID: ${{ vars.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ vars.VERCEL_PROJECT_ID }}
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
run: |
deployment_json="$(pnpm exec vercel deploy \
--archive=tgz \
--format=json \
--prebuilt \
--prod \
--skip-domain \
--yes \
--token="$VERCEL_TOKEN")"
deployment_url="$(jq --exit-status --raw-output \
'select(.target == "production" and .readyState == "READY") |
.url | select(type == "string")' <<<"$deployment_json")"
deployment_url="${deployment_url%/}"
if [[ ! "$deployment_url" =~ ^https://[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*\.vercel\.app$ ]]; then
echo "Vercel did not return a trusted deployment URL." >&2
exit 1
fi
echo "url=$deployment_url" >> "$GITHUB_OUTPUT"
- name: Verify staged frontend release
working-directory: apps/web
env:
DEPLOYMENT_URL: ${{ steps.deploy.outputs.url }}
RELEASE_SHA: ${{ github.sha }}
VERCEL_ORG_ID: ${{ vars.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ vars.VERCEL_PROJECT_ID }}
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
run: |
readonly MAX_ATTEMPTS=24
readonly POLL_INTERVAL_SECONDS=5
if [[ ! "$DEPLOYMENT_URL" =~ ^https://[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*\.vercel\.app$ ]]; then
echo "Refusing to query an untrusted Vercel deployment origin." >&2
exit 1
fi
for ((attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1)); do
if response="$(pnpm exec vercel curl /api/health \
--deployment "$DEPLOYMENT_URL" \
--yes \
--token="$VERCEL_TOKEN" \
-- \
--fail-with-body \
--silent \
--show-error \
--connect-timeout 5 \
--max-time 15 \
--header "Accept: application/json" \
--header "Cache-Control: no-cache")" &&
jq --exit-status --arg sha "$RELEASE_SHA" '
.ok == true and .service == "web" and .releaseSha == $sha
' <<<"$response" > /dev/null 2>&1; then
echo "Staged Vercel deployment reports release $RELEASE_SHA."
exit 0
fi
echo "Staged Vercel deployment has not converged to $RELEASE_SHA (attempt $attempt/$MAX_ATTEMPTS)."
if [ "$attempt" -lt "$MAX_ATTEMPTS" ]; then
sleep "$POLL_INTERVAL_SECONDS"
fi
done
echo "Staged Vercel deployment did not converge to $RELEASE_SHA within the release window." >&2
exit 1
apply-pre-deploy:
needs: prepare-frontend
if: needs.prepare-frontend.result == 'success'
runs-on: ubuntu-24.04
timeout-minutes: 30
environment: production
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ github.sha }}
- name: Verify release commit
env:
RELEASE_SHA: ${{ github.sha }}
run: test "$(git rev-parse HEAD)" = "$RELEASE_SHA"
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 10.34.5
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm turbo build --filter=@cheatcode/db^...
- name: Show pre-deploy migration plan
run: pnpm tsx scripts/migrate.ts --dry-run --phase=pre-deploy
env:
SUPABASE_MIGRATION_EXPECTED_DATABASE: ${{ vars.SUPABASE_MIGRATION_EXPECTED_DATABASE }}
SUPABASE_MIGRATION_URL: ${{ secrets.SUPABASE_MIGRATION_URL }}
SUPABASE_MIGRATION_EXPECTED_HOST: ${{ vars.SUPABASE_MIGRATION_EXPECTED_HOST }}
SUPABASE_MIGRATION_EXPECTED_ROLE: ${{ vars.SUPABASE_MIGRATION_EXPECTED_ROLE }}
SUPABASE_MIGRATION_EXPECTED_SYSTEM_IDENTIFIER: ${{ vars.SUPABASE_MIGRATION_EXPECTED_SYSTEM_IDENTIFIER }}
- name: Apply pre-deploy migrations
env:
SUPABASE_MIGRATION_EXPECTED_DATABASE: ${{ vars.SUPABASE_MIGRATION_EXPECTED_DATABASE }}
SUPABASE_MIGRATION_EXPECTED_HOST: ${{ vars.SUPABASE_MIGRATION_EXPECTED_HOST }}
SUPABASE_MIGRATION_EXPECTED_ROLE: ${{ vars.SUPABASE_MIGRATION_EXPECTED_ROLE }}
SUPABASE_MIGRATION_EXPECTED_SYSTEM_IDENTIFIER: ${{ vars.SUPABASE_MIGRATION_EXPECTED_SYSTEM_IDENTIFIER }}
SUPABASE_MIGRATION_URL: ${{ secrets.SUPABASE_MIGRATION_URL }}
run: pnpm tsx scripts/migrate.ts --apply --phase=pre-deploy
deploy-backend:
needs: [apply-pre-deploy, prepare-frontend]
if: needs['apply-pre-deploy'].result == 'success' && needs.prepare-frontend.result == 'success'
uses: ./.github/workflows/deploy-workers.yml
with:
confirmation: ${{ inputs.confirmation }}
deploy-frontend:
needs: [prepare-frontend, deploy-backend]
if: needs.prepare-frontend.result == 'success' && needs.deploy-backend.result == 'success'
runs-on: ubuntu-24.04
timeout-minutes: 30
environment: production
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ github.sha }}
- name: Verify release commit
env:
RELEASE_SHA: ${{ github.sha }}
run: test "$(git rev-parse HEAD)" = "$RELEASE_SHA"
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 10.34.5
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Pull Vercel project configuration
working-directory: apps/web
env:
VERCEL_ORG_ID: ${{ vars.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ vars.VERCEL_PROJECT_ID }}
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
run: |
test -n "$VERCEL_ORG_ID"
test -n "$VERCEL_PROJECT_ID"
test -n "$VERCEL_TOKEN"
pnpm exec vercel pull --yes --environment=production --token="$VERCEL_TOKEN"
- name: Promote verified frontend release
working-directory: apps/web
env:
DEPLOYMENT_URL: ${{ needs.prepare-frontend.outputs.deployment_url }}
VERCEL_ORG_ID: ${{ vars.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ vars.VERCEL_PROJECT_ID }}
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
run: |
if [[ ! "$DEPLOYMENT_URL" =~ ^https://[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*\.vercel\.app$ ]]; then
echo "Refusing to promote an untrusted Vercel deployment URL." >&2
exit 1
fi
pnpm exec vercel promote "$DEPLOYMENT_URL" \
--yes \
--token="$VERCEL_TOKEN"
- name: Verify production frontend release
working-directory: apps/web
env:
PRODUCTION_URL: ${{ vars.VERCEL_PRODUCTION_URL || 'https://trycheatcode.com' }}
RELEASE_SHA: ${{ github.sha }}
run: |
readonly MAX_ATTEMPTS=24
readonly POLL_INTERVAL_SECONDS=5
production_origin="${PRODUCTION_URL%/}"
if [[ ! "$production_origin" =~ ^https://([A-Za-z0-9-]+\.)*trycheatcode\.com$ ]]; then
echo "Refusing to query an untrusted production origin." >&2
exit 1
fi
curl_options=(
--fail-with-body
--silent
--show-error
--connect-timeout 5
--max-time 15
--header "Accept: application/json"
--header "Cache-Control: no-cache"
)
for ((attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1)); do
if response="$(curl "${curl_options[@]}" "$production_origin/api/health")" &&
jq --exit-status --arg sha "$RELEASE_SHA" '
.ok == true and .service == "web" and .releaseSha == $sha
' <<<"$response" > /dev/null 2>&1; then
echo "Vercel production reports release $RELEASE_SHA."
exit 0
fi
echo "Vercel production has not converged to $RELEASE_SHA (attempt $attempt/$MAX_ATTEMPTS)."
if [ "$attempt" -lt "$MAX_ATTEMPTS" ]; then
sleep "$POLL_INTERVAL_SECONDS"
fi
done
echo "Vercel production did not converge to $RELEASE_SHA within the release window." >&2
exit 1
- name: Record frontend release
env:
DEPLOYMENT_URL: ${{ needs.prepare-frontend.outputs.deployment_url }}
RELEASE_SHA: ${{ github.sha }}
run: |
{
echo "### Vercel frontend release"
echo "- Commit: \`$RELEASE_SHA\`"
echo "- Deployment: $DEPLOYMENT_URL"
} >> "$GITHUB_STEP_SUMMARY"
post-deploy-migrations:
needs: deploy-frontend
if: needs.deploy-frontend.result == 'success'
runs-on: ubuntu-24.04
timeout-minutes: 30
environment: production
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ github.sha }}
- name: Verify release commit
env:
RELEASE_SHA: ${{ github.sha }}
run: test "$(git rev-parse HEAD)" = "$RELEASE_SHA"
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 10.34.5
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm turbo build --filter=@cheatcode/db^...
- name: Show post-deploy migration plan
run: pnpm tsx scripts/migrate.ts --dry-run --phase=post-deploy
env:
SUPABASE_MIGRATION_EXPECTED_DATABASE: ${{ vars.SUPABASE_MIGRATION_EXPECTED_DATABASE }}
SUPABASE_MIGRATION_URL: ${{ secrets.SUPABASE_MIGRATION_URL }}
SUPABASE_MIGRATION_EXPECTED_HOST: ${{ vars.SUPABASE_MIGRATION_EXPECTED_HOST }}
SUPABASE_MIGRATION_EXPECTED_ROLE: ${{ vars.SUPABASE_MIGRATION_EXPECTED_ROLE }}
SUPABASE_MIGRATION_EXPECTED_SYSTEM_IDENTIFIER: ${{ vars.SUPABASE_MIGRATION_EXPECTED_SYSTEM_IDENTIFIER }}
- name: Apply post-deploy migrations
run: pnpm tsx scripts/migrate.ts --apply --phase=post-deploy
env:
SUPABASE_MIGRATION_EXPECTED_DATABASE: ${{ vars.SUPABASE_MIGRATION_EXPECTED_DATABASE }}
SUPABASE_MIGRATION_EXPECTED_HOST: ${{ vars.SUPABASE_MIGRATION_EXPECTED_HOST }}
SUPABASE_MIGRATION_EXPECTED_ROLE: ${{ vars.SUPABASE_MIGRATION_EXPECTED_ROLE }}
SUPABASE_MIGRATION_EXPECTED_SYSTEM_IDENTIFIER: ${{ vars.SUPABASE_MIGRATION_EXPECTED_SYSTEM_IDENTIFIER }}
SUPABASE_MIGRATION_URL: ${{ secrets.SUPABASE_MIGRATION_URL }}