@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
|
@@ -10,8 +10,9 @@ import { mkdtempSync, rmSync } from 'node:fs';
|
|
|
10
10
|
import { tmpdir } from 'node:os';
|
|
11
11
|
import { join } from 'node:path';
|
|
12
12
|
import { type DbClient, getDb } from '../../db/client';
|
|
13
|
-
import { modules } from '../../db/schema';
|
|
13
|
+
import { moduleConfigs, modules } from '../../db/schema';
|
|
14
14
|
import { resolveDeployPosture } from '../../services/deploy-posture';
|
|
15
|
+
import { resetTestDbPath } from '../../test-utils/db-path';
|
|
15
16
|
import {
|
|
16
17
|
FRAMEWORK_CONFIG_KEYS,
|
|
17
18
|
handleModuleConfigGet,
|
|
@@ -41,6 +42,7 @@ describe('handleModuleConfigSet — infra-key contract (ISS-0069)', () => {
|
|
|
41
42
|
{ name: 'vmid', type: 'integer', source: 'infrastructure' },
|
|
42
43
|
{ name: 'target_node', type: 'string', source: 'infrastructure' },
|
|
43
44
|
{ name: 'app_port', type: 'integer' },
|
|
45
|
+
{ name: 'public_ip', type: 'string', source: 'hook' },
|
|
44
46
|
],
|
|
45
47
|
},
|
|
46
48
|
},
|
|
@@ -50,7 +52,7 @@ describe('handleModuleConfigSet — infra-key contract (ISS-0069)', () => {
|
|
|
50
52
|
|
|
51
53
|
afterEach(() => {
|
|
52
54
|
rmSync(tempDir, { recursive: true, force: true });
|
|
53
|
-
|
|
55
|
+
resetTestDbPath();
|
|
54
56
|
});
|
|
55
57
|
|
|
56
58
|
test('rejects an infrastructure-managed key (no silent accept-then-override)', async () => {
|
|
@@ -79,6 +81,24 @@ describe('handleModuleConfigSet — infra-key contract (ISS-0069)', () => {
|
|
|
79
81
|
expect(result.success).toBe(true);
|
|
80
82
|
});
|
|
81
83
|
|
|
84
|
+
// Hook-owned-state task 2.4 (design D2/D6): a `source: hook` variable is
|
|
85
|
+
// written ONLY by the module's own hook via `context.config.set`. A hand set
|
|
86
|
+
// value would be meaningless — the next hook run overwrites it — so the
|
|
87
|
+
// command refuses rather than reporting success over nothing.
|
|
88
|
+
test('rejects a hook-owned key and points at the accessor', async () => {
|
|
89
|
+
const result = await handleModuleConfigSet(['testmod', 'public_ip', '203.0.113.7']);
|
|
90
|
+
expect(result.success).toBe(false);
|
|
91
|
+
if (!result.success) {
|
|
92
|
+
expect(result.error).toContain('source: hook');
|
|
93
|
+
expect(result.error).toContain('not operator-settable');
|
|
94
|
+
expect(result.error).toContain('context.config.set');
|
|
95
|
+
}
|
|
96
|
+
// And no row may exist — a refusal that leaves the row behind is the
|
|
97
|
+
// accept-then-override bug in a different coat.
|
|
98
|
+
const rows = db.select().from(moduleConfigs).all();
|
|
99
|
+
expect(rows).toEqual([]);
|
|
100
|
+
});
|
|
101
|
+
|
|
82
102
|
test('still rejects an undeclared key, and omits infra keys from the valid-keys hint', async () => {
|
|
83
103
|
const result = await handleModuleConfigSet(['testmod', 'nope', 'x']);
|
|
84
104
|
expect(result.success).toBe(false);
|
|
@@ -14,7 +14,7 @@ import type { CommandResult } from '../types';
|
|
|
14
14
|
* Handle module deploy command
|
|
15
15
|
*
|
|
16
16
|
* Usage:
|
|
17
|
-
* celilo module deploy <module-id> [--debug] [--preflight] [--verbose]
|
|
17
|
+
* celilo module deploy <module-id> [--debug] [--preflight] [--verbose] [--keep]
|
|
18
18
|
*
|
|
19
19
|
* Note: there is no `--no-interactive` flag. The deploy interview
|
|
20
20
|
* runs through the bus event system; automation answers via a
|
|
@@ -59,8 +59,14 @@ export async function handleModuleDeploy(
|
|
|
59
59
|
const debug = hasFlag(flags, 'debug');
|
|
60
60
|
const verbose = hasFlag(flags, 'verbose');
|
|
61
61
|
const stopAfterInterview = hasFlag(flags, 'stop-after-interview');
|
|
62
|
+
const keepGeneratedProject = hasFlag(flags, 'keep');
|
|
62
63
|
|
|
63
|
-
const result = await deployModule(moduleId, db, {
|
|
64
|
+
const result = await deployModule(moduleId, db, {
|
|
65
|
+
debug,
|
|
66
|
+
verbose,
|
|
67
|
+
stopAfterInterview,
|
|
68
|
+
keepGeneratedProject,
|
|
69
|
+
});
|
|
64
70
|
|
|
65
71
|
if (!result.success) {
|
|
66
72
|
return {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import type { ModuleManifest } from '../../manifest/schema';
|
|
6
|
+
import { missingBuildOutputs } from './module-generate';
|
|
7
|
+
|
|
8
|
+
const DECLARED = join('ansible', 'roles', 'fixture', 'files', 'binary');
|
|
9
|
+
|
|
10
|
+
function manifestWithArtifacts(artifacts: string[]): ModuleManifest {
|
|
11
|
+
return {
|
|
12
|
+
celilo_contract: '1.0',
|
|
13
|
+
id: 'fixture',
|
|
14
|
+
name: 'Fixture',
|
|
15
|
+
version: '1.0.0',
|
|
16
|
+
description: 'fixture',
|
|
17
|
+
build: { command: 'true', artifacts },
|
|
18
|
+
} as ModuleManifest;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('missingBuildOutputs', () => {
|
|
22
|
+
let payload: string;
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
payload = mkdtempSync(join(tmpdir(), 'celilo-missing-outputs-'));
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
rmSync(payload, { recursive: true, force: true });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('names the declared output the payload is missing', () => {
|
|
33
|
+
expect(missingBuildOutputs(payload, manifestWithArtifacts([DECLARED]))).toEqual([DECLARED]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('reports nothing when the payload carries its declared outputs', () => {
|
|
37
|
+
mkdirSync(join(payload, 'ansible', 'roles', 'fixture', 'files'), { recursive: true });
|
|
38
|
+
writeFileSync(join(payload, DECLARED), 'built-bytes');
|
|
39
|
+
expect(missingBuildOutputs(payload, manifestWithArtifacts([DECLARED]))).toEqual([]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('reports nothing for a module with no build block', () => {
|
|
43
|
+
expect(
|
|
44
|
+
missingBuildOutputs(payload, {
|
|
45
|
+
celilo_contract: '1.0',
|
|
46
|
+
id: 'fixture',
|
|
47
|
+
name: 'Fixture',
|
|
48
|
+
version: '1.0.0',
|
|
49
|
+
description: 'fixture',
|
|
50
|
+
} as ModuleManifest),
|
|
51
|
+
).toEqual([]);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { existsSync } from 'node:fs';
|
|
6
|
-
import { resolve } from 'node:path';
|
|
6
|
+
import { join, resolve } from 'node:path';
|
|
7
7
|
import { eq } from 'drizzle-orm';
|
|
8
8
|
import {
|
|
9
9
|
promptForMissingCapabilitySecrets,
|
|
@@ -13,12 +13,25 @@ import { getDb } from '../../db/client';
|
|
|
13
13
|
import { moduleConfigs, modules } from '../../db/schema';
|
|
14
14
|
import type { ModuleManifest } from '../../manifest/schema';
|
|
15
15
|
import { interviewForMissingSecrets, validateModuleSecrets } from '../../services/config-interview';
|
|
16
|
-
import { getModuleBuildStatus, verifyArtifactsExist } from '../../services/module-build';
|
|
17
16
|
import { generateTemplates } from '../../templates/generator';
|
|
18
17
|
import { promptForMissingConfig } from '../interactive-config';
|
|
19
18
|
import { getArg, getFlag, validateRequiredArgs } from '../parser';
|
|
20
19
|
import type { CommandResult } from '../types';
|
|
21
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Declared `build.artifacts` absent from the module's delivered payload.
|
|
23
|
+
*
|
|
24
|
+
* D1 (control-plane-stops-building-modules): generation consumes the payload.
|
|
25
|
+
* A declared output the payload does not carry is a publish defect, so it is
|
|
26
|
+
* reported to the caller and named, never repaired by running a build here.
|
|
27
|
+
* A fallback build is the shape that hid celilo#1307 for four months: the
|
|
28
|
+
* failure must fail loudly at the boundary, not be repaired downstream.
|
|
29
|
+
*/
|
|
30
|
+
export function missingBuildOutputs(modulePath: string, manifest: ModuleManifest): string[] {
|
|
31
|
+
const artifacts = manifest.build?.artifacts ?? [];
|
|
32
|
+
return artifacts.filter((artifact) => !existsSync(join(modulePath, artifact)));
|
|
33
|
+
}
|
|
34
|
+
|
|
22
35
|
/**
|
|
23
36
|
* Handle module generate command
|
|
24
37
|
*
|
|
@@ -71,31 +84,23 @@ export async function handleModuleGenerate(
|
|
|
71
84
|
};
|
|
72
85
|
}
|
|
73
86
|
|
|
74
|
-
//
|
|
87
|
+
// D1 (control-plane-stops-building-modules): generation consumes the
|
|
88
|
+
// delivered payload. Whether this machine ever ran a build is a fact about
|
|
89
|
+
// its own history, not about the module in front of it — gating on it is
|
|
90
|
+
// what made celilo#1307 refuse generation on a production control plane
|
|
91
|
+
// whose payload carried the correct binaries one directory away.
|
|
75
92
|
const manifest = module.manifestData as ModuleManifest;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Verify artifacts still exist
|
|
89
|
-
if (buildStatus.artifacts.length > 0) {
|
|
90
|
-
const artifactsValid = verifyArtifactsExist(buildStatus.artifacts);
|
|
91
|
-
|
|
92
|
-
if (!artifactsValid) {
|
|
93
|
-
return {
|
|
94
|
-
success: false,
|
|
95
|
-
error: `Build artifacts missing or invalid.\n\nRebuild: celilo module build ${moduleId}`,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
}
|
|
93
|
+
const missingOutputs = missingBuildOutputs(modulePath, manifest);
|
|
94
|
+
if (missingOutputs.length > 0) {
|
|
95
|
+
return {
|
|
96
|
+
success: false,
|
|
97
|
+
error: [
|
|
98
|
+
`Module payload is missing ${missingOutputs.length} declared build output(s):`,
|
|
99
|
+
...missingOutputs.map((artifact) => ` ${artifact}`),
|
|
100
|
+
'',
|
|
101
|
+
"The payload must carry the module's build outputs; a control plane never builds. This is a publish defect: re-package the module and update it. Generation will not fall back to running a build.",
|
|
102
|
+
].join('\n'),
|
|
103
|
+
};
|
|
99
104
|
}
|
|
100
105
|
|
|
101
106
|
const outputPathFlag = getFlag(flags, 'output', '');
|
|
@@ -12,6 +12,7 @@ import { closeDb, getDb } from '../../db/client';
|
|
|
12
12
|
import { runMigrations } from '../../db/migrate';
|
|
13
13
|
import { modules } from '../../db/schema';
|
|
14
14
|
import type { BaseModuleAspect } from '../../manifest/schema';
|
|
15
|
+
import { resetTestDbPath } from '../../test-utils/db-path';
|
|
15
16
|
import { handleAspectApprovalAfterImport } from './module-import';
|
|
16
17
|
|
|
17
18
|
const aspect: BaseModuleAspect = {
|
|
@@ -61,7 +62,7 @@ describe('handleAspectApprovalAfterImport (non-interactive)', () => {
|
|
|
61
62
|
afterEach(() => {
|
|
62
63
|
setTTY(originalIsTTY);
|
|
63
64
|
closeDb();
|
|
64
|
-
|
|
65
|
+
resetTestDbPath();
|
|
65
66
|
try {
|
|
66
67
|
rmSync(dir, { recursive: true, force: true });
|
|
67
68
|
} catch {
|
|
@@ -17,6 +17,7 @@ import { tmpdir } from 'node:os';
|
|
|
17
17
|
import { join } from 'node:path';
|
|
18
18
|
import type { IndexEntry } from '../../registry/client';
|
|
19
19
|
import { RegistryClient } from '../../registry/client';
|
|
20
|
+
import { resetTestDbPath } from '../../test-utils/db-path';
|
|
20
21
|
import { handlePublicRegistryImport } from './module-import';
|
|
21
22
|
|
|
22
23
|
// handlePublicRegistryImport is not exported by default — re-exported at end of module-import.ts
|
|
@@ -42,7 +43,7 @@ afterEach(() => {
|
|
|
42
43
|
getIndexSpy.mockRestore();
|
|
43
44
|
downloadSpy.mockRestore();
|
|
44
45
|
rmSync(tempDir, { recursive: true, force: true });
|
|
45
|
-
|
|
46
|
+
resetTestDbPath();
|
|
46
47
|
process.env.CELILO_DATA_DIR = undefined;
|
|
47
48
|
});
|
|
48
49
|
|
|
@@ -339,7 +339,7 @@ async function handlePublicRegistryImport(
|
|
|
339
339
|
// Download the .netapp
|
|
340
340
|
const tmpPath = join(tmpdir(), `${name}-${latest.vers}-${Date.now()}.netapp`);
|
|
341
341
|
try {
|
|
342
|
-
const pkgData = await client.download(name, latest.vers);
|
|
342
|
+
const pkgData = await client.download(name, latest.vers, latest.cksum);
|
|
343
343
|
await Bun.write(tmpPath, pkgData);
|
|
344
344
|
} catch (err) {
|
|
345
345
|
return {
|
|
@@ -7,6 +7,7 @@ import { closeDb, getDb } from '../../db/client';
|
|
|
7
7
|
import { runMigrations } from '../../db/migrate';
|
|
8
8
|
import { moduleOperations } from '../../db/schema';
|
|
9
9
|
import { ABANDONED_RELEASE_MESSAGE, OPERATION_TTL_MS } from '../../services/module-operations';
|
|
10
|
+
import { resetTestDbPath } from '../../test-utils/db-path';
|
|
10
11
|
import { handleModuleOperations } from './module-operations';
|
|
11
12
|
|
|
12
13
|
describe('celilo module operations', () => {
|
|
@@ -20,7 +21,7 @@ describe('celilo module operations', () => {
|
|
|
20
21
|
|
|
21
22
|
afterEach(() => {
|
|
22
23
|
closeDb();
|
|
23
|
-
|
|
24
|
+
resetTestDbPath();
|
|
24
25
|
rmSync(dir, { recursive: true, force: true });
|
|
25
26
|
});
|
|
26
27
|
|
|
@@ -15,6 +15,7 @@ import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
|
15
15
|
import { mkdir, rm, writeFile } from 'node:fs/promises';
|
|
16
16
|
import { join } from 'node:path';
|
|
17
17
|
import type { IndexEntry } from '../../registry/client';
|
|
18
|
+
import { resetTestDbPath } from '../../test-utils/db-path';
|
|
18
19
|
import { handleModulePublish, resolveToken } from './module-publish';
|
|
19
20
|
|
|
20
21
|
const TEST_DIR = `/tmp/test-module-publish-${Date.now()}`;
|
|
@@ -41,7 +42,6 @@ describe('handleModulePublish — validation', () => {
|
|
|
41
42
|
// fallback in resolveToken() may read a real publish token and the test
|
|
42
43
|
// will skip the validation path it's trying to exercise.
|
|
43
44
|
const origToken = process.env.CELILO_PUBLISH_TOKEN;
|
|
44
|
-
const origDbPath = process.env.CELILO_DB_PATH;
|
|
45
45
|
const origDataDir = process.env.CELILO_DATA_DIR;
|
|
46
46
|
process.env.CELILO_PUBLISH_TOKEN = undefined;
|
|
47
47
|
process.env.CELILO_DB_PATH = `/tmp/test-celilo-no-token-${Date.now()}.db`;
|
|
@@ -52,8 +52,7 @@ describe('handleModulePublish — validation', () => {
|
|
|
52
52
|
if (!result.success) expect(result.error).toContain('Publish token required');
|
|
53
53
|
} finally {
|
|
54
54
|
if (origToken !== undefined) process.env.CELILO_PUBLISH_TOKEN = origToken;
|
|
55
|
-
|
|
56
|
-
else process.env.CELILO_DB_PATH = undefined;
|
|
55
|
+
resetTestDbPath();
|
|
57
56
|
if (origDataDir !== undefined) process.env.CELILO_DATA_DIR = origDataDir;
|
|
58
57
|
else process.env.CELILO_DATA_DIR = undefined;
|
|
59
58
|
}
|
|
@@ -163,7 +162,6 @@ describe('revision auto-detection algorithm', () => {
|
|
|
163
162
|
describe('resolveToken — precedence', () => {
|
|
164
163
|
test('--token flag wins over env var and secret store', async () => {
|
|
165
164
|
const origEnv = process.env.CELILO_PUBLISH_TOKEN;
|
|
166
|
-
const origDbPath = process.env.CELILO_DB_PATH;
|
|
167
165
|
process.env.CELILO_PUBLISH_TOKEN = 'env-token';
|
|
168
166
|
process.env.CELILO_DB_PATH = `/tmp/test-celilo-resolve-${Date.now()}.db`;
|
|
169
167
|
try {
|
|
@@ -172,14 +170,12 @@ describe('resolveToken — precedence', () => {
|
|
|
172
170
|
} finally {
|
|
173
171
|
if (origEnv !== undefined) process.env.CELILO_PUBLISH_TOKEN = origEnv;
|
|
174
172
|
else process.env.CELILO_PUBLISH_TOKEN = undefined;
|
|
175
|
-
|
|
176
|
-
else process.env.CELILO_DB_PATH = undefined;
|
|
173
|
+
resetTestDbPath();
|
|
177
174
|
}
|
|
178
175
|
});
|
|
179
176
|
|
|
180
177
|
test('env var used when --token flag is empty', async () => {
|
|
181
178
|
const origEnv = process.env.CELILO_PUBLISH_TOKEN;
|
|
182
|
-
const origDbPath = process.env.CELILO_DB_PATH;
|
|
183
179
|
process.env.CELILO_PUBLISH_TOKEN = 'env-token';
|
|
184
180
|
process.env.CELILO_DB_PATH = `/tmp/test-celilo-resolve-${Date.now()}.db`;
|
|
185
181
|
try {
|
|
@@ -188,8 +184,7 @@ describe('resolveToken — precedence', () => {
|
|
|
188
184
|
} finally {
|
|
189
185
|
if (origEnv !== undefined) process.env.CELILO_PUBLISH_TOKEN = origEnv;
|
|
190
186
|
else process.env.CELILO_PUBLISH_TOKEN = undefined;
|
|
191
|
-
|
|
192
|
-
else process.env.CELILO_DB_PATH = undefined;
|
|
187
|
+
resetTestDbPath();
|
|
193
188
|
}
|
|
194
189
|
});
|
|
195
190
|
|
|
@@ -197,7 +192,6 @@ describe('resolveToken — precedence', () => {
|
|
|
197
192
|
// All three sources empty: secret-store fallback hits an isolated empty
|
|
198
193
|
// DB and returns null gracefully.
|
|
199
194
|
const origEnv = process.env.CELILO_PUBLISH_TOKEN;
|
|
200
|
-
const origDbPath = process.env.CELILO_DB_PATH;
|
|
201
195
|
const origDataDir = process.env.CELILO_DATA_DIR;
|
|
202
196
|
process.env.CELILO_PUBLISH_TOKEN = undefined;
|
|
203
197
|
process.env.CELILO_DB_PATH = `/tmp/test-celilo-resolve-empty-${Date.now()}.db`;
|
|
@@ -207,8 +201,7 @@ describe('resolveToken — precedence', () => {
|
|
|
207
201
|
expect(t).toBe('');
|
|
208
202
|
} finally {
|
|
209
203
|
if (origEnv !== undefined) process.env.CELILO_PUBLISH_TOKEN = origEnv;
|
|
210
|
-
|
|
211
|
-
else process.env.CELILO_DB_PATH = undefined;
|
|
204
|
+
resetTestDbPath();
|
|
212
205
|
if (origDataDir !== undefined) process.env.CELILO_DATA_DIR = origDataDir;
|
|
213
206
|
else process.env.CELILO_DATA_DIR = undefined;
|
|
214
207
|
}
|
|
@@ -18,8 +18,10 @@ import {
|
|
|
18
18
|
import { tmpdir } from 'node:os';
|
|
19
19
|
import { join, relative } from 'node:path';
|
|
20
20
|
import { eq } from 'drizzle-orm';
|
|
21
|
+
import { create as tarCreate } from 'tar';
|
|
21
22
|
import { type DbClient, getDb } from '../../db/client';
|
|
22
23
|
import { moduleIntegrity, modules } from '../../db/schema';
|
|
24
|
+
import { resetTestDbPath } from '../../test-utils/db-path';
|
|
23
25
|
import { classifyVersionChange, handleModuleUpdate, updateOne } from './module-update';
|
|
24
26
|
|
|
25
27
|
describe('classifyVersionChange', () => {
|
|
@@ -123,7 +125,7 @@ description: fixture
|
|
|
123
125
|
|
|
124
126
|
afterEach(() => {
|
|
125
127
|
rmSync(tempDir, { recursive: true, force: true });
|
|
126
|
-
|
|
128
|
+
resetTestDbPath();
|
|
127
129
|
process.env.CELILO_ORIGINAL_CWD = undefined;
|
|
128
130
|
});
|
|
129
131
|
|
|
@@ -312,7 +314,7 @@ describe('registry sweep — an unanswered breaking update is not a decline', ()
|
|
|
312
314
|
afterEach(() => {
|
|
313
315
|
server.stop(true);
|
|
314
316
|
rmSync(tempDir, { recursive: true, force: true });
|
|
315
|
-
|
|
317
|
+
resetTestDbPath();
|
|
316
318
|
process.env.CELILO_ORIGINAL_CWD = undefined;
|
|
317
319
|
process.env.EVENT_BUS_DB = undefined;
|
|
318
320
|
});
|
|
@@ -386,7 +388,7 @@ description: fixture
|
|
|
386
388
|
|
|
387
389
|
afterEach(() => {
|
|
388
390
|
rmSync(tempDir, { recursive: true, force: true });
|
|
389
|
-
|
|
391
|
+
resetTestDbPath();
|
|
390
392
|
process.env.CELILO_ORIGINAL_CWD = undefined;
|
|
391
393
|
});
|
|
392
394
|
|
|
@@ -498,7 +500,7 @@ describe('updateOne — an update that fails partway leaves the install untouche
|
|
|
498
500
|
|
|
499
501
|
afterEach(() => {
|
|
500
502
|
rmSync(tempDir, { recursive: true, force: true });
|
|
501
|
-
|
|
503
|
+
resetTestDbPath();
|
|
502
504
|
process.env.CELILO_ORIGINAL_CWD = undefined;
|
|
503
505
|
});
|
|
504
506
|
|
|
@@ -556,3 +558,84 @@ describe('updateOne — an update that fails partway leaves the install untouche
|
|
|
556
558
|
expect(mod?.version).toBe('1.0.0');
|
|
557
559
|
});
|
|
558
560
|
});
|
|
561
|
+
|
|
562
|
+
describe('updateOne — a .netapp package source', () => {
|
|
563
|
+
let tempDir: string;
|
|
564
|
+
let installedDir: string;
|
|
565
|
+
let db: DbClient;
|
|
566
|
+
|
|
567
|
+
beforeEach(() => {
|
|
568
|
+
tempDir = mkdtempSync(join(tmpdir(), 'celilo-netapp-'));
|
|
569
|
+
process.env.CELILO_DB_PATH = join(tempDir, 'test.db');
|
|
570
|
+
process.env.CELILO_ORIGINAL_CWD = tempDir;
|
|
571
|
+
|
|
572
|
+
installedDir = join(tempDir, 'installed', 'testmod');
|
|
573
|
+
mkdirSync(installedDir, { recursive: true });
|
|
574
|
+
writeFileSync(
|
|
575
|
+
join(installedDir, 'manifest.yml'),
|
|
576
|
+
'celilo_contract: "1.0"\nid: testmod\nname: Test Module\nversion: 1.0.0\ndescription: fixture\n',
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
db = getDb();
|
|
580
|
+
db.insert(modules)
|
|
581
|
+
.values({
|
|
582
|
+
id: 'testmod',
|
|
583
|
+
name: 'Test Module',
|
|
584
|
+
sourcePath: installedDir,
|
|
585
|
+
version: '1.0.0+5',
|
|
586
|
+
manifestData: {
|
|
587
|
+
celilo_contract: '1.0',
|
|
588
|
+
id: 'testmod',
|
|
589
|
+
name: 'Test Module',
|
|
590
|
+
version: '1.0.0',
|
|
591
|
+
},
|
|
592
|
+
})
|
|
593
|
+
.run();
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
afterEach(() => {
|
|
597
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
598
|
+
resetTestDbPath();
|
|
599
|
+
process.env.CELILO_ORIGINAL_CWD = undefined;
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
// ce-h4of: the temp-dir cleanup ran BEFORE the staged swap, and
|
|
603
|
+
// `replaceInstalledModule` still reads the extracted package through
|
|
604
|
+
// `actualPath`, which IS the temp dir for a .netapp source. Every
|
|
605
|
+
// registry-driven upgrade died with `ENOENT ... scandir
|
|
606
|
+
// .tmp-module-extract/<ts>`. Directory sources never set tempDir, which
|
|
607
|
+
// is why the path-source tests above stayed green. This test drives the
|
|
608
|
+
// real extraction path to keep that ordering honest.
|
|
609
|
+
test('the staged swap reads the package before the temp dir is cleaned', async () => {
|
|
610
|
+
const srcDir = join(tempDir, 'pkg-src');
|
|
611
|
+
mkdirSync(srcDir, { recursive: true });
|
|
612
|
+
writeFileSync(
|
|
613
|
+
join(srcDir, 'manifest.yml'),
|
|
614
|
+
'celilo_contract: "1.0"\nid: testmod\nname: Test Module\nversion: 2.0.0\ndescription: fixture v2\n',
|
|
615
|
+
);
|
|
616
|
+
const pkgPath = join(tempDir, 'testmod-2.0.0.netapp');
|
|
617
|
+
await tarCreate({ file: pkgPath, cwd: srcDir }, ['manifest.yml']);
|
|
618
|
+
|
|
619
|
+
const extractRoot = join(process.cwd(), '.tmp-module-extract');
|
|
620
|
+
const extractCountBefore = existsSync(extractRoot) ? readdirSync(extractRoot).length : 0;
|
|
621
|
+
|
|
622
|
+
// Registry packages are pre-verified at publish time; updateOne gets
|
|
623
|
+
// skip-verify from the sweep, so no signature.sig is needed here.
|
|
624
|
+
const result = await updateOne(
|
|
625
|
+
pkgPath,
|
|
626
|
+
db,
|
|
627
|
+
{ 'skip-verify': true },
|
|
628
|
+
{ quiet: true, displayVersion: '2.0.0+7' },
|
|
629
|
+
);
|
|
630
|
+
|
|
631
|
+
expect(result.status).toBe('success');
|
|
632
|
+
if (result.status !== 'success') return;
|
|
633
|
+
expect(result.previousVersion).toBe('1.0.0+5');
|
|
634
|
+
expect(result.newVersion).toBe('2.0.0+7');
|
|
635
|
+
expect(readFileSync(join(installedDir, 'manifest.yml'), 'utf-8')).toContain('version: 2.0.0');
|
|
636
|
+
|
|
637
|
+
// The extraction scratch space is gone again once the swap has read it.
|
|
638
|
+
const extractCountAfter = existsSync(extractRoot) ? readdirSync(extractRoot).length : 0;
|
|
639
|
+
expect(extractCountAfter).toBe(extractCountBefore);
|
|
640
|
+
});
|
|
641
|
+
});
|
|
@@ -128,7 +128,13 @@ export async function fetchAndUpdate(
|
|
|
128
128
|
): Promise<UpdateOutcome> {
|
|
129
129
|
const tmpPath = join(tmpdir(), `${moduleId}-${version}-${Date.now()}.netapp`);
|
|
130
130
|
try {
|
|
131
|
-
|
|
131
|
+
// The index entry carries the published sha256. Look it up here rather than
|
|
132
|
+
// thread it through five call sites, so a short download is caught as a bad
|
|
133
|
+
// transfer instead of surfacing later as "zlib: unexpected end of file".
|
|
134
|
+
// Absence is tolerated: an entry predating cksum still downloads.
|
|
135
|
+
const entries = await client.getIndex(moduleId).catch(() => []);
|
|
136
|
+
const cksum = entries.find((entry) => entry.vers === version)?.cksum;
|
|
137
|
+
const pkgData = await client.download(moduleId, version, cksum);
|
|
132
138
|
await Bun.write(tmpPath, pkgData);
|
|
133
139
|
} catch (err) {
|
|
134
140
|
return {
|
|
@@ -410,9 +416,6 @@ export async function updateOne(
|
|
|
410
416
|
? readFileSync(packagedSignaturePath, 'utf-8').trim()
|
|
411
417
|
: null;
|
|
412
418
|
|
|
413
|
-
// Clean up temp dir if we extracted a .netapp
|
|
414
|
-
if (tempDir) await cleanupTempDir(tempDir);
|
|
415
|
-
|
|
416
419
|
// Remove what the new version dropped. `updateOne` only ever overlaid files,
|
|
417
420
|
// so a hook script deleted in 1.1.0 stayed on the box and stayed runnable —
|
|
418
421
|
// code celilo no longer believes it has installed, which is the same class of
|
|
@@ -440,6 +443,13 @@ export async function updateOne(
|
|
|
440
443
|
// below it runs only once the files are really in place.
|
|
441
444
|
replaceInstalledModule(actualPath, installedPath, pruneDropped);
|
|
442
445
|
|
|
446
|
+
// Clean up the extracted package. This waits until after the swap because
|
|
447
|
+
// `replaceInstalledModule` reads `actualPath`, which IS `tempDir` for a
|
|
448
|
+
// .netapp source — deleting first is what made every registry-driven
|
|
449
|
+
// upgrade die with `ENOENT ... scandir .tmp-module-extract/<ts>`
|
|
450
|
+
// (ce-h4of, a regression from the celilo#1008 staging swap).
|
|
451
|
+
if (tempDir) await cleanupTempDir(tempDir);
|
|
452
|
+
|
|
443
453
|
// Record it. `updateOne` never touched this table, so the baseline stayed
|
|
444
454
|
// frozen at the module's FIRST import no matter how many times it was
|
|
445
455
|
// updated. That is why `module verify` reported the same violations whether
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type PollCandidate,
|
|
5
5
|
isPollInvocation,
|
|
6
6
|
needsPreUpgradeBackup,
|
|
7
|
+
needsSystemStateSnapshot,
|
|
7
8
|
pickAutoUpgrade,
|
|
8
9
|
pickUpgradePolicy,
|
|
9
10
|
selectPollTargets,
|
|
@@ -102,6 +103,20 @@ describe('selectPollTargets (ISS-0139 — opted-in + a newer registry version)',
|
|
|
102
103
|
});
|
|
103
104
|
});
|
|
104
105
|
|
|
106
|
+
// A 0.x minor/major bump can migrate the celilo DB schema; the system_state
|
|
107
|
+
// snapshot is what makes a later `apt install celilo=<old>` (or dpkg -i from
|
|
108
|
+
// the apt cache) a real rollback instead of a gamble. Patch/revision bumps
|
|
109
|
+
// don't migrate, so they stay fast.
|
|
110
|
+
describe('needsSystemStateSnapshot (safe posture snapshots the celilo DB before mutating)', () => {
|
|
111
|
+
test('safe posture (minor/major) requires the snapshot', () => {
|
|
112
|
+
expect(needsSystemStateSnapshot('safe')).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('fast posture (patch/revision) skips the snapshot', () => {
|
|
116
|
+
expect(needsSystemStateSnapshot('fast')).toBe(false);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
105
120
|
describe('needsPreUpgradeBackup (ISS-0168 — gate on the TARGET version manifest)', () => {
|
|
106
121
|
// The regression: the upgrade that FIRST adds on_backup must still back up.
|
|
107
122
|
// The installed version lacked the hook; the target (passed here) adds it.
|
|
@@ -20,7 +20,8 @@ import { type DbClient, getDb } from '../../db/client';
|
|
|
20
20
|
import { modules } from '../../db/schema';
|
|
21
21
|
import type { ModuleManifest } from '../../manifest/schema';
|
|
22
22
|
import { RegistryClient } from '../../registry/client';
|
|
23
|
-
import { createModuleBackup } from '../../services/backup-create';
|
|
23
|
+
import { createModuleBackup, createSystemStateBackup } from '../../services/backup-create';
|
|
24
|
+
import { getDefaultBackupStorage } from '../../services/backup-storage';
|
|
24
25
|
import {
|
|
25
26
|
type DeployPosture,
|
|
26
27
|
type UpgradePolicy,
|
|
@@ -79,6 +80,47 @@ export function needsPreUpgradeBackup(
|
|
|
79
80
|
return posture === 'safe' && Boolean(targetManifest.hooks?.on_backup);
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Pure (Rule 10): does this upgrade snapshot the celilo DB first? Safe posture
|
|
85
|
+
* means a minor/major bump, and a minor bump can migrate the DB schema — the
|
|
86
|
+
* snapshot is what makes a later package downgrade a real rollback instead of
|
|
87
|
+
* a gamble on an older binary reading a newer-migrated database.
|
|
88
|
+
*/
|
|
89
|
+
export function needsSystemStateSnapshot(posture: DeployPosture): boolean {
|
|
90
|
+
return posture === 'safe';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Take the system_state snapshot a safe-posture upgrade requires.
|
|
95
|
+
*
|
|
96
|
+
* A host with no default backup storage keeps today's behavior (warn and
|
|
97
|
+
* proceed): refusing every safe upgrade there would break CD for a config
|
|
98
|
+
* gap, and the module-level backup below still runs when the target manifest
|
|
99
|
+
* declares on_backup. A configured-but-failing snapshot refuses the upgrade,
|
|
100
|
+
* same rule `system update` applies to its own snapshot (D7): the DB snapshot
|
|
101
|
+
* is the safety net, and an upgrade that cannot take it should not mutate.
|
|
102
|
+
*/
|
|
103
|
+
async function snapshotSystemStateBeforeUpgrade(
|
|
104
|
+
moduleId: string,
|
|
105
|
+
): Promise<{ ok: true } | { ok: false; error: string }> {
|
|
106
|
+
try {
|
|
107
|
+
if (!getDefaultBackupStorage()) {
|
|
108
|
+
log.warn(
|
|
109
|
+
`${moduleId}: no default backup storage configured — skipping pre-upgrade system_state snapshot`,
|
|
110
|
+
);
|
|
111
|
+
return { ok: true };
|
|
112
|
+
}
|
|
113
|
+
const snapshot = await createSystemStateBackup();
|
|
114
|
+
if (!snapshot.success) {
|
|
115
|
+
return { ok: false, error: snapshot.error ?? 'system_state backup failed' };
|
|
116
|
+
}
|
|
117
|
+
log.success(`System state snapshot taken before upgrading ${moduleId}`);
|
|
118
|
+
return { ok: true };
|
|
119
|
+
} catch (err) {
|
|
120
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
82
124
|
export interface PollCandidate {
|
|
83
125
|
moduleId: string;
|
|
84
126
|
installed: string;
|
|
@@ -168,7 +210,17 @@ async function upgradeOneModule(
|
|
|
168
210
|
});
|
|
169
211
|
log.info(`Upgrading ${moduleId} ${mod.version} → ${targetVersion} (${posture} — ${reason})`);
|
|
170
212
|
|
|
171
|
-
// Safe →
|
|
213
|
+
// Safe → snapshot the celilo DB first (the package rollback pair), then
|
|
214
|
+
// back up the module itself, gated on the TARGET manifest's on_backup hook.
|
|
215
|
+
if (needsSystemStateSnapshot(posture)) {
|
|
216
|
+
const snapshot = await snapshotSystemStateBeforeUpgrade(moduleId);
|
|
217
|
+
if (!snapshot.ok) {
|
|
218
|
+
return {
|
|
219
|
+
success: false,
|
|
220
|
+
error: `Pre-upgrade system_state snapshot failed for ${moduleId}: ${snapshot.error}`,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
}
|
|
172
224
|
if (needsPreUpgradeBackup(posture, targetManifest)) {
|
|
173
225
|
const backup = await createModuleBackup(moduleId);
|
|
174
226
|
if (!backup.success) {
|
|
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
|
2
2
|
import { mkdtempSync, rmSync } from 'node:fs';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
+
import { resetTestDbPath } from '../../test-utils/db-path';
|
|
5
6
|
import { moduleAudit } from './module-audit';
|
|
6
7
|
import { moduleVerify } from './module-verify';
|
|
7
8
|
|
|
@@ -19,7 +20,6 @@ describe('moduleVerify', () => {
|
|
|
19
20
|
describe('moduleAudit (deprecation alias)', () => {
|
|
20
21
|
let dataDir: string;
|
|
21
22
|
const originalDataDir = process.env.CELILO_DATA_DIR;
|
|
22
|
-
const originalDbPath = process.env.CELILO_DB_PATH;
|
|
23
23
|
|
|
24
24
|
beforeEach(() => {
|
|
25
25
|
dataDir = mkdtempSync(join(tmpdir(), 'celilo-audit-alias-'));
|
|
@@ -29,10 +29,9 @@ describe('moduleAudit (deprecation alias)', () => {
|
|
|
29
29
|
|
|
30
30
|
afterEach(() => {
|
|
31
31
|
rmSync(dataDir, { recursive: true, force: true });
|
|
32
|
+
resetTestDbPath();
|
|
32
33
|
if (originalDataDir === undefined) process.env.CELILO_DATA_DIR = undefined;
|
|
33
34
|
else process.env.CELILO_DATA_DIR = originalDataDir;
|
|
34
|
-
if (originalDbPath === undefined) process.env.CELILO_DB_PATH = undefined;
|
|
35
|
-
else process.env.CELILO_DB_PATH = originalDbPath;
|
|
36
35
|
});
|
|
37
36
|
|
|
38
37
|
test('writes a deprecation warning to stderr and delegates to verify', async () => {
|