From cdadd74c9bbb1954629ffb58bdc6f8351b3e49e8 Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Thu, 8 Jan 2026 13:11:05 +0200 Subject: [PATCH] feat: Add GH workflow for authorizing Vercel deploys (#41717) * Add a script and call it from a GH action. * Minor updates. * Minor fixes. --- .../workflows/authorize-vercel-deploys.yml | 43 ++++++ package.json | 7 +- pnpm-lock.yaml | 6 + scripts/authorizeVercelDeploys.ts | 145 ++++++++++++++++++ 4 files changed, 199 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/authorize-vercel-deploys.yml create mode 100644 scripts/authorizeVercelDeploys.ts diff --git a/.github/workflows/authorize-vercel-deploys.yml b/.github/workflows/authorize-vercel-deploys.yml new file mode 100644 index 00000000000..4e14c277382 --- /dev/null +++ b/.github/workflows/authorize-vercel-deploys.yml @@ -0,0 +1,43 @@ +name: Authorize Vercel Deploys + +on: + pull_request: + branches: + - 'master' + +# Cancel old builds on new commit for same workflow + branch/PR +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + authorize-vercel-deploys: + runs-on: blacksmith-4vcpu-ubuntu-2404 + + steps: + - name: Check out repo + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + # fetch only the root files and scripts folder + sparse-checkout: | + scripts + - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 + name: Install pnpm + with: + run_install: false + - name: Setup node + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version-file: '.nvmrc' + cache: 'pnpm' + - name: Download dependencies + run: | + pnpm install --frozen-lockfile + - name: Authorize Vercel Deploys + run: |- + pnpm run authorize-vercel-deploys + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} diff --git a/package.json b/package.json index a35f01b65ab..b266c2bf463 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,8 @@ "setup:cli": "supabase start -x studio && supabase status --output json > keys.json && node scripts/generateLocalEnv.js", "generate:types": "supabase gen types typescript --local > ./supabase/functions/common/database-types.ts", "api:codegen": "cd packages/api-types && pnpm run codegen", - "knip": "pnpx knip@~5.50.0" + "knip": "pnpx knip@~5.50.0", + "authorize-vercel-deploys": "tsx scripts/authorizeVercelDeploys.ts" }, "devDependencies": { "@aws-sdk/client-secrets-manager": "^3.823.0", @@ -52,8 +53,10 @@ "supabase": "^2.65.6", "supports-color": "^8.0.0", "tailwindcss": "catalog:", + "tsx": "catalog:", "turbo": "2.3.3", - "typescript": "catalog:" + "typescript": "catalog:", + "zod": "catalog:" }, "repository": { "type": "git", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 45c68ff3c67..0aa8037bbbc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -112,12 +112,18 @@ importers: tailwindcss: specifier: 'catalog:' version: 3.4.1(ts-node@10.9.2(@types/node@22.13.14)(typescript@5.9.2)) + tsx: + specifier: 'catalog:' + version: 4.20.3 turbo: specifier: 2.3.3 version: 2.3.3 typescript: specifier: 'catalog:' version: 5.9.2 + zod: + specifier: 'catalog:' + version: 3.25.76 apps/cms: dependencies: diff --git a/scripts/authorizeVercelDeploys.ts b/scripts/authorizeVercelDeploys.ts new file mode 100644 index 00000000000..9aabfb72745 --- /dev/null +++ b/scripts/authorizeVercelDeploys.ts @@ -0,0 +1,145 @@ +/** + * Script to authorize Vercel deployments from PR information. + * + * Gets the current SHA from environment variable, fetches GitHub statuses, + * finds authorization-required statuses, and authorizes them via Vercel API. + */ + +import { z } from 'zod' + +interface GitHubStatus { + url: string + avatar_url: string + id: number + node_id: string + state: 'success' | 'pending' | 'failure' | 'error' + description: string + target_url: string + context: string + created_at: string + updated_at: string +} + +const jobInfoSchema = z.object({ + job: z.object({ + headInfo: z.object({ + sha: z.string().min(1, 'SHA is required'), + }), + org: z.literal('supabase'), + prId: z.number().int().positive('PR ID must be a positive integer'), + repo: z.literal('supabase'), + }), +}) + +type JobInfo = z.infer + +async function fetchGitHubStatuses(sha: string): Promise { + const url = `https://api.github.com/repos/supabase/supabase/statuses/${sha}` + console.log(`Fetching GitHub statuses for SHA: ${sha}`) + + const response = await fetch(url) + if (!response.ok) { + throw new Error(`Failed to fetch GitHub statuses: ${response.status} ${response.statusText}`) + } + + return response.json() +} + +function extractJobInfoFromTargetUrl(targetUrl: string): JobInfo { + const url = new URL(targetUrl) + const jobParam = url.searchParams.get('job') + + if (!jobParam) { + throw new Error('No job parameter found in target URL') + } + + try { + const jobData = JSON.parse(jobParam) + const parsed = jobInfoSchema.parse({ job: jobData }) + return parsed + } catch (e) { + if (e instanceof z.ZodError) { + throw new Error( + `Invalid job info structure: ${e.errors.map((err) => `${err.path.join('.')}: ${err.message}`).join(', ')}` + ) + } + throw new Error(`Failed to parse job parameter as JSON: ${e}`) + } +} + +async function authorizeVercelJob(jobInfo: JobInfo, vercelToken: string): Promise { + const url = 'https://vercel.com/api/v1/integrations/authorize-job' + + const response = await fetch(url, { + method: 'POST', + headers: { + Accept: '*/*', + 'Content-Type': 'application/json; charset=utf-8', + Authorization: `Bearer ${vercelToken}`, + }, + body: JSON.stringify(jobInfo), + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error( + `Failed to authorize Vercel job: ${response.status} ${response.statusText}\n${errorText}` + ) + } + + console.log('āœ“ Vercel job authorized successfully!') +} + +async function main() { + const sha = process.env.GITHUB_SHA + if (!sha) { + throw new Error('GITHUB_SHA environment variable is required') + } + + const vercelToken = process.env.VERCEL_TOKEN + if (!vercelToken) { + throw new Error('VERCEL_TOKEN environment variable is required') + } + + console.log(`Starting authorization process for SHA: ${sha}`) + + // Fetch GitHub statuses + const statuses = await fetchGitHubStatuses(sha) + console.log(`Found ${statuses.length} statuses`) + + // Filter for authorization-required statuses + const authRequiredStatuses = statuses.filter( + (status) => status.description === 'Authorization required to deploy.' + ) + + if (authRequiredStatuses.length === 0) { + console.log('No authorization-required statuses found. Nothing to authorize.') + return + } + + console.log(`Found ${authRequiredStatuses.length} authorization-required status(es)`) + + // Process each authorization-required status + for (const status of authRequiredStatuses) { + try { + console.log(`\nProcessing status: ${status.context}`) + console.log(`Target URL: ${status.target_url}`) + + // Extract job info from target URL + const jobInfo = extractJobInfoFromTargetUrl(status.target_url) + + // Authorize the job + await authorizeVercelJob(jobInfo, vercelToken) + } catch (error) { + console.error(`Failed to process status ${status.context}:`, error) + // Continue with other statuses even if one fails + } + } + + console.log('\nāœ“ Authorization process completed!') +} + +main().catch((error) => { + console.error('Fatal error:', error) + process.exit(1) +})