@celilo/cli 0.14.4 → 0.16.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/CELILO_CORE_MODULES.md +1 -1
- package/CELILO_SUBSYSTEMS.md +19 -2
- package/drizzle/0018_drop_alert_policy_snapshot.sql +46 -0
- package/drizzle/meta/_journal.json +8 -1
- package/package.json +3 -3
- package/src/cli/commands/alerts-list.ts +10 -0
- package/src/cli/commands/alerts-poll.ts +12 -6
- package/src/cli/commands/alerts-sweep.ts +22 -81
- package/src/cli/commands/backup-sweep.ts +65 -0
- package/src/cli/commands/module-config.test.ts +77 -1
- package/src/cli/commands/module-config.ts +45 -3
- package/src/cli/commands/module-journal.test.ts +47 -0
- package/src/cli/commands/module-journal.ts +98 -0
- package/src/cli/commands/module-operations.test.ts +93 -0
- package/src/cli/commands/module-operations.ts +134 -0
- package/src/cli/commands/module-upgrade.test.ts +32 -20
- package/src/cli/commands/module-upgrade.ts +37 -32
- package/src/cli/commands/monitor.ts +26 -6
- package/src/cli/commands/system-audit.ts +3 -30
- package/src/cli/completion.ts +20 -1
- package/src/cli/generate-zsh-completion.ts +4 -0
- package/src/cli/index.ts +14 -0
- package/src/db/schema.ts +5 -3
- package/src/manifest/schema.ts +4 -1
- package/src/module/packaging/build.ts +4 -0
- package/src/services/alerting/builtin-source.ts +17 -2
- package/src/services/alerting/delivery-loop.test.ts +5 -1
- package/src/services/alerting/format.test.ts +0 -1
- package/src/services/alerting/inbound-poller.test.ts +235 -8
- package/src/services/alerting/inbound-poller.ts +95 -34
- package/src/services/alerting/inbound.test.ts +213 -2
- package/src/services/alerting/inbound.ts +161 -32
- package/src/services/alerting/interview-responder.test.ts +0 -32
- package/src/services/alerting/interview-responder.ts +6 -17
- package/src/services/alerting/notify-deps.ts +113 -0
- package/src/services/alerting/run-monitor.ts +0 -1
- package/src/services/alerting/store.test.ts +1 -1
- package/src/services/alerting/store.ts +0 -2
- package/src/services/alerting/sweep-runner.test.ts +11 -2
- package/src/services/alerting/sweep-runner.ts +14 -7
- package/src/services/alerting/tokens.ts +39 -1
- package/src/services/audit/backup-source.ts +54 -0
- package/src/services/audit/backups.test.ts +7 -2
- package/src/services/audit/backups.ts +10 -18
- package/src/services/backup-cipher.test.ts +188 -0
- package/src/services/backup-cipher.ts +178 -0
- package/src/services/backup-create.ts +20 -30
- package/src/services/backup-envelope-roundtrip.test.ts +6 -26
- package/src/services/backup-restore.ts +10 -16
- package/src/services/backup-schedule.ts +35 -0
- package/src/services/backup-sweep.test.ts +148 -0
- package/src/services/backup-sweep.ts +124 -0
- package/src/services/deploy-posture.ts +15 -2
- package/src/services/module-journal.test.ts +302 -0
- package/src/services/module-journal.ts +160 -0
- package/src/services/module-operations.test.ts +67 -6
- package/src/services/module-operations.ts +69 -19
- package/src/services/module-subscriptions.test.ts +33 -2
- package/src/services/module-subscriptions.ts +10 -1
- package/src/services/module-validator/typescript-build.test.ts +20 -1
- package/src/services/module-validator/typescript-build.ts +9 -5
- package/src/services/restore-from-file.ts +6 -21
- package/src/templates/generator.test.ts +88 -0
- package/src/templates/generator.ts +119 -16
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { execSync } from 'node:child_process';
|
|
8
|
-
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync
|
|
8
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs';
|
|
9
9
|
import { tmpdir } from 'node:os';
|
|
10
10
|
import { join } from 'node:path';
|
|
11
11
|
import { eq } from 'drizzle-orm';
|
|
@@ -19,6 +19,7 @@ import type { ModuleManifest } from '../manifest/schema';
|
|
|
19
19
|
import { decryptSecret } from '../secrets/encryption';
|
|
20
20
|
import { getOrCreateMasterKey } from '../secrets/master-key';
|
|
21
21
|
import { shellEscape } from '../utils/shell';
|
|
22
|
+
import { decryptFileToFile } from './backup-cipher';
|
|
22
23
|
import { assertCompatibleSchema, parseManifest } from './backup-manifest';
|
|
23
24
|
import { createStorageProvider } from './backup-storage';
|
|
24
25
|
import { applyCrossModuleWriteRoot, moduleHasCrossModuleRead } from './cross-module-read';
|
|
@@ -61,17 +62,13 @@ export async function restoreSystemStateBackup(backup: Backup): Promise<RestoreR
|
|
|
61
62
|
const encryptedPath = join(tempDir, 'system.enc');
|
|
62
63
|
await provider.download(backup.storagePath, encryptedPath);
|
|
63
64
|
|
|
64
|
-
// Decrypt → tar
|
|
65
|
-
|
|
65
|
+
// Decrypt → tar, streamed. Reads both the current format and the legacy
|
|
66
|
+
// JSON envelope (see backup-cipher.ts).
|
|
66
67
|
const masterKey = await getOrCreateMasterKey();
|
|
67
|
-
const base64Data = decryptSecret(encryptedData, masterKey);
|
|
68
|
-
const tarData = Buffer.from(base64Data, 'base64');
|
|
69
|
-
|
|
70
|
-
// Extract the envelope tar (manifest.json + celilo.db [+celilo.db-wal])
|
|
71
68
|
const envelopeDir = join(tempDir, 'envelope');
|
|
72
69
|
mkdirSync(envelopeDir, { recursive: true });
|
|
73
70
|
const tarPath = join(tempDir, 'envelope.tar');
|
|
74
|
-
|
|
71
|
+
await decryptFileToFile(encryptedPath, tarPath, masterKey);
|
|
75
72
|
execSync(`tar -xf ${shellEscape(tarPath)} -C ${shellEscape(envelopeDir)}`);
|
|
76
73
|
|
|
77
74
|
// Read + validate manifest BEFORE touching the live DB. An
|
|
@@ -215,17 +212,14 @@ export async function restoreModuleBackup(
|
|
|
215
212
|
const encryptedPath = join(tempDir, 'backup.tar.enc');
|
|
216
213
|
await provider.download(backup.storagePath, encryptedPath);
|
|
217
214
|
|
|
218
|
-
// Decrypt → tar
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const base64Data = decryptSecret(encryptedData, masterKey);
|
|
222
|
-
const tarData = Buffer.from(base64Data, 'base64');
|
|
223
|
-
|
|
224
|
-
// Write tar and extract into envelopeDir. The envelope contains:
|
|
215
|
+
// Decrypt → tar, streamed. Reads both the current format and the legacy
|
|
216
|
+
// JSON envelope (see backup-cipher.ts). Extract into envelopeDir; the
|
|
217
|
+
// envelope contains:
|
|
225
218
|
// manifest.json - validated below
|
|
226
219
|
// data/ - on_backup hook artifacts (passed to on_restore)
|
|
220
|
+
const masterKey = await getOrCreateMasterKey();
|
|
227
221
|
const tarPath = join(tempDir, 'envelope.tar');
|
|
228
|
-
|
|
222
|
+
await decryptFileToFile(encryptedPath, tarPath, masterKey);
|
|
229
223
|
execSync(`tar -xf ${shellEscape(tarPath)} -C ${shellEscape(envelopeDir)}`);
|
|
230
224
|
|
|
231
225
|
// Read + validate envelope manifest BEFORE invoking the hook.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What backup cadence a module actually has.
|
|
3
|
+
*
|
|
4
|
+
* One accessor, deliberately: the drift audit decides whether to ALERT
|
|
5
|
+
* that a backup is stale, and the backup sweep decides whether to RUN
|
|
6
|
+
* one. If those two read the manifest differently, a module can be
|
|
7
|
+
* alerted-on-but-never-backed-up — an alert no human action can clear.
|
|
8
|
+
*
|
|
9
|
+
* Absent means `daily`, not `manual`. Treating "the author didn't say"
|
|
10
|
+
* as "never check and never run" is what let celilo-mgmt go 55 days
|
|
11
|
+
* without a backup and forgejo and signal go without one entirely, all
|
|
12
|
+
* silently. Opting out is a decision worth writing down, so it takes an
|
|
13
|
+
* explicit `schedule: manual`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { ModuleManifest } from '../manifest/schema';
|
|
17
|
+
|
|
18
|
+
export type BackupSchedule = 'hourly' | 'daily' | 'weekly' | 'monthly' | 'manual';
|
|
19
|
+
|
|
20
|
+
/** Used when a manifest declares an `on_backup` hook but no cadence. */
|
|
21
|
+
export const DEFAULT_BACKUP_SCHEDULE: BackupSchedule = 'daily';
|
|
22
|
+
|
|
23
|
+
export function effectiveBackupSchedule(manifest: ModuleManifest): BackupSchedule {
|
|
24
|
+
const declared = manifest.backup?.schedule;
|
|
25
|
+
switch (declared) {
|
|
26
|
+
case 'hourly':
|
|
27
|
+
case 'daily':
|
|
28
|
+
case 'weekly':
|
|
29
|
+
case 'monthly':
|
|
30
|
+
case 'manual':
|
|
31
|
+
return declared;
|
|
32
|
+
default:
|
|
33
|
+
return DEFAULT_BACKUP_SCHEDULE;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import type { ModuleManifest } from '../manifest/schema';
|
|
3
|
+
import type { BackupSchedule } from './backup-schedule';
|
|
4
|
+
import {
|
|
5
|
+
BACKUP_SWEEP_PATTERN,
|
|
6
|
+
BACKUP_SWEEP_SUBSCRIBER,
|
|
7
|
+
type BackupSweepDeps,
|
|
8
|
+
type BackupSweepModule,
|
|
9
|
+
ensureBackupSweepSubscriber,
|
|
10
|
+
runBackupSweep,
|
|
11
|
+
} from './backup-sweep';
|
|
12
|
+
import { InFlightError } from './module-operations';
|
|
13
|
+
|
|
14
|
+
function moduleWith(id: string, schedule?: BackupSchedule): BackupSweepModule {
|
|
15
|
+
const manifest = {
|
|
16
|
+
id,
|
|
17
|
+
hooks: { on_backup: { script: 'backup.ts' } },
|
|
18
|
+
...(schedule ? { backup: { schedule } } : {}),
|
|
19
|
+
} as unknown as ModuleManifest;
|
|
20
|
+
return { id, manifest };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function deps(
|
|
24
|
+
modules: BackupSweepModule[],
|
|
25
|
+
overrides: Partial<BackupSweepDeps> = {},
|
|
26
|
+
): BackupSweepDeps & { pruned: string[] } {
|
|
27
|
+
const pruned: string[] = [];
|
|
28
|
+
return {
|
|
29
|
+
pruned,
|
|
30
|
+
listEligible: () => modules,
|
|
31
|
+
isDue: () => true,
|
|
32
|
+
backup: async () => ({ success: true }),
|
|
33
|
+
prune: async ({ id }) => {
|
|
34
|
+
pruned.push(id);
|
|
35
|
+
},
|
|
36
|
+
...overrides,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('runBackupSweep', () => {
|
|
41
|
+
test('backs up a module whose declared cadence is due', async () => {
|
|
42
|
+
const d = deps([moduleWith('authentik', 'daily')]);
|
|
43
|
+
const report = await runBackupSweep(d);
|
|
44
|
+
|
|
45
|
+
expect(report.backedUp).toEqual(['authentik']);
|
|
46
|
+
expect(report.failures).toEqual([]);
|
|
47
|
+
expect(d.pruned).toEqual(['authentik']);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('skips a module that is not yet due', async () => {
|
|
51
|
+
const report = await runBackupSweep(
|
|
52
|
+
deps([moduleWith('authentik', 'daily')], { isDue: () => false }),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
expect(report.backedUp).toEqual([]);
|
|
56
|
+
expect(report.skippedNotDue).toEqual(['authentik']);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('never auto-backs-up an explicit manual schedule', async () => {
|
|
60
|
+
const report = await runBackupSweep(deps([moduleWith('scratch', 'manual')]));
|
|
61
|
+
|
|
62
|
+
expect(report.backedUp).toEqual([]);
|
|
63
|
+
expect(report.skippedManual).toEqual(['scratch']);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('an undeclared schedule is backed up, not treated as manual', async () => {
|
|
67
|
+
// The regression this whole subsystem exists for: forgejo and signal
|
|
68
|
+
// declare no `backup:` block and had never been backed up.
|
|
69
|
+
const report = await runBackupSweep(deps([moduleWith('forgejo')]));
|
|
70
|
+
|
|
71
|
+
expect(report.backedUp).toEqual(['forgejo']);
|
|
72
|
+
expect(report.skippedManual).toEqual([]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('a held operation lock is a skip, not a failure, and stops the pass', async () => {
|
|
76
|
+
const d = deps([moduleWith('authentik', 'daily'), moduleWith('forgejo', 'daily')], {
|
|
77
|
+
backup: async () => {
|
|
78
|
+
throw new InFlightError([]);
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
const report = await runBackupSweep(d);
|
|
82
|
+
|
|
83
|
+
expect(report.skippedLocked).toEqual(['authentik']);
|
|
84
|
+
expect(report.failures).toEqual([]);
|
|
85
|
+
expect(report.backedUp).toEqual([]);
|
|
86
|
+
expect(d.pruned).toEqual([]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('a failed backup is recorded and the remaining modules still run', async () => {
|
|
90
|
+
const report = await runBackupSweep(
|
|
91
|
+
deps([moduleWith('authentik', 'daily'), moduleWith('forgejo', 'daily')], {
|
|
92
|
+
backup: async (moduleId) =>
|
|
93
|
+
moduleId === 'authentik' ? { success: false, error: 'hook exited 1' } : { success: true },
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
expect(report.failures).toEqual([{ moduleId: 'authentik', error: 'hook exited 1' }]);
|
|
98
|
+
expect(report.backedUp).toEqual(['forgejo']);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('a thrown storage error fails only that module', async () => {
|
|
102
|
+
const report = await runBackupSweep(
|
|
103
|
+
deps([moduleWith('authentik', 'daily'), moduleWith('forgejo', 'daily')], {
|
|
104
|
+
backup: async (moduleId) => {
|
|
105
|
+
if (moduleId === 'authentik') throw new Error('Storage not verified');
|
|
106
|
+
return { success: true };
|
|
107
|
+
},
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
expect(report.failures).toEqual([{ moduleId: 'authentik', error: 'Storage not verified' }]);
|
|
112
|
+
expect(report.backedUp).toEqual(['forgejo']);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('does not prune when a backup failed', async () => {
|
|
116
|
+
const d = deps([moduleWith('authentik', 'daily')], {
|
|
117
|
+
backup: async () => ({ success: false, error: 'nope' }),
|
|
118
|
+
});
|
|
119
|
+
await runBackupSweep(d);
|
|
120
|
+
|
|
121
|
+
expect(d.pruned).toEqual([]);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('ensureBackupSweepSubscriber', () => {
|
|
126
|
+
test('registers the hourly sweep handler', () => {
|
|
127
|
+
const calls: Array<{
|
|
128
|
+
name: string;
|
|
129
|
+
pattern: string;
|
|
130
|
+
handler: string;
|
|
131
|
+
registeredBy?: string;
|
|
132
|
+
}> = [];
|
|
133
|
+
ensureBackupSweepSubscriber({
|
|
134
|
+
subscribe: (options) => calls.push(options),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
expect(calls).toEqual([
|
|
138
|
+
{
|
|
139
|
+
name: BACKUP_SWEEP_SUBSCRIBER,
|
|
140
|
+
pattern: BACKUP_SWEEP_PATTERN,
|
|
141
|
+
handler: 'celilo backup sweep',
|
|
142
|
+
registeredBy: 'celilo-backup',
|
|
143
|
+
},
|
|
144
|
+
]);
|
|
145
|
+
// 1h is the coarsest tick that can still serve an `hourly` cadence.
|
|
146
|
+
expect(BACKUP_SWEEP_PATTERN).toBe('timer.tick.1h');
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The thing that actually runs backups on a schedule.
|
|
3
|
+
*
|
|
4
|
+
* A manifest's `backup.schedule` was decorative until this existed: the drift
|
|
5
|
+
* audit reported staleness and `celilo module backup` needed a human. This is
|
|
6
|
+
* the pass that makes a module declaring `daily` get backed up daily with
|
|
7
|
+
* nobody watching.
|
|
8
|
+
*
|
|
9
|
+
* No new scheduling mechanism. Registered as an ordinary bus subscriber whose
|
|
10
|
+
* handler is `celilo backup sweep` against `timer.tick.1h`, exactly like the
|
|
11
|
+
* alerting sweep (`services/alerting/monitors.ts`) — the timer already exists,
|
|
12
|
+
* already survives restarts, and already has retry and dedup. One hour is the
|
|
13
|
+
* coarsest tick that can still serve an `hourly` cadence; `1d` cannot.
|
|
14
|
+
*
|
|
15
|
+
* Dependencies are injected so the pass itself tests with no database, no
|
|
16
|
+
* storage, and no hook execution.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { ModuleManifest } from '../manifest/schema';
|
|
20
|
+
import { type BackupSchedule, effectiveBackupSchedule } from './backup-schedule';
|
|
21
|
+
import { InFlightError } from './module-operations';
|
|
22
|
+
|
|
23
|
+
export const BACKUP_SWEEP_SUBSCRIBER = 'celilo-backup-sweep';
|
|
24
|
+
export const BACKUP_SWEEP_PATTERN = 'timer.tick.1h';
|
|
25
|
+
|
|
26
|
+
export interface SubscriberRegistrar {
|
|
27
|
+
subscribe(options: {
|
|
28
|
+
name: string;
|
|
29
|
+
pattern: string;
|
|
30
|
+
handler: string;
|
|
31
|
+
registeredBy?: string;
|
|
32
|
+
}): unknown;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Idempotent: `bus.subscribe` upserts by name, so this is safe to call on
|
|
37
|
+
* every module install and update.
|
|
38
|
+
*/
|
|
39
|
+
export function ensureBackupSweepSubscriber(bus: SubscriberRegistrar): void {
|
|
40
|
+
bus.subscribe({
|
|
41
|
+
name: BACKUP_SWEEP_SUBSCRIBER,
|
|
42
|
+
pattern: BACKUP_SWEEP_PATTERN,
|
|
43
|
+
handler: 'celilo backup sweep',
|
|
44
|
+
registeredBy: 'celilo-backup',
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface BackupSweepModule {
|
|
49
|
+
id: string;
|
|
50
|
+
manifest: ModuleManifest;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface BackupSweepDeps {
|
|
54
|
+
/** Installed modules that declare an `on_backup` hook. */
|
|
55
|
+
listEligible(): BackupSweepModule[];
|
|
56
|
+
isDue(moduleId: string, schedule: BackupSchedule): boolean;
|
|
57
|
+
backup(moduleId: string): Promise<{ success: boolean; error?: string }>;
|
|
58
|
+
/** Apply the module's declared retention. No-op when it declares none. */
|
|
59
|
+
prune(module: BackupSweepModule): Promise<void>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface BackupSweepReport {
|
|
63
|
+
backedUp: string[];
|
|
64
|
+
/** Explicit `schedule: manual` — the author opted out. */
|
|
65
|
+
skippedManual: string[];
|
|
66
|
+
skippedNotDue: string[];
|
|
67
|
+
/** Another module operation held the lock. Not a failure; retried next tick. */
|
|
68
|
+
skippedLocked: string[];
|
|
69
|
+
failures: Array<{ moduleId: string; error: string }>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function runBackupSweep(deps: BackupSweepDeps): Promise<BackupSweepReport> {
|
|
73
|
+
const report: BackupSweepReport = {
|
|
74
|
+
backedUp: [],
|
|
75
|
+
skippedManual: [],
|
|
76
|
+
skippedNotDue: [],
|
|
77
|
+
skippedLocked: [],
|
|
78
|
+
failures: [],
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
for (const module of deps.listEligible()) {
|
|
82
|
+
const schedule = effectiveBackupSchedule(module.manifest);
|
|
83
|
+
if (schedule === 'manual') {
|
|
84
|
+
report.skippedManual.push(module.id);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (!deps.isDue(module.id, schedule)) {
|
|
88
|
+
report.skippedNotDue.push(module.id);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let result: { success: boolean; error?: string };
|
|
93
|
+
try {
|
|
94
|
+
result = await deps.backup(module.id);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
// Refusing to run while a deploy/restore is in flight is correct, and a
|
|
97
|
+
// scheduled run must not bypass it. The lock is global, so once it is
|
|
98
|
+
// held every remaining module would refuse identically — stop the pass
|
|
99
|
+
// rather than collect the same refusal N times. The next tick retries.
|
|
100
|
+
if (error instanceof InFlightError) {
|
|
101
|
+
report.skippedLocked.push(module.id);
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
// Everything else — an unverified storage destination, a missing default
|
|
105
|
+
// — throws before a backup row exists. Record it against the module and
|
|
106
|
+
// keep going; one module's failure must not cancel the rest.
|
|
107
|
+
report.failures.push({
|
|
108
|
+
moduleId: module.id,
|
|
109
|
+
error: error instanceof Error ? error.message : String(error),
|
|
110
|
+
});
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!result.success) {
|
|
115
|
+
report.failures.push({ moduleId: module.id, error: result.error ?? 'backup failed' });
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
report.backedUp.push(module.id);
|
|
120
|
+
await deps.prune(module);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return report;
|
|
124
|
+
}
|
|
@@ -3,14 +3,27 @@
|
|
|
3
3
|
* openspec/changes/build-bus-poll-cd/proposal.md).
|
|
4
4
|
*
|
|
5
5
|
* CD is CI-driven, so no operator is present to pass `--no-backup`. The posture
|
|
6
|
-
* (fast = skip
|
|
7
|
-
*
|
|
6
|
+
* (fast = skip the pre-deploy backup; safe = back up first) is DERIVED, with
|
|
7
|
+
* this precedence:
|
|
8
8
|
* 1. per-module `upgrade_policy: always-safe` — a hard floor (always safe).
|
|
9
9
|
* 2. per-release `deploy_posture` stamped in the .netapp release metadata.
|
|
10
10
|
* 3. per-module `upgrade_policy: always-fast`.
|
|
11
11
|
* 4. default: the semver delta of installed → next (revision/patch = fast;
|
|
12
12
|
* minor/major = safe).
|
|
13
13
|
*
|
|
14
|
+
* Posture gates the BACKUP ONLY. Both paths run the same post-deploy verify
|
|
15
|
+
* (module-upgrade.ts calls runModuleHealthCheck either way), and neither
|
|
16
|
+
* gates the deploy: verify is the last step, so a failing health check reports
|
|
17
|
+
* "upgraded but verify failed" on an upgrade that has already landed — there
|
|
18
|
+
* is no rollback and no auto-restore. This comment previously claimed safe
|
|
19
|
+
* posture ran a "full verify" against fast's "extended verify"; no such
|
|
20
|
+
* distinction has ever existed in the code, and believing it makes
|
|
21
|
+
* `auto_upgrade` sound far more guarded than it is.
|
|
22
|
+
*
|
|
23
|
+
* `upgrade_policy` (like `auto_upgrade`) is OPERATOR CONFIG, not a manifest
|
|
24
|
+
* key — `celilo module config set <module> upgrade_policy always-safe`. The
|
|
25
|
+
* manifest schema is `.strict()` and declares neither.
|
|
26
|
+
*
|
|
14
27
|
* celilo reads version NUMBERS, never changesets — the default needs only the
|
|
15
28
|
* versions. All functions here are pure (Rule 10).
|
|
16
29
|
*/
|