Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
104 linesCodeBlameRaw
1#!/usr/bin/env node
2// After a deploy: the ways in for someone new still work. The pages a
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.
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
14import { pathToFileURL } from "node:url";
15
16/** The checks, in order. `body` is the page's text with tags removed. */
17export const CHECKS = [
18 { name: "Landing page", path: "/", expect: ["Sign up", "Get started"] },
19 { name: "Sign in", path: "/login", expect: ["Sign in"] },
20 { name: "Sign up", path: "/register", expect: ["Create your account", "Create account"] },
21 { name: "Pricing", path: "/pricing", expect: ["Pricing"] },
22 {
23 name: "Sign up refuses a bad form",
24 path: "/register",
25 // Not an address and no Turnstile answer, so nothing is written or
26 // counted: with Turnstile on, the site refuses it first (no person
27 // solved the check); with it off, identity answers "Enter a valid
28 // email address." before its sign-up rate limit.
29 form: { username: "smoke-test", email: "smoke-test-not-an-address", password: "not-a-real-password" },
30 status: 422,
31 expect: [],
32 expectOne: ["We could not tell that a person sent this form.", "Enter a valid email address."],
33 },
34];
35
36/** A page's visible text: scripts and styles dropped, tags removed, spaces collapsed. */
37export function pageText(html) {
38 return html
39 .replace(/<(script|style)\b[\s\S]*?<\/\1>/gi, " ")
40 .replace(/<[^>]+>/g, " ")
41 .replace(/&nbsp;|&#160;/g, " ")
42 .replace(/&amp;/g, "&")
43 .replace(/&#x27;|&#39;/g, "'")
44 .replace(/&quot;/g, '"')
45 .replace(/\s+/g, " ")
46 .trim();
47}
48
49/** What is wrong with one answer, or null when it is as expected. */
50export function verdict(check, status, html) {
51 const want = check.status ?? 200;
52 if (status !== want) return `answered ${status}, expected ${want}`;
53 const text = pageText(html);
54 const missing = check.expect.filter((phrase) => !text.includes(phrase));
55 if (missing.length) return `missing ${missing.map((m) => JSON.stringify(m)).join(", ")}`;
56 // `expectOne`: at least one of these phrases.
57 if (check.expectOne && !check.expectOne.some((phrase) => text.includes(phrase))) {
58 return `missing one of ${check.expectOne.map((m) => JSON.stringify(m)).join(", ")}`;
59 }
60 return null;
61}
62
63async function run(site) {
64 const origin = new URL(site).origin;
65 let failed = 0;
66 for (const check of CHECKS) {
67 const url = new URL(check.path, origin);
68 const started = Date.now();
69 let problem;
70 try {
71 const response = await fetch(url, {
72 method: check.form ? "POST" : "GET",
73 redirect: "manual",
74 headers: {
75 "user-agent": "g1t-smoke/1 (+https://g1t.sh)",
76 ...(check.form ? { origin, "content-type": "application/x-www-form-urlencoded" } : {}),
77 },
78 body: check.form ? new URLSearchParams(check.form).toString() : undefined,
79 signal: AbortSignal.timeout(20_000),
80 });
81 problem = verdict(check, response.status, await response.text());
82 } catch (error) {
83 problem = String(error?.message ?? error);
84 }
85 const took = `${Date.now() - started} ms`;
86 if (problem) {
87 failed += 1;
88 console.log(`FAIL ${check.name} (${check.form ? "POST" : "GET"} ${url.pathname}): ${problem} · ${took}`);
89 } else {
90 console.log(`ok ${check.name} · ${took}`);
91 }
92 }
93 return failed;
94}
95
96if (import.meta.url === `file://${process.argv[1]?.replace(/\\/g, "/").replace(/^(?=[A-Za-z]:)/, "/")}`) {
97 const at = process.argv.indexOf("--site");
98 const site = at > -1 ? process.argv[at + 1] : "https://g1t.sh";
99 const failed = await run(site);
100 if (failed) {
101 console.log(`${failed} of ${CHECKS.length} checks failed on ${site}`);
102 process.exit(1);
103 }
104}