cursedops 0.16.0 → 0.16.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/edgeFetch.ts +15 -6
- package/src/smoke.ts +13 -2
- package/src/workerDeploy.ts +106 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursedops",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.3",
|
|
4
4
|
"description": "The build-and-ops answers this generation's apps wrote independently and identically: finding a generation's roots — and printing a command that runs when pasted — without knowing a path, the generation's whole-tree laws run over one repo from a checkout or a worktree, macOS launchd agent install/replace/remove and the live port a job serves, the scaffolding and verdicts of a deployed smoke (origin probe, the smoke's own environment, a network that lies about DNS, a settled version), the static-serving helpers eight apps copied — the path-traversal guard among them — the API floor that keeps an unmatched /api/... from ever being answered with the app shell, the commit and dirty flag a checkout-served process reports, the Cloudflare Worker deploy toolkit four apps copied (the deploy sequence, exact-set secrets over a pipe, origin-first rollback, the curl edge fetch, the row-for-row D1 import proof, the billed-CPU tail check around a deploy's walk and smoke, and each app's worker:secrets and worker:smoke main as one function of its data), the relay a Worker fronts a Mac-bound app with (the Durable Object, the frames, the Mac's dialer and key rotation — lifted from station for roms — and the signed-in stage walk's skeleton and the relay app's whole worker:deploy), the Worker import-graph and await-port checks every Worker app's suite runs over its own source, and the public-surface ratchet three published libraries each carried a forked copy of. Mechanism only — no app knows its name from here. Bun, zero runtime dependencies (typescript is an optional peer, for public-surface only), ships source.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
package/src/edgeFetch.ts
CHANGED
|
@@ -59,10 +59,18 @@ export interface EdgeFetchOptions {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
|
-
* A `RequestInit` this fetch reads
|
|
63
|
-
*
|
|
62
|
+
* A `RequestInit` this fetch reads two more fields from. A plain `fetch` ignores both.
|
|
63
|
+
*
|
|
64
|
+
* · `maxTimeSec` — curl's `--max-time` for THIS request, over the instance's
|
|
65
|
+
* {@link EdgeFetchOptions.maxTimeSec};
|
|
66
|
+
* · `replaySafe` — the caller's word that sending this request twice does nothing the first did
|
|
67
|
+
* not, WHATEVER its method: a smoke's anonymous census probe, which the origin gate refuses
|
|
68
|
+
* before any handler runs. Such a request is replayed after a timeout or a dropped reply like a
|
|
69
|
+
* GET. Measured 2026-10-13: family's stage smoke died on census route 55 of 101, an anonymous
|
|
70
|
+
* `POST /api/media` that never reached the Worker (no trace of it), because a POST's timeout
|
|
71
|
+
* was never replayed — a green Worker, a red ship lane (forge task 3115).
|
|
64
72
|
*/
|
|
65
|
-
export type EdgeRequestInit = RequestInit & { maxTimeSec?: number };
|
|
73
|
+
export type EdgeRequestInit = RequestInit & { maxTimeSec?: number; replaySafe?: boolean };
|
|
66
74
|
|
|
67
75
|
/**
|
|
68
76
|
* 🔴 How many MORE times a request is sent after a connection-level curl failure that carried
|
|
@@ -85,12 +93,13 @@ const EDGE_RETRIES = 2;
|
|
|
85
93
|
* three of them. Measured 2026-09-26: flix's prod smoke went red on one `curl exit 28` for
|
|
86
94
|
* `GET /healthz` while the same URL answered 200 in 0.2 s seconds later (task 2190).
|
|
87
95
|
*
|
|
96
|
+
* A request its caller marked `replaySafe` ({@link EdgeRequestInit}) counts as safe to run twice.
|
|
88
97
|
* Every other exit, and every HTTP status, whatever it is, is returned exactly as before.
|
|
89
98
|
*/
|
|
90
|
-
function retryableCurlFailure(exit: number | null, stdout: Buffer, method = "GET", retry = 1): boolean {
|
|
99
|
+
function retryableCurlFailure(exit: number | null, stdout: Buffer, method = "GET", retry = 1, replaySafe = false): boolean {
|
|
91
100
|
if (stdout.length > 0 || exit === null) return false;
|
|
92
101
|
if (exit === 6 || exit === 7 || exit === 35) return true;
|
|
93
|
-
const replayable = ["GET", "HEAD", "OPTIONS", "PUT", "DELETE"].includes(method.toUpperCase());
|
|
102
|
+
const replayable = replaySafe || ["GET", "HEAD", "OPTIONS", "PUT", "DELETE"].includes(method.toUpperCase());
|
|
94
103
|
if (exit === 28) return replayable && retry === 1;
|
|
95
104
|
return (exit === 52 || exit === 56) && replayable;
|
|
96
105
|
}
|
|
@@ -181,7 +190,7 @@ export function createEdgeFetch(options: EdgeFetchOptions = {}): (url: string, i
|
|
|
181
190
|
}
|
|
182
191
|
try {
|
|
183
192
|
let out = curl([...head(), ...args, target.toString()]);
|
|
184
|
-
for (let retry = 1; retry <= EDGE_RETRIES && retryableCurlFailure(out.status, out.stdout, init.method, retry); retry++) {
|
|
193
|
+
for (let retry = 1; retry <= EDGE_RETRIES && retryableCurlFailure(out.status, out.stdout, init.method, retry, init.replaySafe); retry++) {
|
|
185
194
|
await new Promise((r) => setTimeout(r, options.retryDelayMs?.(retry) ?? 500 * retry));
|
|
186
195
|
// `head()` again: a host with no pin yet is asked again, so a retry can find the address.
|
|
187
196
|
out = curl([...head(), ...args, target.toString()]);
|
package/src/smoke.ts
CHANGED
|
@@ -1127,9 +1127,15 @@ export async function runWorkerSmoke(spec: WorkerSmokeSpec): Promise<number> {
|
|
|
1127
1127
|
`credentials go back to ${JSON.stringify(health.publicUrl)} — this deployment's ${deployment.publicUrl}`,
|
|
1128
1128
|
);
|
|
1129
1129
|
|
|
1130
|
+
// Each probe is anonymous and refused at the gate, so it is replay-safe whatever its method,
|
|
1131
|
+
// and one that gets no answer even then is this check's red — never a crash of the whole smoke.
|
|
1130
1132
|
const open: string[] = [];
|
|
1131
1133
|
for (const route of spec.census) {
|
|
1132
|
-
const
|
|
1134
|
+
const probe: EdgeRequestInit = { method: route.method, replaySafe: true };
|
|
1135
|
+
const answered = await ask(route.path, probe).then(
|
|
1136
|
+
(res) => res.status,
|
|
1137
|
+
(error: Error) => `no answer (${error.message})`,
|
|
1138
|
+
);
|
|
1133
1139
|
if (answered !== 401) open.push(`${route.method} ${route.path} → ${answered}`);
|
|
1134
1140
|
}
|
|
1135
1141
|
smoke.record(
|
|
@@ -1214,7 +1220,12 @@ async function accessDoorSmoke(
|
|
|
1214
1220
|
} else if (address === "preview") {
|
|
1215
1221
|
const open: string[] = [];
|
|
1216
1222
|
for (const route of spec.census) {
|
|
1217
|
-
|
|
1223
|
+
// Refused by Access before the Worker sees it: replay-safe, as the origin-gate census.
|
|
1224
|
+
const probe: EdgeRequestInit = { method: route.method, replaySafe: true };
|
|
1225
|
+
const answered = await ask(route.path, probe).then(
|
|
1226
|
+
(res) => res.status,
|
|
1227
|
+
(error: Error) => `no answer (${error.message})`,
|
|
1228
|
+
);
|
|
1218
1229
|
if (answered !== 403) open.push(`${route.method} ${route.path} → ${answered}`);
|
|
1219
1230
|
}
|
|
1220
1231
|
for (const path of spec.shellPaths) {
|
package/src/workerDeploy.ts
CHANGED
|
@@ -54,7 +54,8 @@
|
|
|
54
54
|
* refusal without wrangler, git or a network.
|
|
55
55
|
*/
|
|
56
56
|
import { spawn, spawnSync } from "node:child_process";
|
|
57
|
-
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
57
|
+
import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
58
|
+
import { tmpdir } from "node:os";
|
|
58
59
|
import { join } from "node:path";
|
|
59
60
|
import { liveColumns, liveColumnsQueries, missingColumnStatements, schemaTables } from "cursedops/d1-columns";
|
|
60
61
|
import { wranglerRows } from "cursedops/d1-import";
|
|
@@ -345,6 +346,19 @@ export interface DeployDeps {
|
|
|
345
346
|
* generation's lease; absent (or `null` with no state root), nothing is leased.
|
|
346
347
|
*/
|
|
347
348
|
lease?: (app: string, what: string) => StageLeaseResult | null;
|
|
349
|
+
/**
|
|
350
|
+
* A fresh file for wrangler's ND-JSON output (`WRANGLER_OUTPUT_FILE_PATH`), which is where a failed
|
|
351
|
+
* `wrangler deploy` says WHY in a form a program can read ({@link transientDeployFailure}).
|
|
352
|
+
* {@link workerDeployDeps} makes one under the OS temp dir; absent, a failed deploy is not retried.
|
|
353
|
+
*/
|
|
354
|
+
wranglerOutput?: () => WranglerOutputFile;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** One `wrangler` run's output file: the env that points wrangler at it, its text, and its removal. */
|
|
358
|
+
export interface WranglerOutputFile {
|
|
359
|
+
env: Record<string, string>;
|
|
360
|
+
read: () => string;
|
|
361
|
+
dispose: () => void;
|
|
348
362
|
}
|
|
349
363
|
|
|
350
364
|
/**
|
|
@@ -420,6 +434,15 @@ export function workerDeployDeps(cwd: string): DeployDeps {
|
|
|
420
434
|
},
|
|
421
435
|
git: (args) => (spawnSync("git", [...args], { cwd, encoding: "utf8" }).stdout ?? "").trim(),
|
|
422
436
|
read: (path) => readFileSync(join(cwd, path), "utf8"),
|
|
437
|
+
wranglerOutput: () => {
|
|
438
|
+
const dir = mkdtempSync(join(tmpdir(), "cursedops-wrangler-"));
|
|
439
|
+
const path = join(dir, "output.ndjson");
|
|
440
|
+
return {
|
|
441
|
+
env: { WRANGLER_OUTPUT_FILE_PATH: path },
|
|
442
|
+
read: () => (existsSync(path) ? readFileSync(path, "utf8") : ""),
|
|
443
|
+
dispose: () => rmSync(dir, { recursive: true, force: true }),
|
|
444
|
+
};
|
|
445
|
+
},
|
|
423
446
|
record: (line) => {
|
|
424
447
|
recordDeployEvent(forgeState(cwd), line);
|
|
425
448
|
},
|
|
@@ -572,6 +595,38 @@ export function wranglerError(stdout: string): { reason: string; api: boolean }
|
|
|
572
595
|
return { reason, api: name === "APIError" || (kind === "error" && /cloudflare api/i.test(reason)) };
|
|
573
596
|
}
|
|
574
597
|
|
|
598
|
+
/**
|
|
599
|
+
* Why a failed `wrangler deploy` is worth ONE more try — or `null` when it would fail the same way.
|
|
600
|
+
*
|
|
601
|
+
* Measured 2026-10-11 (forge task 3053): roms' production deploy uploaded the Worker and its routes,
|
|
602
|
+
* then Cloudflare answered the cron-schedules PUT with `Internal authentication error: internal
|
|
603
|
+
* server error [code: 10500]`. wrangler exited 1, the ship's roms lane went red, and the ship batch
|
|
604
|
+
* re-ran a 50-minute `ship run` to redo a step that a second request would have done. A deploy is
|
|
605
|
+
* idempotent — the same script, the same triggers — so a Cloudflare-side failure is retried once.
|
|
606
|
+
* A bundling error, a refused binding or a script that throws at startup (10021) is not: wrangler's
|
|
607
|
+
* own `command-failed` line ({@link WranglerOutputFile}) must name the server's fault or a retry-after.
|
|
608
|
+
*/
|
|
609
|
+
export function transientDeployFailure(ndjson: string): string | null {
|
|
610
|
+
type Failed = { code?: unknown; message?: unknown; retry_after_ms?: unknown };
|
|
611
|
+
let failed: Failed | null = null;
|
|
612
|
+
for (const line of ndjson.split("\n")) {
|
|
613
|
+
if (!line.trim()) continue;
|
|
614
|
+
try {
|
|
615
|
+
const entry = JSON.parse(line) as (Failed & { type?: unknown }) | null;
|
|
616
|
+
if (entry?.type === "command-failed") failed = entry;
|
|
617
|
+
} catch {
|
|
618
|
+
// a torn line from a killed run — the ones around it still count
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
if (!failed) return null;
|
|
622
|
+
const message = typeof failed.message === "string" ? failed.message : "";
|
|
623
|
+
const first = message.split("\n").find((l) => l.trim()) ?? "";
|
|
624
|
+
const reason = (detail: string) => `${first.trim() || "wrangler deploy failed"} — ${detail}`;
|
|
625
|
+
if (typeof failed.retry_after_ms === "number") return reason(`Cloudflare asked for a retry after ${failed.retry_after_ms} ms`);
|
|
626
|
+
const server = message.match(/internal server error|\[code: (?:10500|10013|7403)\]|\b50[234]\b|ETIMEDOUT|ECONNRESET|socket hang up|fetch failed/i);
|
|
627
|
+
return server ? reason(`"${server[0]}" is Cloudflare's side`) : null;
|
|
628
|
+
}
|
|
629
|
+
|
|
575
630
|
/**
|
|
576
631
|
* Did one schema chunk APPLY? Its exit code alone does not say.
|
|
577
632
|
*
|
|
@@ -895,7 +950,22 @@ export function runWorkerDeploy(spec: WorkerDeploySpec, deps: DeployDeps): Deplo
|
|
|
895
950
|
? ["--var", `${spec.stampVars.commit}:${commit}`, "--var", `${spec.stampVars.dirty}:${shipsDirty ? "true" : "false"}`]
|
|
896
951
|
: [];
|
|
897
952
|
enter("upload");
|
|
898
|
-
|
|
953
|
+
const deployArgv = ["bunx", "wrangler", "deploy", ...envArgs, ...stamp];
|
|
954
|
+
const deployOnce = (): { code: number; transient: string | null } => {
|
|
955
|
+
const output = deps.wranglerOutput?.();
|
|
956
|
+
try {
|
|
957
|
+
const code = deps.run(deployArgv, { ...spec.credential, ...output?.env });
|
|
958
|
+
return { code, transient: code !== 0 && output ? transientDeployFailure(output.read()) : null };
|
|
959
|
+
} finally {
|
|
960
|
+
output?.dispose();
|
|
961
|
+
}
|
|
962
|
+
};
|
|
963
|
+
let deployed = deployOnce();
|
|
964
|
+
if (deployed.code !== 0 && deployed.transient) {
|
|
965
|
+
error(` 🟡 ${deployed.transient} — retrying the deploy once`);
|
|
966
|
+
deployed = deployOnce();
|
|
967
|
+
}
|
|
968
|
+
if (deployed.code !== 0) return stop("deploy", "wrangler deploy failed.");
|
|
899
969
|
if (spec.secrets) {
|
|
900
970
|
step("attach the secrets — exactly this deployment's set, over a pipe");
|
|
901
971
|
enter("secrets");
|
|
@@ -919,7 +989,7 @@ export function runWorkerDeploy(spec: WorkerDeploySpec, deps: DeployDeps): Deplo
|
|
|
919
989
|
const detail = `${spec.workerName} deployed${spec.secrets ? ", attached" : ""}${spec.smoke ? " and smoked" : ""} @ ${commit.slice(0, 8)}.`;
|
|
920
990
|
if (unmeasured.length > 0) {
|
|
921
991
|
const why =
|
|
922
|
-
`${detail} Worker CPU UNMEASURED — the tail
|
|
992
|
+
`${detail} Worker CPU UNMEASURED — the tail heard nothing of ${unmeasured.join(" and ")}, which PASSED. ` +
|
|
923
993
|
"Nothing is wrong with the deployment and nothing needs rolling back; rerun the smoke under the app's CPU wrapper to measure it.";
|
|
924
994
|
error(`\n🟡 ${tag} ${why}`);
|
|
925
995
|
return finish({ code: TAIL_UNATTACHED_EXIT, step: "cpu-unmeasured", detail: why, commit, dirty });
|
|
@@ -1268,6 +1338,29 @@ export function workerTailEvents(traces: readonly unknown[], scriptName: string)
|
|
|
1268
1338
|
return out;
|
|
1269
1339
|
}
|
|
1270
1340
|
|
|
1341
|
+
/**
|
|
1342
|
+
* How many of this Worker's invocations a capture holds AFTER the trace carrying `cpu-tail=<marker>`,
|
|
1343
|
+
* not counting any other marker. Pure. 0 when the start marker is not in the capture at all.
|
|
1344
|
+
*
|
|
1345
|
+
* 🔴 Why it exists (task 3110): music's stage walk PASSED under a tail that carried the start marker
|
|
1346
|
+
* and then nothing — ~20 requests and the end marker, none heard in 45 s. A tail that went deaf after
|
|
1347
|
+
* attaching measured nothing, exactly like one that never attached (decision 6), so it is unmeasured,
|
|
1348
|
+
* not red. A PARTIAL capture (any of the command's requests heard) stays red: that one may really be
|
|
1349
|
+
* missing the last requests.
|
|
1350
|
+
*/
|
|
1351
|
+
export function heardAfterMarker(traces: readonly unknown[], scriptName: string, marker: string): number {
|
|
1352
|
+
let started = false;
|
|
1353
|
+
let heard = 0;
|
|
1354
|
+
for (const raw of traces as TailTraceLike[]) {
|
|
1355
|
+
if (!raw || raw.scriptName !== scriptName) continue;
|
|
1356
|
+
const url = raw.event?.request?.url;
|
|
1357
|
+
const sent = url ? URL.parse(url)?.searchParams.get("cpu-tail") : null;
|
|
1358
|
+
if (!started) started = sent === marker;
|
|
1359
|
+
else if (sent == null) heard++;
|
|
1360
|
+
}
|
|
1361
|
+
return heard;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1271
1364
|
/** The verdict in words, for the deploy log. */
|
|
1272
1365
|
export function formatTailVerdict(verdict: TailVerdict, bench: Pick<TailBench, "formatCpuBudgetReport">, scriptName: string): string {
|
|
1273
1366
|
const r = verdict.recorded;
|
|
@@ -1405,7 +1498,8 @@ export interface WorkerCpuTailSpec extends Omit<JudgeOpts, "scriptName"> {
|
|
|
1405
1498
|
}
|
|
1406
1499
|
|
|
1407
1500
|
/**
|
|
1408
|
-
* The exit code of a CPU tail that never attached, twice
|
|
1501
|
+
* The exit code of a CPU tail that never attached, twice — or went deaf once it had (task 3110) — around a
|
|
1502
|
+
* command that PASSED (`EX_UNAVAILABLE`):
|
|
1409
1503
|
* nothing was measured, and nothing was wrong with the deployment. Distinct from the command's own
|
|
1410
1504
|
* codes so {@link runWorkerDeploy} can say which of the two happened — decision 6.
|
|
1411
1505
|
*/
|
|
@@ -1488,6 +1582,13 @@ export async function runWorkerCpuTail(spec: WorkerCpuTailSpec, deps: WorkerCpuD
|
|
|
1488
1582
|
);
|
|
1489
1583
|
return { code: ran, step: "command", verdict };
|
|
1490
1584
|
}
|
|
1491
|
-
if (!drained)
|
|
1585
|
+
if (!drained) {
|
|
1586
|
+
// Decision 6, again: a tail that heard none of a PASSED command went deaf — nothing was measured.
|
|
1587
|
+
if (heardAfterMarker(spec.bench.parseTailLines(tail.captured()), spec.workerName, marker) === 0) {
|
|
1588
|
+
deps.error(`🟡 ${tag} wrangler tail went deaf after it attached — it heard none of \`${spec.command.join(" ")}\`, which PASSED. CPU UNMEASURED.`);
|
|
1589
|
+
return { code: TAIL_UNATTACHED_EXIT, step: "unmeasured", verdict };
|
|
1590
|
+
}
|
|
1591
|
+
return { code: 1, step: "drain", verdict };
|
|
1592
|
+
}
|
|
1492
1593
|
return verdict.ok ? { code: 0, step: "done", verdict } : { code: 1, step: "budget", verdict };
|
|
1493
1594
|
}
|