cursedops 0.10.24 → 0.10.25
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/README.md +1 -1
- package/package.json +1 -1
- package/src/workerDeploy.ts +103 -5
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ bun add cursedops
|
|
|
12
12
|
| `cursedops/paths` | the generation's whole-tree laws (`check-paths`, `check-doc-citations`, plus any `--also`) run over ONE repo, from a checkout or a worktree — and its `forge-paths` bin (0.5.0) |
|
|
13
13
|
| `cursedops/launchd` | installing, replacing and removing a macOS launchd user agent, and the port a LIVE job serves on (`livePort`, 0.5.0) |
|
|
14
14
|
| `cursedops/smoke` | the scaffolding of a deployed smoke — the ledger, the fetch, the DNS hint, the exit code — the one check no app owns (every address of a deployment serving the same built client), and since 0.5.0 its VERDICTS: the origin asked on loopback, the smoke's own environment, a network that lies about DNS, and a version that has settled |
|
|
15
|
-
| `cursedops/worker-deploy` | the sequence that ships a Cloudflare Worker — clean tree, stage first, schema, `--var` stamp, secrets, smoke — and the readers it needs (0.5.0); `readWranglerFile(root)` since 0.9.0, lifted from four apps' `readWrangler` (task 2153) |
|
|
15
|
+
| `cursedops/worker-deploy` | the sequence that ships a Cloudflare Worker — clean tree, stage first, schema, `--var` stamp, secrets, smoke — and the readers it needs (0.5.0); `readWranglerFile(root)` since 0.9.0, lifted from four apps' `readWrangler` (task 2153); since 0.10.25 every run, red or green, appends a `kind: "deploy"` line with each leg's wall clock (`install`, `stage`, `walk`, `build`, `schema`, `upload`, `secrets`, `smoke`) to the generation's ledger (task 2246) |
|
|
16
16
|
| `cursedops/worker-deploy` (CPU) | a `wrangler tail` around the traffic a deploy already sends, each request's BILLED CPU charged to its declared route, red on an undeclared, over-budget or uncapped route — and `runWorkerDeploy`'s `stageWalk` + `cpuTail` that wrap the walk and the smoke in it (0.7.0, task 2134, lifted from collections) |
|
|
17
17
|
| `cursedops/worker-secrets` | a Worker holding EXACTLY its deployment's secrets, uploaded over a pipe, read back (0.5.0) |
|
|
18
18
|
| `cursedops/worker-rollback` | a hostname back on its Mac origin: origin first, route second, the route found rather than typed (0.5.0) |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursedops",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.25",
|
|
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/workerDeploy.ts
CHANGED
|
@@ -53,8 +53,9 @@
|
|
|
53
53
|
* refusal without wrangler, git or a network.
|
|
54
54
|
*/
|
|
55
55
|
import { spawn, spawnSync } from "node:child_process";
|
|
56
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
57
|
-
import { join } from "node:path";
|
|
56
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
57
|
+
import { dirname, join } from "node:path";
|
|
58
|
+
import { forgeState } from "cursedops/roots";
|
|
58
59
|
|
|
59
60
|
/** The two deployments every Worker app here has. */
|
|
60
61
|
export type WorkerEnv = "production" | "stage";
|
|
@@ -305,6 +306,58 @@ export interface DeployDeps {
|
|
|
305
306
|
read?: (path: string) => string;
|
|
306
307
|
log?: (line: string) => void;
|
|
307
308
|
error?: (line: string) => void;
|
|
309
|
+
/** The clock the legs are timed on. Default `Date.now`. */
|
|
310
|
+
now?: () => number;
|
|
311
|
+
/**
|
|
312
|
+
* Where the run's {@link DeployEventLine} goes, on EVERY exit — refused, red or green.
|
|
313
|
+
* {@link workerDeployDeps} appends it to the generation's ledger; absent, nothing is recorded.
|
|
314
|
+
* A throw here is swallowed: a ledger that can fail a deploy is worse than none.
|
|
315
|
+
*/
|
|
316
|
+
record?: (line: DeployEventLine) => void;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* One line per deploy into the generation's ledger (`$FORGE_STATE/state/autopilot/events.jsonl`,
|
|
321
|
+
* the file `autopilot timings` reads), with the wall clock of every LEG.
|
|
322
|
+
*
|
|
323
|
+
* 🔴 Why here: until 0.10.25 the whole ledger held 3 `deploy` events, all backfilled by hand, while
|
|
324
|
+
* `autopilot phases` measured deploys at ~110 agent-minutes a day (2026-09-24 → 28) — the slowest
|
|
325
|
+
* thing a task did once gates stopped re-proving unchanged bytes. Every app's `worker:deploy`,
|
|
326
|
+
* stage and production, passes through {@link runWorkerDeploy}, so this is the one seam that sees
|
|
327
|
+
* all of them. A production deploy runs its stage deploy as a child, which records its OWN line
|
|
328
|
+
* (`env: "stage"`) — so the `stage` leg here and that line's legs together say where it went.
|
|
329
|
+
*/
|
|
330
|
+
export interface DeployEventLine {
|
|
331
|
+
at: string;
|
|
332
|
+
kind: "deploy";
|
|
333
|
+
what: "cursedops/worker-deploy";
|
|
334
|
+
repo: string;
|
|
335
|
+
env: WorkerEnv;
|
|
336
|
+
ms: number;
|
|
337
|
+
ok: boolean;
|
|
338
|
+
/** The step it stopped at, or `done` / `cpu-unmeasured` — {@link DeployResult.step}. */
|
|
339
|
+
outcome: string;
|
|
340
|
+
commit: string;
|
|
341
|
+
dirty: boolean;
|
|
342
|
+
/**
|
|
343
|
+
* Milliseconds per leg, in the order they ran: `prepare` (git and the refusals), `install`,
|
|
344
|
+
* `stage` (the stage deploy a production deploy runs first), `walk`, `build`, `schema`,
|
|
345
|
+
* `upload` (`wrangler deploy`), `secrets`, `smoke`. A leg that did not run is absent.
|
|
346
|
+
*/
|
|
347
|
+
legs: Record<string, number>;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Append one line to the ledger under a state root. Never throws; `false` when it did not write. */
|
|
351
|
+
export function recordDeployEvent(state: string | null, line: DeployEventLine): boolean {
|
|
352
|
+
if (!state) return false;
|
|
353
|
+
try {
|
|
354
|
+
const file = join(state, "state", "autopilot", "events.jsonl");
|
|
355
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
356
|
+
appendFileSync(file, `${JSON.stringify(line)}\n`);
|
|
357
|
+
return true;
|
|
358
|
+
} catch {
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
308
361
|
}
|
|
309
362
|
|
|
310
363
|
export interface DeployResult {
|
|
@@ -344,6 +397,9 @@ export function workerDeployDeps(cwd: string): DeployDeps {
|
|
|
344
397
|
},
|
|
345
398
|
git: (args) => (spawnSync("git", [...args], { cwd, encoding: "utf8" }).stdout ?? "").trim(),
|
|
346
399
|
read: (path) => readFileSync(join(cwd, path), "utf8"),
|
|
400
|
+
record: (line) => {
|
|
401
|
+
recordDeployEvent(forgeState(cwd), line);
|
|
402
|
+
},
|
|
347
403
|
};
|
|
348
404
|
}
|
|
349
405
|
|
|
@@ -498,13 +554,46 @@ export const INSTALL: readonly string[] = ["bun", "install", "--frozen-lockfile"
|
|
|
498
554
|
export function runWorkerDeploy(spec: WorkerDeploySpec, deps: DeployDeps): DeployResult {
|
|
499
555
|
const log = deps.log ?? ((line: string) => console.log(line));
|
|
500
556
|
const error = deps.error ?? ((line: string) => console.error(line));
|
|
557
|
+
const now = deps.now ?? Date.now;
|
|
558
|
+
const started = now();
|
|
559
|
+
const legs: Record<string, number> = {};
|
|
560
|
+
let leg = "prepare";
|
|
561
|
+
let legFrom = started;
|
|
562
|
+
/** Close the leg in flight and open `next` — see {@link DeployEventLine.legs}. */
|
|
563
|
+
const enter = (next: string): void => {
|
|
564
|
+
const at = now();
|
|
565
|
+
legs[leg] = (legs[leg] ?? 0) + (at - legFrom);
|
|
566
|
+
leg = next;
|
|
567
|
+
legFrom = at;
|
|
568
|
+
};
|
|
501
569
|
const tag = `[${spec.app}]`;
|
|
502
570
|
const commit = deps.git(["rev-parse", "HEAD"]);
|
|
503
571
|
const porcelain = deps.git(["status", "--porcelain"]);
|
|
504
572
|
const dirty = porcelain.length > 0;
|
|
573
|
+
const finish = (result: DeployResult): DeployResult => {
|
|
574
|
+
enter("");
|
|
575
|
+
try {
|
|
576
|
+
deps.record?.({
|
|
577
|
+
at: new Date(started).toISOString(),
|
|
578
|
+
kind: "deploy",
|
|
579
|
+
what: "cursedops/worker-deploy",
|
|
580
|
+
repo: spec.app,
|
|
581
|
+
env: spec.env,
|
|
582
|
+
ms: now() - started,
|
|
583
|
+
ok: result.code !== 1,
|
|
584
|
+
outcome: result.step,
|
|
585
|
+
commit: commit.slice(0, 8),
|
|
586
|
+
dirty,
|
|
587
|
+
legs,
|
|
588
|
+
});
|
|
589
|
+
} catch {
|
|
590
|
+
// never the deploy's verdict — see DeployDeps.record
|
|
591
|
+
}
|
|
592
|
+
return result;
|
|
593
|
+
};
|
|
505
594
|
const stop = (step: string, detail: string): DeployResult => {
|
|
506
595
|
error(`\n🔴 ${tag} ${detail}`);
|
|
507
|
-
return { code: 1, step, detail, commit, dirty };
|
|
596
|
+
return finish({ code: 1, step, detail, commit, dirty });
|
|
508
597
|
};
|
|
509
598
|
const step = (message: string): void => log(`\n▸ ${message}`);
|
|
510
599
|
/** What passed under a CPU tail that never attached — decision 6 of the CPU check. */
|
|
@@ -538,17 +627,20 @@ export function runWorkerDeploy(spec: WorkerDeploySpec, deps: DeployDeps): Deplo
|
|
|
538
627
|
* no-op when they already agree, and refuses, rather than rewriting the lock, when they cannot.
|
|
539
628
|
*/
|
|
540
629
|
step("make node_modules match bun.lock");
|
|
630
|
+
enter("install");
|
|
541
631
|
if (deps.run(INSTALL, {}) !== 0) {
|
|
542
632
|
return stop("install", `\`${INSTALL.join(" ")}\` failed — node_modules cannot be made to match bun.lock, so nothing was built or deployed.`);
|
|
543
633
|
}
|
|
544
634
|
if (spec.env === "production") {
|
|
545
635
|
for (const command of spec.stageFirst ?? []) {
|
|
546
636
|
step(`the stage first — production waits on it: ${command.join(" ")}`);
|
|
637
|
+
enter("stage");
|
|
547
638
|
if (deps.run(command, {}) !== 0) return stop("stage", `\`${command.join(" ")}\` failed — production was NOT touched.`);
|
|
548
639
|
}
|
|
549
640
|
for (const walk of spec.stageWalk ?? []) {
|
|
550
641
|
const command = spec.cpuTail ? underCpuTail(spec.cpuTail, "stage", walk) : [...walk];
|
|
551
642
|
step(`walk the stage signed in${spec.cpuTail ? ", under a CPU tail" : ""}: ${command.join(" ")}`);
|
|
643
|
+
enter("walk");
|
|
552
644
|
const walked = deps.run(command, {});
|
|
553
645
|
if (spec.cpuTail && walked === TAIL_UNATTACHED_EXIT) unmeasured.push("the stage walk");
|
|
554
646
|
else if (walked !== 0) return stop("stage", `\`${command.join(" ")}\` failed — production was NOT touched.`);
|
|
@@ -556,8 +648,10 @@ export function runWorkerDeploy(spec: WorkerDeploySpec, deps: DeployDeps): Deplo
|
|
|
556
648
|
}
|
|
557
649
|
if (spec.build) {
|
|
558
650
|
step("build");
|
|
651
|
+
enter("build");
|
|
559
652
|
if (deps.run(spec.build, {}) !== 0) return stop("build", "the build failed — nothing was deployed.");
|
|
560
653
|
}
|
|
654
|
+
enter("prepare");
|
|
561
655
|
for (const refusal of spec.beforeShip ?? []) {
|
|
562
656
|
const why = refusal();
|
|
563
657
|
if (why) return stop("refusal", `${why} — nothing was deployed.`);
|
|
@@ -566,6 +660,7 @@ export function runWorkerDeploy(spec: WorkerDeploySpec, deps: DeployDeps): Deplo
|
|
|
566
660
|
if (spec.databaseName) {
|
|
567
661
|
const schema = spec.schemaFile ?? "db/schema.sql";
|
|
568
662
|
step(`apply ${schema} to D1 \`${spec.databaseName}\``);
|
|
663
|
+
enter("schema");
|
|
569
664
|
let commands: string[];
|
|
570
665
|
try {
|
|
571
666
|
commands = schemaCommands((deps.read ?? ((path: string) => readFileSync(path, "utf8")))(schema));
|
|
@@ -592,15 +687,18 @@ export function runWorkerDeploy(spec: WorkerDeploySpec, deps: DeployDeps): Deplo
|
|
|
592
687
|
const stamp = spec.stampVars
|
|
593
688
|
? ["--var", `${spec.stampVars.commit}:${commit}`, "--var", `${spec.stampVars.dirty}:${dirty ? "true" : "false"}`]
|
|
594
689
|
: [];
|
|
690
|
+
enter("upload");
|
|
595
691
|
if (deps.run(["bunx", "wrangler", "deploy", ...envArgs, ...stamp], spec.credential) !== 0) return stop("deploy", "wrangler deploy failed.");
|
|
596
692
|
if (spec.secrets) {
|
|
597
693
|
step("attach the secrets — exactly this deployment's set, over a pipe");
|
|
694
|
+
enter("secrets");
|
|
598
695
|
if (deps.run(spec.secrets, spec.credential) !== 0) {
|
|
599
696
|
return stop("secrets", "the Worker is deployed and its secrets are NOT attached — it serves nothing until they are.");
|
|
600
697
|
}
|
|
601
698
|
}
|
|
602
699
|
if (spec.smoke) {
|
|
603
700
|
step(`smoke every address this deployment answers on${spec.cpuTail ? ", under a CPU tail" : ""}`);
|
|
701
|
+
enter("smoke");
|
|
604
702
|
const smoke = spec.cpuTail ? underCpuTail(spec.cpuTail, spec.env, spec.smoke) : spec.smoke;
|
|
605
703
|
const smoked = deps.run(smoke, {});
|
|
606
704
|
if (spec.cpuTail && smoked === TAIL_UNATTACHED_EXIT) unmeasured.push("the smoke");
|
|
@@ -617,10 +715,10 @@ export function runWorkerDeploy(spec: WorkerDeploySpec, deps: DeployDeps): Deplo
|
|
|
617
715
|
`${detail} Worker CPU UNMEASURED — the tail never attached around ${unmeasured.join(" and ")}, which PASSED. ` +
|
|
618
716
|
"Nothing is wrong with the deployment and nothing needs rolling back; rerun the smoke under the app's CPU wrapper to measure it.";
|
|
619
717
|
error(`\n🟡 ${tag} ${why}`);
|
|
620
|
-
return { code: TAIL_UNATTACHED_EXIT, step: "cpu-unmeasured", detail: why, commit, dirty };
|
|
718
|
+
return finish({ code: TAIL_UNATTACHED_EXIT, step: "cpu-unmeasured", detail: why, commit, dirty });
|
|
621
719
|
}
|
|
622
720
|
log(`\n✅ ${detail}`);
|
|
623
|
-
return { code: 0, step: "done", detail, commit, dirty };
|
|
721
|
+
return finish({ code: 0, step: "done", detail, commit, dirty });
|
|
624
722
|
}
|
|
625
723
|
|
|
626
724
|
/**
|