@celilo/cli 0.3.29 → 0.3.30
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
CHANGED
|
@@ -8,6 +8,7 @@ import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
|
8
8
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
9
9
|
import { tmpdir } from 'node:os';
|
|
10
10
|
import { join } from 'node:path';
|
|
11
|
+
import { eq } from 'drizzle-orm';
|
|
11
12
|
import { type DbClient, getDb } from '../../db/client';
|
|
12
13
|
import { modules } from '../../db/schema';
|
|
13
14
|
import { classifyVersionChange, upgradeOne } from './module-upgrade';
|
|
@@ -153,4 +154,62 @@ description: fixture
|
|
|
153
154
|
if (quietResult.status !== 'success') return;
|
|
154
155
|
expect(quietResult.newVersion).toBe('1.0.0');
|
|
155
156
|
});
|
|
157
|
+
|
|
158
|
+
// Regression for the namecheap stale-findings problem: the upgrade
|
|
159
|
+
// path MUST overwrite manifestData with the new manifest — otherwise
|
|
160
|
+
// any subsequent audit (or any code path that reads from the DB)
|
|
161
|
+
// sees the OLD manifest's required vars even after the operator
|
|
162
|
+
// upgraded to a version that removed them. The user hit this on
|
|
163
|
+
// celilo-mgmt: namecheap@3.1.1+6 dropped the `domains` variable, but
|
|
164
|
+
// post-upgrade the audit still complained "required config 'domains'
|
|
165
|
+
// is not set" because the DB's manifestData hadn't been refreshed.
|
|
166
|
+
test('persists new manifest to modules.manifestData (not just .version)', async () => {
|
|
167
|
+
// Write a brand-new manifest.yml in srcDir that REMOVES a
|
|
168
|
+
// variable the old DB record had. After upgrade, the modules
|
|
169
|
+
// row's manifestData should reflect the removal.
|
|
170
|
+
writeFileSync(
|
|
171
|
+
join(srcDir, 'manifest.yml'),
|
|
172
|
+
`celilo_contract: "1.0"
|
|
173
|
+
id: testmod
|
|
174
|
+
name: Test Module Renamed
|
|
175
|
+
version: 2.0.0
|
|
176
|
+
description: fixture v2
|
|
177
|
+
variables:
|
|
178
|
+
owns: []
|
|
179
|
+
imports: []
|
|
180
|
+
`,
|
|
181
|
+
);
|
|
182
|
+
// Simulate the DB starting in a state where manifestData has a
|
|
183
|
+
// `variables.owns` array — the namecheap-3.1.0 → 3.1.1 case.
|
|
184
|
+
db.update(modules)
|
|
185
|
+
.set({
|
|
186
|
+
manifestData: {
|
|
187
|
+
celilo_contract: '1.0',
|
|
188
|
+
id: 'testmod',
|
|
189
|
+
name: 'Test Module',
|
|
190
|
+
version: '1.0.0',
|
|
191
|
+
variables: { owns: [{ name: 'domains', type: 'array', required: true }], imports: [] },
|
|
192
|
+
},
|
|
193
|
+
})
|
|
194
|
+
.where(eq(modules.id, 'testmod'))
|
|
195
|
+
.run();
|
|
196
|
+
|
|
197
|
+
const result = await upgradeOne(srcDir, db, {}, { quiet: true, displayVersion: '2.0.0+1' });
|
|
198
|
+
expect(result.status).toBe('success');
|
|
199
|
+
|
|
200
|
+
const row = db.select().from(modules).all()[0];
|
|
201
|
+
// version field updates (this part already worked):
|
|
202
|
+
expect(row.version).toBe('2.0.0+1');
|
|
203
|
+
expect(row.name).toBe('Test Module Renamed');
|
|
204
|
+
// manifestData reflects the NEW manifest — the dropped variable
|
|
205
|
+
// is gone. This is the regression assertion.
|
|
206
|
+
const manifest = row.manifestData as {
|
|
207
|
+
version: string;
|
|
208
|
+
name: string;
|
|
209
|
+
variables?: { owns: unknown[] };
|
|
210
|
+
};
|
|
211
|
+
expect(manifest.version).toBe('2.0.0');
|
|
212
|
+
expect(manifest.name).toBe('Test Module Renamed');
|
|
213
|
+
expect(manifest.variables?.owns ?? []).toEqual([]);
|
|
214
|
+
});
|
|
156
215
|
});
|
|
@@ -685,6 +685,21 @@ export async function handleSystemUpdate(
|
|
|
685
685
|
onlyModule,
|
|
686
686
|
});
|
|
687
687
|
|
|
688
|
+
// The audit baked into `result.audit` ran BEFORE the orchestrator
|
|
689
|
+
// upgraded any modules. Its findings are now stale for whatever
|
|
690
|
+
// we just upgraded — `module_versions` drift, `module_configs`
|
|
691
|
+
// referencing the OLD manifest's required vars, `capability_abi`
|
|
692
|
+
// checking pre-upgrade providers. Re-query the modules table and
|
|
693
|
+
// re-run the audit so the displayed findings reflect post-upgrade
|
|
694
|
+
// reality. Only do this on a successful run (a partial-failure
|
|
695
|
+
// run keeps its original audit so the operator sees what the
|
|
696
|
+
// orchestrator was reacting to).
|
|
697
|
+
const successfulModuleSteps = result.modules.filter((m) => m.step === 'done');
|
|
698
|
+
if (result.ok && successfulModuleSteps.length > 0) {
|
|
699
|
+
const refreshedAudit = await runAudit(rebuildAuditDepsForRerun(auditDeps, db));
|
|
700
|
+
result.audit = refreshedAudit;
|
|
701
|
+
}
|
|
702
|
+
|
|
688
703
|
if (json) {
|
|
689
704
|
if (result.ok) {
|
|
690
705
|
return { success: true, message: JSON.stringify(result, null, 2), rawOutput: true };
|
|
@@ -700,3 +715,84 @@ export async function handleSystemUpdate(
|
|
|
700
715
|
error: `${formatResult(result)}\n\none or more modules failed; see output above`,
|
|
701
716
|
};
|
|
702
717
|
}
|
|
718
|
+
|
|
719
|
+
type AuditDeps = Parameters<typeof runAudit>[0];
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Build a fresh AuditDeps object whose module-state-dependent
|
|
723
|
+
* sub-deps are re-queried from the DB. The non-module deps
|
|
724
|
+
* (cliVersion fetcher, migrations, terraform plan runner, registry
|
|
725
|
+
* fetcher, etc.) are reused from the original deps because they
|
|
726
|
+
* don't change during a single system-update run.
|
|
727
|
+
*
|
|
728
|
+
* Used by the post-orchestrator re-audit so displayed findings
|
|
729
|
+
* reflect post-upgrade reality (e.g., a module_versions drift
|
|
730
|
+
* finding for a module we just upgraded is no longer reported).
|
|
731
|
+
*/
|
|
732
|
+
export function rebuildAuditDepsForRerun(
|
|
733
|
+
original: AuditDeps,
|
|
734
|
+
db: ReturnType<typeof getDb>,
|
|
735
|
+
): AuditDeps {
|
|
736
|
+
const installed = db.select().from(modules).all();
|
|
737
|
+
const upgradeEligibleStates = new Set(['INSTALLED', 'VERIFIED', 'IMPORTED']);
|
|
738
|
+
const upgradable = installed.filter((m) => upgradeEligibleStates.has(m.state));
|
|
739
|
+
|
|
740
|
+
const allConfigs = db.select().from(moduleConfigsTbl).all();
|
|
741
|
+
const configsByModule = new Map<string, Record<string, unknown>>();
|
|
742
|
+
for (const c of allConfigs) {
|
|
743
|
+
const m = configsByModule.get(c.moduleId) ?? {};
|
|
744
|
+
m[c.key] = c.valueJson ? JSON.parse(c.valueJson) : c.value;
|
|
745
|
+
configsByModule.set(c.moduleId, m);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Backup recency is unchanged across an orchestrator run (only
|
|
749
|
+
// celilo-DB snapshots happen, not per-module backup writes), so
|
|
750
|
+
// we look up each module's prior lastSuccessfulBackupAt by id
|
|
751
|
+
// rather than re-querying the backups table.
|
|
752
|
+
const priorBackupByModule = new Map<string, number | null>();
|
|
753
|
+
for (const b of original.backups.modules) {
|
|
754
|
+
priorBackupByModule.set(b.id, b.lastSuccessfulBackupAt);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
return {
|
|
758
|
+
...original,
|
|
759
|
+
capabilityAbi: {
|
|
760
|
+
modules: upgradable.map((m) => ({
|
|
761
|
+
id: m.id,
|
|
762
|
+
state: m.state,
|
|
763
|
+
manifest: m.manifestData as ModuleManifest,
|
|
764
|
+
})),
|
|
765
|
+
},
|
|
766
|
+
moduleVersions: {
|
|
767
|
+
...original.moduleVersions,
|
|
768
|
+
installed: upgradable.map((m) => ({ id: m.id, version: m.version })),
|
|
769
|
+
},
|
|
770
|
+
moduleConfigs: {
|
|
771
|
+
modules: upgradable.map((m) => ({
|
|
772
|
+
id: m.id,
|
|
773
|
+
state: m.state,
|
|
774
|
+
manifest: m.manifestData as ModuleManifest,
|
|
775
|
+
configs: configsByModule.get(m.id) ?? {},
|
|
776
|
+
})),
|
|
777
|
+
},
|
|
778
|
+
backups: {
|
|
779
|
+
...original.backups,
|
|
780
|
+
modules: upgradable.map((m) => ({
|
|
781
|
+
id: m.id,
|
|
782
|
+
state: m.state,
|
|
783
|
+
manifest: m.manifestData as ModuleManifest,
|
|
784
|
+
lastSuccessfulBackupAt: priorBackupByModule.get(m.id) ?? null,
|
|
785
|
+
})),
|
|
786
|
+
},
|
|
787
|
+
undeployedModules: {
|
|
788
|
+
modules: installed.map((m) => ({ id: m.id, state: m.state })),
|
|
789
|
+
},
|
|
790
|
+
unconfiguredModules: {
|
|
791
|
+
modules: installed.map((m) => ({
|
|
792
|
+
id: m.id,
|
|
793
|
+
state: m.state,
|
|
794
|
+
configCount: Object.keys(configsByModule.get(m.id) ?? {}).length,
|
|
795
|
+
})),
|
|
796
|
+
},
|
|
797
|
+
};
|
|
798
|
+
}
|