@mettlecast/domain-cli 0.2.89 → 0.2.90
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.
|
@@ -33,88 +33,135 @@ const TOOLCHAIN_NPM_NAMES = {
|
|
|
33
33
|
* Write exact toolchain versions into infra/modules/package.json.
|
|
34
34
|
* Replaces the @mettlecast/* dependency versions with the pinned exact versions
|
|
35
35
|
* from the toolchain manifest. Preserves all other fields unchanged.
|
|
36
|
+
* Only writes the file if at least one version actually changes.
|
|
36
37
|
*
|
|
37
38
|
* @param projectRoot - Project root directory.
|
|
38
39
|
* @param manifest - The resolved toolchain manifest.
|
|
40
|
+
* @returns Whether any dependency version was actually changed.
|
|
39
41
|
*/
|
|
40
42
|
async function pinInfraPackageVersions(projectRoot, manifest) {
|
|
41
43
|
const infraPkgPath = join(projectRoot, 'infra', 'modules', 'package.json');
|
|
42
44
|
if (!existsSync(infraPkgPath)) {
|
|
43
45
|
cliLogger.warn({ path: infraPkgPath }, 'infra/modules/package.json not found — skipping version pinning');
|
|
44
|
-
return;
|
|
46
|
+
return { changed: false };
|
|
45
47
|
}
|
|
46
48
|
const raw = await readFile(infraPkgPath, 'utf-8');
|
|
47
49
|
const pkg = JSON.parse(raw);
|
|
48
50
|
const pkgDeps = (pkg.dependencies ?? {});
|
|
49
51
|
const pkgDevDeps = (pkg.devDependencies ?? {});
|
|
52
|
+
let changed = false;
|
|
50
53
|
// Pin @mettlecast/domain-cdk-packer
|
|
51
54
|
if (TOOLCHAIN_NPM_NAMES.domainCdkPacker in pkgDeps) {
|
|
52
|
-
pkgDeps[TOOLCHAIN_NPM_NAMES.domainCdkPacker]
|
|
55
|
+
if (pkgDeps[TOOLCHAIN_NPM_NAMES.domainCdkPacker] !== manifest.packages.domainCdkPacker) {
|
|
56
|
+
pkgDeps[TOOLCHAIN_NPM_NAMES.domainCdkPacker] = manifest.packages.domainCdkPacker;
|
|
57
|
+
changed = true;
|
|
58
|
+
}
|
|
53
59
|
}
|
|
54
60
|
// Pin @mettlecast/domain-runtime
|
|
55
61
|
if (TOOLCHAIN_NPM_NAMES.domainRuntime in pkgDeps) {
|
|
56
|
-
pkgDeps[TOOLCHAIN_NPM_NAMES.domainRuntime]
|
|
62
|
+
if (pkgDeps[TOOLCHAIN_NPM_NAMES.domainRuntime] !== manifest.packages.domainRuntime) {
|
|
63
|
+
pkgDeps[TOOLCHAIN_NPM_NAMES.domainRuntime] = manifest.packages.domainRuntime;
|
|
64
|
+
changed = true;
|
|
65
|
+
}
|
|
57
66
|
}
|
|
58
67
|
// Pin @mettlecast/domain-cli
|
|
59
68
|
if (TOOLCHAIN_NPM_NAMES.domainCli in pkgDevDeps) {
|
|
60
|
-
pkgDevDeps[TOOLCHAIN_NPM_NAMES.domainCli]
|
|
69
|
+
if (pkgDevDeps[TOOLCHAIN_NPM_NAMES.domainCli] !== manifest.packages.domainCli) {
|
|
70
|
+
pkgDevDeps[TOOLCHAIN_NPM_NAMES.domainCli] = manifest.packages.domainCli;
|
|
71
|
+
changed = true;
|
|
72
|
+
}
|
|
61
73
|
}
|
|
62
74
|
// Pin @mettlecast/eslint-plugin-domain-module
|
|
63
75
|
if (TOOLCHAIN_NPM_NAMES.eslintPluginDomainModule in pkgDevDeps) {
|
|
64
|
-
pkgDevDeps[TOOLCHAIN_NPM_NAMES.eslintPluginDomainModule]
|
|
76
|
+
if (pkgDevDeps[TOOLCHAIN_NPM_NAMES.eslintPluginDomainModule] !== manifest.packages.eslintPluginDomainModule) {
|
|
77
|
+
pkgDevDeps[TOOLCHAIN_NPM_NAMES.eslintPluginDomainModule] = manifest.packages.eslintPluginDomainModule;
|
|
78
|
+
changed = true;
|
|
79
|
+
}
|
|
65
80
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
81
|
+
if (changed) {
|
|
82
|
+
pkg.dependencies = pkgDeps;
|
|
83
|
+
pkg.devDependencies = pkgDevDeps;
|
|
84
|
+
await writeFile(infraPkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
|
|
85
|
+
cliLogger.info({ path: infraPkgPath }, 'Pinned toolchain versions in infra/modules/package.json');
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
cliLogger.info({ path: infraPkgPath }, 'infra/modules/package.json already up-to-date');
|
|
89
|
+
}
|
|
90
|
+
return { changed };
|
|
70
91
|
}
|
|
71
92
|
/**
|
|
72
93
|
* Write exact toolchain versions into root package.json.
|
|
73
94
|
* Replaces the @mettlecast/* devDependency versions with the pinned exact versions
|
|
74
95
|
* from the toolchain manifest. Preserves all other fields unchanged.
|
|
96
|
+
* Only writes the file if at least one version actually changes.
|
|
75
97
|
*
|
|
76
98
|
* @param projectRoot - Project root directory.
|
|
77
99
|
* @param manifest - The resolved toolchain manifest.
|
|
100
|
+
* @returns Whether any devDependency version was actually changed.
|
|
78
101
|
*/
|
|
79
102
|
async function pinRootPackageVersions(projectRoot, manifest) {
|
|
80
103
|
const rootPkgPath = join(projectRoot, 'package.json');
|
|
81
104
|
if (!existsSync(rootPkgPath)) {
|
|
82
105
|
cliLogger.warn({ path: rootPkgPath }, 'root package.json not found — skipping version pinning');
|
|
83
|
-
return;
|
|
106
|
+
return { changed: false };
|
|
84
107
|
}
|
|
85
108
|
const raw = await readFile(rootPkgPath, 'utf-8');
|
|
86
109
|
const pkg = JSON.parse(raw);
|
|
87
110
|
const pkgDevDeps = (pkg.devDependencies ?? {});
|
|
111
|
+
let changed = false;
|
|
88
112
|
// Pin @mettlecast/domain-cli (root devDependency)
|
|
89
113
|
if (TOOLCHAIN_NPM_NAMES.domainCli in pkgDevDeps) {
|
|
90
|
-
pkgDevDeps[TOOLCHAIN_NPM_NAMES.domainCli]
|
|
114
|
+
if (pkgDevDeps[TOOLCHAIN_NPM_NAMES.domainCli] !== manifest.packages.domainCli) {
|
|
115
|
+
pkgDevDeps[TOOLCHAIN_NPM_NAMES.domainCli] = manifest.packages.domainCli;
|
|
116
|
+
changed = true;
|
|
117
|
+
}
|
|
91
118
|
}
|
|
92
119
|
// Pin @mettlecast/domain-runtime (root devDependency)
|
|
93
120
|
if (TOOLCHAIN_NPM_NAMES.domainRuntime in pkgDevDeps) {
|
|
94
|
-
pkgDevDeps[TOOLCHAIN_NPM_NAMES.domainRuntime]
|
|
121
|
+
if (pkgDevDeps[TOOLCHAIN_NPM_NAMES.domainRuntime] !== manifest.packages.domainRuntime) {
|
|
122
|
+
pkgDevDeps[TOOLCHAIN_NPM_NAMES.domainRuntime] = manifest.packages.domainRuntime;
|
|
123
|
+
changed = true;
|
|
124
|
+
}
|
|
95
125
|
}
|
|
96
126
|
// Pin @mettlecast/eslint-plugin-domain-module (root devDependency)
|
|
97
127
|
if (TOOLCHAIN_NPM_NAMES.eslintPluginDomainModule in pkgDevDeps) {
|
|
98
|
-
pkgDevDeps[TOOLCHAIN_NPM_NAMES.eslintPluginDomainModule]
|
|
128
|
+
if (pkgDevDeps[TOOLCHAIN_NPM_NAMES.eslintPluginDomainModule] !== manifest.packages.eslintPluginDomainModule) {
|
|
129
|
+
pkgDevDeps[TOOLCHAIN_NPM_NAMES.eslintPluginDomainModule] = manifest.packages.eslintPluginDomainModule;
|
|
130
|
+
changed = true;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (changed) {
|
|
134
|
+
pkg.devDependencies = pkgDevDeps;
|
|
135
|
+
await writeFile(rootPkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
|
|
136
|
+
cliLogger.info({ path: rootPkgPath }, 'Pinned toolchain versions in root package.json');
|
|
99
137
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
138
|
+
else {
|
|
139
|
+
cliLogger.info({ path: rootPkgPath }, 'root package.json already up-to-date');
|
|
140
|
+
}
|
|
141
|
+
return { changed };
|
|
103
142
|
}
|
|
104
143
|
/**
|
|
105
|
-
* Regenerate lockfiles
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
144
|
+
* Regenerate lockfiles after version pinning.
|
|
145
|
+
* Always regenerates the root package-lock.json on every run to ensure it
|
|
146
|
+
* fully reconciles with the pinned versions (not just on manifest changes).
|
|
147
|
+
* Infra/modules lockfile is regenerated only when infra deps actually changed.
|
|
148
|
+
*
|
|
149
|
+
* Uses normal npm semantics (`npm install --package-lock-only --package-lock --no-save`)
|
|
150
|
+
* — no forced, legacy-peer-deps, or override flags — to produce deterministic
|
|
151
|
+
* lockfiles that match the pinned exact versions.
|
|
152
|
+
*
|
|
153
|
+
* Root lockfile failure is fatal (deploy blocker); infra/modules lockfile
|
|
154
|
+
* failure is logged as a warning but does not abort the pipeline.
|
|
110
155
|
*
|
|
111
156
|
* @param projectRoot - Project root directory.
|
|
157
|
+
* @param options - Options with infraChanged flag.
|
|
158
|
+
* @throws When root lockfile regeneration fails.
|
|
112
159
|
*/
|
|
113
|
-
async function regenerateLockfiles(projectRoot) {
|
|
114
|
-
const rootPkgPath = join(projectRoot, 'package.json');
|
|
115
|
-
const infraPkgPath = join(projectRoot, 'infra', 'modules', 'package.json');
|
|
160
|
+
async function regenerateLockfiles(projectRoot, options) {
|
|
116
161
|
const npmLockCmd = 'npm install --package-lock-only --package-lock --no-save';
|
|
117
|
-
//
|
|
162
|
+
// Root lockfile is always regenerated to ensure it is fully reconciled
|
|
163
|
+
// with the pinned exact versions on every update-all run.
|
|
164
|
+
const rootPkgPath = join(projectRoot, 'package.json');
|
|
118
165
|
if (existsSync(rootPkgPath)) {
|
|
119
166
|
try {
|
|
120
167
|
cliLogger.info({ projectRoot }, 'Regenerating root package-lock.json');
|
|
@@ -127,24 +174,28 @@ async function regenerateLockfiles(projectRoot) {
|
|
|
127
174
|
}
|
|
128
175
|
catch (err) {
|
|
129
176
|
const message = err instanceof Error ? err.message : String(err);
|
|
130
|
-
cliLogger.
|
|
177
|
+
cliLogger.error({ err: message }, 'Root lockfile regeneration FAILED');
|
|
178
|
+
throw new Error(`Root lockfile regeneration FAILED: ${message}`);
|
|
131
179
|
}
|
|
132
180
|
}
|
|
133
|
-
//
|
|
134
|
-
if (
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
181
|
+
// Infra/modules lockfile is regenerated only when infra deps actually changed.
|
|
182
|
+
if (options.infraChanged) {
|
|
183
|
+
const infraPkgPath = join(projectRoot, 'infra', 'modules', 'package.json');
|
|
184
|
+
if (existsSync(infraPkgPath)) {
|
|
185
|
+
try {
|
|
186
|
+
const infraDir = join(projectRoot, 'infra', 'modules');
|
|
187
|
+
cliLogger.info({ path: infraDir }, 'Regenerating infra/modules package-lock.json');
|
|
188
|
+
execSync(npmLockCmd, {
|
|
189
|
+
cwd: infraDir,
|
|
190
|
+
stdio: 'pipe',
|
|
191
|
+
timeout: 120_000,
|
|
192
|
+
});
|
|
193
|
+
cliLogger.info({ path: infraDir }, 'infra/modules package-lock.json regenerated');
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
197
|
+
cliLogger.warn({ err: message }, 'infra/modules lockfile regeneration failed (non-fatal — continuing)');
|
|
198
|
+
}
|
|
148
199
|
}
|
|
149
200
|
}
|
|
150
201
|
}
|
|
@@ -185,20 +236,31 @@ export async function runUpdateAll(opts) {
|
|
|
185
236
|
cliLogger.error({ err: message }, 'update-all: toolchain resolution failed — aborting');
|
|
186
237
|
return { success: false, summary: `Toolchain resolution FAIL: ${message}` };
|
|
187
238
|
}
|
|
188
|
-
// Pin exact versions from manifest into infra/modules/package.json and root package.json
|
|
239
|
+
// Pin exact versions from manifest into infra/modules/package.json and root package.json.
|
|
240
|
+
// Returns whether each manifest actually changed.
|
|
241
|
+
let infraChanged = false;
|
|
189
242
|
try {
|
|
190
|
-
await pinInfraPackageVersions(projectRoot, manifest);
|
|
243
|
+
const infraResult = await pinInfraPackageVersions(projectRoot, manifest);
|
|
191
244
|
await pinRootPackageVersions(projectRoot, manifest);
|
|
245
|
+
infraChanged = infraResult.changed;
|
|
192
246
|
}
|
|
193
247
|
catch (err) {
|
|
194
248
|
const message = err instanceof Error ? err.message : String(err);
|
|
195
249
|
cliLogger.error({ err: message }, 'update-all: version pinning failed — aborting');
|
|
196
250
|
return { success: false, summary: `Version pinning FAIL: ${message}` };
|
|
197
251
|
}
|
|
198
|
-
// Regenerate lockfiles
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
|
|
252
|
+
// Regenerate lockfiles. Root lockfile is always regenerated (every run) to ensure
|
|
253
|
+
// full reconciliation. Infra/modules lockfile is regenerated only when its deps changed.
|
|
254
|
+
// Root lockfile failure is fatal (deploy blocker); infra/modules failure is not.
|
|
255
|
+
cliLogger.info({ projectRoot, infraChanged }, 'update-all: regenerating root package-lock.json');
|
|
256
|
+
try {
|
|
257
|
+
await regenerateLockfiles(projectRoot, { infraChanged });
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
261
|
+
cliLogger.error({ err: message }, 'update-all: root lockfile regeneration failed — aborting');
|
|
262
|
+
return { success: false, summary: `Lockfile regeneration FAIL: ${message}` };
|
|
263
|
+
}
|
|
202
264
|
// 1. Read scaffold-config to discover domains
|
|
203
265
|
const scaffoldConfig = await readScaffoldConfig(projectRoot);
|
|
204
266
|
const domainIds = scaffoldConfig.domainIds ?? [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mettlecast/domain-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.90",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"registry": "https://registry.npmjs.org",
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@aws-sdk/client-s3": "^3.0.0",
|
|
24
24
|
"@aws-sdk/client-sfn": "^3.0.0",
|
|
25
|
-
"@mettlecast/domain-cdk-packer": "0.2.
|
|
26
|
-
"@mettlecast/domain-runtime": "0.2.
|
|
25
|
+
"@mettlecast/domain-cdk-packer": "0.2.91",
|
|
26
|
+
"@mettlecast/domain-runtime": "0.2.90",
|
|
27
27
|
"commander": "^12.0.0",
|
|
28
28
|
"dotenv": "^16.0.0",
|
|
29
29
|
"fastify": "^5.0.0",
|
|
@@ -32,6 +32,12 @@ vi.mock('../../utils/scaffold-config.js', () => ({
|
|
|
32
32
|
readScaffoldConfig: vi.fn(),
|
|
33
33
|
}));
|
|
34
34
|
|
|
35
|
+
vi.mock('node:child_process', () => ({
|
|
36
|
+
execSync: vi.fn(),
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
import { execSync } from 'node:child_process';
|
|
40
|
+
|
|
35
41
|
import { runUpdateAll } from '../../commands/update-all.js';
|
|
36
42
|
import { runBuild } from '../../commands/build.js';
|
|
37
43
|
import { runBuildCatalog } from '../../commands/build-catalog.js';
|
|
@@ -48,6 +54,7 @@ const mockRunBuildUi = vi.mocked(runBuildUi);
|
|
|
48
54
|
const mockRunRegenerateModulesHashes = vi.mocked(runRegenerateModulesHashes);
|
|
49
55
|
const mockRunDoctor = vi.mocked(runDoctor);
|
|
50
56
|
const mockReadScaffoldConfig = vi.mocked(readScaffoldConfig);
|
|
57
|
+
const mockExecSync = vi.mocked(execSync);
|
|
51
58
|
|
|
52
59
|
/** Build a minimal but valid DoctorReport. */
|
|
53
60
|
function makeDoctorReport(pass: boolean) {
|
|
@@ -616,45 +623,112 @@ describe('update-all command', () => {
|
|
|
616
623
|
it('regenerates root package-lock.json after version pinning', async () => {
|
|
617
624
|
mockReadScaffoldConfig.mockResolvedValue(makeScaffoldConfig([]));
|
|
618
625
|
|
|
619
|
-
//
|
|
620
|
-
|
|
621
|
-
const rootExists = existsSync(rootPkgPath);
|
|
622
|
-
expect(rootExists).toBe(true);
|
|
626
|
+
// Since root package.json has '*' versions and manifest has '0.2.53',
|
|
627
|
+
// pinning WILL change root — execSync should be called for root.
|
|
623
628
|
|
|
624
|
-
// Remove any existing lockfile
|
|
625
|
-
const rootLockPath = join(tempDir, 'package-lock.json');
|
|
626
|
-
if (existsSync(rootLockPath)) {
|
|
627
|
-
rmSync(rootLockPath, { force: true });
|
|
628
|
-
}
|
|
629
|
-
const infraLockPath = join(tempDir, 'infra', 'modules', 'package-lock.json');
|
|
630
|
-
if (existsSync(infraLockPath)) {
|
|
631
|
-
rmSync(infraLockPath, { force: true });
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
// Run update-all which should regenerate lockfiles
|
|
635
629
|
const result = await runUpdateAll({ projectRoot: tempDir });
|
|
636
630
|
|
|
637
631
|
// update-all should succeed (doctor mock passes)
|
|
638
632
|
expect(result.success).toBe(true);
|
|
639
633
|
|
|
640
|
-
//
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
634
|
+
// execSync must be called at least for root lockfile regeneration
|
|
635
|
+
expect(mockExecSync).toHaveBeenCalledWith(
|
|
636
|
+
'npm install --package-lock-only --package-lock --no-save',
|
|
637
|
+
expect.objectContaining({ cwd: tempDir }),
|
|
638
|
+
);
|
|
639
|
+
// Both root and infra changed (both had wildcard versions)
|
|
640
|
+
expect(mockExecSync).toHaveBeenCalledTimes(2);
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
it('always regenerates root lockfile even when root package.json already has exact versions', async () => {
|
|
644
|
+
// Overwrite root package.json with already-pinned versions
|
|
645
|
+
await createRootPackageJson(tempDir, {
|
|
646
|
+
'@mettlecast/domain-cli': '0.2.53',
|
|
647
|
+
'@mettlecast/domain-runtime': '0.2.53',
|
|
648
|
+
'@mettlecast/eslint-plugin-domain-module': '0.2.53',
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
mockReadScaffoldConfig.mockResolvedValue(makeScaffoldConfig([]));
|
|
652
|
+
|
|
653
|
+
const result = await runUpdateAll({ projectRoot: tempDir });
|
|
654
|
+
|
|
655
|
+
expect(result.success).toBe(true);
|
|
656
|
+
|
|
657
|
+
// Root lockfile is regenerated unconditionally on every run.
|
|
658
|
+
expect(mockExecSync).toHaveBeenCalledWith(
|
|
659
|
+
'npm install --package-lock-only --package-lock --no-save',
|
|
660
|
+
expect.objectContaining({ cwd: tempDir }),
|
|
661
|
+
);
|
|
662
|
+
// Infra lockfile is regenerated only when infra deps changed (it still has '*' wildcards)
|
|
663
|
+
const infraDir = join(tempDir, 'infra', 'modules');
|
|
664
|
+
expect(mockExecSync).toHaveBeenCalledWith(
|
|
665
|
+
'npm install --package-lock-only --package-lock --no-save',
|
|
666
|
+
expect.objectContaining({ cwd: infraDir }),
|
|
667
|
+
);
|
|
668
|
+
expect(mockExecSync).toHaveBeenCalledTimes(2);
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
it('always regenerates root lockfile even when no manifests changed', async () => {
|
|
672
|
+
// Overwrite both package.json files with already-pinned versions
|
|
673
|
+
await createRootPackageJson(tempDir, {
|
|
674
|
+
'@mettlecast/domain-cli': '0.2.53',
|
|
675
|
+
'@mettlecast/domain-runtime': '0.2.53',
|
|
676
|
+
'@mettlecast/eslint-plugin-domain-module': '0.2.53',
|
|
677
|
+
});
|
|
678
|
+
await createInfraPackageJson(tempDir, {
|
|
679
|
+
'@mettlecast/domain-cdk-packer': '0.2.53',
|
|
680
|
+
'@mettlecast/domain-runtime': '0.2.53',
|
|
681
|
+
}, {
|
|
682
|
+
'@mettlecast/domain-cli': '0.2.53',
|
|
683
|
+
'@mettlecast/eslint-plugin-domain-module': '0.2.53',
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
mockReadScaffoldConfig.mockResolvedValue(makeScaffoldConfig([]));
|
|
687
|
+
|
|
688
|
+
const result = await runUpdateAll({ projectRoot: tempDir });
|
|
689
|
+
|
|
690
|
+
expect(result.success).toBe(true);
|
|
691
|
+
|
|
692
|
+
// Root lockfile is always regenerated even with no manifest changes.
|
|
693
|
+
expect(mockExecSync).toHaveBeenCalledTimes(1);
|
|
694
|
+
expect(mockExecSync).toHaveBeenCalledWith(
|
|
695
|
+
'npm install --package-lock-only --package-lock --no-save',
|
|
696
|
+
expect.objectContaining({ cwd: tempDir }),
|
|
697
|
+
);
|
|
698
|
+
// Infra lockfile is NOT regenerated because infra deps already match.
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
it('fails explicitly when root lockfile regeneration fails', async () => {
|
|
702
|
+
mockReadScaffoldConfig.mockResolvedValue(makeScaffoldConfig([]));
|
|
703
|
+
|
|
704
|
+
// Make execSync throw on first call (root lockfile)
|
|
705
|
+
mockExecSync.mockImplementationOnce(() => {
|
|
706
|
+
throw new Error('npm install failed');
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
const result = await runUpdateAll({ projectRoot: tempDir });
|
|
710
|
+
|
|
711
|
+
// update-all should fail with lockfile error
|
|
712
|
+
expect(result.success).toBe(false);
|
|
713
|
+
expect(result.summary).toContain('Lockfile regeneration FAIL');
|
|
714
|
+
expect(result.summary).toContain('npm install failed');
|
|
645
715
|
});
|
|
646
716
|
|
|
647
|
-
it('continues
|
|
717
|
+
it('continues when infra lockfile fails but root succeeds', async () => {
|
|
648
718
|
mockReadScaffoldConfig.mockResolvedValue(makeScaffoldConfig([]));
|
|
649
719
|
|
|
650
|
-
//
|
|
651
|
-
|
|
652
|
-
|
|
720
|
+
// execSync: first call succeeds (root), second call throws (infra)
|
|
721
|
+
mockExecSync
|
|
722
|
+
.mockImplementationOnce(() => Buffer.from(''))
|
|
723
|
+
.mockImplementationOnce(() => {
|
|
724
|
+
throw new Error('infra npm install failed');
|
|
725
|
+
});
|
|
653
726
|
|
|
654
|
-
// Should not throw — lockfile regeneration for missing infra dir is graceful
|
|
655
727
|
const result = await runUpdateAll({ projectRoot: tempDir });
|
|
656
728
|
|
|
729
|
+
// update-all should succeed — infra failure is non-fatal
|
|
657
730
|
expect(result.success).toBe(true);
|
|
731
|
+
expect(mockExecSync).toHaveBeenCalledTimes(2);
|
|
658
732
|
});
|
|
659
733
|
});
|
|
660
734
|
});
|
|
@@ -39,19 +39,21 @@ const TOOLCHAIN_NPM_NAMES: Record<string, string> = {
|
|
|
39
39
|
* Write exact toolchain versions into infra/modules/package.json.
|
|
40
40
|
* Replaces the @mettlecast/* dependency versions with the pinned exact versions
|
|
41
41
|
* from the toolchain manifest. Preserves all other fields unchanged.
|
|
42
|
+
* Only writes the file if at least one version actually changes.
|
|
42
43
|
*
|
|
43
44
|
* @param projectRoot - Project root directory.
|
|
44
45
|
* @param manifest - The resolved toolchain manifest.
|
|
46
|
+
* @returns Whether any dependency version was actually changed.
|
|
45
47
|
*/
|
|
46
48
|
async function pinInfraPackageVersions(
|
|
47
49
|
projectRoot: string,
|
|
48
50
|
manifest: ToolchainManifest,
|
|
49
|
-
): Promise<
|
|
51
|
+
): Promise<{ changed: boolean }> {
|
|
50
52
|
const infraPkgPath = join(projectRoot, 'infra', 'modules', 'package.json');
|
|
51
53
|
|
|
52
54
|
if (!existsSync(infraPkgPath)) {
|
|
53
55
|
cliLogger.warn({ path: infraPkgPath }, 'infra/modules/package.json not found — skipping version pinning');
|
|
54
|
-
return;
|
|
56
|
+
return { changed: false };
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
const raw = await readFile(infraPkgPath, 'utf-8');
|
|
@@ -60,50 +62,72 @@ async function pinInfraPackageVersions(
|
|
|
60
62
|
const pkgDeps = (pkg.dependencies ?? {}) as Record<string, string>;
|
|
61
63
|
const pkgDevDeps = (pkg.devDependencies ?? {}) as Record<string, string>;
|
|
62
64
|
|
|
65
|
+
let changed = false;
|
|
66
|
+
|
|
63
67
|
// Pin @mettlecast/domain-cdk-packer
|
|
64
68
|
if (TOOLCHAIN_NPM_NAMES.domainCdkPacker in pkgDeps) {
|
|
65
|
-
pkgDeps[TOOLCHAIN_NPM_NAMES.domainCdkPacker]
|
|
69
|
+
if (pkgDeps[TOOLCHAIN_NPM_NAMES.domainCdkPacker] !== manifest.packages.domainCdkPacker) {
|
|
70
|
+
pkgDeps[TOOLCHAIN_NPM_NAMES.domainCdkPacker] = manifest.packages.domainCdkPacker;
|
|
71
|
+
changed = true;
|
|
72
|
+
}
|
|
66
73
|
}
|
|
67
74
|
|
|
68
75
|
// Pin @mettlecast/domain-runtime
|
|
69
76
|
if (TOOLCHAIN_NPM_NAMES.domainRuntime in pkgDeps) {
|
|
70
|
-
pkgDeps[TOOLCHAIN_NPM_NAMES.domainRuntime]
|
|
77
|
+
if (pkgDeps[TOOLCHAIN_NPM_NAMES.domainRuntime] !== manifest.packages.domainRuntime) {
|
|
78
|
+
pkgDeps[TOOLCHAIN_NPM_NAMES.domainRuntime] = manifest.packages.domainRuntime;
|
|
79
|
+
changed = true;
|
|
80
|
+
}
|
|
71
81
|
}
|
|
72
82
|
|
|
73
83
|
// Pin @mettlecast/domain-cli
|
|
74
84
|
if (TOOLCHAIN_NPM_NAMES.domainCli in pkgDevDeps) {
|
|
75
|
-
pkgDevDeps[TOOLCHAIN_NPM_NAMES.domainCli]
|
|
85
|
+
if (pkgDevDeps[TOOLCHAIN_NPM_NAMES.domainCli] !== manifest.packages.domainCli) {
|
|
86
|
+
pkgDevDeps[TOOLCHAIN_NPM_NAMES.domainCli] = manifest.packages.domainCli;
|
|
87
|
+
changed = true;
|
|
88
|
+
}
|
|
76
89
|
}
|
|
77
90
|
|
|
78
91
|
// Pin @mettlecast/eslint-plugin-domain-module
|
|
79
92
|
if (TOOLCHAIN_NPM_NAMES.eslintPluginDomainModule in pkgDevDeps) {
|
|
80
|
-
pkgDevDeps[TOOLCHAIN_NPM_NAMES.eslintPluginDomainModule]
|
|
93
|
+
if (pkgDevDeps[TOOLCHAIN_NPM_NAMES.eslintPluginDomainModule] !== manifest.packages.eslintPluginDomainModule) {
|
|
94
|
+
pkgDevDeps[TOOLCHAIN_NPM_NAMES.eslintPluginDomainModule] = manifest.packages.eslintPluginDomainModule;
|
|
95
|
+
changed = true;
|
|
96
|
+
}
|
|
81
97
|
}
|
|
82
98
|
|
|
83
|
-
|
|
84
|
-
|
|
99
|
+
if (changed) {
|
|
100
|
+
pkg.dependencies = pkgDeps;
|
|
101
|
+
pkg.devDependencies = pkgDevDeps;
|
|
102
|
+
|
|
103
|
+
await writeFile(infraPkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
|
|
104
|
+
cliLogger.info({ path: infraPkgPath }, 'Pinned toolchain versions in infra/modules/package.json');
|
|
105
|
+
} else {
|
|
106
|
+
cliLogger.info({ path: infraPkgPath }, 'infra/modules/package.json already up-to-date');
|
|
107
|
+
}
|
|
85
108
|
|
|
86
|
-
|
|
87
|
-
cliLogger.info({ path: infraPkgPath }, 'Pinned toolchain versions in infra/modules/package.json');
|
|
109
|
+
return { changed };
|
|
88
110
|
}
|
|
89
111
|
|
|
90
112
|
/**
|
|
91
113
|
* Write exact toolchain versions into root package.json.
|
|
92
114
|
* Replaces the @mettlecast/* devDependency versions with the pinned exact versions
|
|
93
115
|
* from the toolchain manifest. Preserves all other fields unchanged.
|
|
116
|
+
* Only writes the file if at least one version actually changes.
|
|
94
117
|
*
|
|
95
118
|
* @param projectRoot - Project root directory.
|
|
96
119
|
* @param manifest - The resolved toolchain manifest.
|
|
120
|
+
* @returns Whether any devDependency version was actually changed.
|
|
97
121
|
*/
|
|
98
122
|
async function pinRootPackageVersions(
|
|
99
123
|
projectRoot: string,
|
|
100
124
|
manifest: ToolchainManifest,
|
|
101
|
-
): Promise<
|
|
125
|
+
): Promise<{ changed: boolean }> {
|
|
102
126
|
const rootPkgPath = join(projectRoot, 'package.json');
|
|
103
127
|
|
|
104
128
|
if (!existsSync(rootPkgPath)) {
|
|
105
129
|
cliLogger.warn({ path: rootPkgPath }, 'root package.json not found — skipping version pinning');
|
|
106
|
-
return;
|
|
130
|
+
return { changed: false };
|
|
107
131
|
}
|
|
108
132
|
|
|
109
133
|
const raw = await readFile(rootPkgPath, 'utf-8');
|
|
@@ -111,43 +135,70 @@ async function pinRootPackageVersions(
|
|
|
111
135
|
|
|
112
136
|
const pkgDevDeps = (pkg.devDependencies ?? {}) as Record<string, string>;
|
|
113
137
|
|
|
138
|
+
let changed = false;
|
|
139
|
+
|
|
114
140
|
// Pin @mettlecast/domain-cli (root devDependency)
|
|
115
141
|
if (TOOLCHAIN_NPM_NAMES.domainCli in pkgDevDeps) {
|
|
116
|
-
pkgDevDeps[TOOLCHAIN_NPM_NAMES.domainCli]
|
|
142
|
+
if (pkgDevDeps[TOOLCHAIN_NPM_NAMES.domainCli] !== manifest.packages.domainCli) {
|
|
143
|
+
pkgDevDeps[TOOLCHAIN_NPM_NAMES.domainCli] = manifest.packages.domainCli;
|
|
144
|
+
changed = true;
|
|
145
|
+
}
|
|
117
146
|
}
|
|
118
147
|
|
|
119
148
|
// Pin @mettlecast/domain-runtime (root devDependency)
|
|
120
149
|
if (TOOLCHAIN_NPM_NAMES.domainRuntime in pkgDevDeps) {
|
|
121
|
-
pkgDevDeps[TOOLCHAIN_NPM_NAMES.domainRuntime]
|
|
150
|
+
if (pkgDevDeps[TOOLCHAIN_NPM_NAMES.domainRuntime] !== manifest.packages.domainRuntime) {
|
|
151
|
+
pkgDevDeps[TOOLCHAIN_NPM_NAMES.domainRuntime] = manifest.packages.domainRuntime;
|
|
152
|
+
changed = true;
|
|
153
|
+
}
|
|
122
154
|
}
|
|
123
155
|
|
|
124
156
|
// Pin @mettlecast/eslint-plugin-domain-module (root devDependency)
|
|
125
157
|
if (TOOLCHAIN_NPM_NAMES.eslintPluginDomainModule in pkgDevDeps) {
|
|
126
|
-
pkgDevDeps[TOOLCHAIN_NPM_NAMES.eslintPluginDomainModule]
|
|
158
|
+
if (pkgDevDeps[TOOLCHAIN_NPM_NAMES.eslintPluginDomainModule] !== manifest.packages.eslintPluginDomainModule) {
|
|
159
|
+
pkgDevDeps[TOOLCHAIN_NPM_NAMES.eslintPluginDomainModule] = manifest.packages.eslintPluginDomainModule;
|
|
160
|
+
changed = true;
|
|
161
|
+
}
|
|
127
162
|
}
|
|
128
163
|
|
|
129
|
-
|
|
164
|
+
if (changed) {
|
|
165
|
+
pkg.devDependencies = pkgDevDeps;
|
|
130
166
|
|
|
131
|
-
|
|
132
|
-
|
|
167
|
+
await writeFile(rootPkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
|
|
168
|
+
cliLogger.info({ path: rootPkgPath }, 'Pinned toolchain versions in root package.json');
|
|
169
|
+
} else {
|
|
170
|
+
cliLogger.info({ path: rootPkgPath }, 'root package.json already up-to-date');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return { changed };
|
|
133
174
|
}
|
|
134
175
|
|
|
135
176
|
/**
|
|
136
|
-
* Regenerate lockfiles
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
177
|
+
* Regenerate lockfiles after version pinning.
|
|
178
|
+
* Always regenerates the root package-lock.json on every run to ensure it
|
|
179
|
+
* fully reconciles with the pinned versions (not just on manifest changes).
|
|
180
|
+
* Infra/modules lockfile is regenerated only when infra deps actually changed.
|
|
181
|
+
*
|
|
182
|
+
* Uses normal npm semantics (`npm install --package-lock-only --package-lock --no-save`)
|
|
183
|
+
* — no forced, legacy-peer-deps, or override flags — to produce deterministic
|
|
184
|
+
* lockfiles that match the pinned exact versions.
|
|
185
|
+
*
|
|
186
|
+
* Root lockfile failure is fatal (deploy blocker); infra/modules lockfile
|
|
187
|
+
* failure is logged as a warning but does not abort the pipeline.
|
|
141
188
|
*
|
|
142
189
|
* @param projectRoot - Project root directory.
|
|
190
|
+
* @param options - Options with infraChanged flag.
|
|
191
|
+
* @throws When root lockfile regeneration fails.
|
|
143
192
|
*/
|
|
144
|
-
async function regenerateLockfiles(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
193
|
+
async function regenerateLockfiles(
|
|
194
|
+
projectRoot: string,
|
|
195
|
+
options: { infraChanged: boolean },
|
|
196
|
+
): Promise<void> {
|
|
148
197
|
const npmLockCmd = 'npm install --package-lock-only --package-lock --no-save';
|
|
149
198
|
|
|
150
|
-
//
|
|
199
|
+
// Root lockfile is always regenerated to ensure it is fully reconciled
|
|
200
|
+
// with the pinned exact versions on every update-all run.
|
|
201
|
+
const rootPkgPath = join(projectRoot, 'package.json');
|
|
151
202
|
if (existsSync(rootPkgPath)) {
|
|
152
203
|
try {
|
|
153
204
|
cliLogger.info({ projectRoot }, 'Regenerating root package-lock.json');
|
|
@@ -159,30 +210,31 @@ async function regenerateLockfiles(projectRoot: string): Promise<void> {
|
|
|
159
210
|
cliLogger.info({ projectRoot }, 'Root package-lock.json regenerated');
|
|
160
211
|
} catch (err) {
|
|
161
212
|
const message = err instanceof Error ? err.message : String(err);
|
|
162
|
-
cliLogger.
|
|
163
|
-
|
|
164
|
-
'Root lockfile regeneration failed (non-fatal — continuing)',
|
|
165
|
-
);
|
|
213
|
+
cliLogger.error({ err: message }, 'Root lockfile regeneration FAILED');
|
|
214
|
+
throw new Error(`Root lockfile regeneration FAILED: ${message}`);
|
|
166
215
|
}
|
|
167
216
|
}
|
|
168
217
|
|
|
169
|
-
//
|
|
170
|
-
if (
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
218
|
+
// Infra/modules lockfile is regenerated only when infra deps actually changed.
|
|
219
|
+
if (options.infraChanged) {
|
|
220
|
+
const infraPkgPath = join(projectRoot, 'infra', 'modules', 'package.json');
|
|
221
|
+
if (existsSync(infraPkgPath)) {
|
|
222
|
+
try {
|
|
223
|
+
const infraDir = join(projectRoot, 'infra', 'modules');
|
|
224
|
+
cliLogger.info({ path: infraDir }, 'Regenerating infra/modules package-lock.json');
|
|
225
|
+
execSync(npmLockCmd, {
|
|
226
|
+
cwd: infraDir,
|
|
227
|
+
stdio: 'pipe',
|
|
228
|
+
timeout: 120_000,
|
|
229
|
+
});
|
|
230
|
+
cliLogger.info({ path: infraDir }, 'infra/modules package-lock.json regenerated');
|
|
231
|
+
} catch (err) {
|
|
232
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
233
|
+
cliLogger.warn(
|
|
234
|
+
{ err: message },
|
|
235
|
+
'infra/modules lockfile regeneration failed (non-fatal — continuing)',
|
|
236
|
+
);
|
|
237
|
+
}
|
|
186
238
|
}
|
|
187
239
|
}
|
|
188
240
|
}
|
|
@@ -229,20 +281,30 @@ export async function runUpdateAll(opts?: { projectRoot?: string }): Promise<{ s
|
|
|
229
281
|
return { success: false, summary: `Toolchain resolution FAIL: ${message}` };
|
|
230
282
|
}
|
|
231
283
|
|
|
232
|
-
// Pin exact versions from manifest into infra/modules/package.json and root package.json
|
|
284
|
+
// Pin exact versions from manifest into infra/modules/package.json and root package.json.
|
|
285
|
+
// Returns whether each manifest actually changed.
|
|
286
|
+
let infraChanged = false;
|
|
233
287
|
try {
|
|
234
|
-
await pinInfraPackageVersions(projectRoot, manifest);
|
|
288
|
+
const infraResult = await pinInfraPackageVersions(projectRoot, manifest);
|
|
235
289
|
await pinRootPackageVersions(projectRoot, manifest);
|
|
290
|
+
infraChanged = infraResult.changed;
|
|
236
291
|
} catch (err) {
|
|
237
292
|
const message = err instanceof Error ? err.message : String(err);
|
|
238
293
|
cliLogger.error({ err: message }, 'update-all: version pinning failed — aborting');
|
|
239
294
|
return { success: false, summary: `Version pinning FAIL: ${message}` };
|
|
240
295
|
}
|
|
241
296
|
|
|
242
|
-
// Regenerate lockfiles
|
|
243
|
-
//
|
|
244
|
-
|
|
245
|
-
|
|
297
|
+
// Regenerate lockfiles. Root lockfile is always regenerated (every run) to ensure
|
|
298
|
+
// full reconciliation. Infra/modules lockfile is regenerated only when its deps changed.
|
|
299
|
+
// Root lockfile failure is fatal (deploy blocker); infra/modules failure is not.
|
|
300
|
+
cliLogger.info({ projectRoot, infraChanged }, 'update-all: regenerating root package-lock.json');
|
|
301
|
+
try {
|
|
302
|
+
await regenerateLockfiles(projectRoot, { infraChanged });
|
|
303
|
+
} catch (err) {
|
|
304
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
305
|
+
cliLogger.error({ err: message }, 'update-all: root lockfile regeneration failed — aborting');
|
|
306
|
+
return { success: false, summary: `Lockfile regeneration FAIL: ${message}` };
|
|
307
|
+
}
|
|
246
308
|
|
|
247
309
|
// 1. Read scaffold-config to discover domains
|
|
248
310
|
const scaffoldConfig = await readScaffoldConfig(projectRoot);
|
package/toolchain-manifest.json
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"registrySchemaVersion": "1",
|
|
4
4
|
"packages": {
|
|
5
|
-
"domainCli": "0.2.
|
|
6
|
-
"domainCdkPacker": "0.2.
|
|
7
|
-
"domainRuntime": "0.2.
|
|
8
|
-
"eslintPluginDomainModule": "0.2.
|
|
5
|
+
"domainCli": "0.2.90",
|
|
6
|
+
"domainCdkPacker": "0.2.91",
|
|
7
|
+
"domainRuntime": "0.2.90",
|
|
8
|
+
"eslintPluginDomainModule": "0.2.90"
|
|
9
9
|
}
|
|
10
10
|
}
|