feat: Add GH workflow for authorizing Vercel deploys (#41717)

* Add a script and call it from a GH action.

* Minor updates.

* Minor fixes.
This commit is contained in:
Ivan Vasilov
2026-01-08 13:11:05 +02:00
committed by GitHub
parent 4844e96dcf
commit cdadd74c9b
4 changed files with 199 additions and 2 deletions

View File

@@ -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 }}

View File

@@ -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",

6
pnpm-lock.yaml generated
View File

@@ -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:

View File

@@ -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<typeof jobInfoSchema>
async function fetchGitHubStatuses(sha: string): Promise<GitHubStatus[]> {
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<void> {
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)
})