deploy-history.ts73 lines · main
| 1 | import { desc, eq } from 'drizzle-orm'; |
| 2 | |
| 3 | import { newId } from '@briven/shared'; |
| 4 | |
| 5 | import { getDb } from '../db/client.js'; |
| 6 | import { deployHistory, type DeployHistoryEntry } from '../db/schema.js'; |
| 7 | import { log } from '../lib/logger.js'; |
| 8 | |
| 9 | /** |
| 10 | * Records one row in deploy_history per api boot. Best-effort: a failed |
| 11 | * insert is logged but never bubbles up — the api still serves requests |
| 12 | * if the audit-trail table is unreachable. Returns the inserted row id |
| 13 | * (or null on failure) so tests / callers can assert it ran. |
| 14 | */ |
| 15 | export async function recordDeploy(args: { |
| 16 | service: string; |
| 17 | buildSha: string; |
| 18 | buildAt: string | null; |
| 19 | env: string; |
| 20 | }): Promise<string | null> { |
| 21 | // "dev" is the explicit "we don't have build identity" sentinel from |
| 22 | // health.ts. Recording it would pollute the deploy timeline with noise |
| 23 | // on every local `bun dev` boot — skip the row in that case. |
| 24 | if (args.buildSha === 'dev') { |
| 25 | return null; |
| 26 | } |
| 27 | try { |
| 28 | const db = getDb(); |
| 29 | const id = newId('dh'); |
| 30 | await db.insert(deployHistory).values({ |
| 31 | id, |
| 32 | service: args.service, |
| 33 | buildSha: args.buildSha, |
| 34 | buildAt: args.buildAt, |
| 35 | env: args.env, |
| 36 | }); |
| 37 | log.info('deploy_history_recorded', { |
| 38 | service: args.service, |
| 39 | buildSha: args.buildSha.slice(0, 12), |
| 40 | env: args.env, |
| 41 | }); |
| 42 | return id; |
| 43 | } catch (err) { |
| 44 | log.warn('deploy_history_insert_failed', { |
| 45 | service: args.service, |
| 46 | buildSha: args.buildSha.slice(0, 12), |
| 47 | message: err instanceof Error ? err.message : String(err), |
| 48 | }); |
| 49 | return null; |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | /** |
| 54 | * Most-recent N deploys for the admin "Deploys" widget. Optionally |
| 55 | * filtered to one service (api / realtime / runtime) — when omitted we |
| 56 | * return the merged stream so the operator sees the actual rollout |
| 57 | * sequence across services. |
| 58 | */ |
| 59 | export async function listDeploys( |
| 60 | args: { service?: string; limit?: number } = {}, |
| 61 | ): Promise<DeployHistoryEntry[]> { |
| 62 | const db = getDb(); |
| 63 | const limit = Math.min(Math.max(args.limit ?? 50, 1), 500); |
| 64 | if (args.service) { |
| 65 | return db |
| 66 | .select() |
| 67 | .from(deployHistory) |
| 68 | .where(eq(deployHistory.service, args.service)) |
| 69 | .orderBy(desc(deployHistory.bootedAt)) |
| 70 | .limit(limit); |
| 71 | } |
| 72 | return db.select().from(deployHistory).orderBy(desc(deployHistory.bootedAt)).limit(limit); |
| 73 | } |