ReviewOS

also looking at this

loghqorg/loghq

Promote main to production: signed-out chrome and the subscriber tables

#17
Merged glennmichael123 wants to merge main into production
54 files +4252 -1353

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

scripts/smoke-deployment.tsmodified+86-6
Changes to scripts/smoke-deployment.ts
@@ -4,15 +4,17 @@ import { resolve } from 'node:path'
44interface SmokeContract {
55 domain: string
66 homeText: string
7 /** Whether POST /api/email/subscribe is registered in that app. */
8 subscribe: boolean
79}
810
911const CONTRACTS: Record<string, SmokeContract> = {
10 analyticshq: { domain: 'analyticshq.org', homeText: 'analyticshq' },
11 bughq: { domain: 'bughq.org', homeText: 'Error tracking for people who ship.' },
12 commshq: { domain: 'commshq.org', homeText: 'Grow an audience worth knowing.' },
13 loghq: { domain: 'loghq.org', homeText: 'Your logs, finally worth reading.' },
14 reportshq: { domain: 'reportshq.org', homeText: 'Reports that build themselves' },
15 status: { domain: 'statushq.org', homeText: 'Know the moment' },
12 analyticshq: { domain: 'analyticshq.org', homeText: 'analyticshq', subscribe: true },
13 bughq: { domain: 'bughq.org', homeText: 'Error tracking for people who ship.', subscribe: false },
14 commshq: { domain: 'commshq.org', homeText: 'Grow an audience worth knowing.', subscribe: true },
15 loghq: { domain: 'loghq.org', homeText: 'Your logs, finally worth reading.', subscribe: true },
16 reportshq: { domain: 'reportshq.org', homeText: 'Reports that build themselves', subscribe: true },
17 status: { domain: 'statushq.org', homeText: 'Know the moment', subscribe: true },
1618}
1719
1820const projectRoot = resolve(import.meta.dir, '..')
@@ -89,6 +91,84 @@ for (const [label, body] of [['homepage', home], ['login', login]] as const) {
8991 }
9092}
9193
94/*
95 * The newsletter endpoint, which is really a database read wearing a form.
96 *
97 * Added because two sites served a 500 here for an unknown length of time and
98 * every gate stayed green: loghq had never created the `subscribers` table at
99 * all, and reportshq deploys had never run a migration. The checks above cannot
100 * see it. They read the homepage, the login page and robots.txt, and all three
101 * were perfect on both sites throughout.
102 *
103 * THE PAYLOAD IS THE WHOLE POINT, and an invalid one would be useless here.
104 * The framework action validates before it queries:
105 *
106 * if (!email || !email.includes('@')) return { success: false, ... }
107 *
108 * so an empty body returns 200 WITHOUT TOUCHING THE DATABASE. Measured against
109 * the two broken sites while they were broken: `{}` gave 200 and a real address
110 * gave 500. A smoke check built on the polite empty payload would have passed
111 * on both.
112 *
113 * So it sends a real address, and a FIXED one. The action looks the address up
114 * before it writes and returns "Already subscribed" when it finds it, so the
115 * first run after a database is created writes one row and every run after that
116 * is a pure read of the table this is here to prove exists. A fresh address per
117 * run would add a row per deploy forever.
118 *
119 * `.invalid` is reserved by RFC 2606 and resolves nowhere, so the row can never
120 * become mail to a real person even if someone later points a campaign at the
121 * subscriber list.
122 */
123const SMOKE_SUBSCRIBER = 'smoke-probe@example.invalid'
124
125async function assertSubscribeWorks(): Promise<void> {
126 let lastFailure = 'no response'
127 for (let attempt = 1; attempt <= attempts; attempt += 1) {
128 try {
129 const response = await fetch(`${baseUrl}/api/email/subscribe`, {
130 method: 'POST',
131 headers: { 'content-type': 'application/json', 'accept': 'application/json' },
132 body: JSON.stringify({ email: SMOKE_SUBSCRIBER, source: 'deploy-smoke' }),
133 signal: AbortSignal.timeout(10_000),
134 })
135 const body = await response.text()
136
137 // A 5xx is the failure this exists to catch, and it is worth naming
138 // separately: it means the route resolved and the handler threw, which is
139 // the missing-table shape rather than a missing route.
140 if (response.status >= 500) {
141 throw new Error(
142 `${baseUrl}/api/email/subscribe answered ${response.status}. The route is registered and `
143 + `the handler threw, which is what a missing \`subscribers\` table looks like from outside. `
144 + `Check that migrations ran on this box. Body: ${body.slice(0, 200)}`,
145 )
146 }
147
148 if (!response.ok) {
149 lastFailure = `HTTP ${response.status}`
150 }
151 else {
152 const parsed = JSON.parse(body) as { success?: boolean, message?: string }
153 if (parsed.success !== true)
154 throw new Error(`Subscribe endpoint returned success=${parsed.success}: ${body.slice(0, 200)}`)
155 return
156 }
157 }
158 catch (error) {
159 // A thrown assertion is a real failure and must not be retried into a
160 // timeout; only transport problems are worth another attempt.
161 if (error instanceof Error && error.message.startsWith(baseUrl)) throw error
162 if (error instanceof Error && error.message.startsWith('Subscribe endpoint')) throw error
163 lastFailure = error instanceof Error ? error.message : String(error)
164 }
165 if (attempt < attempts) await Bun.sleep(pauseMs)
166 }
167 throw new Error(`${baseUrl}/api/email/subscribe failed after ${attempts} attempts: ${lastFailure}`)
168}
169
170if (contract.subscribe) await assertSubscribeWorks()
171
92172const robots = await readWithRetry('/robots.txt')
93173if (robots.includes('http://localhost')) throw new Error('Deployed robots.txt contains a localhost URL.')
94174if (!robots.includes(new URL(baseUrl).hostname)) throw new Error(`Deployed robots.txt does not name ${new URL(baseUrl).hostname}.`)