@elench/testkit 0.1.7 → 0.1.8
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/lib/config.mjs +32 -0
- package/lib/runner.mjs +113 -3
- package/package.json +1 -1
package/lib/config.mjs
CHANGED
|
@@ -87,6 +87,14 @@ export function loadConfigs(opts = {}) {
|
|
|
87
87
|
throw new Error(`Secret "${key}" (service "${name}") not found in env or .env`);
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
|
+
// Check dependent secrets
|
|
91
|
+
for (const dep of tk.depends || []) {
|
|
92
|
+
for (const key of dep.fly?.secrets || []) {
|
|
93
|
+
if (!process.env[key]) {
|
|
94
|
+
throw new Error(`Secret "${key}" (service "${name}", dep "${dep.name}") not found in env or .env`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
90
98
|
|
|
91
99
|
// Each service gets its own state dir: .testkit/<service>/
|
|
92
100
|
const stateDir = entries.length === 1
|
|
@@ -193,6 +201,30 @@ function validateService(name, svc, manifestPath) {
|
|
|
193
201
|
if (tk.dal !== undefined && !isObject(tk.dal)) {
|
|
194
202
|
errors.push(`${ctx}: testkit.dal must be an object`);
|
|
195
203
|
}
|
|
204
|
+
|
|
205
|
+
if (tk.depends !== undefined) {
|
|
206
|
+
if (!Array.isArray(tk.depends)) {
|
|
207
|
+
errors.push(`${ctx}: testkit.depends must be an array`);
|
|
208
|
+
} else {
|
|
209
|
+
for (let i = 0; i < tk.depends.length; i++) {
|
|
210
|
+
const dep = tk.depends[i];
|
|
211
|
+
const dp = `${ctx}: testkit.depends[${i}]`;
|
|
212
|
+
if (!isObject(dep)) {
|
|
213
|
+
errors.push(`${dp} must be an object`);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
requireString(errors, dep, `${dp}.name`, "name");
|
|
217
|
+
requireString(errors, dep, `${dp}.dockerfile`, "dockerfile");
|
|
218
|
+
if (!isObject(dep.fly)) {
|
|
219
|
+
errors.push(`${dp}.fly is required and must be an object`);
|
|
220
|
+
} else {
|
|
221
|
+
requireString(errors, dep.fly, `${dp}.fly.app`, "app");
|
|
222
|
+
requireString(errors, dep.fly, `${dp}.fly.org`, "org");
|
|
223
|
+
optionalStringArray(errors, dep.fly, `${dp}.fly.secrets`, "secrets");
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
196
228
|
}
|
|
197
229
|
|
|
198
230
|
if (!isObject(svc.suites)) {
|
package/lib/runner.mjs
CHANGED
|
@@ -96,6 +96,69 @@ export async function flyUp(config) {
|
|
|
96
96
|
});
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Build a dependent service's Docker image.
|
|
101
|
+
*/
|
|
102
|
+
async function buildDep(config, dep) {
|
|
103
|
+
const { productDir, stateDir } = config;
|
|
104
|
+
const depStateDir = path.join(stateDir, "deps", dep.name);
|
|
105
|
+
fs.mkdirSync(depStateDir, { recursive: true });
|
|
106
|
+
await runScript("fly-build.sh", {
|
|
107
|
+
FLY_APP: dep.fly.app,
|
|
108
|
+
FLY_ORG: dep.fly.org,
|
|
109
|
+
API_DIR: productDir,
|
|
110
|
+
DOCKERFILE_DIR: path.join(productDir, dep.dockerfile),
|
|
111
|
+
STATE_DIR: depStateDir,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Deploy a dependent service's Fly machine.
|
|
117
|
+
* Copies database_url from primary, generates fly-env.sh, calls fly-up.sh.
|
|
118
|
+
*/
|
|
119
|
+
async function flyUpDep(config, dep) {
|
|
120
|
+
const { stateDir } = config;
|
|
121
|
+
const depStateDir = path.join(stateDir, "deps", dep.name);
|
|
122
|
+
fs.mkdirSync(depStateDir, { recursive: true });
|
|
123
|
+
|
|
124
|
+
// Copy database_url from primary state dir
|
|
125
|
+
const primaryDbUrl = path.join(stateDir, "database_url");
|
|
126
|
+
if (fs.existsSync(primaryDbUrl)) {
|
|
127
|
+
fs.copyFileSync(primaryDbUrl, path.join(depStateDir, "database_url"));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Generate fly-env.sh for dependent
|
|
131
|
+
const envLines = [];
|
|
132
|
+
for (const [k, v] of Object.entries(dep.fly.env || {})) {
|
|
133
|
+
envLines.push(` --env ${k}=${v}`);
|
|
134
|
+
}
|
|
135
|
+
for (const k of dep.fly.secrets || []) {
|
|
136
|
+
envLines.push(` --env ${k}=$${k}`); // shell substitution at source-time
|
|
137
|
+
}
|
|
138
|
+
const flyEnvPath = path.join(depStateDir, "fly-env.sh");
|
|
139
|
+
fs.writeFileSync(flyEnvPath, `FLY_ENV_FLAGS="\n${envLines.join("\n")}\n"\n`);
|
|
140
|
+
|
|
141
|
+
await runScript("fly-up.sh", {
|
|
142
|
+
FLY_APP: dep.fly.app,
|
|
143
|
+
FLY_ORG: dep.fly.org,
|
|
144
|
+
FLY_REGION: dep.fly.region || "lhr",
|
|
145
|
+
FLY_PORT: dep.fly.port,
|
|
146
|
+
FLY_ENV_FILE: flyEnvPath,
|
|
147
|
+
STATE_DIR: depStateDir,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Stop a dependent service's Fly machine.
|
|
153
|
+
*/
|
|
154
|
+
async function flyDownDep(config, dep) {
|
|
155
|
+
const depStateDir = path.join(config.stateDir, "deps", dep.name);
|
|
156
|
+
await runScript("fly-down.sh", {
|
|
157
|
+
FLY_APP: dep.fly.app,
|
|
158
|
+
STATE_DIR: depStateDir,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
99
162
|
/**
|
|
100
163
|
* Stop the Fly machine (preserved for reuse).
|
|
101
164
|
*/
|
|
@@ -109,10 +172,20 @@ export async function flyDown(config) {
|
|
|
109
172
|
|
|
110
173
|
/**
|
|
111
174
|
* Destroy machine + neon branch + state.
|
|
175
|
+
* Destroys dependent machines first, then primary.
|
|
112
176
|
*/
|
|
113
177
|
export async function destroy(config) {
|
|
114
178
|
const { stateDir, manifest } = config;
|
|
115
179
|
const tk = manifest.testkit;
|
|
180
|
+
|
|
181
|
+
// Destroy dependents first
|
|
182
|
+
for (const dep of tk.depends || []) {
|
|
183
|
+
const depStateDir = path.join(stateDir, "deps", dep.name);
|
|
184
|
+
if (fs.existsSync(depStateDir)) {
|
|
185
|
+
await runScript("fly-destroy.sh", { FLY_APP: dep.fly.app, STATE_DIR: depStateDir });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
116
189
|
await runScript("fly-destroy.sh", { FLY_APP: tk.fly.app, STATE_DIR: stateDir });
|
|
117
190
|
await runScript("neon-down.sh", {
|
|
118
191
|
NEON_PROJECT_ID: tk.neon.projectId,
|
|
@@ -257,6 +330,23 @@ export function showStatus(config) {
|
|
|
257
330
|
const val = fs.readFileSync(filePath, "utf8").trim();
|
|
258
331
|
console.log(` ${file}: ${val}`);
|
|
259
332
|
}
|
|
333
|
+
|
|
334
|
+
// Show dependent state dirs
|
|
335
|
+
const depsDir = path.join(stateDir, "deps");
|
|
336
|
+
if (fs.existsSync(depsDir)) {
|
|
337
|
+
for (const depName of fs.readdirSync(depsDir)) {
|
|
338
|
+
const depDir = path.join(depsDir, depName);
|
|
339
|
+
if (!fs.statSync(depDir).isDirectory()) continue;
|
|
340
|
+
console.log(` ── dep: ${depName} ──`);
|
|
341
|
+
for (const file of fs.readdirSync(depDir)) {
|
|
342
|
+
if (file === "fly-env.sh") continue;
|
|
343
|
+
const filePath = path.join(depDir, file);
|
|
344
|
+
if (fs.statSync(filePath).isDirectory()) continue;
|
|
345
|
+
const val = fs.readFileSync(filePath, "utf8").trim();
|
|
346
|
+
console.log(` ${file}: ${val}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
260
350
|
}
|
|
261
351
|
|
|
262
352
|
/**
|
|
@@ -317,11 +407,27 @@ async function runService(config, suiteType, suiteNames, opts) {
|
|
|
317
407
|
// HTTP flow: build → neon → migrate → fly → run → fly down
|
|
318
408
|
if (httpFiles.length) {
|
|
319
409
|
requireFlyToken(config.name);
|
|
320
|
-
|
|
410
|
+
const deps = manifest.testkit.depends || [];
|
|
411
|
+
|
|
412
|
+
// Phase 1: Build primary + deps in parallel
|
|
413
|
+
if (opts.build) {
|
|
414
|
+
await Promise.all([
|
|
415
|
+
build(config),
|
|
416
|
+
...deps.map(dep => buildDep(config, dep)),
|
|
417
|
+
]);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Phase 2: Database (unchanged)
|
|
321
421
|
await neonUp(config);
|
|
322
422
|
await migrate(config);
|
|
323
423
|
neonReady = true;
|
|
324
|
-
|
|
424
|
+
|
|
425
|
+
// Phase 3: Deploy primary + deps in parallel
|
|
426
|
+
await Promise.all([
|
|
427
|
+
flyUp(config),
|
|
428
|
+
...deps.map(dep => flyUpDep(config, dep)),
|
|
429
|
+
]);
|
|
430
|
+
|
|
325
431
|
try {
|
|
326
432
|
for (const suite of httpSuites) {
|
|
327
433
|
await runSuitePre(config, suite);
|
|
@@ -329,7 +435,11 @@ async function runService(config, suiteType, suiteNames, opts) {
|
|
|
329
435
|
const result = await runTests(config, httpFiles);
|
|
330
436
|
if (result?.failed) failed = true;
|
|
331
437
|
} finally {
|
|
332
|
-
|
|
438
|
+
// Phase 5: Teardown primary + deps in parallel
|
|
439
|
+
await Promise.allSettled([
|
|
440
|
+
flyDown(config),
|
|
441
|
+
...deps.map(dep => flyDownDep(config, dep)),
|
|
442
|
+
]);
|
|
333
443
|
}
|
|
334
444
|
}
|
|
335
445
|
|