@astralarium/repo-deploy-release 0.0.0-stage → 0.2.0

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.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +61 -2
  3. package/dist/bin/deploy-release.d.ts +2 -0
  4. package/dist/bin/deploy-release.js +7 -0
  5. package/dist/src/app.d.ts +143 -0
  6. package/dist/src/app.js +102 -0
  7. package/dist/src/bundle.d.ts +18 -0
  8. package/dist/src/bundle.js +41 -0
  9. package/dist/src/check.d.ts +44 -0
  10. package/dist/src/check.js +128 -0
  11. package/dist/src/cloudflare.d.ts +70 -0
  12. package/dist/src/cloudflare.js +170 -0
  13. package/dist/src/commands.d.ts +55 -0
  14. package/dist/src/commands.js +651 -0
  15. package/dist/src/cutover-at.d.ts +23 -0
  16. package/dist/src/cutover-at.js +152 -0
  17. package/dist/src/cutovers.d.ts +62 -0
  18. package/dist/src/cutovers.js +112 -0
  19. package/dist/src/deployer.d.ts +36 -0
  20. package/dist/src/deployer.js +63 -0
  21. package/dist/src/index.d.ts +2 -0
  22. package/dist/src/index.js +2 -0
  23. package/dist/src/lease.d.ts +36 -0
  24. package/dist/src/lease.js +112 -0
  25. package/dist/src/main.d.ts +6 -0
  26. package/dist/src/main.js +43 -0
  27. package/dist/src/process.d.ts +34 -0
  28. package/dist/src/process.js +68 -0
  29. package/dist/src/prompt.d.ts +5 -0
  30. package/dist/src/prompt.js +69 -0
  31. package/dist/src/provision.d.ts +36 -0
  32. package/dist/src/provision.js +230 -0
  33. package/dist/src/schema.d.ts +2 -0
  34. package/dist/src/schema.js +12 -0
  35. package/dist/src/session.d.ts +81 -0
  36. package/dist/src/session.js +275 -0
  37. package/dist/src/state.d.ts +55 -0
  38. package/dist/src/state.js +115 -0
  39. package/dist/src/status.d.ts +65 -0
  40. package/dist/src/status.js +224 -0
  41. package/dist/src/steps.d.ts +80 -0
  42. package/dist/src/steps.js +389 -0
  43. package/dist/src/target.d.ts +94 -0
  44. package/dist/src/target.js +246 -0
  45. package/dist/src/web.d.ts +28 -0
  46. package/dist/src/web.js +70 -0
  47. package/dist/src/workspace.d.ts +11 -0
  48. package/dist/src/workspace.js +40 -0
  49. package/dist/testkit/index.d.ts +4 -0
  50. package/dist/testkit/index.js +36 -0
  51. package/dist/testkit/release-bundles.d.ts +16 -0
  52. package/dist/testkit/release-bundles.js +56 -0
  53. package/dist/testkit/release-checks.d.ts +19 -0
  54. package/dist/testkit/release-checks.js +71 -0
  55. package/dist/testkit/release.d.ts +44 -0
  56. package/dist/testkit/release.js +61 -0
  57. package/package.json +64 -3
  58. package/sql/before-reset.sql +22 -0
  59. package/sql/schema.sql +164 -0
  60. package/sql/scrub.sql +5 -0
@@ -0,0 +1,68 @@
1
+ /** Child processes, pauses and the exit status of one invocation. */
2
+ import { spawn } from "node:child_process";
3
+ import { setTimeout } from "node:timers/promises";
4
+ import { ExpectedError, log } from "@astralarium/repo-telemetry";
5
+ /** Exit status after Ctrl-C. */
6
+ export const INTERRUPT_EXIT = 130;
7
+ /** Ctrl-C: exits `INTERRUPT_EXIT`. */
8
+ export class Interrupted extends ExpectedError {
9
+ constructor() {
10
+ super("interrupted");
11
+ this.name = "Interrupted";
12
+ }
13
+ }
14
+ /** Children `run` has started that are still running. */
15
+ const children = new Set();
16
+ /** SIGKILLs every running child. */
17
+ export function killChildren() {
18
+ for (const child of children)
19
+ child.kill("SIGKILL");
20
+ }
21
+ /** Logs `error` as the command's failure and sets the exit status:
22
+ * `INTERRUPT_EXIT` after Ctrl-C, else 1. */
23
+ export function reportFailure(error) {
24
+ log.error("deploy_release.failed", "deploy-release failed", {
25
+ exception: error,
26
+ });
27
+ process.exitCode = error instanceof Interrupted ? INTERRUPT_EXIT : 1;
28
+ }
29
+ /** Why `signal` aborted, as an error. */
30
+ export const abortReason = (signal) => signal.reason instanceof Error ? signal.reason : new Interrupted();
31
+ /** Runs `cmd` with `args` until it exits; resolves to its stdout when
32
+ * captured. Throws on a nonzero exit, and with `signal`'s reason once it
33
+ * aborts (killing the child). */
34
+ export function run(cmd, args, signal, { cwd, env, input, capture = false, quiet = false } = {}) {
35
+ signal.throwIfAborted();
36
+ return new Promise((resolve, reject) => {
37
+ const child = spawn(cmd, args, {
38
+ cwd,
39
+ env: { ...process.env, ...env },
40
+ signal,
41
+ stdio: [
42
+ input === undefined ? "inherit" : "pipe",
43
+ capture ? "pipe" : "inherit",
44
+ quiet ? "pipe" : "inherit",
45
+ ],
46
+ });
47
+ children.add(child);
48
+ let stdout = "";
49
+ let stderr = "";
50
+ child.stdout?.setEncoding("utf8").on("data", (d) => (stdout += d));
51
+ child.stderr?.setEncoding("utf8").on("data", (d) => (stderr += d));
52
+ child.stdin?.end(input);
53
+ child.on("error", (e) => reject(signal.aborted ? abortReason(signal) : e));
54
+ child.on("close", (code, killed) => {
55
+ children.delete(child);
56
+ if (signal.aborted)
57
+ reject(abortReason(signal));
58
+ else if (code === 0)
59
+ resolve(stdout);
60
+ else
61
+ reject(new ExpectedError(`${[cmd, ...args].join(" ")} ${killed ? `died of ${killed}` : `exited with ${code}`}${stderr ? `:\n${stderr.trim()}` : ""}`));
62
+ });
63
+ });
64
+ }
65
+ /** Resolves after `ms`; throws `signal`'s reason once it aborts. */
66
+ export const sleep = (ms, signal) => setTimeout(ms, undefined, { signal }).catch(() => {
67
+ throw abortReason(signal);
68
+ });
@@ -0,0 +1,5 @@
1
+ /** True when the operator answers y or yes, any case, to `question`. */
2
+ export declare function confirm(question: string, signal: AbortSignal): Promise<boolean>;
3
+ /** Reads a line after `question`, hidden; Ctrl-C interrupts as SIGINT
4
+ * does. */
5
+ export declare function readSecret(question: string, signal: AbortSignal): Promise<string>;
@@ -0,0 +1,69 @@
1
+ /** Operator prompts on stderr. */
2
+ import { createInterface } from "node:readline/promises";
3
+ import { abortReason, Interrupted } from "./process.js";
4
+ /** Reads one line after `question`. */
5
+ async function readLine(question, signal) {
6
+ // Terminal mode would echo piped input, a pasted token included.
7
+ const rl = createInterface({
8
+ input: process.stdin,
9
+ output: process.stderr,
10
+ terminal: false,
11
+ });
12
+ try {
13
+ return await rl.question(question, { signal });
14
+ }
15
+ catch (e) {
16
+ if (signal.aborted)
17
+ throw abortReason(signal);
18
+ // readline closes on Ctrl-C, rejecting with an AbortError.
19
+ throw e instanceof Error && e.name === "AbortError" ? new Interrupted() : e;
20
+ }
21
+ finally {
22
+ rl.close();
23
+ }
24
+ }
25
+ /** True when the operator answers y or yes, any case, to `question`. */
26
+ export async function confirm(question, signal) {
27
+ return /^y(?:es)?$/i.test((await readLine(question, signal)).trim());
28
+ }
29
+ /** Reads a line after `question`, hidden; Ctrl-C interrupts as SIGINT
30
+ * does. */
31
+ export function readSecret(question, signal) {
32
+ const { stdin, stderr } = process;
33
+ if (!stdin.isTTY)
34
+ return readLine(question, signal);
35
+ signal.throwIfAborted();
36
+ stderr.write(question);
37
+ stdin.setRawMode(true);
38
+ stdin.setEncoding("utf8");
39
+ stdin.resume();
40
+ return new Promise((resolve, reject) => {
41
+ let text = "";
42
+ const finish = (settle) => {
43
+ stdin.off("data", onData);
44
+ signal.removeEventListener("abort", onAbort);
45
+ stdin.setRawMode(false);
46
+ stdin.pause();
47
+ stderr.write("\n");
48
+ settle();
49
+ };
50
+ const onAbort = () => finish(() => reject(abortReason(signal)));
51
+ const onData = (chunk) => {
52
+ for (const ch of chunk) {
53
+ if (ch === "\r" || ch === "\n")
54
+ return finish(() => resolve(text));
55
+ if (ch === "\u0003")
56
+ return finish(() => {
57
+ process.kill(process.pid, "SIGINT");
58
+ reject(new Interrupted());
59
+ });
60
+ if (ch === "\u007f" || ch === "\b")
61
+ text = text.slice(0, -1);
62
+ else if (ch >= " ")
63
+ text += ch;
64
+ }
65
+ };
66
+ stdin.on("data", onData);
67
+ signal.addEventListener("abort", onAbort, { once: true });
68
+ });
69
+ }
@@ -0,0 +1,36 @@
1
+ import { ExpectedError } from "@astralarium/repo-telemetry";
2
+ /** One thing an env lacks, and how to supply it. */
3
+ export interface Gap {
4
+ /** What it holds up: `config` commands deploying from its package's
5
+ * config, `secrets` the `secrets` upload, `database` commands on the
6
+ * deploy state; a `template` gap holds up the next operator's copy
7
+ * alone. */
8
+ area: "config" | "secrets" | "database" | "template";
9
+ /** Workspace package whose wrangler config holds it; absent for a gap
10
+ * of the env as a whole. */
11
+ package?: string;
12
+ /** File and field, or variable, holding it. */
13
+ where: string;
14
+ /** What is missing or unset there. */
15
+ problem: string;
16
+ /** The next step closing it. */
17
+ step: string;
18
+ }
19
+ /** `path` as messages show it: relative to the workspace root when
20
+ * within it. */
21
+ export declare function shown(path: string): string;
22
+ /** Gaps of env block `block` (named `where`) in the bindings it declares:
23
+ * a resource field malformed or left at `PLACEHOLDER`, and a binding of
24
+ * config top level `top` it lacks. */
25
+ export declare function bindingGaps(block: Readonly<Record<string, unknown>>, top: Readonly<Record<string, unknown>>, where: string): Gap[];
26
+ /** Gaps of secrets file `file`'s `.tmpl` in naming `keys`: a missing
27
+ * template, a key it lacks. */
28
+ export declare function templateGaps(file: string, keys: readonly string[]): Gap[];
29
+ /** Gaps of secrets file `file` in holding `keys`: a missing file, a key
30
+ * unset, empty or at `SECRET_PLACEHOLDER`. */
31
+ export declare function secretsGaps(file: string, keys: readonly string[]): Gap[];
32
+ /** The gap of `env`'s database URL variable `variable`, unset. */
33
+ export declare const databaseGap: (env: string, variable: string) => Gap;
34
+ /** The refusal of a command on `env` while `gaps` hold it up, pointing at
35
+ * `check` under `invocation`. */
36
+ export declare function unprovisioned(invocation: string, env: string, gaps: readonly Gap[]): ExpectedError;
@@ -0,0 +1,230 @@
1
+ /** Provisioning: what an env still needs before its commands run, each
2
+ * gap with its next step. Reads files and the process environment only. */
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import { isAbsolute, relative } from "node:path";
5
+ import { parseEnv } from "node:util";
6
+ import { ExpectedError } from "@astralarium/repo-telemetry";
7
+ import { ownWorkspaceRoot } from "./workspace.js";
8
+ /** `path` as messages show it: relative to the workspace root when
9
+ * within it. */
10
+ export function shown(path) {
11
+ const within = relative(ownWorkspaceRoot(), path);
12
+ return within.startsWith("..") || isAbsolute(within) ? path : within;
13
+ }
14
+ /** An unprovisioned resource id: its kind's shape with every digit zero
15
+ * (`00000000000000000000000000000000`, a zero UUID, `0`). */
16
+ const PLACEHOLDER = /^0+(?:-0+)*$/;
17
+ /** A secret's value in a `.tmpl`, unset in a copy still holding it. */
18
+ const SECRET_PLACEHOLDER = "xxxx";
19
+ /** 32 lowercase hex digits: Hyperdrive and KV ids. */
20
+ const HEX32 = /^[0-9a-f]{32}$/;
21
+ /** A lowercase UUID: D1 database ids. */
22
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
23
+ /** A queue name. */
24
+ const QUEUE = {
25
+ field: "queue",
26
+ shape: "queue name (1-63 lowercase letters, digits, hyphens)",
27
+ test: /^[a-z0-9][a-z0-9-]{0,62}$/,
28
+ placeholder: false,
29
+ step: () => "Name the queue here; create it with wrangler queues create <name>.",
30
+ };
31
+ /** `entry[field]` when a non-empty string, else `fallback`. */
32
+ function named(entry, field, fallback) {
33
+ const value = entry[field];
34
+ return typeof value === "string" && value ? value : fallback;
35
+ }
36
+ /** Binding lists `bindingGaps` checks. */
37
+ const BINDING_KINDS = [
38
+ {
39
+ path: ["hyperdrive"],
40
+ binding: "binding",
41
+ resource: {
42
+ field: "id",
43
+ shape: "Hyperdrive id (32 hex digits)",
44
+ test: HEX32,
45
+ placeholder: true,
46
+ step: () => "Create the Hyperdrive config (wrangler hyperdrive create <name> --connection-string <postgres url>); set its id here.",
47
+ },
48
+ },
49
+ {
50
+ path: ["d1_databases"],
51
+ binding: "binding",
52
+ resource: {
53
+ field: "database_id",
54
+ shape: "D1 database id (a UUID)",
55
+ test: UUID,
56
+ placeholder: true,
57
+ step: (e) => `Create the database (wrangler d1 create ${named(e, "database_name", "<name>")}); set its database_id here.`,
58
+ },
59
+ },
60
+ {
61
+ path: ["kv_namespaces"],
62
+ binding: "binding",
63
+ resource: {
64
+ field: "id",
65
+ shape: "KV namespace id (32 hex digits)",
66
+ test: HEX32,
67
+ placeholder: true,
68
+ step: (e) => `Create the namespace (wrangler kv namespace create ${named(e, "binding", "<name>")}); set its id here.`,
69
+ },
70
+ },
71
+ {
72
+ path: ["ratelimits"],
73
+ binding: "name",
74
+ resource: {
75
+ field: "namespace_id",
76
+ shape: "rate-limit namespace id (a positive integer)",
77
+ test: /^[1-9][0-9]*$/,
78
+ placeholder: true,
79
+ step: () => "Pick a positive integer no other rate limiter of the account uses; set it here.",
80
+ },
81
+ },
82
+ {
83
+ path: ["r2_buckets"],
84
+ binding: "binding",
85
+ resource: {
86
+ field: "bucket_name",
87
+ shape: "R2 bucket name (3-63 lowercase letters, digits, hyphens)",
88
+ test: /^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$/,
89
+ placeholder: false,
90
+ step: () => "Name the bucket here; create it with wrangler r2 bucket create <name>.",
91
+ },
92
+ },
93
+ {
94
+ path: ["queues", "producers"],
95
+ binding: "binding",
96
+ resource: QUEUE,
97
+ },
98
+ {
99
+ path: ["queues", "consumers"],
100
+ resource: QUEUE,
101
+ },
102
+ { path: ["durable_objects", "bindings"], binding: "name" },
103
+ { path: ["services"], binding: "binding" },
104
+ ];
105
+ /** True for a plain object. */
106
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
107
+ /** The entries of the list at `path` in `block`. */
108
+ function entriesAt(block, path) {
109
+ const list = path.reduce((value, key) => (isRecord(value) ? value[key] : undefined), block);
110
+ return Array.isArray(list) ? list.filter(isRecord) : [];
111
+ }
112
+ /** Gaps of env block `block` (named `where`) in the bindings it declares:
113
+ * a resource field malformed or left at `PLACEHOLDER`, and a binding of
114
+ * config top level `top` it lacks. */
115
+ export function bindingGaps(block, top, where) {
116
+ const gaps = [];
117
+ for (const { path, binding, resource } of BINDING_KINDS) {
118
+ const list = path.join(".");
119
+ const entries = entriesAt(block, path);
120
+ const nameOf = (entry) => binding ? named(entry, binding, "") : "";
121
+ const names = entries.map(nameOf);
122
+ if (resource)
123
+ for (const [i, entry] of entries.entries()) {
124
+ const value = entry[resource.field];
125
+ const label = nameOf(entry) && ` (${nameOf(entry)})`;
126
+ const field = `${where} ${list}[${i}].${resource.field}${label}`;
127
+ const gap = (problem) => gaps.push({
128
+ area: "config",
129
+ where: field,
130
+ problem,
131
+ step: resource.step(entry),
132
+ });
133
+ if (resource.placeholder &&
134
+ typeof value === "string" &&
135
+ PLACEHOLDER.test(value))
136
+ gap(`is a placeholder (${value})`);
137
+ else if (typeof value !== "string" || !resource.test.test(value))
138
+ gap(`${JSON.stringify(value)} is not a ${resource.shape}`);
139
+ }
140
+ if (binding)
141
+ for (const entry of entriesAt(top, path)) {
142
+ const name = nameOf(entry);
143
+ if (name && !names.includes(name))
144
+ gaps.push({
145
+ area: "config",
146
+ where: `${where} ${list}`,
147
+ problem: `lacks ${name}, bound at the top level`,
148
+ step: "Redeclare it here: bindings do not inherit into env blocks.",
149
+ });
150
+ }
151
+ }
152
+ return gaps;
153
+ }
154
+ /** Keys of dotenv `file` with their values. */
155
+ const readDotenv = (file) => parseEnv(readFileSync(file, "utf8"));
156
+ /** Gaps of secrets file `file`'s `.tmpl` in naming `keys`: a missing
157
+ * template, a key it lacks. */
158
+ export function templateGaps(file, keys) {
159
+ if (!keys.length)
160
+ return [];
161
+ const tmpl = `${file}.tmpl`;
162
+ if (!existsSync(tmpl))
163
+ return [
164
+ {
165
+ area: "template",
166
+ where: shown(tmpl),
167
+ problem: "is missing",
168
+ step: `Write it naming each secret with ${SECRET_PLACEHOLDER} values: ${keys.join(", ")}.`,
169
+ },
170
+ ];
171
+ const template = readDotenv(tmpl);
172
+ const lacking = keys.filter((key) => !(key in template));
173
+ if (!lacking.length)
174
+ return [];
175
+ return [
176
+ {
177
+ area: "template",
178
+ where: shown(tmpl),
179
+ problem: `lacks ${lacking.join(", ")}`,
180
+ step: `Add ${lacking.map((key) => `${key}=${SECRET_PLACEHOLDER}`).join(", ")}.`,
181
+ },
182
+ ];
183
+ }
184
+ /** Gaps of secrets file `file` in holding `keys`: a missing file, a key
185
+ * unset, empty or at `SECRET_PLACEHOLDER`. */
186
+ export function secretsGaps(file, keys) {
187
+ if (!keys.length)
188
+ return [];
189
+ const tmpl = `${file}.tmpl`;
190
+ const listed = keys.join(", ");
191
+ if (!existsSync(file))
192
+ return [
193
+ {
194
+ area: "secrets",
195
+ where: shown(file),
196
+ problem: "is missing",
197
+ step: existsSync(tmpl)
198
+ ? `Copy ${shown(tmpl)} to it; fill ${listed}.`
199
+ : `Write it with values for ${listed}.`,
200
+ },
201
+ ];
202
+ const values = readDotenv(file);
203
+ const unset = keys.filter((key) => !values[key] || values[key] === SECRET_PLACEHOLDER);
204
+ if (!unset.length)
205
+ return [];
206
+ return [
207
+ {
208
+ area: "secrets",
209
+ where: shown(file),
210
+ problem: `lacks values for ${unset.join(", ")}`,
211
+ step: `Fill them in; empty and ${SECRET_PLACEHOLDER} count as unset.`,
212
+ },
213
+ ];
214
+ }
215
+ /** The gap of `env`'s database URL variable `variable`, unset. */
216
+ export const databaseGap = (env, variable) => ({
217
+ area: "database",
218
+ where: `$${variable}`,
219
+ problem: "is unset",
220
+ step: `Export ${env}'s database URL as ${variable}.`,
221
+ });
222
+ /** The refusal of a command on `env` while `gaps` hold it up, pointing at
223
+ * `check` under `invocation`. */
224
+ export function unprovisioned(invocation, env, gaps) {
225
+ return new ExpectedError([
226
+ `${env} is unprovisioned:`,
227
+ ...gaps.map((g) => ` ${g.where} ${g.problem}`),
228
+ `${invocation} check --env ${env} lists each next step.`,
229
+ ].join("\n"));
230
+ }
@@ -0,0 +1,2 @@
1
+ /** The `deploy` schema: deploy state and the lease; `reset.sql` refuses while a deploy command holds the lease. */
2
+ export declare const deploySchema: import("@astralarium/repo-postgres/schema").SchemaModule;
@@ -0,0 +1,12 @@
1
+ /** Deploy state as a schema module: `deploy.state` and the lease. */
2
+ import { resolve } from "node:path";
3
+ import { packageRoot } from "@astralarium/repo-package-files";
4
+ import { defineSchemaModule } from "@astralarium/repo-postgres/schema";
5
+ /** The `deploy` schema: deploy state and the lease; `reset.sql` refuses while a deploy command holds the lease. */
6
+ export const deploySchema = defineSchemaModule({
7
+ name: "deploy",
8
+ root: resolve(packageRoot(import.meta.filename), "sql"),
9
+ files: ["schema.sql"],
10
+ beforeReset: "before-reset.sql",
11
+ scrub: "scrub.sql",
12
+ });
@@ -0,0 +1,81 @@
1
+ import type { DeployInfo } from "@astralarium/repo-deploy-info";
2
+ import { type Sql } from "postgres";
3
+ import { type ReleaseApp } from "./app.ts";
4
+ import { type Needs } from "./check.ts";
5
+ import { Cloudflare } from "./cloudflare.ts";
6
+ import { type DeployOperation, Lease } from "./lease.ts";
7
+ import { type RunOptions } from "./process.ts";
8
+ import { type InPlaceTarget, type SlotTarget, type Target } from "./target.ts";
9
+ /** Flags every session command takes. */
10
+ export interface EnvFlags {
11
+ readonly env: string;
12
+ }
13
+ /** A command function running `fn` in a session against `--env`'s
14
+ * target, held up by its gaps in `needs` (`provisionedTarget`); `fn`
15
+ * resolves to the exit status, void is 0. Failures log as
16
+ * `deploy_release.failed`. */
17
+ export declare function inSession<F extends EnvFlags>(app: ReleaseApp, invocation: string, needs: Needs, fn: (flags: F, s: Session) => Promise<number | void>): (flags: F) => Promise<void>;
18
+ /** `inSession` for a blue/green command: an in-place env refuses. */
19
+ export declare function inSlots<F extends EnvFlags>(app: ReleaseApp, invocation: string, needs: Needs, fn: (flags: F, s: SlotSession) => Promise<number | void>): (flags: F) => Promise<void>;
20
+ /** `s` against its blue/green target; throws for an in-place env,
21
+ * naming its commands. */
22
+ export declare function slotSession(s: Session): SlotSession;
23
+ /** A session against a blue/green target. */
24
+ export type SlotSession = Session<SlotTarget>;
25
+ /** A session against an in-place target. */
26
+ export type InPlaceSession = Session<InPlaceTarget>;
27
+ /** Disposal drops the lease, closes the database and removes the scratch
28
+ * dir. */
29
+ export declare class Session<T extends Target = Target> implements AsyncDisposable {
30
+ #private;
31
+ readonly app: ReleaseApp;
32
+ /** Starts each printed command line: the program and its `--app`. */
33
+ readonly invocation: string;
34
+ readonly target: T;
35
+ readonly cf: Cloudflare;
36
+ /** The deployer is up with this session's code (`deployDeployer`). */
37
+ deployerDeployed: boolean;
38
+ constructor(app: ReleaseApp, invocation: string, target: T);
39
+ /** Aborts on Ctrl-C or a lost lease: children die, waits end. */
40
+ get signal(): AbortSignal;
41
+ /** Aborts with `reason`. */
42
+ abort(reason: unknown): void;
43
+ /** `command` as a runnable line for this target. */
44
+ cli(command: string): string;
45
+ /** Runs `cmd` with `args` until it exits, aborting with the session;
46
+ * resolves to its stdout when captured. */
47
+ run(cmd: string, args: readonly string[], options?: RunOptions): Promise<string>;
48
+ /** `fetch` aborting with the session. */
49
+ fetch(input: URL | string, init?: RequestInit): Promise<Response>;
50
+ /** Pauses `ms`, aborting with the session. */
51
+ sleep(ms: number): Promise<void>;
52
+ /** Reads a hidden line from the operator after `question`. */
53
+ secret(question: string): Promise<string>;
54
+ /** True when the operator answers yes to `question`. */
55
+ confirm(question: string): Promise<boolean>;
56
+ /** Throws while the app checkout holds uncommitted or untracked
57
+ * changes (the deployer bundles it). */
58
+ requireClean(): void;
59
+ /** Readies the checkouts for a build: throws while any holds
60
+ * uncommitted or untracked changes (builds bundle them), then
61
+ * fast-forwards the `pull` siblings. */
62
+ ready(): void;
63
+ /** Fresh deploy info for the target: a build id and each checkout's
64
+ * commit; throws on a dirty checkout. */
65
+ mint(): DeployInfo;
66
+ /** Variable holding the target's database URL; throws for a target
67
+ * without a database. */
68
+ get dbUrlVar(): string;
69
+ /** True while the target has a database and its URL is set. */
70
+ get hasDb(): boolean;
71
+ /** The target's database; throws while its URL is unset. */
72
+ get sql(): Sql;
73
+ /** Takes the deploy lease for `operation`; a held lease keeps its
74
+ * operation. Throws while another command holds it. */
75
+ takeLease(operation: DeployOperation): Promise<Lease>;
76
+ /** The held lease. */
77
+ get lease(): Lease;
78
+ /** A scratch dir, removed at disposal. */
79
+ tmpDir(): Promise<string>;
80
+ [Symbol.asyncDispose](): Promise<void>;
81
+ }