CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history
version.ts
Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.
| 05cdb85 | 1 | /** |
| 2 | * /api/version — public build-info endpoint. | |
| 3 | * | |
| 4 | * Returns the running process's commit SHA, branch, boot time, and uptime | |
| 5 | * as a tiny JSON payload. Used by: | |
| 6 | * - The client-side auto-update banner (polls every 15s, prompts reload | |
| 7 | * when sha changes) | |
| 8 | * - Operators sanity-checking 'did my push actually deploy?' | |
| 9 | * - Monitoring (latency to seeing a new sha = end-to-end deploy time) | |
| 10 | * | |
| 11 | * Cache-control: no-store. Must be live, never cached. | |
| b1be050 | 12 | * |
| 13 | * Block S3 (2026-05-14): additively reports the 5 most recently applied | |
| 14 | * migrations from the live DB so the post-deploy smoke suite can verify | |
| 15 | * the latest drizzle/*.sql file actually landed in the running schema. | |
| 16 | * The migrations field is best-effort: if the DB query fails or the | |
| 17 | * connection isn't configured the field is omitted, never throws. | |
| 05cdb85 | 18 | */ |
| 19 | ||
| 20 | import { Hono } from "hono"; | |
| b1be050 | 21 | import { sql } from "drizzle-orm"; |
| 05cdb85 | 22 | import { getBuildInfo } from "../lib/build-info"; |
| b1be050 | 23 | import { db } from "../db"; |
| 05cdb85 | 24 | |
| 25 | const version = new Hono(); | |
| 26 | ||
| b1be050 | 27 | // In-process cache for the migrations list. /api/version is polled |
| 28 | // every 15s by the auto-update banner — re-querying _migrations on | |
| 29 | // every hit would be wasteful. 10s TTL is a reasonable compromise: a | |
| 30 | // fresh deploy's new migration will show up in the smoke check within | |
| 31 | // at most 10s of when migrate.ts inserted the row. | |
| 32 | const MIGRATIONS_CACHE_TTL_MS = 10_000; | |
| 33 | let _migrationsCache: { at: number; names: string[] } | null = null; | |
| 34 | ||
| 35 | async function readRecentMigrations(): Promise<string[] | null> { | |
| 36 | const now = Date.now(); | |
| 37 | if (_migrationsCache && now - _migrationsCache.at < MIGRATIONS_CACHE_TTL_MS) { | |
| 38 | return _migrationsCache.names; | |
| 39 | } | |
| 40 | try { | |
| 41 | // _migrations is created on first migration run; if the table doesn't | |
| 42 | // exist (very first boot before migrate.ts has ever run) we degrade | |
| 43 | // silently to an empty list. | |
| 44 | const rows = (await db.execute( | |
| 45 | sql`SELECT name FROM _migrations ORDER BY applied_at DESC LIMIT 5` | |
| 46 | )) as unknown as Array<{ name: string }>; | |
| 47 | const names = (rows ?? []).map((r) => r.name).filter(Boolean); | |
| 48 | _migrationsCache = { at: now, names }; | |
| 49 | return names; | |
| 50 | } catch { | |
| 51 | // _migrations table missing, DB down, etc. — return null so the | |
| 52 | // endpoint can omit the field without 500ing. | |
| 53 | return null; | |
| 54 | } | |
| 55 | } | |
| 56 | ||
| 57 | /** | |
| 58 | * Test seam: lets `src/__tests__/post-deploy-smoke.test.ts` reset the | |
| 59 | * cache between assertions. Not part of the public API. | |
| 60 | */ | |
| 61 | export const __test = { | |
| 62 | clearMigrationsCache: () => { | |
| 63 | _migrationsCache = null; | |
| 64 | }, | |
| 65 | }; | |
| 66 | ||
| 67 | version.get("/api/version", async (c) => { | |
| 68 | c.header("cache-control", "no-store, no-cache, must-revalidate"); | |
| 69 | c.header("pragma", "no-cache"); | |
| 70 | const build = getBuildInfo(); | |
| 71 | const migrations = await readRecentMigrations(); | |
| 72 | if (migrations !== null) { | |
| 73 | return c.json({ ...build, migrations }); | |
| 74 | } | |
| 75 | return c.json(build); | |
| 76 | }); | |
| 77 | ||
| 78 | // Block S2 — minimal `/version` alias used by the service-worker cache | |
| 79 | // bust + external uptime/deploy monitors. Kept additive (single new | |
| 80 | // handler, no edits to existing shape) so the S1+S3 smoke-suite agent's | |
| 81 | // edits to this file can land alongside without merge conflicts. | |
| 82 | // | |
| 83 | // S3 (2026-05-14): mirror the migrations field here so the smoke | |
| 84 | // suite can check either endpoint. | |
| 85 | version.get("/version", async (c) => { | |
| 05cdb85 | 86 | c.header("cache-control", "no-store, no-cache, must-revalidate"); |
| 87 | c.header("pragma", "no-cache"); | |
| b1be050 | 88 | const migrations = await readRecentMigrations(); |
| 89 | const body: Record<string, unknown> = { | |
| 90 | sha: process.env.BUILD_SHA || "dev", | |
| 91 | buildAt: process.env.BUILD_TIME || null, | |
| 92 | }; | |
| 93 | if (migrations !== null) body.migrations = migrations; | |
| 94 | return c.json(body); | |
| 05cdb85 | 95 | }); |
| 96 | ||
| 97 | export default version; |