mirror of
https://github.com/supabase/supabase.git
synced 2026-05-06 22:18:00 +08:00
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:
43
.github/workflows/authorize-vercel-deploys.yml
vendored
Normal file
43
.github/workflows/authorize-vercel-deploys.yml
vendored
Normal 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 }}
|
||||
@@ -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
6
pnpm-lock.yaml
generated
@@ -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:
|
||||
|
||||
145
scripts/authorizeVercelDeploys.ts
Normal file
145
scripts/authorizeVercelDeploys.ts
Normal 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)
|
||||
})
|
||||
Reference in New Issue
Block a user