cursedops 0.10.14 → 0.10.16
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/d1Import.ts +18 -4
- package/src/workerDeploy.ts +66 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursedops",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.16",
|
|
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/d1Import.ts
CHANGED
|
@@ -176,8 +176,13 @@ export const D1_READ_PAGE_ROWS = 500;
|
|
|
176
176
|
* back flix's 9,296 `films` EMPTY or cut mid-string (`JSON Parse error: Unterminated string`) and
|
|
177
177
|
* music's tracks cut mid-property, so both apps' `worker:rollback --check` — whose one job is to
|
|
178
178
|
* print what a rollback would lose — crashed before printing it. Every app's `worker-import.ts`
|
|
179
|
-
* read that way.
|
|
180
|
-
*
|
|
179
|
+
* read that way.
|
|
180
|
+
*
|
|
181
|
+
* 🔴 And no FIXED page is small enough for every table: desk's sheet rows cut 500 at a time the
|
|
182
|
+
* same way. So a page that fails is asked again at half the size, and every page that succeeds
|
|
183
|
+
* doubles back toward `pageRows` (`cursedbelt-server`'s `pullD1ToSqlite` learned the second half
|
|
184
|
+
* the same week). Only a failure at ONE row throws — with the read's own error. A `WITHOUT ROWID`
|
|
185
|
+
* table has no rowid to page on, so it throws D1's `no such column: rowid` that way, loudly.
|
|
181
186
|
*/
|
|
182
187
|
export function readD1Table(
|
|
183
188
|
read: (sql: string) => Array<Record<string, unknown>>,
|
|
@@ -187,11 +192,20 @@ export function readD1Table(
|
|
|
187
192
|
): Array<Record<string, unknown>> {
|
|
188
193
|
const rows: Array<Record<string, unknown>> = [];
|
|
189
194
|
let after: number | null = null;
|
|
195
|
+
let limit = pageRows;
|
|
190
196
|
for (;;) {
|
|
191
197
|
const where = after === null ? "" : ` WHERE rowid > ${after}`;
|
|
192
|
-
|
|
198
|
+
let page: Array<Record<string, unknown>>;
|
|
199
|
+
try {
|
|
200
|
+
page = read(`SELECT rowid AS "__page_key", ${select} FROM "${table}"${where} ORDER BY rowid LIMIT ${limit}`);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
if (limit === 1) throw error;
|
|
203
|
+
limit = Math.max(1, Math.floor(limit / 2));
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
193
206
|
for (const { __page_key, ...row } of page) rows.push(row);
|
|
194
|
-
if (page.length <
|
|
207
|
+
if (page.length < limit) return rows;
|
|
195
208
|
after = Number(page[page.length - 1]?.__page_key);
|
|
209
|
+
limit = Math.min(pageRows, limit * 2);
|
|
196
210
|
}
|
|
197
211
|
}
|
package/src/workerDeploy.ts
CHANGED
|
@@ -292,6 +292,11 @@ export interface WorkerDeploySpec {
|
|
|
292
292
|
export interface DeployDeps {
|
|
293
293
|
/** Run a command, inheriting stdio; returns its exit code. */
|
|
294
294
|
run: (argv: readonly string[], env: Record<string, string>) => number;
|
|
295
|
+
/**
|
|
296
|
+
* Run a command with its STDOUT captured (stderr inherited) — for a step whose exit code alone
|
|
297
|
+
* proves nothing: the schema step reads wrangler's `--json` answer back ({@link judgeSchemaApply}).
|
|
298
|
+
*/
|
|
299
|
+
capture: (argv: readonly string[], env: Record<string, string>) => { code: number; stdout: string };
|
|
295
300
|
/** `git <args>` in the checkout; trimmed stdout, `""` on failure. */
|
|
296
301
|
git: (args: readonly string[]) => string;
|
|
297
302
|
/** A file in the checkout, as text — the schema. Default: `readFileSync` against the process's cwd. */
|
|
@@ -316,12 +321,25 @@ export interface DeployResult {
|
|
|
316
321
|
|
|
317
322
|
/** The real {@link DeployDeps}, rooted at the app's checkout. */
|
|
318
323
|
export function workerDeployDeps(cwd: string): DeployDeps {
|
|
324
|
+
// a schema `--command` is kilobytes of DDL — the echo names it, it does not reprint it
|
|
325
|
+
const echo = (argv: readonly string[]): void =>
|
|
326
|
+
console.log(` $ ${argv.map((arg) => (arg.length > 160 ? `${arg.slice(0, 120)}… (${arg.length} chars)` : arg)).join(" ")}`);
|
|
319
327
|
return {
|
|
320
328
|
run: (argv, env) => {
|
|
321
|
-
|
|
322
|
-
console.log(` $ ${argv.map((arg) => (arg.length > 160 ? `${arg.slice(0, 120)}… (${arg.length} chars)` : arg)).join(" ")}`);
|
|
329
|
+
echo(argv);
|
|
323
330
|
return spawnSync(argv[0] as string, argv.slice(1), { cwd, stdio: "inherit", env: { ...process.env, ...env } }).status ?? 1;
|
|
324
331
|
},
|
|
332
|
+
capture: (argv, env) => {
|
|
333
|
+
echo(argv);
|
|
334
|
+
const r = spawnSync(argv[0] as string, argv.slice(1), {
|
|
335
|
+
cwd,
|
|
336
|
+
stdio: ["inherit", "pipe", "inherit"],
|
|
337
|
+
encoding: "utf8",
|
|
338
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
339
|
+
env: { ...process.env, ...env },
|
|
340
|
+
});
|
|
341
|
+
return { code: r.status ?? 1, stdout: r.stdout ?? "" };
|
|
342
|
+
},
|
|
325
343
|
git: (args) => (spawnSync("git", [...args], { cwd, encoding: "utf8" }).stdout ?? "").trim(),
|
|
326
344
|
read: (path) => readFileSync(join(cwd, path), "utf8"),
|
|
327
345
|
};
|
|
@@ -433,7 +451,39 @@ export function schemaCommands(text: string, maxBytes: number = SCHEMA_COMMAND_B
|
|
|
433
451
|
* `workerDeploy.test.ts` pins that no argv here ever carries `--file`.
|
|
434
452
|
*/
|
|
435
453
|
export function schemaApplyArgv(databaseName: string, commands: readonly string[], envArgs: readonly string[]): string[][] {
|
|
436
|
-
return commands.map((command) => ["bunx", "wrangler", "d1", "execute", databaseName, "--remote", "--command", command, "-y", ...envArgs]);
|
|
454
|
+
return commands.map((command) => ["bunx", "wrangler", "d1", "execute", databaseName, "--remote", "--command", command, "--json", "-y", ...envArgs]);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Did one schema chunk APPLY? Its exit code alone does not say.
|
|
459
|
+
*
|
|
460
|
+
* Measured 2026-09-26 (task 2173): this Mac has no `node` on a non-interactive PATH, so `bunx
|
|
461
|
+
* wrangler` runs wrangler under Bun — and under Bun it sometimes exits 0 part-way through its
|
|
462
|
+
* first API request, having printed nothing but its banner. `binary-server`'s edge deploy did it
|
|
463
|
+
* twice in a row and deployed nothing. For this step that is a schema that "passed" without
|
|
464
|
+
* running: the new code ships, and the first request touching the new column 500s in production
|
|
465
|
+
* while the smoke's census of existing routes stays green.
|
|
466
|
+
*
|
|
467
|
+
* So the step asks for `--json` and requires what wrangler prints when a query ran: an array with
|
|
468
|
+
* one result per statement, every one `success: true`. Anything else — no JSON, an empty array,
|
|
469
|
+
* a failed result — is not applied. Pure, so its failure path is tested without wrangler.
|
|
470
|
+
*/
|
|
471
|
+
export function judgeSchemaApply(code: number, stdout: string): { applied: boolean; line: string } {
|
|
472
|
+
if (code !== 0) return { applied: false, line: `wrangler exited ${code}` };
|
|
473
|
+
// the first `[` that opens a JSON array, never the `[ERROR]`/`[WARNING]` of a banner — as `wranglerRows`
|
|
474
|
+
const start = stdout.search(/\[\s*[{\]]/);
|
|
475
|
+
const banner = stdout.trim().slice(0, 160).replace(/\s+/g, " ") || "(nothing)";
|
|
476
|
+
if (start < 0) return { applied: false, line: `wrangler exited 0 and printed no JSON result — it did not run the query. It printed: ${banner}` };
|
|
477
|
+
let parsed: unknown;
|
|
478
|
+
try {
|
|
479
|
+
parsed = JSON.parse(stdout.slice(start));
|
|
480
|
+
} catch (cause) {
|
|
481
|
+
return { applied: false, line: `wrangler's JSON result did not parse (${(cause as Error).message})` };
|
|
482
|
+
}
|
|
483
|
+
if (!Array.isArray(parsed) || parsed.length === 0) return { applied: false, line: "wrangler printed an empty result — no statement ran" };
|
|
484
|
+
const failed = parsed.filter((result) => (result as { success?: unknown } | null)?.success !== true);
|
|
485
|
+
if (failed.length > 0) return { applied: false, line: `${failed.length} of ${parsed.length} result(s) did not report success` };
|
|
486
|
+
return { applied: true, line: `applied — ${parsed.length} result(s), every one success` };
|
|
437
487
|
}
|
|
438
488
|
|
|
439
489
|
/**
|
|
@@ -505,7 +555,19 @@ export function runWorkerDeploy(spec: WorkerDeploySpec, deps: DeployDeps): Deplo
|
|
|
505
555
|
return stop("schema", `${schema} could not be read as statements (${(cause as Error).message}) — nothing was deployed.`);
|
|
506
556
|
}
|
|
507
557
|
for (const argv of schemaApplyArgv(spec.databaseName, commands, envArgs)) {
|
|
508
|
-
|
|
558
|
+
// a silent exit 0 is intermittent and the schema is idempotent, so ONE retry; a real
|
|
559
|
+
// failure (non-zero) is not retried — it would fail the same way
|
|
560
|
+
const attempt = (): { code: number; verdict: ReturnType<typeof judgeSchemaApply> } => {
|
|
561
|
+
const { code, stdout } = deps.capture(argv, spec.credential);
|
|
562
|
+
return { code, verdict: judgeSchemaApply(code, stdout) };
|
|
563
|
+
};
|
|
564
|
+
let { code, verdict } = attempt();
|
|
565
|
+
if (!verdict.applied && code === 0) {
|
|
566
|
+
error(` 🟡 ${verdict.line} — retrying once`);
|
|
567
|
+
({ code, verdict } = attempt());
|
|
568
|
+
}
|
|
569
|
+
if (!verdict.applied) return stop("schema", `the schema did not apply (${verdict.line}) — nothing was deployed.`);
|
|
570
|
+
log(` ${verdict.line}`);
|
|
509
571
|
}
|
|
510
572
|
}
|
|
511
573
|
step(`deploy ${spec.workerName} @ ${commit.slice(0, 8)}${dirty ? " (dirty — stage only)" : ""}`);
|