@celilo/e2e 0.8.0 → 0.9.1
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 +26 -0
- package/bin/e2e-bake-management +221 -85
- package/docker/Dockerfile.management +6 -0
- package/package.json +3 -3
- package/scripts/pack-celilo-packages.ts +10 -0
- package/simulators/namecheap-ddns/zone-updater.test.ts +31 -0
- package/simulators/namecheap-ddns/zone-updater.ts +26 -0
- package/src/cli/build.test.ts +47 -0
- package/src/cli/build.ts +283 -11
- package/src/cli/command-registry.ts +3 -1
- package/src/cli/completion.ts +2 -0
- package/src/cli/index.ts +4 -1
- package/src/cli-version-contract.test.ts +22 -0
- package/src/cli-version-contract.ts +37 -0
- package/src/container-manager.ts +503 -618
- package/src/docker-compose-generator.ts +5 -3
- package/src/network-builder.ts +20 -28
- package/src/runner.ts +74 -0
- package/src/shared-infra.ts +2 -28
- package/src/types.ts +16 -1
package/src/cli/build.ts
CHANGED
|
@@ -19,10 +19,11 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { execSync, spawnSync } from 'node:child_process';
|
|
22
|
-
import { cpSync, existsSync, mkdirSync, readdirSync, rmSync } from 'node:fs';
|
|
22
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
23
23
|
import { basename, join, resolve } from 'node:path';
|
|
24
|
-
import {
|
|
24
|
+
import { gunzipSync } from 'node:zlib';
|
|
25
25
|
import { stageAptRepo } from '../../scripts/stage-apt-repo';
|
|
26
|
+
import { ensureRegistryServerBundle } from '../registry-bundle';
|
|
26
27
|
|
|
27
28
|
/** Walk up from pkgDir to find the monorepo root (contains a `modules/` dir). */
|
|
28
29
|
function findMonorepoRoot(pkgDir: string): string | null {
|
|
@@ -56,6 +57,13 @@ interface BuildOptions {
|
|
|
56
57
|
moduleDirs: string[];
|
|
57
58
|
save: boolean;
|
|
58
59
|
skipModules: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Bake `management:latest` from the PUBLISHED @celilo/cli on real npm
|
|
62
|
+
* instead of the monorepo DEV cli served by the sim registry. Runs a
|
|
63
|
+
* standalone container on the default docker network (real internet) —
|
|
64
|
+
* the simulated topology has no route to npmjs.org.
|
|
65
|
+
*/
|
|
66
|
+
published: boolean;
|
|
59
67
|
}
|
|
60
68
|
|
|
61
69
|
/**
|
|
@@ -75,7 +83,7 @@ interface BuildOptions {
|
|
|
75
83
|
* keeps build-infra working in npm-installed @celilo/e2e, where these
|
|
76
84
|
* staging dirs aren't relevant.
|
|
77
85
|
*/
|
|
78
|
-
function stageSimulatorInputs(pkgDir: string): void {
|
|
86
|
+
async function stageSimulatorInputs(pkgDir: string): Promise<void> {
|
|
79
87
|
// Try CWD first — when cele2e is installed globally, `pkgDir` is
|
|
80
88
|
// somewhere like `~/.bun/install/global/node_modules/@celilo/e2e`,
|
|
81
89
|
// which doesn't have a `modules/` sibling. Walking up from CWD
|
|
@@ -84,7 +92,13 @@ function stageSimulatorInputs(pkgDir: string): void {
|
|
|
84
92
|
// Mirrors the same CWD-then-pkgDir pattern used for module
|
|
85
93
|
// discovery further down.
|
|
86
94
|
const repoRoot = findMonorepoRoot(process.cwd()) ?? findMonorepoRoot(pkgDir);
|
|
87
|
-
if (!repoRoot)
|
|
95
|
+
if (!repoRoot) {
|
|
96
|
+
// No monorepo (npm-installed @celilo/e2e) → fetch the same caches from
|
|
97
|
+
// the public celilo sources instead of building them from workspace
|
|
98
|
+
// source, so build-infra still works for a monorepo-free consumer (ce-i2i).
|
|
99
|
+
await stageFromPublic(pkgDir);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
88
102
|
|
|
89
103
|
const websiteSrc = join(repoRoot, 'modules', 'celilo-website', 'site');
|
|
90
104
|
const websiteCache = join(pkgDir, '.celilo-website-cache');
|
|
@@ -154,6 +168,201 @@ function stageSimulatorInputs(pkgDir: string): void {
|
|
|
154
168
|
console.log('');
|
|
155
169
|
}
|
|
156
170
|
|
|
171
|
+
/**
|
|
172
|
+
* The published @celilo/* packages the npm-registry-sim must serve so
|
|
173
|
+
* install.sh's `bun add -g @celilo/cli @celilo/event-bus @celilo/e2e`
|
|
174
|
+
* resolves its full transitive closure against the sim. Mirrors
|
|
175
|
+
* PACKAGES_TO_PACK in scripts/pack-celilo-packages.ts (the monorepo
|
|
176
|
+
* equivalent that packs from workspace source instead of fetching).
|
|
177
|
+
*/
|
|
178
|
+
const PUBLISHED_CELILO_PACKAGES = [
|
|
179
|
+
'@celilo/cli',
|
|
180
|
+
'@celilo/capabilities',
|
|
181
|
+
'@celilo/cli-display',
|
|
182
|
+
'@celilo/e2e',
|
|
183
|
+
'@celilo/event-bus',
|
|
184
|
+
] as const;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* npm-consumer counterpart to the monorepo staging above (ce-i2i). With no
|
|
188
|
+
* `modules/` source tree to build from, fetch the sim-content caches from
|
|
189
|
+
* the public celilo sources so `cele2e build-infra` still produces working
|
|
190
|
+
* simulator images. Everything is pulled over the public internet, which
|
|
191
|
+
* build-infra already requires (docker base images, the caddy apt repo, ACME):
|
|
192
|
+
*
|
|
193
|
+
* .npm-registry-cache/ ← published @celilo/* tarballs (npm registry)
|
|
194
|
+
* .celilo-website-cache/ ← install.sh from https://celilo.computer (all the
|
|
195
|
+
* bake step's `curl celilo.computer/install.sh` needs)
|
|
196
|
+
* .apt-repo-cache/pool/ ← real celilo + celilo-bootstrap .debs fetched from
|
|
197
|
+
* apt.celilo.computer (only bootstrap-apt needs them).
|
|
198
|
+
*
|
|
199
|
+
* Sources are overridable via CELILO_E2E_NPM_REGISTRY / CELILO_E2E_WEBSITE_BASE
|
|
200
|
+
* / CELILO_E2E_APT_BASE (used by the monorepo-free CI smoke test, ce-1pe).
|
|
201
|
+
*/
|
|
202
|
+
async function stageFromPublic(pkgDir: string): Promise<void> {
|
|
203
|
+
console.log(`${bold}Staging simulator inputs (npm-consumer mode)...${reset}\n`);
|
|
204
|
+
|
|
205
|
+
const registry = (process.env.CELILO_E2E_NPM_REGISTRY ?? 'https://registry.npmjs.org').replace(
|
|
206
|
+
/\/+$/,
|
|
207
|
+
'',
|
|
208
|
+
);
|
|
209
|
+
const websiteBase = (process.env.CELILO_E2E_WEBSITE_BASE ?? 'https://celilo.computer').replace(
|
|
210
|
+
/\/+$/,
|
|
211
|
+
'',
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
// (a) npm-registry-cache: the published @celilo/* tarballs.
|
|
215
|
+
const npmCache = join(pkgDir, '.npm-registry-cache');
|
|
216
|
+
rmSync(npmCache, { recursive: true, force: true });
|
|
217
|
+
mkdirSync(npmCache, { recursive: true });
|
|
218
|
+
process.stdout.write(` ${'npm-registry (fetch)'.padEnd(28)} `);
|
|
219
|
+
const t0 = Date.now();
|
|
220
|
+
for (const pkg of PUBLISHED_CELILO_PACKAGES) {
|
|
221
|
+
await fetchPublishedTarball(pkg, npmCache, registry);
|
|
222
|
+
}
|
|
223
|
+
console.log(
|
|
224
|
+
`${green}✔${reset} ${dim}${Math.round((Date.now() - t0) / 1000)}s, ${PUBLISHED_CELILO_PACKAGES.length} tarball(s)${reset}`,
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
// (b) celilo-website-cache: the bake step's `curl celilo.computer/install.sh`
|
|
228
|
+
// only needs install.sh served, so fetch it (bootstrap.sh best-effort — it's
|
|
229
|
+
// referenced by install.sh's help text but not needed for the bake path).
|
|
230
|
+
const websiteCache = join(pkgDir, '.celilo-website-cache');
|
|
231
|
+
rmSync(websiteCache, { recursive: true, force: true });
|
|
232
|
+
mkdirSync(websiteCache, { recursive: true });
|
|
233
|
+
process.stdout.write(` ${'celilo-website (fetch)'.padEnd(28)} `);
|
|
234
|
+
const t1 = Date.now();
|
|
235
|
+
await fetchSiteFile(websiteBase, 'install.sh', websiteCache, { required: true });
|
|
236
|
+
await fetchSiteFile(websiteBase, 'bootstrap.sh', websiteCache, { required: false });
|
|
237
|
+
console.log(`${green}✔${reset} ${dim}${Math.round((Date.now() - t1) / 1000)}s${reset}`);
|
|
238
|
+
|
|
239
|
+
// (c) apt-repo-cache/pool: the real celilo + celilo-bootstrap .debs, fetched
|
|
240
|
+
// from apt.celilo.computer so the bootstrap-apt test's `apt install
|
|
241
|
+
// celilo-bootstrap` resolves. Only that test exercises this path; if the
|
|
242
|
+
// fetch fails we leave the pool empty (build-infra still succeeds for
|
|
243
|
+
// everyone) and bootstrap-apt fails loudly with the sim's "empty repo".
|
|
244
|
+
const aptBase = (process.env.CELILO_E2E_APT_BASE ?? 'https://apt.celilo.computer').replace(
|
|
245
|
+
/\/+$/,
|
|
246
|
+
'',
|
|
247
|
+
);
|
|
248
|
+
const pool = join(pkgDir, '.apt-repo-cache', 'pool');
|
|
249
|
+
rmSync(join(pkgDir, '.apt-repo-cache'), { recursive: true, force: true });
|
|
250
|
+
mkdirSync(pool, { recursive: true });
|
|
251
|
+
process.stdout.write(` ${'apt-repo (fetch)'.padEnd(28)} `);
|
|
252
|
+
const t2 = Date.now();
|
|
253
|
+
try {
|
|
254
|
+
const count = await fetchAptPool(aptBase, pool);
|
|
255
|
+
console.log(
|
|
256
|
+
`${green}✔${reset} ${dim}${Math.round((Date.now() - t2) / 1000)}s, ${count} deb(s)${reset}`,
|
|
257
|
+
);
|
|
258
|
+
} catch (err) {
|
|
259
|
+
console.log(
|
|
260
|
+
`${yellow}empty${reset} ${dim}(${err instanceof Error ? err.message : String(err)})${reset}`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
console.log('');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Fetch the celilo + celilo-bootstrap .debs from a real apt repo laid out like
|
|
269
|
+
* apt.celilo.computer (`deb <base>/ stable main`) into `pool`. Parses the
|
|
270
|
+
* per-arch Packages index for its `Filename: pool/...` entries and downloads
|
|
271
|
+
* each referenced .deb (deduped across arches, so the arch-all bootstrap deb is
|
|
272
|
+
* fetched once). Returns the number of debs staged; throws if none were found.
|
|
273
|
+
*/
|
|
274
|
+
export async function fetchAptPool(base: string, pool: string): Promise<number> {
|
|
275
|
+
const filenames = new Set<string>();
|
|
276
|
+
for (const arch of ['amd64', 'arm64']) {
|
|
277
|
+
const dir = `${base}/dists/stable/main/binary-${arch}`;
|
|
278
|
+
const packages = await fetchPackagesIndex(dir);
|
|
279
|
+
for (const line of packages.split('\n')) {
|
|
280
|
+
const m = line.match(/^Filename:\s*(\S+)/);
|
|
281
|
+
if (m) filenames.add(m[1]);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (filenames.size === 0) {
|
|
285
|
+
throw new Error(`no debs in ${base} stable/main — repo layout changed?`);
|
|
286
|
+
}
|
|
287
|
+
for (const filename of filenames) {
|
|
288
|
+
const url = `${base}/${filename}`;
|
|
289
|
+
const res = await fetch(url);
|
|
290
|
+
if (!res.ok) throw new Error(`fetching ${url}: HTTP ${res.status}`);
|
|
291
|
+
writeFileSync(join(pool, basename(filename)), Buffer.from(await res.arrayBuffer()));
|
|
292
|
+
}
|
|
293
|
+
return filenames.size;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Fetch a Packages index from an apt binary-<arch> dir, uncompressed or .gz. */
|
|
297
|
+
async function fetchPackagesIndex(dir: string): Promise<string> {
|
|
298
|
+
const plain = await fetch(`${dir}/Packages`);
|
|
299
|
+
if (plain.ok) return await plain.text();
|
|
300
|
+
const gz = await fetch(`${dir}/Packages.gz`);
|
|
301
|
+
if (!gz.ok) throw new Error(`fetching ${dir}/Packages(.gz): HTTP ${gz.status}`);
|
|
302
|
+
return gunzipSync(Buffer.from(await gz.arrayBuffer())).toString('utf8');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Download a package's `latest` published tarball from the npm registry into
|
|
307
|
+
* destDir. Uses the registry's own tarball filename so the npm-registry-sim
|
|
308
|
+
* (which indexes tarballs by their embedded package.json) serves it verbatim.
|
|
309
|
+
*/
|
|
310
|
+
export async function fetchPublishedTarball(
|
|
311
|
+
pkg: string,
|
|
312
|
+
destDir: string,
|
|
313
|
+
registry: string,
|
|
314
|
+
): Promise<void> {
|
|
315
|
+
const packumentUrl = `${registry}/${pkg}`;
|
|
316
|
+
const packumentRes = await fetch(packumentUrl);
|
|
317
|
+
if (!packumentRes.ok) {
|
|
318
|
+
throw new Error(
|
|
319
|
+
`Fetching packument for ${pkg} (${packumentUrl}): HTTP ${packumentRes.status}. ` +
|
|
320
|
+
`Is ${pkg} published to ${registry}?`,
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
const packument = (await packumentRes.json()) as {
|
|
324
|
+
'dist-tags'?: Record<string, string>;
|
|
325
|
+
versions?: Record<string, { dist?: { tarball?: string } }>;
|
|
326
|
+
};
|
|
327
|
+
const latest = packument['dist-tags']?.latest;
|
|
328
|
+
const tarballUrl = latest ? packument.versions?.[latest]?.dist?.tarball : undefined;
|
|
329
|
+
if (!latest || !tarballUrl) {
|
|
330
|
+
throw new Error(`${pkg}: registry ${registry} has no dist-tags.latest tarball`);
|
|
331
|
+
}
|
|
332
|
+
const tgzRes = await fetch(tarballUrl);
|
|
333
|
+
if (!tgzRes.ok) {
|
|
334
|
+
throw new Error(`Fetching tarball for ${pkg}@${latest} (${tarballUrl}): HTTP ${tgzRes.status}`);
|
|
335
|
+
}
|
|
336
|
+
const filename =
|
|
337
|
+
tarballUrl.split('/').pop() ?? `${pkg.replace('@', '').replace('/', '-')}-${latest}.tgz`;
|
|
338
|
+
writeFileSync(join(destDir, filename), Buffer.from(await tgzRes.arrayBuffer()));
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Download a single file from the published site into destDir. Throws on a
|
|
343
|
+
* missing required file; silently skips an absent optional one.
|
|
344
|
+
*/
|
|
345
|
+
export async function fetchSiteFile(
|
|
346
|
+
base: string,
|
|
347
|
+
name: string,
|
|
348
|
+
destDir: string,
|
|
349
|
+
opts: { required: boolean },
|
|
350
|
+
): Promise<void> {
|
|
351
|
+
const url = `${base}/${name}`;
|
|
352
|
+
let res: Response;
|
|
353
|
+
try {
|
|
354
|
+
res = await fetch(url);
|
|
355
|
+
} catch (err) {
|
|
356
|
+
if (opts.required) throw err;
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (!res.ok) {
|
|
360
|
+
if (opts.required) throw new Error(`Fetching ${url}: HTTP ${res.status}`);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
writeFileSync(join(destDir, name), Buffer.from(await res.arrayBuffer()));
|
|
364
|
+
}
|
|
365
|
+
|
|
157
366
|
function packageNetapp(moduleDir: string, netappsDir: string): void {
|
|
158
367
|
const absDir = resolve(moduleDir);
|
|
159
368
|
if (!existsSync(absDir)) {
|
|
@@ -201,6 +410,58 @@ function packageNetapp(moduleDir: string, netappsDir: string): void {
|
|
|
201
410
|
console.log(`${green}✔${reset} ${dim}${elapsed}s ${size}${reset}`);
|
|
202
411
|
}
|
|
203
412
|
|
|
413
|
+
/**
|
|
414
|
+
* Consumer (npm-installed) staging: fetch standard-module .netapps from the
|
|
415
|
+
* public celilo registry into `netappsDir`. In the monorepo we package these
|
|
416
|
+
* from source (packageNetapp); an npm consumer has no `modules/` source, so
|
|
417
|
+
* getRegistryVolumes() binds `./netapps` into the sim registry and we fill it
|
|
418
|
+
* here at build-infra time (which has network — the test RUN stays hermetic
|
|
419
|
+
* against the local sim). No version-pinning: the capability system governs
|
|
420
|
+
* compatibility at import time (validateCapabilityAccess), surfacing a
|
|
421
|
+
* mismatch as an actionable import error rather than a brittle lockfile.
|
|
422
|
+
*/
|
|
423
|
+
export async function stageNetappsFromRegistry(netappsDir: string): Promise<void> {
|
|
424
|
+
const base = (process.env.CELILO_REGISTRY_URL ?? 'https://celilo.computer/registry').replace(
|
|
425
|
+
/\/+$/,
|
|
426
|
+
'',
|
|
427
|
+
);
|
|
428
|
+
console.log(`${bold}Fetching standard-module netapps from registry...${reset}`);
|
|
429
|
+
console.log(` ${dim}${base}${reset}\n`);
|
|
430
|
+
|
|
431
|
+
// ponytail: single page (per_page=100). Paginate if >100 modules ever ship.
|
|
432
|
+
const listUrl = `${base}/api/v1/modules?per_page=100`;
|
|
433
|
+
const listResp = await fetch(listUrl, { signal: AbortSignal.timeout(30_000) });
|
|
434
|
+
if (!listResp.ok) {
|
|
435
|
+
console.error(`${red}Registry list failed: HTTP ${listResp.status} ${listUrl}${reset}`);
|
|
436
|
+
process.exit(1);
|
|
437
|
+
}
|
|
438
|
+
const { modules } = (await listResp.json()) as {
|
|
439
|
+
modules: Array<{ name: string; max_version: string }>;
|
|
440
|
+
};
|
|
441
|
+
if (!modules || modules.length === 0) {
|
|
442
|
+
console.error(`${red}Registry returned no modules — nothing to stage${reset}`);
|
|
443
|
+
process.exit(1);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
for (const { name, max_version } of modules) {
|
|
447
|
+
process.stdout.write(` ${name.padEnd(20)} `);
|
|
448
|
+
const t0 = Date.now();
|
|
449
|
+
const dlUrl = `${base}/api/v1/modules/${encodeURIComponent(name)}/${encodeURIComponent(max_version)}/download`;
|
|
450
|
+
const resp = await fetch(dlUrl, { signal: AbortSignal.timeout(60_000) });
|
|
451
|
+
if (!resp.ok) {
|
|
452
|
+
console.log(`${red}✗${reset}`);
|
|
453
|
+
console.error(`${red} download failed: HTTP ${resp.status} ${dlUrl}${reset}`);
|
|
454
|
+
process.exit(1);
|
|
455
|
+
}
|
|
456
|
+
const bytes = new Uint8Array(await resp.arrayBuffer());
|
|
457
|
+
writeFileSync(join(netappsDir, `${name}.netapp`), bytes);
|
|
458
|
+
console.log(
|
|
459
|
+
`${green}✔${reset} ${dim}${max_version} ${Math.round((Date.now() - t0) / 1000)}s ${(bytes.length / 1024).toFixed(0)}KB${reset}`,
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
console.log('');
|
|
463
|
+
}
|
|
464
|
+
|
|
204
465
|
function buildDockerImages(pkgDir: string): void {
|
|
205
466
|
// Refresh the bundled registry-server source before docker build —
|
|
206
467
|
// Dockerfile.registry copies from <pkgDir>/registry-server, which is
|
|
@@ -295,7 +556,7 @@ function tagVanillaManagement(): void {
|
|
|
295
556
|
* build-infra so the operator can't accidentally proceed with a
|
|
296
557
|
* non-functional default management image.
|
|
297
558
|
*/
|
|
298
|
-
function bakeManagement(pkgDir: string): void {
|
|
559
|
+
function bakeManagement(pkgDir: string, published: boolean): void {
|
|
299
560
|
const bakeScript = join(pkgDir, 'bin', 'e2e-bake-management');
|
|
300
561
|
if (!existsSync(bakeScript)) {
|
|
301
562
|
console.error(
|
|
@@ -305,9 +566,14 @@ function bakeManagement(pkgDir: string): void {
|
|
|
305
566
|
process.exit(1);
|
|
306
567
|
}
|
|
307
568
|
|
|
308
|
-
console.log(
|
|
569
|
+
console.log(
|
|
570
|
+
published
|
|
571
|
+
? `${bold}Baking PUBLISHED @celilo/cli into management:latest from real npm...${reset}\n`
|
|
572
|
+
: `${bold}Baking celilo into management:latest via install.sh...${reset}\n`,
|
|
573
|
+
);
|
|
309
574
|
const start = Date.now();
|
|
310
|
-
const
|
|
575
|
+
const bakeArgs = ['run', bakeScript, ...(published ? ['--published'] : [])];
|
|
576
|
+
const result = spawnSync('bun', bakeArgs, { stdio: 'inherit' });
|
|
311
577
|
const elapsed = Math.round((Date.now() - start) / 1000);
|
|
312
578
|
|
|
313
579
|
if (result.status !== 0) {
|
|
@@ -381,8 +647,8 @@ function saveImages(pkgDir: string): void {
|
|
|
381
647
|
console.log('\nTo restore after colima restart:\n cele2e load');
|
|
382
648
|
}
|
|
383
649
|
|
|
384
|
-
export function runBuild(options: BuildOptions): void {
|
|
385
|
-
const { pkgDir, moduleDirs, save, skipModules } = options;
|
|
650
|
+
export async function runBuild(options: BuildOptions): Promise<void> {
|
|
651
|
+
const { pkgDir, moduleDirs, save, skipModules, published } = options;
|
|
386
652
|
const netappsDir = join(pkgDir, 'netapps');
|
|
387
653
|
mkdirSync(netappsDir, { recursive: true });
|
|
388
654
|
|
|
@@ -390,7 +656,7 @@ export function runBuild(options: BuildOptions): void {
|
|
|
390
656
|
// and npm-registry-sim COPY from the staged dirs at docker-build time,
|
|
391
657
|
// so they have to exist (and reflect current workspace state) before
|
|
392
658
|
// buildDockerImages() runs.
|
|
393
|
-
stageSimulatorInputs(pkgDir);
|
|
659
|
+
await stageSimulatorInputs(pkgDir);
|
|
394
660
|
|
|
395
661
|
if (!skipModules) {
|
|
396
662
|
// Explicit dirs take precedence; fall back to auto-discovering all
|
|
@@ -412,6 +678,12 @@ export function runBuild(options: BuildOptions): void {
|
|
|
412
678
|
packageNetapp(dir, netappsDir);
|
|
413
679
|
}
|
|
414
680
|
console.log('');
|
|
681
|
+
} else {
|
|
682
|
+
// npm consumer: no `modules/` source to package from — fetch the
|
|
683
|
+
// standard-module netapps from the registry so the sim registry
|
|
684
|
+
// (which binds ./netapps) has something to serve. Consumers need
|
|
685
|
+
// zero vendored module binaries.
|
|
686
|
+
await stageNetappsFromRegistry(netappsDir);
|
|
415
687
|
}
|
|
416
688
|
}
|
|
417
689
|
|
|
@@ -429,7 +701,7 @@ export function runBuild(options: BuildOptions): void {
|
|
|
429
701
|
// simulated celilo.computer / npm registry. This is what makes the
|
|
430
702
|
// default management image every test uses an actual product of
|
|
431
703
|
// install.sh, rather than a parallel `bun add -g` pinning.
|
|
432
|
-
bakeManagement(pkgDir);
|
|
704
|
+
bakeManagement(pkgDir, published);
|
|
433
705
|
|
|
434
706
|
// Pre-seed heavy app images so app-zone deploys load them from the cache
|
|
435
707
|
// instead of pulling GB through the sim (ISS-0154).
|
|
@@ -34,6 +34,8 @@ export const COMMANDS: CommandDef[] = [
|
|
|
34
34
|
{ name: '--live', description: 'Use live (non-simulated) internet' },
|
|
35
35
|
{ name: '--published', description: 'Use published .netapp packages' },
|
|
36
36
|
{ name: '--notify', description: 'Desktop notification when the run finishes' },
|
|
37
|
+
{ name: '--shuffle', description: 'Randomize test order to surface order-dependent bugs (seed is logged)' },
|
|
38
|
+
{ name: '--seed=', description: 'Replay a specific --shuffle order (e.g. --seed=12345)' },
|
|
37
39
|
],
|
|
38
40
|
},
|
|
39
41
|
{
|
|
@@ -111,4 +113,4 @@ export const RUN_FLAGS = ['--all', '--complete', '--ci-safe', '--all-modules', '
|
|
|
111
113
|
|
|
112
114
|
export const DOWN_FLAGS = ['--keep', '--all'];
|
|
113
115
|
|
|
114
|
-
export const BUILD_INFRA_FLAGS = ['--save'];
|
|
116
|
+
export const BUILD_INFRA_FLAGS = ['--save', '--skip-modules', '--published'];
|
package/src/cli/completion.ts
CHANGED
|
@@ -138,6 +138,8 @@ _cele2e() {
|
|
|
138
138
|
build-infra)
|
|
139
139
|
_arguments \\
|
|
140
140
|
'--save[Save Docker images to tarball after build]' \\
|
|
141
|
+
'--skip-modules[Skip .netapp packaging (only rebuild Docker images)]' \\
|
|
142
|
+
'--published[Bake management:latest with the published @celilo/cli from real npm]' \\
|
|
141
143
|
'*:module directory:_directories'
|
|
142
144
|
;;
|
|
143
145
|
shell)
|
package/src/cli/index.ts
CHANGED
|
@@ -157,6 +157,8 @@ Options for \`down\`:
|
|
|
157
157
|
Options for \`build-infra\`:
|
|
158
158
|
--save Save Docker images to tarball after build
|
|
159
159
|
--skip-modules Skip .netapp packaging (only rebuild Docker images)
|
|
160
|
+
--published Bake management:latest with the PUBLISHED @celilo/cli from
|
|
161
|
+
real npm (default: bake the monorepo DEV cli via the sim)
|
|
160
162
|
|
|
161
163
|
Examples:
|
|
162
164
|
cele2e run --all # FULL regression: every module suite + top-level e2e/tests/
|
|
@@ -373,6 +375,7 @@ switch (command) {
|
|
|
373
375
|
case 'build-infra': {
|
|
374
376
|
const save = args.includes('--save');
|
|
375
377
|
const skipModules = args.includes('--skip-modules');
|
|
378
|
+
const published = args.includes('--published');
|
|
376
379
|
const moduleDirs = args.filter((a) => !a.startsWith('--'));
|
|
377
380
|
// build-infra mutates the shared Docker images — serialize against runs.
|
|
378
381
|
// Released via the lock's exit handler (runBuild may process.exit on failure).
|
|
@@ -385,7 +388,7 @@ switch (command) {
|
|
|
385
388
|
}
|
|
386
389
|
throw err;
|
|
387
390
|
}
|
|
388
|
-
runBuild({ pkgDir: PKG_DIR, moduleDirs, save, skipModules });
|
|
391
|
+
await runBuild({ pkgDir: PKG_DIR, moduleDirs, save, skipModules, published });
|
|
389
392
|
break;
|
|
390
393
|
}
|
|
391
394
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test';
|
|
2
|
+
import { checkCliVersion } from './cli-version-contract';
|
|
3
|
+
|
|
4
|
+
// ce-5qp: harness<->CLI version contract fails fast on a stale management image.
|
|
5
|
+
|
|
6
|
+
test('accepts a CLI at or above the minimum', () => {
|
|
7
|
+
expect(checkCliVersion('celilo 0.9.1', '0.9.0')).toBeNull();
|
|
8
|
+
expect(checkCliVersion('celilo 0.9.0', '0.9.0')).toBeNull();
|
|
9
|
+
expect(checkCliVersion('celilo 1.2.3', '0.9.0')).toBeNull();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('rejects a stale CLI with an actionable message', () => {
|
|
13
|
+
const msg = checkCliVersion('celilo 0.5.0-alpha.5', '0.9.0');
|
|
14
|
+
expect(msg).toContain('celilo 0.5.0-alpha.5');
|
|
15
|
+
expect(msg).toContain('>=0.9.0');
|
|
16
|
+
expect(msg).toContain('build-infra');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('reports unparseable version output instead of silently passing', () => {
|
|
20
|
+
expect(checkCliVersion('command not found', '0.9.0')).toContain('Could not parse');
|
|
21
|
+
expect(checkCliVersion('', '0.9.0')).toContain('Could not parse');
|
|
22
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness<->CLI version contract (ce-5qp). The management image ships a
|
|
3
|
+
* baked-in `celilo` CLI; a published harness run against a stale image
|
|
4
|
+
* (e.g. an old npm consumer's `cele2e build-infra`) used to hang ~90s in
|
|
5
|
+
* `system init` and surface only "canceled". Instead we check the baked CLI
|
|
6
|
+
* version up front and fail fast (<10s) with an actionable message.
|
|
7
|
+
*
|
|
8
|
+
* Bump MIN_CLI_VERSION whenever the harness starts relying on a CLI
|
|
9
|
+
* feature/flag that older CLIs lack. It's the MINIMUM the harness needs,
|
|
10
|
+
* not an exact pin.
|
|
11
|
+
*/
|
|
12
|
+
export const MIN_CLI_VERSION = '0.9.0';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Pure core of the contract: given the raw `celilo --version` stdout, decide
|
|
16
|
+
* whether it satisfies `minVersion`. Returns an actionable error string when
|
|
17
|
+
* it doesn't, or `null` when OK. No I/O, so it's unit-testable without a
|
|
18
|
+
* running management container.
|
|
19
|
+
*/
|
|
20
|
+
export function checkCliVersion(versionStdout: string, minVersion = MIN_CLI_VERSION): string | null {
|
|
21
|
+
// Output is `celilo <version>` (apps/celilo displayVersion). Grab the token.
|
|
22
|
+
const version = versionStdout.match(/celilo\s+(\S+)/)?.[1];
|
|
23
|
+
if (!version) {
|
|
24
|
+
return (
|
|
25
|
+
`Could not parse celilo version from "${versionStdout.trim()}" ` +
|
|
26
|
+
`(harness requires >=${minVersion}). Is the management image's celilo CLI intact?`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
if (Bun.semver.order(version, minVersion) < 0) {
|
|
30
|
+
return (
|
|
31
|
+
`Management image has celilo ${version}, harness requires >=${minVersion}. ` +
|
|
32
|
+
`Rebuild the management image against a newer @celilo/cli ` +
|
|
33
|
+
`(e.g. \`cele2e build-infra\`, or \`--published\` after publishing).`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|