@celilo/cli 2.1.0 → 2.2.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/drizzle/0031_module_config_source.sql +20 -0
- package/drizzle/meta/_journal.json +8 -1
- package/package.json +3 -3
- package/schemas/system_config.json +7 -1
- package/src/ansible/inventory.test.ts +2 -1
- package/src/api/sessions.test.ts +2 -1
- package/src/capabilities/public-web-publish.test.ts +61 -0
- package/src/cli/backup-rename.test.ts +2 -1
- package/src/cli/cli.test.ts +2 -1
- package/src/cli/commands/console-get-chain.test.ts +2 -1
- package/src/cli/commands/firewall-interface-list.test.ts +158 -8
- package/src/cli/commands/firewall-interface-list.ts +73 -7
- package/src/cli/commands/machine-add.ts +12 -55
- package/src/cli/commands/module-config.test.ts +22 -2
- package/src/cli/commands/module-deploy.ts +8 -2
- package/src/cli/commands/module-generate.test.ts +53 -0
- package/src/cli/commands/module-generate.ts +31 -26
- package/src/cli/commands/module-import-aspect.test.ts +2 -1
- package/src/cli/commands/module-import-registry.test.ts +2 -1
- package/src/cli/commands/module-import.ts +1 -1
- package/src/cli/commands/module-operations.test.ts +2 -1
- package/src/cli/commands/module-publish.test.ts +5 -12
- package/src/cli/commands/module-update.test.ts +87 -4
- package/src/cli/commands/module-update.ts +14 -4
- package/src/cli/commands/module-upgrade.test.ts +15 -0
- package/src/cli/commands/module-upgrade.ts +54 -2
- package/src/cli/commands/module-verify.test.ts +2 -3
- package/src/cli/commands/module-verify.ts +0 -1
- package/src/cli/commands/monitor.ts +2 -10
- package/src/cli/commands/notify-config.test.ts +5 -3
- package/src/cli/commands/registry-owner.test.ts +2 -1
- package/src/cli/commands/registry-token.test.ts +2 -1
- package/src/cli/commands/restore.ts +16 -6
- package/src/cli/commands/system-apply-config-equivalence.test.ts +5 -7
- package/src/cli/commands/system-config.test.ts +148 -0
- package/src/cli/commands/system-config.ts +26 -1
- package/src/cli/commands/system-doctor.test.ts +71 -0
- package/src/cli/commands/system-doctor.ts +110 -24
- package/src/cli/commands/system-init-deprecation.test.ts +6 -3
- package/src/cli/commands/system-migrate.test.ts +2 -1
- package/src/cli/generate-zsh-completion.ts +1 -1
- package/src/cli/index.ts +6 -4
- package/src/cli/restore-command.test.ts +2 -1
- package/src/cli/restore-migration-failure.test.ts +160 -0
- package/src/config/paths.test.ts +3 -3
- package/src/db/client.ts +5 -0
- package/src/db/migrate.test.ts +62 -135
- package/src/db/migrate.ts +17 -12
- package/src/db/schema.ts +10 -0
- package/src/hooks/broker.test.ts +106 -2
- package/src/hooks/broker.ts +91 -1
- package/src/hooks/capability-loader-firewall.test.ts +37 -0
- package/src/hooks/capability-loader.test.ts +78 -0
- package/src/hooks/capability-loader.ts +52 -3
- package/src/hooks/define-hook.test.ts +4 -3
- package/src/hooks/executor.test.ts +106 -19
- package/src/hooks/executor.ts +120 -13
- package/src/hooks/hook-jail-toolchain-reach.test.ts +3 -2
- package/src/hooks/hook-jail-unreachability.test.ts +4 -3
- package/src/hooks/hook-protocol.ts +46 -1
- package/src/hooks/hook-runner.ts +36 -0
- package/src/hooks/hook-store-proxy.test.ts +109 -0
- package/src/hooks/hook-store-proxy.ts +85 -0
- package/src/hooks/hook-store.test.ts +168 -0
- package/src/hooks/hook-store.ts +290 -0
- package/src/hooks/hook-timeout.test.ts +3 -2
- package/src/hooks/hook-trespass.test.ts +39 -5
- package/src/hooks/jail.test.ts +62 -3
- package/src/hooks/jail.ts +67 -8
- package/src/hooks/mount-set.test.ts +208 -0
- package/src/hooks/mount-set.ts +62 -14
- package/src/hooks/run-named-hook.ts +2 -0
- package/src/hooks/test-fixtures/jail-probe-hook.ts +1 -1
- package/src/hooks/test-fixtures/on-restore-staging-hook.ts +26 -0
- package/src/hooks/test-fixtures/store-backed.ts +47 -0
- package/src/hooks/test-fixtures/store-writing-hook.ts +63 -0
- package/src/hooks/unjailed-lint.test.ts +22 -6
- package/src/manifest/schema.ts +1 -0
- package/src/module/packaging/audit.ts +9 -26
- package/src/module/packaging/build-paths.test.ts +127 -0
- package/src/module/packaging/build-paths.ts +175 -0
- package/src/module/packaging/build.test.ts +71 -1
- package/src/module/packaging/build.ts +130 -2
- package/src/module/packaging/extract.ts +1 -5
- package/src/module/web-root.ts +17 -1
- package/src/policy/fixture-capability-coverage.test.ts +322 -0
- package/src/policy/module-script-scan.test.ts +42 -1
- package/src/policy/module-script-scan.ts +235 -5
- package/src/policy/no-hand-built-ssh.test.ts +34 -1
- package/src/policy/no-swallowed-refusal.test.ts +264 -0
- package/src/policy/no-tar-shell-out-in-services.test.ts +43 -0
- package/src/registry/client.test.ts +149 -0
- package/src/registry/client.ts +203 -11
- package/src/services/alerting/ack.test.ts +2 -1
- package/src/services/alerting/cadence-migration.test.ts +3 -2
- package/src/services/alerting/coverage-source.test.ts +2 -1
- package/src/services/alerting/deferral.test.ts +2 -1
- package/src/services/alerting/delivery-loop.test.ts +2 -1
- package/src/services/alerting/deploy-hooks.test.ts +2 -1
- package/src/services/alerting/format.test.ts +57 -0
- package/src/services/alerting/format.ts +24 -0
- package/src/services/alerting/inbound-poller.test.ts +2 -1
- package/src/services/alerting/inbound.test.ts +2 -1
- package/src/services/alerting/notification-responder.test.ts +2 -1
- package/src/services/alerting/run-monitor.test.ts +2 -1
- package/src/services/alerting/run-monitor.ts +2 -2
- package/src/services/alerting/store.test.ts +2 -1
- package/src/services/alerting/sweep-runner.test.ts +2 -1
- package/src/services/alerting/tokens.test.ts +2 -1
- package/src/services/aspect-approvals.test.ts +2 -1
- package/src/services/aspect-reconcile.test.ts +4 -3
- package/src/services/aspect-runner.test.ts +2 -1
- package/src/services/audit/module-integrity.test.ts +0 -21
- package/src/services/audit/module-integrity.ts +0 -14
- package/src/services/backup-age-agreement.test.ts +2 -1
- package/src/services/backup-create.ts +7 -7
- package/src/services/backup-envelope-roundtrip.test.ts +47 -3
- package/src/services/backup-in-flight-refusal.test.ts +2 -1
- package/src/services/backup-restore.ts +8 -4
- package/src/services/bus-ensure-flow.test.ts +2 -1
- package/src/services/bus-interview-park.test.ts +2 -1
- package/src/services/bus-interview.ts +37 -14
- package/src/services/bus-secret-flow.test.ts +2 -1
- package/src/services/capability-table-rows.test.ts +2 -1
- package/src/services/config-provenance.ts +4 -0
- package/src/services/consumer-cleanup.test.ts +3 -2
- package/src/services/container-service.test.ts +2 -1
- package/src/services/control-plane-bootstrap.test.ts +123 -2
- package/src/services/control-plane-bootstrap.ts +51 -4
- package/src/services/cross-module-read.test.ts +2 -1
- package/src/services/deploy-preflight.ts +8 -2
- package/src/services/deploy-validation.test.ts +25 -2
- package/src/services/deploy-validation.ts +8 -0
- package/src/services/dns-discovery.test.ts +54 -0
- package/src/services/dns-discovery.ts +47 -5
- package/src/services/dns-internal-records.test.ts +3 -2
- package/src/services/dns-provider-backfill.test.ts +2 -1
- package/src/services/dns-registrations.test.ts +2 -1
- package/src/services/ensure-interview.test.ts +3 -2
- package/src/services/fleet-checks.test.ts +3 -2
- package/src/services/fleet-key.test.ts +68 -3
- package/src/services/fleet-key.ts +54 -0
- package/src/services/health-runner.ts +2 -0
- package/src/services/infrastructure-selector.test.ts +2 -1
- package/src/services/infrastructure-variable-resolver.test.ts +2 -1
- package/src/services/machine-pool.test.ts +2 -1
- package/src/services/module-config.test.ts +2 -1
- package/src/services/module-config.ts +20 -2
- package/src/services/module-deploy.dns-repoint.test.ts +188 -0
- package/src/services/module-deploy.ts +199 -21
- package/src/services/module-operations.test.ts +2 -1
- package/src/services/module-subscriptions.test.ts +2 -1
- package/src/services/module-validator/git-hygiene.test.ts +122 -3
- package/src/services/module-validator/git-hygiene.ts +83 -14
- package/src/services/port-forwards.test.ts +2 -1
- package/src/services/programmatic-responder.aspect.test.ts +2 -1
- package/src/services/proxmox-reconcile.test.ts +2 -1
- package/src/services/restore-from-file.test.ts +23 -2
- package/src/services/restore-from-file.ts +21 -6
- package/src/services/restore-preflight.test.ts +2 -1
- package/src/services/secret-schema-loader.test.ts +2 -1
- package/src/services/ssh-key-manager.test.ts +2 -1
- package/src/services/static-content-converge.test.ts +144 -5
- package/src/services/static-content-converge.ts +87 -17
- package/src/services/system-config-schema-types.ts +1 -1
- package/src/services/system-config-validator.test.ts +36 -0
- package/src/services/system-config-validator.ts +11 -0
- package/src/services/system-state-stage.test.ts +2 -1
- package/src/services/trusted-sources.test.ts +33 -2
- package/src/services/trusted-sources.ts +47 -10
- package/src/services/zone-detector.test.ts +2 -1
- package/src/templates/generator.ts +9 -2
- package/src/test-utils/bus-responder.ts +5 -3
- package/src/test-utils/db-path.ts +25 -0
- package/src/test-utils/integration.ts +7 -0
- package/src/test-utils/module-fixtures.ts +5 -6
- package/src/variables/context.ts +16 -5
- package/src/module/packaging/generated-plane.test.ts +0 -79
- package/src/module/packaging/generated-plane.ts +0 -134
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Coordinates full module deployment workflow across all phases
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { rmSync } from 'node:fs';
|
|
7
8
|
import { join } from 'node:path';
|
|
8
9
|
import { ProgressDisplay } from '@celilo/cli-display';
|
|
9
10
|
import { and, eq } from 'drizzle-orm';
|
|
@@ -11,9 +12,10 @@ import { generateInventory } from '../ansible/inventory';
|
|
|
11
12
|
import { FuelGauge } from '../cli/fuel-gauge';
|
|
12
13
|
import { log, setActiveDisplay } from '../cli/prompts';
|
|
13
14
|
import type { DbClient } from '../db/client';
|
|
14
|
-
import { capabilities, machines, modules } from '../db/schema';
|
|
15
|
+
import { capabilities, machines, modules, systemConfig } from '../db/schema';
|
|
15
16
|
import { loadCapabilityFunctions } from '../hooks/capability-loader';
|
|
16
17
|
import { invokeHook } from '../hooks/executor';
|
|
18
|
+
import { createHookStores } from '../hooks/hook-store';
|
|
17
19
|
import { createGaugeLogger } from '../hooks/logger';
|
|
18
20
|
import { describeArtifacts } from '../hooks/types';
|
|
19
21
|
import type { HookDefinition, HookLogger, HookResult } from '../hooks/types';
|
|
@@ -22,6 +24,7 @@ import { decryptSecret } from '../secrets/encryption';
|
|
|
22
24
|
import { getOrCreateMasterKey } from '../secrets/master-key';
|
|
23
25
|
import { buildResolutionContext } from '../variables/context';
|
|
24
26
|
import { maybeRunAspectForTrigger } from './aspect-runner';
|
|
27
|
+
import { askConfirm } from './bus-interview';
|
|
25
28
|
import {
|
|
26
29
|
autoDeriveMachineConfig,
|
|
27
30
|
findEnsureOnProvider,
|
|
@@ -40,6 +43,7 @@ import { CONTROL_PLANE_MODULE_ID } from './deployed-systems';
|
|
|
40
43
|
import { getModuleSystems } from './deployed-systems';
|
|
41
44
|
import { E2E_CONFLICT_MESSAGE, runningE2eContainers } from './e2e-guard';
|
|
42
45
|
import { resolveInfrastructureVariables } from './infrastructure-variable-resolver';
|
|
46
|
+
import { InterviewAbandonedError, InterviewUnansweredError } from './interview-errors';
|
|
43
47
|
import { findMachineForModule } from './machine-pool';
|
|
44
48
|
import { ensureRequiredNetworks } from './network-ensure';
|
|
45
49
|
import {
|
|
@@ -101,6 +105,17 @@ export interface DeployOptions {
|
|
|
101
105
|
* this mode — that requires a real hook run.
|
|
102
106
|
*/
|
|
103
107
|
stopAfterInterview?: boolean;
|
|
108
|
+
/**
|
|
109
|
+
* D4 (control-plane-stops-building-modules): retain the generated project
|
|
110
|
+
* after a successful deploy instead of deleting it. The tree is ephemeral
|
|
111
|
+
* by design — rendered for this deploy from the signed payload plus live
|
|
112
|
+
* config, used, then removed — because a persisted generated tree cannot be
|
|
113
|
+
* validated (only its verbatim role assets have a meaningful digest, the
|
|
114
|
+
* rest is rendered and SUPPOSED to differ), it holds rendered secrets, and
|
|
115
|
+
* it is what the next deploy would ship. This flag keeps it for deliberate
|
|
116
|
+
* inspection. It is always kept when the deploy fails.
|
|
117
|
+
*/
|
|
118
|
+
keepGeneratedProject?: boolean;
|
|
104
119
|
}
|
|
105
120
|
|
|
106
121
|
/**
|
|
@@ -365,6 +380,26 @@ export async function deployModule(
|
|
|
365
380
|
closeDeployWindows(db, moduleId, new Date());
|
|
366
381
|
|
|
367
382
|
if (result.success) {
|
|
383
|
+
// D4 (control-plane-stops-building-modules): the generated project is
|
|
384
|
+
// ephemeral. It was rendered for THIS deploy from the signed payload plus
|
|
385
|
+
// live config; a persisted copy is a tree nothing can validate, it holds
|
|
386
|
+
// rendered secrets, and it is what a later deploy would ship. Now that
|
|
387
|
+
// the deploy that needed it is done — ansible, hooks, DNS backfill and
|
|
388
|
+
// aspects all ran inside the impl — it goes. Kept on failure (the impl's
|
|
389
|
+
// early returns) and under --keep.
|
|
390
|
+
if (!options.keepGeneratedProject) {
|
|
391
|
+
const deployed = db.select().from(modules).where(eq(modules.id, moduleId)).get();
|
|
392
|
+
if (deployed) {
|
|
393
|
+
const generatedPath = `${deployed.sourcePath}/generated`;
|
|
394
|
+
try {
|
|
395
|
+
rmSync(generatedPath, { recursive: true, force: true });
|
|
396
|
+
log.info(`Removed generated/ for '${moduleId}' (ephemeral by design; --keep retains it)`);
|
|
397
|
+
} catch (cleanupError) {
|
|
398
|
+
const msg = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
|
|
399
|
+
log.warn(`Failed to remove generated/ for '${moduleId}': ${msg}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
368
403
|
// A module that declares a suggested interval starts being watched on its
|
|
369
404
|
// first successful deploy — watching is the default, not a thing to
|
|
370
405
|
// remember.
|
|
@@ -388,6 +423,154 @@ export async function deployModule(
|
|
|
388
423
|
return result;
|
|
389
424
|
}
|
|
390
425
|
|
|
426
|
+
/**
|
|
427
|
+
* Headless bound for the repoint confirm. A staged responder replies in
|
|
428
|
+
* milliseconds; this exists so an attached-but-unstaged responder produces a
|
|
429
|
+
* defined skip instead of hanging until the suite budget kills the deploy
|
|
430
|
+
* (module-orchestrator-primitives design.md D5, "never a hang"). On a TTY
|
|
431
|
+
* there is no bound: the operator IS the responder and may take minutes.
|
|
432
|
+
*/
|
|
433
|
+
const DNS_REPOINT_HEADLESS_TIMEOUT_MS = 60_000;
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* The deploy-time DNS repoint for a `dns_internal` provider
|
|
437
|
+
* (module-orchestrator-primitives slice 3, design D5). celilo owns the whole
|
|
438
|
+
* operation because every input is already celilo's: the resolver address is
|
|
439
|
+
* the system celilo just placed, and the prior primary/fallback are celilo's
|
|
440
|
+
* own rows. knot's on_install used to do this through the CLI, one key at a
|
|
441
|
+
* time, under a catch that reported success over a repoint that never
|
|
442
|
+
* happened.
|
|
443
|
+
*
|
|
444
|
+
* D5 is a recorded DELIBERATE EXCEPTION to the framework-does-not-infer-intent
|
|
445
|
+
* rule. What makes it acceptable is the interview: the deploy ASKS before it
|
|
446
|
+
* touches the fleet resolver, and a declined repoint is an operator decision,
|
|
447
|
+
* not an error. Do not cite this as precedent (design.md D5, "So do not cite
|
|
448
|
+
* this as precedent").
|
|
449
|
+
*/
|
|
450
|
+
export interface DnsRepointPlan {
|
|
451
|
+
serverIp: string;
|
|
452
|
+
priorPrimary: string | undefined;
|
|
453
|
+
existingFallback: string | undefined;
|
|
454
|
+
/** Merged dns.fallback when the repoint demotes the prior primary; undefined leaves the key untouched. */
|
|
455
|
+
newFallback: string | undefined;
|
|
456
|
+
/** False when dns.primary already names this resolver: the deploy asks nothing. */
|
|
457
|
+
needsRepoint: boolean;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Pure half of the repoint. The demote keeps an existing fallback (D5's
|
|
462
|
+
* secondary observation: knot's one-key-at-a-time hook overwrote it, dropping
|
|
463
|
+
* e.g. 8.8.8.8 from the fleet's resolver list).
|
|
464
|
+
*/
|
|
465
|
+
export function planDnsRepoint(args: {
|
|
466
|
+
serverIp: string;
|
|
467
|
+
priorPrimary: string | undefined;
|
|
468
|
+
existingFallback: string | undefined;
|
|
469
|
+
}): DnsRepointPlan {
|
|
470
|
+
const { serverIp, priorPrimary, existingFallback } = args;
|
|
471
|
+
if (priorPrimary === serverIp) {
|
|
472
|
+
return {
|
|
473
|
+
serverIp,
|
|
474
|
+
priorPrimary,
|
|
475
|
+
existingFallback,
|
|
476
|
+
newFallback: undefined,
|
|
477
|
+
needsRepoint: false,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
const newFallback = priorPrimary ? mergeFallback(existingFallback, priorPrimary) : undefined;
|
|
481
|
+
return { serverIp, priorPrimary, existingFallback, newFallback, needsRepoint: true };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** Comma-separated, demoted primary first, deduped (public-dns-probe.ts parses commas). */
|
|
485
|
+
function mergeFallback(existing: string | undefined, demoted: string): string {
|
|
486
|
+
const entries = [demoted];
|
|
487
|
+
for (const part of (existing ?? '').split(',')) {
|
|
488
|
+
const ip = part.trim();
|
|
489
|
+
if (ip && !entries.includes(ip)) entries.push(ip);
|
|
490
|
+
}
|
|
491
|
+
return entries.join(',');
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function readSystemConfigValue(db: DbClient, key: string): string | undefined {
|
|
495
|
+
const row = db.select().from(systemConfig).where(eq(systemConfig.key, key)).get();
|
|
496
|
+
return row?.value && row.value.length > 0 ? row.value : undefined;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function writeSystemConfigValue(db: DbClient, key: string, value: string): void {
|
|
500
|
+
db.insert(systemConfig)
|
|
501
|
+
.values({ key, value })
|
|
502
|
+
.onConflictDoUpdate({ target: systemConfig.key, set: { value } })
|
|
503
|
+
.run();
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Ask, then perform (or skip) the repoint for a just-deployed `dns_internal`
|
|
508
|
+
* provider. Exported for tests; `deployModuleImpl` calls this after the DNS
|
|
509
|
+
* backfill when the deployed module provides `dns_internal`.
|
|
510
|
+
*/
|
|
511
|
+
export async function repointDnsPrimaryForProvider(
|
|
512
|
+
moduleId: string,
|
|
513
|
+
db: DbClient,
|
|
514
|
+
ask: typeof askConfirm = askConfirm,
|
|
515
|
+
): Promise<void> {
|
|
516
|
+
const serverIp = getModuleSystems(moduleId, db)[0]?.ipv4_address;
|
|
517
|
+
if (!serverIp) {
|
|
518
|
+
log.warn(
|
|
519
|
+
`dns_internal provider '${moduleId}' has no deployed system — skipping dns.primary repoint`,
|
|
520
|
+
);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
const plan = planDnsRepoint({
|
|
524
|
+
serverIp,
|
|
525
|
+
priorPrimary: readSystemConfigValue(db, 'dns.primary'),
|
|
526
|
+
existingFallback: readSystemConfigValue(db, 'dns.fallback'),
|
|
527
|
+
});
|
|
528
|
+
if (!plan.needsRepoint) {
|
|
529
|
+
log.info(`dns.primary already points at ${serverIp} — no repoint needed`);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const demoteNote = plan.newFallback
|
|
534
|
+
? ` The old primary ${plan.priorPrimary} moves to dns.fallback (${plan.newFallback}).`
|
|
535
|
+
: '';
|
|
536
|
+
const opts = {
|
|
537
|
+
scope: `deploy:${moduleId}`,
|
|
538
|
+
key: 'repoint_dns_primary',
|
|
539
|
+
message: `Deploying '${moduleId}' made it the fleet's internal DNS resolver. Point dns.primary at ${serverIp}?${demoteNote}`,
|
|
540
|
+
defaultValue: false,
|
|
541
|
+
};
|
|
542
|
+
// Called DIRECTLY, not inside withInterviewSession: deployModuleImpl already
|
|
543
|
+
// opened the terminal responder above and closes it in its finally. A second
|
|
544
|
+
// responder instance would race the first on stdin (design.md D5).
|
|
545
|
+
let confirmed: boolean;
|
|
546
|
+
try {
|
|
547
|
+
confirmed = await ask(
|
|
548
|
+
process.stdin.isTTY ? opts : { ...opts, timeoutMs: DNS_REPOINT_HEADLESS_TIMEOUT_MS },
|
|
549
|
+
);
|
|
550
|
+
} catch (error) {
|
|
551
|
+
if (error instanceof InterviewUnansweredError || error instanceof InterviewAbandonedError) {
|
|
552
|
+
log.warn(
|
|
553
|
+
`dns.primary repoint skipped — nobody answered the confirm (${opts.scope}:${opts.key}). Stage the answer (celilo events respond --values) or set dns.primary manually later.`,
|
|
554
|
+
);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
throw error;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (!confirmed) {
|
|
561
|
+
log.info('dns.primary repoint declined — fleet resolver left unchanged');
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
writeSystemConfigValue(db, 'dns.primary', serverIp);
|
|
566
|
+
if (plan.newFallback !== undefined) {
|
|
567
|
+
writeSystemConfigValue(db, 'dns.fallback', plan.newFallback);
|
|
568
|
+
}
|
|
569
|
+
log.success(
|
|
570
|
+
`dns.primary repointed at internal resolver ${serverIp}${plan.newFallback ? ` (dns.fallback: ${plan.newFallback})` : ''}`,
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
|
|
391
574
|
async function deployModuleImpl(
|
|
392
575
|
moduleId: string,
|
|
393
576
|
db: DbClient,
|
|
@@ -641,26 +824,10 @@ async function deployModuleImpl(
|
|
|
641
824
|
|
|
642
825
|
log.success('Templates generated');
|
|
643
826
|
|
|
644
|
-
//
|
|
645
|
-
//
|
|
646
|
-
//
|
|
647
|
-
//
|
|
648
|
-
// into a refusal at the point of the fault. A verbatim role asset that
|
|
649
|
-
// failed to refresh is invisible downstream: `cp` reports no error, Ansible
|
|
650
|
-
// copies the stale bytes and reports `ok`, unchanged, and the module's
|
|
651
|
-
// version advances past code that never shipped. Local, milliseconds, no
|
|
652
|
-
// SSH. It deliberately does NOT verify after the deploy — Ansible's
|
|
653
|
-
// `changed` is truthful about what Ansible did, and the lie is upstream of
|
|
654
|
-
// Ansible.
|
|
655
|
-
const { refuseIfGeneratedIsStale } = await import('../module/packaging/generated-plane');
|
|
656
|
-
const staleGenerated = await refuseIfGeneratedIsStale(
|
|
657
|
-
moduleId,
|
|
658
|
-
module.sourcePath,
|
|
659
|
-
generatedPath,
|
|
660
|
-
);
|
|
661
|
-
if (staleGenerated) {
|
|
662
|
-
return { success: false, error: staleGenerated, phases };
|
|
663
|
-
}
|
|
827
|
+
// The generated-tree staleness pre-flight (D6 of module-integrity-rigor)
|
|
828
|
+
// retired here (D5 of control-plane-stops-building-modules): the tree is
|
|
829
|
+
// rendered for THIS deploy and deleted when it succeeds, so nothing
|
|
830
|
+
// persists that could be stale relative to what ships.
|
|
664
831
|
|
|
665
832
|
// Run validate_config hook if defined (e.g., credential validation via Playwright)
|
|
666
833
|
if (manifest.hooks?.validate_config) {
|
|
@@ -724,6 +891,7 @@ async function deployModuleImpl(
|
|
|
724
891
|
requiredCapabilities: manifest.requires.capabilities.map((c) => c.name),
|
|
725
892
|
systems: getModuleSystems(moduleId, db),
|
|
726
893
|
remoteAccess: remoteAccessPolicy(moduleId, db),
|
|
894
|
+
hookStores: () => createHookStores(db, moduleId),
|
|
727
895
|
},
|
|
728
896
|
);
|
|
729
897
|
|
|
@@ -863,6 +1031,7 @@ async function deployModuleImpl(
|
|
|
863
1031
|
requiredCapabilities: (manifest.requires?.capabilities ?? []).map((c) => c.name),
|
|
864
1032
|
systems: getModuleSystems(moduleId, db),
|
|
865
1033
|
remoteAccess: remoteAccessPolicy(moduleId, db),
|
|
1034
|
+
hookStores: () => createHookStores(db, moduleId),
|
|
866
1035
|
},
|
|
867
1036
|
};
|
|
868
1037
|
},
|
|
@@ -1165,6 +1334,7 @@ async function deployModuleImpl(
|
|
|
1165
1334
|
requiredCapabilities: providerManifest.requires.capabilities.map((c) => c.name),
|
|
1166
1335
|
systems: getModuleSystems(capRecord.moduleId, db),
|
|
1167
1336
|
remoteAccess: remoteAccessPolicy(capRecord.moduleId, db),
|
|
1337
|
+
hookStores: () => createHookStores(db, capRecord.moduleId),
|
|
1168
1338
|
},
|
|
1169
1339
|
);
|
|
1170
1340
|
|
|
@@ -1483,6 +1653,7 @@ async function deployModuleImpl(
|
|
|
1483
1653
|
requiredCapabilities: manifest.requires.capabilities.map((c) => c.name),
|
|
1484
1654
|
systems: getModuleSystems(moduleId, db),
|
|
1485
1655
|
remoteAccess: remoteAccessPolicy(moduleId, db),
|
|
1656
|
+
hookStores: () => createHookStores(db, moduleId),
|
|
1486
1657
|
},
|
|
1487
1658
|
};
|
|
1488
1659
|
},
|
|
@@ -1608,6 +1779,13 @@ async function deployModuleImpl(
|
|
|
1608
1779
|
`DNS backfill failed for '${moduleId}': ${msg}. Some internal DNS records may need manual setup.`,
|
|
1609
1780
|
);
|
|
1610
1781
|
}
|
|
1782
|
+
|
|
1783
|
+
// The dns_internal handoff (module-orchestrator-primitives slice 3,
|
|
1784
|
+
// design D5): celilo repoints dns.primary at the resolver it just
|
|
1785
|
+
// deployed, gated on an interview. knot's on_install no longer does
|
|
1786
|
+
// this. Runs after the backfill so a failed deploy never repoints the
|
|
1787
|
+
// fleet at a resolver that never came up.
|
|
1788
|
+
await repointDnsPrimaryForProvider(moduleId, db);
|
|
1611
1789
|
}
|
|
1612
1790
|
|
|
1613
1791
|
// Announce each deployed system on the bus (D5/D6) — one
|
|
@@ -5,6 +5,7 @@ import { tmpdir } from 'node:os';
|
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { closeDb } from '../db/client';
|
|
7
7
|
import { runMigrations } from '../db/migrate';
|
|
8
|
+
import { resetTestDbPath } from '../test-utils/db-path';
|
|
8
9
|
import {
|
|
9
10
|
InFlightError,
|
|
10
11
|
OPERATIONS_SWEEP_PATTERN,
|
|
@@ -30,7 +31,7 @@ describe('module-operations', () => {
|
|
|
30
31
|
|
|
31
32
|
afterEach(() => {
|
|
32
33
|
closeDb();
|
|
33
|
-
|
|
34
|
+
resetTestDbPath();
|
|
34
35
|
try {
|
|
35
36
|
rmSync(dir, { recursive: true, force: true });
|
|
36
37
|
} catch {
|
|
@@ -10,6 +10,7 @@ import { ModuleManifestSchema } from '../manifest/schema';
|
|
|
10
10
|
import { ModuleSubscriptionSchema } from '../manifest/schema';
|
|
11
11
|
import type { ModuleManifest } from '../manifest/schema';
|
|
12
12
|
import { setupTestDatabaseAt as migrateDbFile } from '../test-utils/database';
|
|
13
|
+
import { resetTestDbPath } from '../test-utils/db-path';
|
|
13
14
|
import {
|
|
14
15
|
registerModuleSubscriptions,
|
|
15
16
|
resolveSubscription,
|
|
@@ -341,7 +342,7 @@ describe('resyncAllSubscriptions (ISS-0088)', () => {
|
|
|
341
342
|
});
|
|
342
343
|
afterEach(() => {
|
|
343
344
|
closeDb();
|
|
344
|
-
|
|
345
|
+
resetTestDbPath();
|
|
345
346
|
process.env.EVENT_BUS_DB = undefined;
|
|
346
347
|
try {
|
|
347
348
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test';
|
|
2
2
|
import { execSync } from 'node:child_process';
|
|
3
|
-
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
|
-
import { join } from 'node:path';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
import { classifyModulePath } from '../../module/packaging/package-rules';
|
|
6
7
|
import { checkGitHygiene, checkModuleStale } from './git-hygiene';
|
|
7
8
|
|
|
8
9
|
interface TempRepo {
|
|
@@ -18,8 +19,19 @@ interface TempRepo {
|
|
|
18
19
|
*/
|
|
19
20
|
function makeTempRepo(): TempRepo {
|
|
20
21
|
const dir = mkdtempSync(join(tmpdir(), 'celilo-git-hygiene-'));
|
|
22
|
+
// Bound every git child. A git call that hangs used to run until bun's
|
|
23
|
+
// per-test timeout killed it, and bun's dangling-process kill left the
|
|
24
|
+
// WHOLE run parked (reproduced for celilo#1282; the park itself is
|
|
25
|
+
// celilo#1246). Erroring the test instead keeps the failure visible and
|
|
26
|
+
// the run exitable. 30s per command stays under the 60s per-test ceiling
|
|
27
|
+
// this suite runs with.
|
|
21
28
|
const exec = (cmd: string): string =>
|
|
22
|
-
execSync(cmd, {
|
|
29
|
+
execSync(cmd, {
|
|
30
|
+
cwd: dir,
|
|
31
|
+
encoding: 'utf-8',
|
|
32
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
33
|
+
timeout: 30_000,
|
|
34
|
+
});
|
|
23
35
|
exec('git init -q');
|
|
24
36
|
exec('git config user.email "test@example.com"');
|
|
25
37
|
exec('git config user.name "Test"');
|
|
@@ -88,6 +100,113 @@ describe('checkModuleStale', () => {
|
|
|
88
100
|
});
|
|
89
101
|
});
|
|
90
102
|
|
|
103
|
+
describe('staleness reach vs the packager (celilo#1270)', () => {
|
|
104
|
+
// Paths planted to span the classifications classifyModulePath produces.
|
|
105
|
+
// The expected staleness outcome is NOT hand-written per path: each test
|
|
106
|
+
// asks classifyModulePath what the packager does with the path and holds
|
|
107
|
+
// the staleness gate to the same answer.
|
|
108
|
+
const planted = [
|
|
109
|
+
'e2e/split-horizon-views.test.ts',
|
|
110
|
+
'e2e/fixtures/cases.json',
|
|
111
|
+
'scripts/install.sh',
|
|
112
|
+
'scripts/tsconfig.json',
|
|
113
|
+
'ansible/playbook.yml',
|
|
114
|
+
'terraform/main.tf',
|
|
115
|
+
'schema/tables.sql',
|
|
116
|
+
'hooks/run.test.ts',
|
|
117
|
+
'generated/report.json',
|
|
118
|
+
'state/last-run.json',
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
/** Repo with only manifest.yml committed; returns `commit(rel)` helper. */
|
|
122
|
+
function repoWithManifest() {
|
|
123
|
+
const repo = makeTempRepo();
|
|
124
|
+
writeFileSync(join(repo.dir, 'manifest.yml'), 'id: x\nversion: 1.0.0\n');
|
|
125
|
+
repo.exec('git add manifest.yml && git commit -q -m manifest');
|
|
126
|
+
const commit = (rel: string) => {
|
|
127
|
+
const abs = join(repo.dir, rel);
|
|
128
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
129
|
+
writeFileSync(abs, 'planted\n');
|
|
130
|
+
repo.exec(`git add ${rel} && git commit -q -m ${JSON.stringify(rel)}`);
|
|
131
|
+
};
|
|
132
|
+
return { repo, commit };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
test(
|
|
136
|
+
'a commit touching only e2e/ does not mark the manifest stale',
|
|
137
|
+
() => {
|
|
138
|
+
const { repo, commit } = repoWithManifest();
|
|
139
|
+
try {
|
|
140
|
+
commit('e2e/split-horizon-views.test.ts');
|
|
141
|
+
expect(checkModuleStale(repo.dir)).toBeNull();
|
|
142
|
+
} finally {
|
|
143
|
+
repo.cleanup();
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
{ timeout: 30_000 },
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
test(
|
|
150
|
+
'a commit touching scripts/, ansible/, terraform/ or schema/ still fires',
|
|
151
|
+
() => {
|
|
152
|
+
for (const rel of [
|
|
153
|
+
'scripts/install.sh',
|
|
154
|
+
'ansible/playbook.yml',
|
|
155
|
+
'terraform/main.tf',
|
|
156
|
+
'schema/tables.sql',
|
|
157
|
+
]) {
|
|
158
|
+
const { repo, commit } = repoWithManifest();
|
|
159
|
+
try {
|
|
160
|
+
commit(rel);
|
|
161
|
+
expect(classifyModulePath(rel)).toBe('package');
|
|
162
|
+
expect(checkModuleStale(repo.dir)).not.toBeNull();
|
|
163
|
+
} finally {
|
|
164
|
+
repo.cleanup();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
{ timeout: 30_000 },
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
test(
|
|
172
|
+
'reach parity: the gate fires exactly for paths the packager ships',
|
|
173
|
+
async () => {
|
|
174
|
+
for (const rel of planted) {
|
|
175
|
+
const { repo, commit } = repoWithManifest();
|
|
176
|
+
try {
|
|
177
|
+
commit(rel);
|
|
178
|
+
const stale = checkModuleStale(repo.dir);
|
|
179
|
+
if (classifyModulePath(rel) === 'package') {
|
|
180
|
+
expect(stale).not.toBeNull();
|
|
181
|
+
} else {
|
|
182
|
+
expect(stale).toBeNull();
|
|
183
|
+
}
|
|
184
|
+
} finally {
|
|
185
|
+
repo.cleanup();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
{ timeout: 30_000 },
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
test(
|
|
193
|
+
'non-shipped commits between manifest and a shipped change stay quiet until shipped bytes move',
|
|
194
|
+
() => {
|
|
195
|
+
const { repo, commit } = repoWithManifest();
|
|
196
|
+
try {
|
|
197
|
+
commit('e2e/split-horizon-views.test.ts');
|
|
198
|
+
commit('scripts/tsconfig.json');
|
|
199
|
+
expect(checkModuleStale(repo.dir)).toBeNull();
|
|
200
|
+
commit('scripts/install.sh');
|
|
201
|
+
expect(checkModuleStale(repo.dir)).not.toBeNull();
|
|
202
|
+
} finally {
|
|
203
|
+
repo.cleanup();
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
{ timeout: 30_000 },
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
|
|
91
210
|
describe('checkGitHygiene', () => {
|
|
92
211
|
test('all ok on a fresh, clean repo with manifest as latest commit', () => {
|
|
93
212
|
const repo = makeTempRepo();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { classifyModulePath } from '../../module/packaging/package-rules';
|
|
3
4
|
import { collectGitInfo, makeRealGitRunner } from '../../module/packaging/release-metadata';
|
|
4
5
|
import type { Check } from './types';
|
|
5
6
|
|
|
@@ -19,10 +20,14 @@ export interface StalenessIssue {
|
|
|
19
20
|
* anywhere should still talk to lunacycle's git history).
|
|
20
21
|
*/
|
|
21
22
|
function lastCommitTouching(cwd: string, pathspec: string[]): string | null {
|
|
23
|
+
// timeout: a hung git degrades to the "can't determine — skip" path (status
|
|
24
|
+
// null) instead of hanging `module check` forever. 30s is enormous against
|
|
25
|
+
// the milliseconds a bounded `git log -1` takes on any real repo.
|
|
22
26
|
const r = spawnSync('git', ['log', '-1', '--format=%H', '--', ...pathspec], {
|
|
23
27
|
cwd,
|
|
24
28
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
25
29
|
encoding: 'utf-8',
|
|
30
|
+
timeout: 30_000,
|
|
26
31
|
});
|
|
27
32
|
if (r.status !== 0) return null;
|
|
28
33
|
const sha = r.stdout.trim();
|
|
@@ -33,17 +38,90 @@ function isAncestor(cwd: string, maybeAncestor: string, descendant: string): boo
|
|
|
33
38
|
const r = spawnSync('git', ['merge-base', '--is-ancestor', maybeAncestor, descendant], {
|
|
34
39
|
cwd,
|
|
35
40
|
stdio: 'ignore',
|
|
41
|
+
timeout: 30_000,
|
|
36
42
|
});
|
|
37
43
|
return r.status === 0;
|
|
38
44
|
}
|
|
39
45
|
|
|
46
|
+
/**
|
|
47
|
+
* One commit from `git log --name-only`: its SHA and the repo-root-relative
|
|
48
|
+
* paths it changed. A merge commit carries an empty file list (git log omits
|
|
49
|
+
* diffs for merges), so it never qualifies as a src commit — which is right,
|
|
50
|
+
* a merge changes no authored byte of its own.
|
|
51
|
+
*/
|
|
52
|
+
interface CommitWithPaths {
|
|
53
|
+
sha: string;
|
|
54
|
+
paths: string[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The commits touching `pathspec`, newest first, with the repo-root-relative
|
|
59
|
+
* paths each one changed. `git log --name-only` prints repo-root-relative
|
|
60
|
+
* paths even when cwd is a subdirectory (measured: it does not honor cwd for
|
|
61
|
+
* output), so the caller strips the prefix itself via `show-prefix`.
|
|
62
|
+
*/
|
|
63
|
+
function listCommitsTouching(cwd: string, pathspec: string): CommitWithPaths[] {
|
|
64
|
+
const r = spawnSync(
|
|
65
|
+
'git',
|
|
66
|
+
['-c', 'core.quotePath=false', 'log', '--format=%H', '--name-only', '--', pathspec],
|
|
67
|
+
{ cwd, stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf-8' },
|
|
68
|
+
);
|
|
69
|
+
if (r.status !== 0) return [];
|
|
70
|
+
const commits: CommitWithPaths[] = [];
|
|
71
|
+
let current: CommitWithPaths | null = null;
|
|
72
|
+
for (const line of (r.stdout ?? '').split('\n')) {
|
|
73
|
+
const trimmed = line.trim();
|
|
74
|
+
if (!trimmed) continue;
|
|
75
|
+
if (/^[0-9a-f]{40}$/.test(trimmed)) {
|
|
76
|
+
current = { sha: trimmed, paths: [] };
|
|
77
|
+
commits.push(current);
|
|
78
|
+
} else if (current) {
|
|
79
|
+
current.paths.push(trimmed);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return commits;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* SHA of the last commit that touched SHIPPED module source, or null when no
|
|
87
|
+
* commit qualifies (no history, or only non-shipped paths ever committed).
|
|
88
|
+
*
|
|
89
|
+
* The staleness scan's reach is the PACKAGER's rule, not a second list beside
|
|
90
|
+
* it (celilo#1270). A path counts as source exactly when `classifyModulePath`
|
|
91
|
+
* calls it `package` — the same answer that decides whether the path lands in
|
|
92
|
+
* the .netapp. So a commit touching only `e2e/**`, `*.test.ts`, or
|
|
93
|
+
* `tsconfig.json` changes no installed byte and must not read as drift.
|
|
94
|
+
*
|
|
95
|
+
* Excluding manifest.yml from the "src" scan is the gate's own semantics, not
|
|
96
|
+
* packaging: we want to know if anything else shipped past it. node_modules,
|
|
97
|
+
* dist, and other build outputs are gitignored and so never appear here.
|
|
98
|
+
*/
|
|
99
|
+
function lastShippedSourceCommit(moduleDir: string): string | null {
|
|
100
|
+
const prefixR = spawnSync('git', ['rev-parse', '--show-prefix'], {
|
|
101
|
+
cwd: moduleDir,
|
|
102
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
103
|
+
encoding: 'utf-8',
|
|
104
|
+
});
|
|
105
|
+
if (prefixR.status !== 0) return null;
|
|
106
|
+
const repoPrefix = (prefixR.stdout ?? '').trim();
|
|
107
|
+
for (const commit of listCommitsTouching(moduleDir, moduleDir)) {
|
|
108
|
+
for (const p of commit.paths) {
|
|
109
|
+
if (repoPrefix && !p.startsWith(repoPrefix)) continue;
|
|
110
|
+
const rel = repoPrefix ? p.slice(repoPrefix.length) : p;
|
|
111
|
+
if (rel === 'manifest.yml') continue;
|
|
112
|
+
if (classifyModulePath(rel) === 'package') return commit.sha;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
40
118
|
/**
|
|
41
119
|
* Detect "I edited module src but forgot to bump (or touch) manifest.yml."
|
|
42
120
|
*
|
|
43
|
-
* Returns null when the manifest is the most recently-touched file in
|
|
44
|
-
* dir (or when neither side has any commit history — e.g. brand-new module
|
|
45
|
-
* not yet committed). Returns a StalenessIssue when src has commits
|
|
46
|
-
* the last manifest.yml change — the operator must bump the manifest
|
|
121
|
+
* Returns null when the manifest is the most recently-touched shipped file in
|
|
122
|
+
* the dir (or when neither side has any commit history — e.g. brand-new module
|
|
123
|
+
* not yet committed). Returns a StalenessIssue when shipped src has commits
|
|
124
|
+
* AFTER the last manifest.yml change — the operator must bump the manifest
|
|
47
125
|
* (semver change → reset +N to 1) or just touch it (release-only change →
|
|
48
126
|
* auto-bump +N), then re-publish.
|
|
49
127
|
*
|
|
@@ -54,16 +132,7 @@ function isAncestor(cwd: string, maybeAncestor: string, descendant: string): boo
|
|
|
54
132
|
export function checkModuleStale(moduleDir: string): StalenessIssue | null {
|
|
55
133
|
const manifestPath = join(moduleDir, 'manifest.yml');
|
|
56
134
|
const lastManifest = lastCommitTouching(moduleDir, [manifestPath]);
|
|
57
|
-
|
|
58
|
-
// want to know if anything ELSE in the dir moved past it. node_modules
|
|
59
|
-
// and common build outputs are gitignored already, but be explicit
|
|
60
|
-
// defensively.
|
|
61
|
-
const lastSrc = lastCommitTouching(moduleDir, [
|
|
62
|
-
moduleDir,
|
|
63
|
-
`:(exclude)${manifestPath}`,
|
|
64
|
-
`:(exclude)${moduleDir}/node_modules`,
|
|
65
|
-
`:(exclude)${moduleDir}/dist`,
|
|
66
|
-
]);
|
|
135
|
+
const lastSrc = lastShippedSourceCommit(moduleDir);
|
|
67
136
|
if (!lastManifest || !lastSrc) return null;
|
|
68
137
|
if (lastSrc === lastManifest) return null;
|
|
69
138
|
if (!isAncestor(moduleDir, lastManifest, lastSrc)) return null;
|
|
@@ -7,6 +7,7 @@ import { eq } from 'drizzle-orm';
|
|
|
7
7
|
import type { DbClient } from '../db/client';
|
|
8
8
|
import { portForwards } from '../db/schema';
|
|
9
9
|
import { setupTestDatabaseAt } from '../test-utils/database';
|
|
10
|
+
import { resetTestDbPath } from '../test-utils/db-path';
|
|
10
11
|
import { deleteClaimedRows } from './capability-table-rows';
|
|
11
12
|
import { buildPortForwardStore } from './port-forwards';
|
|
12
13
|
|
|
@@ -27,7 +28,7 @@ describe('port-forward store', () => {
|
|
|
27
28
|
});
|
|
28
29
|
afterEach(() => {
|
|
29
30
|
db.$client.close();
|
|
30
|
-
|
|
31
|
+
resetTestDbPath();
|
|
31
32
|
try {
|
|
32
33
|
rmSync(dir, { recursive: true, force: true });
|
|
33
34
|
} catch {
|
|
@@ -17,6 +17,7 @@ import { join } from 'node:path';
|
|
|
17
17
|
import { defineEvents, openBus } from '@celilo/event-bus';
|
|
18
18
|
import { closeDb, getDb } from '../db/client';
|
|
19
19
|
import { runMigrations } from '../db/migrate';
|
|
20
|
+
import { resetTestDbPath } from '../test-utils/db-path';
|
|
20
21
|
import { type AspectRequiredPayload, EVENT_TYPES } from './bus-interview';
|
|
21
22
|
import { startProgrammaticResponder } from './programmatic-responder';
|
|
22
23
|
|
|
@@ -72,7 +73,7 @@ describe('programmatic responder — aspect.required consent (#262)', () => {
|
|
|
72
73
|
afterEach(() => {
|
|
73
74
|
closeDb();
|
|
74
75
|
rmSync(dir, { recursive: true, force: true });
|
|
75
|
-
|
|
76
|
+
resetTestDbPath();
|
|
76
77
|
});
|
|
77
78
|
|
|
78
79
|
it('approves (consented=true) when the policy approves the module', async () => {
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
modules,
|
|
28
28
|
} from '../db/schema';
|
|
29
29
|
import type { BaseModuleAspect } from '../manifest/schema';
|
|
30
|
+
import { resetTestDbPath } from '../test-utils/db-path';
|
|
30
31
|
import { upsertModuleConfig } from './module-config';
|
|
31
32
|
import { executeProxmoxReconcile, planProxmoxReconcile } from './proxmox-reconcile';
|
|
32
33
|
|
|
@@ -153,7 +154,7 @@ describe('proxmox-reconcile', () => {
|
|
|
153
154
|
|
|
154
155
|
afterEach(() => {
|
|
155
156
|
closeDb();
|
|
156
|
-
|
|
157
|
+
resetTestDbPath();
|
|
157
158
|
try {
|
|
158
159
|
rmSync(dir, { recursive: true, force: true });
|
|
159
160
|
} catch {
|