chore(sandbox): promote snapshot 3336016022d8 #21
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }} |