@celilo/cli 2.2.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/package.json +2 -2
- package/schemas/system_config.json +5 -0
- package/src/ansible/inventory.test.ts +2 -1
- package/src/api/sessions.test.ts +2 -1
- 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 +2 -1
- package/src/cli/commands/module-config.test.ts +2 -1
- 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-operations.test.ts +2 -1
- package/src/cli/commands/module-publish.test.ts +5 -12
- package/src/cli/commands/module-update.test.ts +6 -5
- 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/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/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/index.ts +2 -1
- package/src/cli/restore-command.test.ts +2 -1
- package/src/cli/restore-migration-failure.test.ts +2 -1
- package/src/config/paths.test.ts +3 -3
- package/src/db/migrate.test.ts +2 -1
- package/src/db/migrate.ts +16 -16
- package/src/hooks/capability-loader.test.ts +78 -0
- package/src/hooks/capability-loader.ts +37 -2
- package/src/hooks/executor.test.ts +87 -1
- package/src/hooks/executor.ts +57 -21
- package/src/hooks/hook-store.test.ts +6 -0
- package/src/hooks/hook-trespass.test.ts +17 -7
- package/src/hooks/jail.test.ts +61 -2
- package/src/hooks/jail.ts +53 -1
- 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 +60 -0
- package/src/module/packaging/extract.ts +1 -5
- package/src/policy/module-script-scan.ts +0 -40
- package/src/policy/no-swallowed-refusal.test.ts +9 -10
- 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/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/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-envelope-roundtrip.test.ts +2 -1
- package/src/services/backup-in-flight-refusal.test.ts +2 -1
- package/src/services/bus-ensure-flow.test.ts +2 -1
- package/src/services/bus-interview-park.test.ts +2 -1
- package/src/services/bus-secret-flow.test.ts +2 -1
- package/src/services/capability-table-rows.test.ts +2 -1
- 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 +2 -1
- package/src/services/cross-module-read.test.ts +2 -1
- package/src/services/deploy-validation.test.ts +3 -2
- 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 +3 -2
- 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-deploy.dns-repoint.test.ts +2 -1
- package/src/services/module-deploy.ts +36 -20
- package/src/services/module-operations.test.ts +2 -1
- package/src/services/module-subscriptions.test.ts +2 -1
- 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 +3 -2
- 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 +5 -4
- package/src/services/static-content-converge.ts +33 -10
- package/src/services/system-state-stage.test.ts +2 -1
- package/src/services/trusted-sources.test.ts +3 -2
- package/src/services/zone-detector.test.ts +2 -1
- 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/module/packaging/generated-plane.test.ts +0 -79
- package/src/module/packaging/generated-plane.ts +0 -134
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@celilo/cli",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"description": "Celilo — home lab orchestration CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"@aws-sdk/lib-storage": "^3.1101.0",
|
|
61
61
|
"@celilo/capabilities": "^4.2.0",
|
|
62
62
|
"@celilo/cli-display": "^0.2.0",
|
|
63
|
-
"@celilo/core": "^0.11.
|
|
63
|
+
"@celilo/core": "^0.11.1",
|
|
64
64
|
"@celilo/event-bus": "^0.6.0",
|
|
65
65
|
"ajv": "^8.18.0",
|
|
66
66
|
"drizzle-orm": "^0.36.4",
|
|
@@ -118,6 +118,11 @@
|
|
|
118
118
|
"type": "string",
|
|
119
119
|
"description": "SSH public key for container access"
|
|
120
120
|
},
|
|
121
|
+
"hooks.jail_policy": {
|
|
122
|
+
"type": "string",
|
|
123
|
+
"pattern": "^(auto|off|required)$",
|
|
124
|
+
"description": "Hook jail policy: 'auto' (jail when a backend is available), 'required' (an unavailable jail is a hard failure), or 'off' (hooks run unjailed). Precedence: the CELILO_HOOK_JAIL environment variable wins over this stored value. Run 'celilo system doctor' to see the effective policy and which source it came from."
|
|
125
|
+
},
|
|
121
126
|
"public_dns.resolver": {
|
|
122
127
|
"type": "string",
|
|
123
128
|
"pattern": "^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$",
|
|
@@ -4,6 +4,7 @@ import { rm } from 'node:fs/promises';
|
|
|
4
4
|
import { type DbClient, createDbClient } from '../db/client';
|
|
5
5
|
import { systemConfig } from '../db/schema';
|
|
6
6
|
import { upsertModuleConfig } from '../services/module-config';
|
|
7
|
+
import { resetTestDbPath } from '../test-utils/db-path';
|
|
7
8
|
import {
|
|
8
9
|
buildHostVars,
|
|
9
10
|
buildSystemVars,
|
|
@@ -286,7 +287,7 @@ describe('Database integration', () => {
|
|
|
286
287
|
afterEach(async () => {
|
|
287
288
|
db.$client.close();
|
|
288
289
|
// Clean up environment variable
|
|
289
|
-
|
|
290
|
+
resetTestDbPath();
|
|
290
291
|
if (existsSync(TEST_DB_PATH)) {
|
|
291
292
|
await rm(TEST_DB_PATH);
|
|
292
293
|
}
|
package/src/api/sessions.test.ts
CHANGED
|
@@ -32,6 +32,7 @@ import { handleModuleUpdate } from '../cli/commands/module-update';
|
|
|
32
32
|
import { getDb } from '../db/client';
|
|
33
33
|
import { modules } from '../db/schema';
|
|
34
34
|
import { RESPONDER_PROBE_EVENT } from '../services/responder-probe';
|
|
35
|
+
import { resetTestDbPath } from '../test-utils/db-path';
|
|
35
36
|
import { SessionWriter, listSessions, reapExpiredSessions } from './sessions';
|
|
36
37
|
|
|
37
38
|
const NO_SCHEMAS = defineEvents({});
|
|
@@ -99,7 +100,7 @@ beforeEach(() => {
|
|
|
99
100
|
afterEach(() => {
|
|
100
101
|
server.stop(true);
|
|
101
102
|
rmSync(dir, { recursive: true, force: true });
|
|
102
|
-
|
|
103
|
+
resetTestDbPath();
|
|
103
104
|
process.env.CELILO_DATA_DIR = undefined;
|
|
104
105
|
process.env.CELILO_ORIGINAL_CWD = undefined;
|
|
105
106
|
process.env.EVENT_BUS_DB = undefined;
|
|
@@ -16,6 +16,7 @@ import { tmpdir } from 'node:os';
|
|
|
16
16
|
import { join } from 'node:path';
|
|
17
17
|
import { closeDb } from '../db/client';
|
|
18
18
|
import { runMigrations } from '../db/migrate';
|
|
19
|
+
import { resetTestDbPath } from '../test-utils/db-path';
|
|
19
20
|
import { runCli } from './index';
|
|
20
21
|
|
|
21
22
|
describe('celilo module backup (renamed surface)', () => {
|
|
@@ -48,7 +49,7 @@ describe('celilo module backup (renamed surface)', () => {
|
|
|
48
49
|
warnSpy = null;
|
|
49
50
|
}
|
|
50
51
|
closeDb();
|
|
51
|
-
|
|
52
|
+
resetTestDbPath();
|
|
52
53
|
process.env.CELILO_MASTER_KEY_PATH = undefined;
|
|
53
54
|
process.env.CELILO_SUPPRESS_DEPRECATION = undefined;
|
|
54
55
|
try {
|
package/src/cli/cli.test.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { moduleConfigs, modules, secrets } from '../db/schema';
|
|
|
8
8
|
import { encryptSecret } from '../secrets/encryption';
|
|
9
9
|
import { generateMasterKey, writeMasterKey } from '../secrets/master-key';
|
|
10
10
|
import { upsertModuleConfig } from '../services/module-config';
|
|
11
|
+
import { resetTestDbPath } from '../test-utils/db-path';
|
|
11
12
|
import { runCli } from './index';
|
|
12
13
|
|
|
13
14
|
const TEST_DB_PATH = `./test-cli-${Date.now()}-${Math.random()}.db`;
|
|
@@ -147,7 +148,7 @@ description: Test module for CLI
|
|
|
147
148
|
|
|
148
149
|
// Clear environment variables
|
|
149
150
|
process.env.CELILO_MASTER_KEY_PATH = undefined;
|
|
150
|
-
|
|
151
|
+
resetTestDbPath();
|
|
151
152
|
});
|
|
152
153
|
|
|
153
154
|
describe('help', () => {
|
|
@@ -15,6 +15,7 @@ import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
|
15
15
|
import { mkdtempSync, rmSync } from 'node:fs';
|
|
16
16
|
import { tmpdir } from 'node:os';
|
|
17
17
|
import { join } from 'node:path';
|
|
18
|
+
import { resetTestDbPath } from '../../test-utils/db-path';
|
|
18
19
|
|
|
19
20
|
let testDir: string;
|
|
20
21
|
|
|
@@ -26,7 +27,7 @@ beforeEach(() => {
|
|
|
26
27
|
|
|
27
28
|
afterEach(() => {
|
|
28
29
|
rmSync(testDir, { recursive: true, force: true });
|
|
29
|
-
|
|
30
|
+
resetTestDbPath();
|
|
30
31
|
});
|
|
31
32
|
|
|
32
33
|
/** The live fleet's firewall shape: caddy on iptables, iptables on the router. */
|
|
@@ -12,6 +12,7 @@ import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test';
|
|
|
12
12
|
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
13
13
|
import { tmpdir } from 'node:os';
|
|
14
14
|
import { join } from 'node:path';
|
|
15
|
+
import { resetTestDbPath } from '../../test-utils/db-path';
|
|
15
16
|
|
|
16
17
|
// The command reaches boxes ONLY through these two modules, and both are
|
|
17
18
|
// mocked below so the tests can drive the live-read and fallback paths without
|
|
@@ -67,7 +68,7 @@ beforeEach(() => {
|
|
|
67
68
|
afterEach(() => {
|
|
68
69
|
console.log = originalLog;
|
|
69
70
|
rmSync(testDir, { recursive: true, force: true });
|
|
70
|
-
|
|
71
|
+
resetTestDbPath();
|
|
71
72
|
process.env.CELILO_DATA_DIR = undefined;
|
|
72
73
|
process.env.CELILO_MASTER_KEY_PATH = undefined;
|
|
73
74
|
});
|
|
@@ -12,6 +12,7 @@ import { join } from 'node:path';
|
|
|
12
12
|
import { type DbClient, getDb } from '../../db/client';
|
|
13
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,
|
|
@@ -51,7 +52,7 @@ describe('handleModuleConfigSet — infra-key contract (ISS-0069)', () => {
|
|
|
51
52
|
|
|
52
53
|
afterEach(() => {
|
|
53
54
|
rmSync(tempDir, { recursive: true, force: true });
|
|
54
|
-
|
|
55
|
+
resetTestDbPath();
|
|
55
56
|
});
|
|
56
57
|
|
|
57
58
|
test('rejects an infrastructure-managed key (no silent accept-then-override)', async () => {
|
|
@@ -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
|
|
|
@@ -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
|
}
|
|
@@ -21,6 +21,7 @@ import { eq } from 'drizzle-orm';
|
|
|
21
21
|
import { create as tarCreate } from 'tar';
|
|
22
22
|
import { type DbClient, getDb } from '../../db/client';
|
|
23
23
|
import { moduleIntegrity, modules } from '../../db/schema';
|
|
24
|
+
import { resetTestDbPath } from '../../test-utils/db-path';
|
|
24
25
|
import { classifyVersionChange, handleModuleUpdate, updateOne } from './module-update';
|
|
25
26
|
|
|
26
27
|
describe('classifyVersionChange', () => {
|
|
@@ -124,7 +125,7 @@ description: fixture
|
|
|
124
125
|
|
|
125
126
|
afterEach(() => {
|
|
126
127
|
rmSync(tempDir, { recursive: true, force: true });
|
|
127
|
-
|
|
128
|
+
resetTestDbPath();
|
|
128
129
|
process.env.CELILO_ORIGINAL_CWD = undefined;
|
|
129
130
|
});
|
|
130
131
|
|
|
@@ -313,7 +314,7 @@ describe('registry sweep — an unanswered breaking update is not a decline', ()
|
|
|
313
314
|
afterEach(() => {
|
|
314
315
|
server.stop(true);
|
|
315
316
|
rmSync(tempDir, { recursive: true, force: true });
|
|
316
|
-
|
|
317
|
+
resetTestDbPath();
|
|
317
318
|
process.env.CELILO_ORIGINAL_CWD = undefined;
|
|
318
319
|
process.env.EVENT_BUS_DB = undefined;
|
|
319
320
|
});
|
|
@@ -387,7 +388,7 @@ description: fixture
|
|
|
387
388
|
|
|
388
389
|
afterEach(() => {
|
|
389
390
|
rmSync(tempDir, { recursive: true, force: true });
|
|
390
|
-
|
|
391
|
+
resetTestDbPath();
|
|
391
392
|
process.env.CELILO_ORIGINAL_CWD = undefined;
|
|
392
393
|
});
|
|
393
394
|
|
|
@@ -499,7 +500,7 @@ describe('updateOne — an update that fails partway leaves the install untouche
|
|
|
499
500
|
|
|
500
501
|
afterEach(() => {
|
|
501
502
|
rmSync(tempDir, { recursive: true, force: true });
|
|
502
|
-
|
|
503
|
+
resetTestDbPath();
|
|
503
504
|
process.env.CELILO_ORIGINAL_CWD = undefined;
|
|
504
505
|
});
|
|
505
506
|
|
|
@@ -594,7 +595,7 @@ describe('updateOne — a .netapp package source', () => {
|
|
|
594
595
|
|
|
595
596
|
afterEach(() => {
|
|
596
597
|
rmSync(tempDir, { recursive: true, force: true });
|
|
597
|
-
|
|
598
|
+
resetTestDbPath();
|
|
598
599
|
process.env.CELILO_ORIGINAL_CWD = undefined;
|
|
599
600
|
});
|
|
600
601
|
|
|
@@ -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 () => {
|