Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Deploy ends with a smoke test: sign-in, sign-up and the waitlist still work on g1t.sh | 1 | #!/usr/bin/env node |
| 2 | // After a deploy: the ways in for someone new still work. The pages a | |
| Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them. | 3 | // visitor lands on load, and the sign-up form reaches identity and |
| 4 | // answers. It writes nothing: the form is sent an address identity | |
| 5 | // refuses before it keeps or counts anything, so no account is made and | |
| 6 | // the sign-up rate limit is not used up. | |
| Deploy ends with a smoke test: sign-in, sign-up and the waitlist still work on g1t.sh | 7 | // |
| 8 | // node scripts/ops/smoke.mjs # https://g1t.sh | |
| 9 | // node scripts/ops/smoke.mjs --site http://localhost:5173 | |
| 10 | // | |
| 11 | // Exits 1 with what failed. The Deploy workflow runs it once every stage | |
| 12 | // has deployed. | |
| 13 | ||
| 14 | import { pathToFileURL } from "node:url"; | |
| 15 | ||
| 16 | /** The checks, in order. `body` is the page's text with tags removed. */ | |
| 17 | export const CHECKS = [ | |
| The smoke check looks for the landing page's Get started button: the page has said Get started instead of Start for free since 81a83931, so Deploy #248 failed its smoke on a landing page that was fine. | 18 | { name: "Landing page", path: "/", expect: ["Sign up", "Get started"] }, |
| Deploy ends with a smoke test: sign-in, sign-up and the waitlist still work on g1t.sh | 19 | { name: "Sign in", path: "/login", expect: ["Sign in"] }, |
| Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them. | 20 | { name: "Sign up", path: "/register", expect: ["Create your account", "Create account"] }, |
| Deploy ends with a smoke test: sign-in, sign-up and the waitlist still work on g1t.sh | 21 | { name: "Pricing", path: "/pricing", expect: ["Pricing"] }, |
| 22 | { | |
| Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them. | 23 | name: "Sign up reaches identity", |
| Deploy ends with a smoke test: sign-in, sign-up and the waitlist still work on g1t.sh | 24 | path: "/register", |
| 25 | // Not an address, so identity answers "Enter a valid email address." | |
| Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them. | 26 | // before its sign-up rate limit and before any write. |
| 27 | form: { username: "smoke-test", email: "smoke-test-not-an-address", password: "not-a-real-password" }, | |
| Deploy ends with a smoke test: sign-in, sign-up and the waitlist still work on g1t.sh | 28 | status: 422, |
| 29 | expect: ["Enter a valid email address."], | |
| 30 | }, | |
| 31 | ]; | |
| 32 | ||
| 33 | /** A page's visible text: scripts and styles dropped, tags removed, spaces collapsed. */ | |
| 34 | export function pageText(html) { | |
| 35 | return html | |
| 36 | .replace(/<(script|style)\b[\s\S]*?<\/\1>/gi, " ") | |
| 37 | .replace(/<[^>]+>/g, " ") | |
| 38 | .replace(/ | /g, " ") | |
| 39 | .replace(/&/g, "&") | |
| 40 | .replace(/'|'/g, "'") | |
| 41 | .replace(/"/g, '"') | |
| 42 | .replace(/\s+/g, " ") | |
| 43 | .trim(); | |
| 44 | } | |
| 45 | ||
| 46 | /** What is wrong with one answer, or null when it is as expected. */ | |
| 47 | export function verdict(check, status, html) { | |
| 48 | const want = check.status ?? 200; | |
| 49 | if (status !== want) return `answered ${status}, expected ${want}`; | |
| 50 | const text = pageText(html); | |
| 51 | const missing = check.expect.filter((phrase) => !text.includes(phrase)); | |
| 52 | return missing.length ? `missing ${missing.map((m) => JSON.stringify(m)).join(", ")}` : null; | |
| 53 | } | |
| 54 | ||
| 55 | async function run(site) { | |
| 56 | const origin = new URL(site).origin; | |
| 57 | let failed = 0; | |
| 58 | for (const check of CHECKS) { | |
| 59 | const url = new URL(check.path, origin); | |
| 60 | const started = Date.now(); | |
| 61 | let problem; | |
| 62 | try { | |
| 63 | const response = await fetch(url, { | |
| 64 | method: check.form ? "POST" : "GET", | |
| 65 | redirect: "manual", | |
| 66 | headers: { | |
| 67 | "user-agent": "g1t-smoke/1 (+https://g1t.sh)", | |
| 68 | ...(check.form ? { origin, "content-type": "application/x-www-form-urlencoded" } : {}), | |
| 69 | }, | |
| 70 | body: check.form ? new URLSearchParams(check.form).toString() : undefined, | |
| 71 | signal: AbortSignal.timeout(20_000), | |
| 72 | }); | |
| 73 | problem = verdict(check, response.status, await response.text()); | |
| 74 | } catch (error) { | |
| 75 | problem = String(error?.message ?? error); | |
| 76 | } | |
| 77 | const took = `${Date.now() - started} ms`; | |
| 78 | if (problem) { | |
| 79 | failed += 1; | |
| 80 | console.log(`FAIL ${check.name} (${check.form ? "POST" : "GET"} ${url.pathname}): ${problem} · ${took}`); | |
| 81 | } else { | |
| 82 | console.log(`ok ${check.name} · ${took}`); | |
| 83 | } | |
| 84 | } | |
| 85 | return failed; | |
| 86 | } | |
| 87 | ||
| 88 | if (import.meta.url === `file://${process.argv[1]?.replace(/\\/g, "/").replace(/^(?=[A-Za-z]:)/, "/")}`) { | |
| 89 | const at = process.argv.indexOf("--site"); | |
| 90 | const site = at > -1 ? process.argv[at + 1] : "https://g1t.sh"; | |
| 91 | const failed = await run(site); | |
| 92 | if (failed) { | |
| 93 | console.log(`${failed} of ${CHECKS.length} checks failed on ${site}`); | |
| 94 | process.exit(1); | |
| 95 | } | |
| 96 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.