@rehearsal-db/core 0.1.0-beta.5 → 0.1.0-beta.7
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/CHANGELOG.md +56 -1
- package/COMPATIBILITY.md +5 -0
- package/README.md +112 -377
- package/SECURITY.md +2 -1
- package/docs/adapters.md +5 -0
- package/docs/baselines.md +81 -22
- package/docs/commands.md +105 -75
- package/docs/configuration.md +109 -8
- package/docs/getting-started.md +88 -227
- package/docs/glossary.md +7 -8
- package/docs/releasing.md +10 -1
- package/docs/roadmap.md +39 -0
- package/docs/security-model.md +5 -1
- package/docs/troubleshooting.md +69 -1
- package/docs/tutorial.md +70 -59
- package/package.json +4 -2
- package/scripts/lib/environment/local_supabase.mjs +8 -8
- package/scripts/lib/rehearsal/baseline_artifact.mjs +32 -3
- package/scripts/lib/rehearsal/cleanup.mjs +381 -0
- package/scripts/lib/rehearsal/configuration.d.mts +24 -3
- package/scripts/lib/rehearsal/configuration.mjs +337 -59
- package/scripts/lib/rehearsal/diagnostics.mjs +4 -2
- package/scripts/lib/rehearsal/plan.mjs +89 -14
- package/scripts/lib/rehearsal/runtime_restore.mjs +6 -2
- package/scripts/lib/rehearsal/setup.mjs +60 -28
- package/scripts/lib/runtime/postgresql_runtime.mjs +772 -0
- package/scripts/lib/runtime/runtime_target.mjs +41 -0
- package/scripts/lib/runtime/supabase_runtime.mjs +792 -0
- package/scripts/operations/database/manage_rehearsal_database.mjs +6 -785
- package/scripts/operations/rehearsal/rehearsal_cli.mjs +330 -31
|
@@ -3,7 +3,7 @@ export type RehearsalConfigVersion = 1;
|
|
|
3
3
|
export interface RehearsalConfig {
|
|
4
4
|
schemaVersion: RehearsalConfigVersion;
|
|
5
5
|
project: { name: string };
|
|
6
|
-
supabase
|
|
6
|
+
supabase?: {
|
|
7
7
|
workdir: string;
|
|
8
8
|
migrationDirectory: string;
|
|
9
9
|
rehearsalConfig: string;
|
|
@@ -11,10 +11,23 @@ export interface RehearsalConfig {
|
|
|
11
11
|
serviceEnvironmentFile?: string;
|
|
12
12
|
serviceEnvironmentVariables?: string[];
|
|
13
13
|
};
|
|
14
|
+
postgresql?: {
|
|
15
|
+
migrationDirectory: string;
|
|
16
|
+
runtimeWorkdir?: string;
|
|
17
|
+
image?: string;
|
|
18
|
+
database?: string;
|
|
19
|
+
user?: string;
|
|
20
|
+
};
|
|
14
21
|
baseline: {
|
|
15
22
|
artifactDirectory?: string;
|
|
16
23
|
sanitizationPolicy: string;
|
|
17
24
|
};
|
|
25
|
+
containerRuntime?: {
|
|
26
|
+
autoStartColima?: boolean;
|
|
27
|
+
};
|
|
28
|
+
cleanup?: {
|
|
29
|
+
retainBaselineGenerations?: number;
|
|
30
|
+
};
|
|
18
31
|
application: {
|
|
19
32
|
startCommand: string;
|
|
20
33
|
proofCommand: string;
|
|
@@ -22,11 +35,12 @@ export interface RehearsalConfig {
|
|
|
22
35
|
runtimeAdapter?: string;
|
|
23
36
|
};
|
|
24
37
|
runtime: {
|
|
38
|
+
target?: "supabase" | "postgresql";
|
|
25
39
|
applicationUrl?: string;
|
|
26
40
|
projectId?: string;
|
|
27
|
-
apiPort
|
|
41
|
+
apiPort?: number;
|
|
28
42
|
databasePort: number;
|
|
29
|
-
studioPort
|
|
43
|
+
studioPort?: number;
|
|
30
44
|
};
|
|
31
45
|
safety?: {
|
|
32
46
|
allowedHosts?: string[];
|
|
@@ -58,6 +72,13 @@ export declare const renderDetectedConfig: (
|
|
|
58
72
|
ports?: { api: number; database: number; studio: number };
|
|
59
73
|
},
|
|
60
74
|
) => string;
|
|
75
|
+
export declare const renderDetectedPostgresqlConfig: (
|
|
76
|
+
detected: unknown,
|
|
77
|
+
options?: {
|
|
78
|
+
applicationUrl?: string;
|
|
79
|
+
databasePort?: number;
|
|
80
|
+
},
|
|
81
|
+
) => string;
|
|
61
82
|
export declare const SANITIZATION_ACTIONS: Readonly<{
|
|
62
83
|
KEEP: "KEEP";
|
|
63
84
|
PSEUDONYMIZE: "PSEUDONYMIZE";
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { access, readFile, stat } from "node:fs/promises";
|
|
8
8
|
import { basename, isAbsolute, join, relative, resolve } from "node:path";
|
|
9
9
|
import { pathToFileURL } from "node:url";
|
|
10
|
+
import { resolveRuntimeTarget } from "../runtime/runtime_target.mjs";
|
|
10
11
|
|
|
11
12
|
export {
|
|
12
13
|
EXCLUDED_VALUE,
|
|
@@ -22,6 +23,8 @@ export const REHEARSAL_CONFIG_VERSION = 1;
|
|
|
22
23
|
|
|
23
24
|
export const REHEARSAL_DEFAULTS = Object.freeze({
|
|
24
25
|
baseline: Object.freeze({ artifactDirectory: ".rehearsal" }),
|
|
26
|
+
containerRuntime: Object.freeze({ autoStartColima: true }),
|
|
27
|
+
cleanup: Object.freeze({ retainBaselineGenerations: 2 }),
|
|
25
28
|
runtime: Object.freeze({
|
|
26
29
|
applicationUrl: "http://localhost:5175",
|
|
27
30
|
projectId: "rehearsal-local",
|
|
@@ -29,6 +32,11 @@ export const REHEARSAL_DEFAULTS = Object.freeze({
|
|
|
29
32
|
safety: Object.freeze({
|
|
30
33
|
allowedHosts: Object.freeze(["127.0.0.1", "::1", "localhost"]),
|
|
31
34
|
blockedEnvironmentVariables: Object.freeze([
|
|
35
|
+
"DATABASE_URL",
|
|
36
|
+
"PGHOST",
|
|
37
|
+
"PGPASSWORD",
|
|
38
|
+
"PGPORT",
|
|
39
|
+
"PGUSER",
|
|
32
40
|
"SUPABASE_ACCESS_TOKEN",
|
|
33
41
|
"SUPABASE_DB_PASSWORD",
|
|
34
42
|
"SUPABASE_PROJECT_ID",
|
|
@@ -48,6 +56,8 @@ const CONFIG_FILENAMES = Object.freeze([
|
|
|
48
56
|
|
|
49
57
|
const LOOPBACK_HOSTS = new Set(REHEARSAL_DEFAULTS.safety.allowedHosts);
|
|
50
58
|
const IDENTIFIER_PATTERN = /^[a-z0-9][a-z0-9-]{1,62}$/u;
|
|
59
|
+
const DATABASE_IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]{0,62}$/u;
|
|
60
|
+
const POSTGRES_IMAGE_PATTERN = /^postgres:\d+(?:\.\d+)?-alpine$/u;
|
|
51
61
|
const SAFE_COMMAND_PATTERN = /^[^\n\r\0]+$/u;
|
|
52
62
|
|
|
53
63
|
const isPlainObject = (value) =>
|
|
@@ -103,6 +113,20 @@ const assertPort = (value, path) => {
|
|
|
103
113
|
return value;
|
|
104
114
|
};
|
|
105
115
|
|
|
116
|
+
const assertBoolean = (value, path) => {
|
|
117
|
+
if (typeof value !== "boolean") {
|
|
118
|
+
throw new Error(`${path} must be true or false.`);
|
|
119
|
+
}
|
|
120
|
+
return value;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const assertPositiveInteger = (value, path) => {
|
|
124
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
125
|
+
throw new Error(`${path} must be a positive integer.`);
|
|
126
|
+
}
|
|
127
|
+
return value;
|
|
128
|
+
};
|
|
129
|
+
|
|
106
130
|
const assertStringArray = (value, path) => {
|
|
107
131
|
if (
|
|
108
132
|
!Array.isArray(value) ||
|
|
@@ -141,7 +165,10 @@ const normalizeConfig = (input) => {
|
|
|
141
165
|
"schemaVersion",
|
|
142
166
|
"project",
|
|
143
167
|
"supabase",
|
|
168
|
+
"postgresql",
|
|
144
169
|
"baseline",
|
|
170
|
+
"containerRuntime",
|
|
171
|
+
"cleanup",
|
|
145
172
|
"application",
|
|
146
173
|
"runtime",
|
|
147
174
|
"safety",
|
|
@@ -164,25 +191,63 @@ const normalizeConfig = (input) => {
|
|
|
164
191
|
);
|
|
165
192
|
}
|
|
166
193
|
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
supabase
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
"
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
194
|
+
const runtimeTarget = resolveRuntimeTarget(root.runtime?.target).id;
|
|
195
|
+
const supabase =
|
|
196
|
+
runtimeTarget === "supabase"
|
|
197
|
+
? assertPlainObject(root.supabase, "config.supabase")
|
|
198
|
+
: null;
|
|
199
|
+
const postgresql =
|
|
200
|
+
runtimeTarget === "postgresql"
|
|
201
|
+
? assertPlainObject(root.postgresql, "config.postgresql")
|
|
202
|
+
: null;
|
|
203
|
+
if (runtimeTarget === "supabase" && root.postgresql !== undefined) {
|
|
204
|
+
throw new Error(
|
|
205
|
+
"config.postgresql is not allowed when config.runtime.target is supabase.",
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
if (runtimeTarget === "postgresql" && root.supabase !== undefined) {
|
|
209
|
+
throw new Error(
|
|
210
|
+
"config.supabase is not allowed when config.runtime.target is postgresql.",
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
if (supabase) {
|
|
214
|
+
assertKnownKeys(
|
|
215
|
+
supabase,
|
|
216
|
+
[
|
|
217
|
+
"workdir",
|
|
218
|
+
"migrationDirectory",
|
|
219
|
+
"rehearsalConfig",
|
|
220
|
+
"runtimeWorkdir",
|
|
221
|
+
"serviceEnvironmentFile",
|
|
222
|
+
"serviceEnvironmentVariables",
|
|
223
|
+
],
|
|
224
|
+
"config.supabase",
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
if (postgresql) {
|
|
228
|
+
assertKnownKeys(
|
|
229
|
+
postgresql,
|
|
230
|
+
["migrationDirectory", "runtimeWorkdir", "image", "database", "user"],
|
|
231
|
+
"config.postgresql",
|
|
232
|
+
);
|
|
233
|
+
}
|
|
180
234
|
const baseline = assertPlainObject(root.baseline, "config.baseline");
|
|
181
235
|
assertKnownKeys(
|
|
182
236
|
baseline,
|
|
183
237
|
["artifactDirectory", "sanitizationPolicy"],
|
|
184
238
|
"config.baseline",
|
|
185
239
|
);
|
|
240
|
+
const containerRuntime = assertPlainObject(
|
|
241
|
+
root.containerRuntime ?? {},
|
|
242
|
+
"config.containerRuntime",
|
|
243
|
+
);
|
|
244
|
+
assertKnownKeys(
|
|
245
|
+
containerRuntime,
|
|
246
|
+
["autoStartColima"],
|
|
247
|
+
"config.containerRuntime",
|
|
248
|
+
);
|
|
249
|
+
const cleanup = assertPlainObject(root.cleanup ?? {}, "config.cleanup");
|
|
250
|
+
assertKnownKeys(cleanup, ["retainBaselineGenerations"], "config.cleanup");
|
|
186
251
|
const application = assertPlainObject(root.application, "config.application");
|
|
187
252
|
assertKnownKeys(
|
|
188
253
|
application,
|
|
@@ -192,7 +257,14 @@ const normalizeConfig = (input) => {
|
|
|
192
257
|
const runtime = assertPlainObject(root.runtime ?? {}, "config.runtime");
|
|
193
258
|
assertKnownKeys(
|
|
194
259
|
runtime,
|
|
195
|
-
[
|
|
260
|
+
[
|
|
261
|
+
"target",
|
|
262
|
+
"applicationUrl",
|
|
263
|
+
"projectId",
|
|
264
|
+
"apiPort",
|
|
265
|
+
"databasePort",
|
|
266
|
+
"studioPort",
|
|
267
|
+
],
|
|
196
268
|
"config.runtime",
|
|
197
269
|
);
|
|
198
270
|
const safety = assertPlainObject(root.safety ?? {}, "config.safety");
|
|
@@ -234,9 +306,13 @@ const normalizeConfig = (input) => {
|
|
|
234
306
|
}
|
|
235
307
|
|
|
236
308
|
const ports = {
|
|
237
|
-
|
|
309
|
+
...(runtimeTarget === "supabase"
|
|
310
|
+
? { api: assertPort(runtime.apiPort, "config.runtime.apiPort") }
|
|
311
|
+
: {}),
|
|
238
312
|
database: assertPort(runtime.databasePort, "config.runtime.databasePort"),
|
|
239
|
-
|
|
313
|
+
...(runtimeTarget === "supabase"
|
|
314
|
+
? { studio: assertPort(runtime.studioPort, "config.runtime.studioPort") }
|
|
315
|
+
: {}),
|
|
240
316
|
};
|
|
241
317
|
if (new Set(Object.values(ports)).size !== Object.values(ports).length) {
|
|
242
318
|
throw new Error("Rehearsal runtime ports must be unique.");
|
|
@@ -247,7 +323,7 @@ const normalizeConfig = (input) => {
|
|
|
247
323
|
throw new Error("config.verification.commands must be an array.");
|
|
248
324
|
}
|
|
249
325
|
const serviceEnvironmentVariables =
|
|
250
|
-
supabase
|
|
326
|
+
supabase?.serviceEnvironmentVariables ?? [];
|
|
251
327
|
if (!Array.isArray(serviceEnvironmentVariables)) {
|
|
252
328
|
throw new Error(
|
|
253
329
|
"config.supabase.serviceEnvironmentVariables must be an array.",
|
|
@@ -269,41 +345,105 @@ const normalizeConfig = (input) => {
|
|
|
269
345
|
);
|
|
270
346
|
}
|
|
271
347
|
if (
|
|
272
|
-
Boolean(supabase
|
|
348
|
+
Boolean(supabase?.serviceEnvironmentFile) !==
|
|
273
349
|
Boolean(normalizedServiceEnvironmentVariables.length)
|
|
274
350
|
) {
|
|
275
351
|
throw new Error(
|
|
276
352
|
"config.supabase.serviceEnvironmentFile and serviceEnvironmentVariables must be configured together.",
|
|
277
353
|
);
|
|
278
354
|
}
|
|
355
|
+
const authenticationProviders = safety.authenticationProviders
|
|
356
|
+
? assertStringArray(
|
|
357
|
+
safety.authenticationProviders,
|
|
358
|
+
"config.safety.authenticationProviders",
|
|
359
|
+
)
|
|
360
|
+
: [];
|
|
361
|
+
if (runtimeTarget === "postgresql" && authenticationProviders.length > 0) {
|
|
362
|
+
throw new Error(
|
|
363
|
+
"config.safety.authenticationProviders is available only for the Supabase target.",
|
|
364
|
+
);
|
|
365
|
+
}
|
|
279
366
|
|
|
280
367
|
return Object.freeze({
|
|
281
368
|
schemaVersion: REHEARSAL_CONFIG_VERSION,
|
|
282
369
|
project: Object.freeze({ name: projectName }),
|
|
283
|
-
supabase:
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
supabase.
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
370
|
+
supabase: supabase
|
|
371
|
+
? Object.freeze({
|
|
372
|
+
workdir: assertRelativePath(
|
|
373
|
+
supabase.workdir,
|
|
374
|
+
"config.supabase.workdir",
|
|
375
|
+
),
|
|
376
|
+
migrationDirectory: assertRelativePath(
|
|
377
|
+
supabase.migrationDirectory,
|
|
378
|
+
"config.supabase.migrationDirectory",
|
|
379
|
+
),
|
|
380
|
+
rehearsalConfig: assertRelativePath(
|
|
381
|
+
supabase.rehearsalConfig,
|
|
382
|
+
"config.supabase.rehearsalConfig",
|
|
383
|
+
),
|
|
384
|
+
runtimeWorkdir: assertRelativePath(
|
|
385
|
+
supabase.runtimeWorkdir,
|
|
386
|
+
"config.supabase.runtimeWorkdir",
|
|
387
|
+
),
|
|
388
|
+
serviceEnvironmentFile: supabase.serviceEnvironmentFile
|
|
389
|
+
? assertRelativePath(
|
|
390
|
+
supabase.serviceEnvironmentFile,
|
|
391
|
+
"config.supabase.serviceEnvironmentFile",
|
|
392
|
+
)
|
|
393
|
+
: null,
|
|
394
|
+
serviceEnvironmentVariables: Object.freeze(
|
|
395
|
+
normalizedServiceEnvironmentVariables,
|
|
396
|
+
),
|
|
397
|
+
})
|
|
398
|
+
: null,
|
|
399
|
+
postgresql: postgresql
|
|
400
|
+
? Object.freeze({
|
|
401
|
+
migrationDirectory: assertRelativePath(
|
|
402
|
+
postgresql.migrationDirectory,
|
|
403
|
+
"config.postgresql.migrationDirectory",
|
|
404
|
+
),
|
|
405
|
+
runtimeWorkdir: assertRelativePath(
|
|
406
|
+
postgresql.runtimeWorkdir ?? ".rehearsal/runtime",
|
|
407
|
+
"config.postgresql.runtimeWorkdir",
|
|
408
|
+
),
|
|
409
|
+
image: (() => {
|
|
410
|
+
const image = assertNonEmptyString(
|
|
411
|
+
postgresql.image ?? "postgres:17-alpine",
|
|
412
|
+
"config.postgresql.image",
|
|
413
|
+
);
|
|
414
|
+
if (!POSTGRES_IMAGE_PATTERN.test(image)) {
|
|
415
|
+
throw new Error(
|
|
416
|
+
"config.postgresql.image must name an official versioned Alpine PostgreSQL image, such as postgres:17-alpine.",
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
return image;
|
|
420
|
+
})(),
|
|
421
|
+
database: (() => {
|
|
422
|
+
const database = assertNonEmptyString(
|
|
423
|
+
postgresql.database ?? "postgres",
|
|
424
|
+
"config.postgresql.database",
|
|
425
|
+
);
|
|
426
|
+
if (!DATABASE_IDENTIFIER_PATTERN.test(database)) {
|
|
427
|
+
throw new Error(
|
|
428
|
+
"config.postgresql.database must be a safe lowercase PostgreSQL identifier.",
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
return database;
|
|
432
|
+
})(),
|
|
433
|
+
user: (() => {
|
|
434
|
+
const user = assertNonEmptyString(
|
|
435
|
+
postgresql.user ?? "postgres",
|
|
436
|
+
"config.postgresql.user",
|
|
437
|
+
);
|
|
438
|
+
if (!DATABASE_IDENTIFIER_PATTERN.test(user)) {
|
|
439
|
+
throw new Error(
|
|
440
|
+
"config.postgresql.user must be a safe lowercase PostgreSQL identifier.",
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
return user;
|
|
444
|
+
})(),
|
|
445
|
+
})
|
|
446
|
+
: null,
|
|
307
447
|
baseline: Object.freeze({
|
|
308
448
|
artifactDirectory: assertRelativePath(
|
|
309
449
|
baseline.artifactDirectory ??
|
|
@@ -315,6 +455,20 @@ const normalizeConfig = (input) => {
|
|
|
315
455
|
"config.baseline.sanitizationPolicy",
|
|
316
456
|
),
|
|
317
457
|
}),
|
|
458
|
+
containerRuntime: Object.freeze({
|
|
459
|
+
autoStartColima: assertBoolean(
|
|
460
|
+
containerRuntime.autoStartColima ??
|
|
461
|
+
REHEARSAL_DEFAULTS.containerRuntime.autoStartColima,
|
|
462
|
+
"config.containerRuntime.autoStartColima",
|
|
463
|
+
),
|
|
464
|
+
}),
|
|
465
|
+
cleanup: Object.freeze({
|
|
466
|
+
retainBaselineGenerations: assertPositiveInteger(
|
|
467
|
+
cleanup.retainBaselineGenerations ??
|
|
468
|
+
REHEARSAL_DEFAULTS.cleanup.retainBaselineGenerations,
|
|
469
|
+
"config.cleanup.retainBaselineGenerations",
|
|
470
|
+
),
|
|
471
|
+
}),
|
|
318
472
|
application: Object.freeze({
|
|
319
473
|
startCommand: assertCommand(
|
|
320
474
|
application.startCommand,
|
|
@@ -337,6 +491,7 @@ const normalizeConfig = (input) => {
|
|
|
337
491
|
),
|
|
338
492
|
}),
|
|
339
493
|
runtime: Object.freeze({
|
|
494
|
+
target: runtimeTarget,
|
|
340
495
|
applicationUrl: assertLoopbackUrl(
|
|
341
496
|
runtime.applicationUrl ?? REHEARSAL_DEFAULTS.runtime.applicationUrl,
|
|
342
497
|
"config.runtime.applicationUrl",
|
|
@@ -357,14 +512,7 @@ const normalizeConfig = (input) => {
|
|
|
357
512
|
"config.safety.blockedEnvironmentVariables",
|
|
358
513
|
),
|
|
359
514
|
),
|
|
360
|
-
authenticationProviders: Object.freeze(
|
|
361
|
-
safety.authenticationProviders
|
|
362
|
-
? assertStringArray(
|
|
363
|
-
safety.authenticationProviders,
|
|
364
|
-
"config.safety.authenticationProviders",
|
|
365
|
-
)
|
|
366
|
-
: [],
|
|
367
|
-
),
|
|
515
|
+
authenticationProviders: Object.freeze(authenticationProviders),
|
|
368
516
|
hostedAccess: "disabled",
|
|
369
517
|
outboundNetwork: "deny",
|
|
370
518
|
}),
|
|
@@ -428,14 +576,22 @@ export const loadRehearsalConfig = async ({
|
|
|
428
576
|
applicationEnvironment: resolveOwnedPath(
|
|
429
577
|
config.application.environmentFile,
|
|
430
578
|
),
|
|
431
|
-
migrationDirectory: resolveOwnedPath(
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
579
|
+
migrationDirectory: resolveOwnedPath(
|
|
580
|
+
(config.supabase ?? config.postgresql).migrationDirectory,
|
|
581
|
+
),
|
|
582
|
+
rehearsalConfig: config.supabase
|
|
583
|
+
? resolveOwnedPath(config.supabase.rehearsalConfig)
|
|
584
|
+
: null,
|
|
585
|
+
runtimeWorkdir: resolveOwnedPath(
|
|
586
|
+
(config.supabase ?? config.postgresql).runtimeWorkdir,
|
|
587
|
+
),
|
|
588
|
+
serviceEnvironment: config.supabase?.serviceEnvironmentFile
|
|
435
589
|
? resolveOwnedPath(config.supabase.serviceEnvironmentFile)
|
|
436
590
|
: null,
|
|
437
591
|
sanitizationPolicy: resolveOwnedPath(config.baseline.sanitizationPolicy),
|
|
438
|
-
supabaseWorkdir:
|
|
592
|
+
supabaseWorkdir: config.supabase
|
|
593
|
+
? resolveOwnedPath(config.supabase.workdir)
|
|
594
|
+
: null,
|
|
439
595
|
runtimeAdapter: config.application.runtimeAdapter
|
|
440
596
|
? resolveOwnedPath(config.application.runtimeAdapter)
|
|
441
597
|
: null,
|
|
@@ -448,7 +604,7 @@ export const loadRehearsalConfig = async ({
|
|
|
448
604
|
const requiredRuntimeWorkdir = join(paths.artifactDirectory, "runtime");
|
|
449
605
|
if (paths.runtimeWorkdir !== requiredRuntimeWorkdir) {
|
|
450
606
|
throw new Error(
|
|
451
|
-
|
|
607
|
+
`config.${config.runtime.target}.runtimeWorkdir must resolve to the runtime directory inside config.baseline.artifactDirectory.`,
|
|
452
608
|
);
|
|
453
609
|
}
|
|
454
610
|
const environmentRelativePath = relative(
|
|
@@ -496,6 +652,13 @@ export const inspectDetectedProject = async ({
|
|
|
496
652
|
? "yarn"
|
|
497
653
|
: "npm";
|
|
498
654
|
const scripts = packageJson.scripts ?? {};
|
|
655
|
+
const postgresqlMigrationDirectory = (
|
|
656
|
+
await Promise.all(
|
|
657
|
+
["database/migrations", "db/migrations", "migrations"].map(
|
|
658
|
+
async (path) => ({ path, exists: await hasPath(path) }),
|
|
659
|
+
),
|
|
660
|
+
)
|
|
661
|
+
).find(({ exists }) => exists)?.path;
|
|
499
662
|
const normalizedProjectName = String(packageJson.name ?? basename(root))
|
|
500
663
|
.toLowerCase()
|
|
501
664
|
.replace(/[^a-z0-9-]+/gu, "-")
|
|
@@ -508,6 +671,9 @@ export const inspectDetectedProject = async ({
|
|
|
508
671
|
packageManager,
|
|
509
672
|
hasSupabaseConfig: await hasPath("supabase/config.toml"),
|
|
510
673
|
hasMigrations: await hasPath("supabase/migrations"),
|
|
674
|
+
hasPostgresqlMigrations: Boolean(postgresqlMigrationDirectory),
|
|
675
|
+
postgresqlMigrationDirectory:
|
|
676
|
+
postgresqlMigrationDirectory ?? "database/migrations",
|
|
511
677
|
applicationCommand:
|
|
512
678
|
["dev:rehearsal", "dev", "start"]
|
|
513
679
|
.find((name) => typeof scripts[name] === "string")
|
|
@@ -526,34 +692,70 @@ export const renderDetectedConfig = (
|
|
|
526
692
|
applicationUrl = "http://localhost:5175",
|
|
527
693
|
ports = { api: 58321, database: 58322, studio: 58323 },
|
|
528
694
|
} = {},
|
|
529
|
-
) =>
|
|
695
|
+
) =>
|
|
696
|
+
`// @ts-check
|
|
697
|
+
/**
|
|
698
|
+
* Rehearsal configuration for ${detected.projectName}.
|
|
699
|
+
* Generated from this project by \`npx rehearsal\` and safe to commit.
|
|
700
|
+
* Review the CHECK comments. Keep passwords, tokens, and production URLs out of this file.
|
|
701
|
+
* Docs: https://github.com/Ddupasquier/rehearsal-db/blob/main/docs/configuration.md
|
|
702
|
+
*/
|
|
530
703
|
import { defineRehearsalConfig } from "@rehearsal-db/core";
|
|
531
704
|
|
|
532
705
|
export default defineRehearsalConfig({
|
|
706
|
+
// Configuration format. Rehearsal will explain if an upgrade is ever needed.
|
|
533
707
|
schemaVersion: 1,
|
|
708
|
+
// Stable local name used in Rehearsal labels and reports.
|
|
534
709
|
project: { name: ${JSON.stringify(detected.projectName)} },
|
|
710
|
+
|
|
711
|
+
// Project files Rehearsal reads. Every path stays inside this repository.
|
|
535
712
|
supabase: {
|
|
536
713
|
workdir: ".",
|
|
537
714
|
migrationDirectory: "supabase/migrations",
|
|
538
715
|
rehearsalConfig: "infrastructure/rehearsal/supabase/config.toml",
|
|
539
716
|
runtimeWorkdir: ".rehearsal/runtime",
|
|
717
|
+
|
|
718
|
+
// Optional local-only identity-provider credentials. Enable both keys together.
|
|
719
|
+
// serviceEnvironmentFile: ".env.rehearsal-service.local",
|
|
720
|
+
// serviceEnvironmentVariables: ["LOCAL_IDP_CLIENT_ID", "LOCAL_IDP_SECRET"],
|
|
540
721
|
},
|
|
722
|
+
|
|
723
|
+
// Immutable local baseline files. Rehearsal never reads production on your behalf.
|
|
541
724
|
baseline: {
|
|
542
725
|
artifactDirectory: ".rehearsal",
|
|
543
726
|
sanitizationPolicy: "infrastructure/rehearsal/sanitization-policy.json",
|
|
544
727
|
},
|
|
728
|
+
|
|
729
|
+
// Reuse any running Docker-compatible engine; start Colima only when needed.
|
|
730
|
+
containerRuntime: {
|
|
731
|
+
autoStartColima: true,
|
|
732
|
+
},
|
|
733
|
+
|
|
734
|
+
// Keep this many baseline generations, always including the active one.
|
|
735
|
+
cleanup: {
|
|
736
|
+
retainBaselineGenerations: 2,
|
|
737
|
+
},
|
|
738
|
+
|
|
545
739
|
application: {
|
|
740
|
+
// CHECK: commands detected from package.json. Change them if they are not correct.
|
|
546
741
|
startCommand: ${JSON.stringify(detected.applicationCommand)},
|
|
547
742
|
proofCommand: ${JSON.stringify(detected.verificationCommand)},
|
|
548
743
|
environmentFile: ".rehearsal/runtime.env",
|
|
744
|
+
// Optional project-specific restore hook:
|
|
745
|
+
// runtimeAdapter: "infrastructure/rehearsal/runtime-adapter.mjs",
|
|
549
746
|
},
|
|
747
|
+
|
|
748
|
+
// Disposable local runtime identity, URL, and dedicated ports.
|
|
550
749
|
runtime: {
|
|
750
|
+
target: "supabase",
|
|
551
751
|
applicationUrl: ${JSON.stringify(applicationUrl)},
|
|
552
752
|
projectId: "${detected.projectName}-rehearsal",
|
|
553
753
|
apiPort: ${ports.api},
|
|
554
754
|
databasePort: ${ports.database},
|
|
555
755
|
studioPort: ${ports.studio},
|
|
556
756
|
},
|
|
757
|
+
|
|
758
|
+
// Only local runtime URLs are accepted; common hosted credentials are quarantined.
|
|
557
759
|
safety: {
|
|
558
760
|
allowedHosts: ["127.0.0.1", "::1", "localhost"],
|
|
559
761
|
blockedEnvironmentVariables: [
|
|
@@ -564,6 +766,82 @@ export default defineRehearsalConfig({
|
|
|
564
766
|
hostedAccess: "disabled",
|
|
565
767
|
outboundNetwork: "deny",
|
|
566
768
|
},
|
|
567
|
-
verification: { commands: [${JSON.stringify(detected.verificationCommand)}] },
|
|
568
769
|
});
|
|
569
|
-
|
|
770
|
+
`.replaceAll("\t", " ");
|
|
771
|
+
|
|
772
|
+
export const renderDetectedPostgresqlConfig = (
|
|
773
|
+
detected,
|
|
774
|
+
{ applicationUrl = "http://localhost:5175", databasePort = 58322 } = {},
|
|
775
|
+
) =>
|
|
776
|
+
`// @ts-check
|
|
777
|
+
/**
|
|
778
|
+
* Rehearsal configuration for ${detected.projectName}.
|
|
779
|
+
* Generated from this project by \`npx rehearsal\` and safe to commit.
|
|
780
|
+
* Review the CHECK comments. Keep passwords, tokens, and production URLs out of this file.
|
|
781
|
+
* Docs: https://github.com/Ddupasquier/rehearsal-db/blob/main/docs/configuration.md
|
|
782
|
+
*/
|
|
783
|
+
import { defineRehearsalConfig } from "@rehearsal-db/core";
|
|
784
|
+
|
|
785
|
+
export default defineRehearsalConfig({
|
|
786
|
+
// Configuration format. Rehearsal will explain if an upgrade is ever needed.
|
|
787
|
+
schemaVersion: 1,
|
|
788
|
+
// Stable local name used in Rehearsal labels and reports.
|
|
789
|
+
project: { name: ${JSON.stringify(detected.projectName)} },
|
|
790
|
+
|
|
791
|
+
// Disposable PostgreSQL settings. The image must already exist locally.
|
|
792
|
+
postgresql: {
|
|
793
|
+
migrationDirectory: ${JSON.stringify(detected.postgresqlMigrationDirectory)},
|
|
794
|
+
runtimeWorkdir: ".rehearsal/runtime",
|
|
795
|
+
image: "postgres:17-alpine",
|
|
796
|
+
database: "postgres",
|
|
797
|
+
user: "postgres",
|
|
798
|
+
},
|
|
799
|
+
|
|
800
|
+
// Immutable local baseline files. Rehearsal never reads production on your behalf.
|
|
801
|
+
baseline: {
|
|
802
|
+
artifactDirectory: ".rehearsal",
|
|
803
|
+
sanitizationPolicy: "infrastructure/rehearsal/sanitization-policy.json",
|
|
804
|
+
},
|
|
805
|
+
|
|
806
|
+
// Reuse any running Docker-compatible engine; start Colima only when needed.
|
|
807
|
+
containerRuntime: {
|
|
808
|
+
autoStartColima: true,
|
|
809
|
+
},
|
|
810
|
+
|
|
811
|
+
// Keep this many baseline generations, always including the active one.
|
|
812
|
+
cleanup: {
|
|
813
|
+
retainBaselineGenerations: 2,
|
|
814
|
+
},
|
|
815
|
+
|
|
816
|
+
application: {
|
|
817
|
+
// CHECK: commands detected from package.json. Change them if they are not correct.
|
|
818
|
+
startCommand: ${JSON.stringify(detected.applicationCommand)},
|
|
819
|
+
proofCommand: ${JSON.stringify(detected.verificationCommand)},
|
|
820
|
+
environmentFile: ".rehearsal/runtime.env",
|
|
821
|
+
// Optional project-specific restore hook:
|
|
822
|
+
// runtimeAdapter: "infrastructure/rehearsal/runtime-adapter.mjs",
|
|
823
|
+
},
|
|
824
|
+
|
|
825
|
+
// Disposable local runtime identity, URL, and dedicated database port.
|
|
826
|
+
runtime: {
|
|
827
|
+
target: "postgresql",
|
|
828
|
+
applicationUrl: ${JSON.stringify(applicationUrl)},
|
|
829
|
+
projectId: "${detected.projectName}-rehearsal",
|
|
830
|
+
databasePort: ${databasePort},
|
|
831
|
+
},
|
|
832
|
+
|
|
833
|
+
// Only local runtime URLs are accepted; common hosted credentials are quarantined.
|
|
834
|
+
safety: {
|
|
835
|
+
allowedHosts: ["127.0.0.1", "::1", "localhost"],
|
|
836
|
+
blockedEnvironmentVariables: [
|
|
837
|
+
"DATABASE_URL",
|
|
838
|
+
"PGHOST",
|
|
839
|
+
"PGPASSWORD",
|
|
840
|
+
"PGPORT",
|
|
841
|
+
"PGUSER",
|
|
842
|
+
],
|
|
843
|
+
hostedAccess: "disabled",
|
|
844
|
+
outboundNetwork: "deny",
|
|
845
|
+
},
|
|
846
|
+
});
|
|
847
|
+
`.replaceAll("\t", " ");
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
8
|
|
|
9
9
|
export const REHEARSAL_RESULT_VERSION = 1;
|
|
10
|
-
export const REHEARSAL_VERSION = "0.1.0-beta.
|
|
10
|
+
export const REHEARSAL_VERSION = "0.1.0-beta.7";
|
|
11
11
|
|
|
12
12
|
export const REHEARSAL_EXIT_CODES = Object.freeze({
|
|
13
13
|
success: 0,
|
|
@@ -109,7 +109,9 @@ const inferCategory = (error) => {
|
|
|
109
109
|
return "unsafe_environment";
|
|
110
110
|
}
|
|
111
111
|
if (
|
|
112
|
-
/docker|supabase|runtime|command|enoent|node\.js|ports?\b/iu.test(
|
|
112
|
+
/docker|postgres(?:ql)?|supabase|runtime|command|enoent|node\.js|ports?\b/iu.test(
|
|
113
|
+
message,
|
|
114
|
+
)
|
|
113
115
|
) {
|
|
114
116
|
return "runtime_dependency_failure";
|
|
115
117
|
}
|