mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-14 08:53:11 +00:00
feat: db:refresh:stage — weekly Neon branch refresh + secret swap (human only)
This commit is contained in:
parent
a5dc0ea5fb
commit
3120d0b8fa
2 changed files with 107 additions and 0 deletions
|
|
@ -31,6 +31,7 @@
|
|||
"db:seed:consent": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm --import=./scripts/seed-loader.mjs\" node scripts/seed-consent.mjs",
|
||||
"db:seed:projecao": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm --import=./scripts/seed-loader.mjs\" node scripts/import-projecao.mjs",
|
||||
"db:seed:minimal": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm --import=./scripts/seed-loader.mjs\" node scripts/seed-minimal.mjs",
|
||||
"db:refresh:stage": "node scripts/refresh-stage.mjs",
|
||||
"build:demographics": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" node scripts/build-bahia-demographics.mjs",
|
||||
"build:election-aggregates": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm --import=./scripts/seed-loader.mjs\" node scripts/build-election-aggregates.mjs",
|
||||
"build:geometries": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" node scripts/build-bahia-geometries.mjs",
|
||||
|
|
|
|||
106
scripts/refresh-stage.mjs
Normal file
106
scripts/refresh-stage.mjs
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
/**
|
||||
* 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)
|
||||
Loading…
Reference in a new issue