mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
106 lines
4.1 KiB
JavaScript
106 lines
4.1 KiB
JavaScript
/**
|
|
* Refresh the stage database: create a NEW Neon branch from the current prod
|
|
* snapshot and swap the `STAGE_DATABASE_URL` secret in the GitHub `stage`
|
|
* Environment. Human / `workflow_dispatch` only — agents must never run this
|
|
* (it touches the prod Neon project).
|
|
*
|
|
* Refresh is "new branch + swap secret", never DELETE/INSERT of real data:
|
|
* Neon branches are copy-on-write snapshots of prod at creation time.
|
|
*
|
|
* Requirements:
|
|
* NEON_API_KEY — Neon API key with access to the prod project (get one at
|
|
* https://console.neon.tech/app/settings/api-keys). Never
|
|
* committed; export it in your shell or pass via env.
|
|
* gh — authenticated GitHub CLI (updates the secret).
|
|
*
|
|
* Usage:
|
|
* NEON_API_KEY=… pnpm db:refresh:stage # refresh + swap secret
|
|
* NEON_API_KEY=… pnpm db:refresh:stage -- --delete-old # also delete the previous stage branch
|
|
* NEON_API_KEY=… node scripts/refresh-stage.mjs --dry-run
|
|
*/
|
|
|
|
import { dieWithLabel } from './lib/cli.mjs'
|
|
|
|
const LABEL = 'refresh:stage'
|
|
const die = dieWithLabel(LABEL)
|
|
|
|
const PROJECT_ID = process.env.NEON_PROJECT_ID ?? 'lively-math-34249863'
|
|
const DATABASE_NAME = 'neondb'
|
|
const ROLE_NAME = 'neondb_owner'
|
|
const API = 'https://console.neon.tech/api/v2'
|
|
|
|
const args = new Set(process.argv.slice(2))
|
|
const DRY_RUN = args.has('--dry-run')
|
|
const DELETE_OLD = args.has('--delete-old')
|
|
|
|
const apiKey = process.env.NEON_API_KEY
|
|
if (!apiKey) {
|
|
die(
|
|
'NEON_API_KEY is not set. This script is human-only: it creates a branch on the PROD Neon ' +
|
|
'project and swaps the stage secret. Export a Neon API key and re-run.',
|
|
)
|
|
}
|
|
|
|
const request = async (method, path, body) => {
|
|
const response = await fetch(`${API}${path}`, {
|
|
method,
|
|
headers: { authorization: `Bearer ${apiKey}`, 'content-type': 'application/json' },
|
|
...(body ? { body: JSON.stringify(body) } : {}),
|
|
})
|
|
if (!response.ok) {
|
|
die(`Neon API ${method} ${path} → ${response.status}: ${await response.text()}`)
|
|
}
|
|
return response.json()
|
|
}
|
|
|
|
const log = (message) => console.log(`[${LABEL}] ${message}`)
|
|
|
|
// 1. Find the prod default branch (the snapshot source) and the current stage branch.
|
|
const { branches } = await request('GET', `/projects/${PROJECT_ID}/branches`)
|
|
const prodBranch = branches.find((branch) => branch.default)
|
|
if (!prodBranch) die(`No default branch found on project ${PROJECT_ID}.`)
|
|
const oldStage = branches.find((branch) => branch.name === 'stage')
|
|
log(`prod branch: ${prodBranch.id} (${prodBranch.name})`)
|
|
if (oldStage) log(`current stage branch: ${oldStage.id}`)
|
|
|
|
if (DRY_RUN) {
|
|
log('dry-run — would create a new `stage` branch from prod and swap STAGE_DATABASE_URL.')
|
|
process.exit(0)
|
|
}
|
|
|
|
// 2. Replace the old stage branch with a fresh one from prod (delete first so
|
|
// the name is free; the old branch is a stale snapshot, not data we keep).
|
|
if (oldStage) {
|
|
await request('DELETE', `/projects/${PROJECT_ID}/branches/${oldStage.id}`)
|
|
log(`deleted old stage branch ${oldStage.id}`)
|
|
}
|
|
|
|
const timestamp = new Date().toISOString().slice(0, 10)
|
|
const created = await request('POST', `/projects/${PROJECT_ID}/branches`, {
|
|
branch: { name: 'stage', parent_id: prodBranch.id },
|
|
endpoints: [{ type: 'read_write' }],
|
|
})
|
|
const stageBranch = created.branch
|
|
log(`created stage branch ${stageBranch.id} from prod snapshot (${timestamp})`)
|
|
|
|
// 3. Resolve the new pooled connection string.
|
|
const { uri } = await request(
|
|
'GET',
|
|
`/projects/${PROJECT_ID}/connection_uri?branch_id=${stageBranch.id}` +
|
|
`&database_name=${DATABASE_NAME}&role_name=${ROLE_NAME}&pooled=true`,
|
|
)
|
|
log(`new stage connection string: ${uri.replace(/:\/\/[^@]*@/, '://***@')}`)
|
|
|
|
// 4. Swap the GitHub secret in the `stage` Environment.
|
|
const { execFileSync } = await import('node:child_process')
|
|
execFileSync('gh', ['secret', 'set', 'STAGE_DATABASE_URL', '--env', 'stage', '--body', uri], {
|
|
stdio: 'inherit',
|
|
})
|
|
log('STAGE_DATABASE_URL updated in the GitHub `stage` environment.')
|
|
|
|
if (DELETE_OLD && oldStage) {
|
|
log('note: --delete-old is a no-op now — the old branch was already replaced in step 2.')
|
|
}
|
|
|
|
log('OK — stage database refreshed. Next ci-stage run uses the new snapshot.')
|
|
process.exit(0)
|