enablement-build-monorepo-version 2.0.4 → 2.0.5
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/README.md +10 -6
- package/package.json +5 -3
- package/src/index.mjs +182 -16
package/README.md
CHANGED
|
@@ -13,8 +13,8 @@ npx enablement-build-monorepo-version@latest [flags]
|
|
|
13
13
|
## How it works
|
|
14
14
|
|
|
15
15
|
1. **Hash** — Each subfolder under the configured `children` directories is hashed (source files only; `node_modules`, `dist`, etc. are excluded).
|
|
16
|
-
2. **Compare** — Hashes are diffed against
|
|
17
|
-
3. **Propagate** — Any package whose hash changed is marked `CHANGED`. All transitive dependents (read from `
|
|
16
|
+
2. **Compare** — Hashes are diffed against saved state from `{configDir}/manifest.json` when present (`cicd[packageName].hash`), falling back to `{configDir}/hash.json` for legacy repos.
|
|
17
|
+
3. **Propagate** — Any package whose hash changed is marked `CHANGED`. All transitive dependents (read from `{configDir}/manifest.json` `dependencies` when available, or from `--dependencies` when explicitly provided) are also marked `CHANGED` via a breadth-first traversal.
|
|
18
18
|
4. **Version** — The next semver is determined from the current version found in each package's version manifest (`package.json`, `version.json`, or `pyproject.toml`) using conventional-commit rules.
|
|
19
19
|
5. **Output** — Azure DevOps `##vso[task.setvariable]` lines are written to stdout for consumption by downstream pipeline steps.
|
|
20
20
|
|
|
@@ -36,8 +36,8 @@ npx enablement-build-monorepo-version@latest [flags]
|
|
|
36
36
|
| `--children <dirs>` | auto-detect | Comma- or space-separated list of top-level directories to scan (e.g. `components,saas`). When omitted, directories are read from `pnpm-workspace.yaml` or `package.json` workspaces automatically. Falls back to `packages` if neither file is found. |
|
|
37
37
|
| `--platform <name>` | auto-detect | Force the CI output format: `ado` (Azure DevOps) or `github` (GitHub Actions). Auto-detected from the presence of a `.github/` directory. |
|
|
38
38
|
| `--prefixPath <path>` | `./` | Root path prepended to all file lookups. Useful when running from a different working directory. |
|
|
39
|
-
| `--hashFile <path>` | auto-detect | Path (relative to `prefixPath`) where hash state is stored between runs. Defaults to `.github/
|
|
40
|
-
| `--dependencies <path>` | `dependencies.json` | Path to the NX-style dependency graph used for transitive change propagation. |
|
|
39
|
+
| `--hashFile <path>` | auto-detect | Path (relative to `prefixPath`) where hash state is stored between runs. Defaults to `.github/manifest.json` or `.cicd/manifest.json` when those files exist (hashes stored inside `cicd[packageName].hash`); falls back to `.github/hash.json` or `.cicd/hash.json` for legacy standalone hash files. |
|
|
40
|
+
| `--dependencies <path>` | `dependencies.json` | Path to the NX-style dependency graph used for transitive change propagation. By default, manifest dependencies are used when `{configDir}/manifest.json` exists; passing this flag explicitly overrides that and forces file-based dependency input. |
|
|
41
41
|
| `--hashExcludeFolders <list>` | see below | Comma-separated folder names to skip when hashing. |
|
|
42
42
|
| `--hashExcludeFiles <list>` | see below | Comma-separated file names to skip when hashing. |
|
|
43
43
|
| `--hashFiles <list>` | — | Comma-separated individual file paths to include in the hash state (outside of `children` folders). |
|
|
@@ -114,9 +114,11 @@ Consume them in a subsequent step with `${{ steps.<step-id>.outputs.<name> }}`.
|
|
|
114
114
|
|
|
115
115
|
---
|
|
116
116
|
|
|
117
|
-
##
|
|
117
|
+
## Dependency Source
|
|
118
118
|
|
|
119
|
-
|
|
119
|
+
By default, transitive dependency propagation uses `dependencies` from `{configDir}/manifest.json` when that manifest exists.
|
|
120
|
+
|
|
121
|
+
To force file-based dependency input, pass `--dependencies <path>`. The file must be an NX project graph export. The relevant section is `graph.dependencies`, where each key is a package name and its value is an array of `{ source, target }` edges:
|
|
120
122
|
|
|
121
123
|
```json
|
|
122
124
|
{
|
|
@@ -161,6 +163,8 @@ To bump and persist the new versions back to `package.json`:
|
|
|
161
163
|
npx enablement-build-monorepo-version --changed --version --save
|
|
162
164
|
```
|
|
163
165
|
|
|
166
|
+
When `--changed --version --save` detects any changed package, the root `package.json` version is also bumped by a patch increment.
|
|
167
|
+
|
|
164
168
|
To override the auto-detected directories, pass `--children` explicitly:
|
|
165
169
|
|
|
166
170
|
```bash
|
package/package.json
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "enablement-build-monorepo-version",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.5",
|
|
4
4
|
"description": "This detects changes in the children packages of a monorepo.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./src/index.mjs",
|
|
7
|
-
"bin": "./src/index.mjs",
|
|
8
7
|
"license": "MIT",
|
|
9
8
|
"author": "Contributors",
|
|
10
9
|
"jest": {
|
|
@@ -17,11 +16,14 @@
|
|
|
17
16
|
"minimatch": "~10.1.1"
|
|
18
17
|
},
|
|
19
18
|
"devDependencies": {
|
|
20
|
-
"jest": "^
|
|
19
|
+
"jest": "^30.4.2"
|
|
21
20
|
},
|
|
22
21
|
"scripts": {
|
|
23
22
|
"start": "node src/index.mjs",
|
|
24
23
|
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
|
25
24
|
"test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch"
|
|
25
|
+
},
|
|
26
|
+
"bin": {
|
|
27
|
+
"enablement-build-monorepo-version": "./src/index.mjs"
|
|
26
28
|
}
|
|
27
29
|
}
|
package/src/index.mjs
CHANGED
|
@@ -7,6 +7,100 @@ import { hashElement } from "./folder-hash.mjs";
|
|
|
7
7
|
import { dependencyMap, compare, getCurrentVersion, updateVersion, retagToPackageNames, detectOldFormatTags, resolveWorkspaceChildren, applyInitScripts, RELEASE_SCRIPTS, emitVariable } from "./lib.mjs";
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
// ─── Hash Format Conversion Helpers ──────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
// Convert old hash.json format (keyed by shortName) to new nested structure (keyed by fullName)
|
|
13
|
+
function convertOldHashFormat(oldHashData) {
|
|
14
|
+
const result = {};
|
|
15
|
+
Object.keys(oldHashData).forEach(shortName => {
|
|
16
|
+
const entry = oldHashData[shortName];
|
|
17
|
+
if (entry.fullName) {
|
|
18
|
+
result[entry.fullName] = {
|
|
19
|
+
hash: entry.hash,
|
|
20
|
+
version: entry.version
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Convert current hashes to structure for cicd property
|
|
28
|
+
function buildHashesForCicd(current) {
|
|
29
|
+
const hashData = {};
|
|
30
|
+
const versions = {};
|
|
31
|
+
|
|
32
|
+
Object.keys(current).forEach(shortName => {
|
|
33
|
+
const entry = current[shortName];
|
|
34
|
+
if (entry.fullName) {
|
|
35
|
+
hashData[entry.fullName] = {
|
|
36
|
+
hash: entry.hash,
|
|
37
|
+
version: entry.version,
|
|
38
|
+
folderName: shortName,
|
|
39
|
+
packageFolder: entry.packageFolder
|
|
40
|
+
};
|
|
41
|
+
versions[entry.fullName] = entry.version;
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return { hashData, versions };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Extract hashes from manifest.json (from cicd property) keyed by fullName
|
|
49
|
+
function extractHashesFromManifestByFullName(manifest) {
|
|
50
|
+
const result = {};
|
|
51
|
+
|
|
52
|
+
if (manifest.cicd) {
|
|
53
|
+
Object.keys(manifest.cicd).forEach(fullname => {
|
|
54
|
+
if (manifest.cicd[fullname].hash) {
|
|
55
|
+
result[fullname] = {
|
|
56
|
+
hash: manifest.cicd[fullname].hash,
|
|
57
|
+
version: manifest.versions ? manifest.versions[fullname] : manifest.cicd[fullname].version
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function bumpPatchVersion(version) {
|
|
67
|
+
const value = (version || '0.0.0').trim();
|
|
68
|
+
const parts = value.split('-');
|
|
69
|
+
const base = parts[0] || '0.0.0';
|
|
70
|
+
const suffix = parts.length > 1 ? parts.slice(1).join('-') : '';
|
|
71
|
+
|
|
72
|
+
const nums = base.split('.');
|
|
73
|
+
const major = Number.parseInt(nums[0] || '0', 10) || 0;
|
|
74
|
+
const minor = Number.parseInt(nums[1] || '0', 10) || 0;
|
|
75
|
+
const patch = (Number.parseInt(nums[2] || '0', 10) || 0) + 1;
|
|
76
|
+
|
|
77
|
+
const next = `${major}.${minor}.${patch}`;
|
|
78
|
+
return suffix ? `${next}-${suffix}` : next;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function updateRootVersionIfNeeded(options, changeList) {
|
|
82
|
+
if (!options.saveVersion || !options.version || changeList.length === 0) return;
|
|
83
|
+
|
|
84
|
+
const rootPackageFile = path.join(options.prefixPath, 'package.json');
|
|
85
|
+
if (!existsSync(rootPackageFile)) return;
|
|
86
|
+
|
|
87
|
+
const pkg = JSON.parse(readFileSync(rootPackageFile, 'utf8'));
|
|
88
|
+
const previous = pkg.version || '0.0.0';
|
|
89
|
+
const next = bumpPatchVersion(previous);
|
|
90
|
+
|
|
91
|
+
if (options.try) {
|
|
92
|
+
console.log('\x1b[36m%s\x1b[0m', `[try] Would update ${rootPackageFile} version ${previous} -> ${next}`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (previous !== next) {
|
|
97
|
+
pkg.version = next;
|
|
98
|
+
writeFileSync(rootPackageFile, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
|
|
99
|
+
console.log(`root package.json version updated: ${previous} -> ${next}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
10
104
|
async function main(options) {
|
|
11
105
|
return new Promise((mainResolve, mainreject) => {
|
|
12
106
|
const hashOptions = { encoding: 'hex', folders: { exclude: options.hashExcludeFolders }, files: { exclude: options.hashExcludeFiles } };
|
|
@@ -33,8 +127,8 @@ async function main(options) {
|
|
|
33
127
|
const changeConfig = path.join(options.prefixPath, options.hashFile);
|
|
34
128
|
let previous = {};
|
|
35
129
|
|
|
36
|
-
// make sure hash config file exists
|
|
37
|
-
if (!existsSync(changeConfig)) {
|
|
130
|
+
// make sure hash config file exists only when using a dedicated hash.json file
|
|
131
|
+
if (!usesManifestHashFile && !existsSync(changeConfig)) {
|
|
38
132
|
if (options.try) {
|
|
39
133
|
console.log('\x1b[36m%s\x1b[0m', `[try] Hash file ${changeConfig} not found — treating previous state as empty`);
|
|
40
134
|
} else {
|
|
@@ -43,7 +137,16 @@ async function main(options) {
|
|
|
43
137
|
}
|
|
44
138
|
|
|
45
139
|
let dependencies = {};
|
|
46
|
-
|
|
140
|
+
|
|
141
|
+
const manifestDependencyPath = path.join(options.prefixPath, `${configDir}/manifest.json`);
|
|
142
|
+
const shouldPreferManifestDependencies = !options.dependenciesProvided && existsSync(manifestDependencyPath);
|
|
143
|
+
|
|
144
|
+
if (shouldPreferManifestDependencies) {
|
|
145
|
+
const manifestText = readFileSync(manifestDependencyPath, 'utf8');
|
|
146
|
+
const manifest = JSON.parse(manifestText || '{}');
|
|
147
|
+
dependencies = manifest.dependencies || {};
|
|
148
|
+
if (options.debug) console.log(`Loaded dependencies from ${manifestDependencyPath}\n`, JSON.stringify(dependencies));
|
|
149
|
+
} else if (options.dependencies) {
|
|
47
150
|
if (existsSync(options.dependencies)) {
|
|
48
151
|
dependencies = dependencyMap(options.dependencies);
|
|
49
152
|
if (options.debug) console.log('Loaded dependencies\n', JSON.stringify(dependencies));
|
|
@@ -52,9 +155,16 @@ async function main(options) {
|
|
|
52
155
|
}
|
|
53
156
|
}
|
|
54
157
|
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
158
|
+
// Load manifest for later use (will extract hashes after building current)
|
|
159
|
+
let manifestData = {};
|
|
160
|
+
if (usesManifestHashFile) {
|
|
161
|
+
const data = existsSync(changeConfig) ? readFileSync(changeConfig, "utf8") : "{}";
|
|
162
|
+
manifestData = JSON.parse(data || "{}");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// For now, set previous to empty - will populate after building current
|
|
166
|
+
// (because we need the fullName mapping from current)
|
|
167
|
+
previous = {};
|
|
58
168
|
if (options.debug) console.log("PREVIOUS", JSON.stringify(previous));
|
|
59
169
|
|
|
60
170
|
options.hashFiles.forEach(file => {
|
|
@@ -95,7 +205,21 @@ async function main(options) {
|
|
|
95
205
|
}
|
|
96
206
|
|
|
97
207
|
if (options.changed || options.version || options.tag) {
|
|
98
|
-
|
|
208
|
+
// Build previous hashes keyed by short name using fullName mapping
|
|
209
|
+
let previousHashes = {};
|
|
210
|
+
if (usesManifestHashFile) {
|
|
211
|
+
const hashesByFullName = extractHashesFromManifestByFullName(manifestData);
|
|
212
|
+
Object.keys(current).forEach(shortName => {
|
|
213
|
+
const entry = current[shortName];
|
|
214
|
+
if (entry.fullName && hashesByFullName[entry.fullName]) {
|
|
215
|
+
previousHashes[shortName] = hashesByFullName[entry.fullName];
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
} else {
|
|
219
|
+
previousHashes = previous;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
let results = await compare(packageFolder, previousHashes, current, dependencies, options);
|
|
99
223
|
if (options.debug) console.log("COMPARE", JSON.stringify(results));
|
|
100
224
|
|
|
101
225
|
for (let i = 0; i < results.length; i++) {
|
|
@@ -164,7 +288,9 @@ async function main(options) {
|
|
|
164
288
|
}));
|
|
165
289
|
});
|
|
166
290
|
|
|
167
|
-
Promise.all(scanlist).then(() => {
|
|
291
|
+
Promise.all(scanlist).then(async () => {
|
|
292
|
+
await updateRootVersionIfNeeded(options, changeList);
|
|
293
|
+
|
|
168
294
|
if (options.changed) {
|
|
169
295
|
console.log(`CHANGED - ${JSON.stringify(changeList)}`);
|
|
170
296
|
emitVariable('changed', JSON.stringify(changeList), options.platform);
|
|
@@ -180,13 +306,39 @@ async function main(options) {
|
|
|
180
306
|
|
|
181
307
|
// write current hashes
|
|
182
308
|
if (options.hash) {
|
|
183
|
-
let result = Object.keys(current).sort().reduce(
|
|
184
|
-
(obj, key) => { obj[key] = current[key]; return obj; },
|
|
185
|
-
{}
|
|
186
|
-
);
|
|
187
309
|
if (options.try) {
|
|
188
310
|
console.log('\x1b[36m%s\x1b[0m', `[try] Would write updated hashes to ${changeConfig}`);
|
|
311
|
+
} else if (usesManifestHashFile) {
|
|
312
|
+
const manifestData = existsSync(changeConfig) ? readFileSync(changeConfig, "utf8") : "{}";
|
|
313
|
+
const manifest = JSON.parse(manifestData || "{}");
|
|
314
|
+
|
|
315
|
+
const { hashData, versions } = buildHashesForCicd(current);
|
|
316
|
+
|
|
317
|
+
// Update cicd properties with hash and metadata
|
|
318
|
+
Object.keys(hashData).forEach(fullname => {
|
|
319
|
+
if (!manifest.cicd) manifest.cicd = {};
|
|
320
|
+
if (!manifest.cicd[fullname]) manifest.cicd[fullname] = {};
|
|
321
|
+
manifest.cicd[fullname].hash = hashData[fullname].hash;
|
|
322
|
+
manifest.cicd[fullname].folderName = hashData[fullname].folderName;
|
|
323
|
+
manifest.cicd[fullname].packageFolder = hashData[fullname].packageFolder;
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// Update versions section
|
|
327
|
+
if (!manifest.versions) manifest.versions = {};
|
|
328
|
+
Object.keys(versions).forEach(fullname => {
|
|
329
|
+
manifest.versions[fullname] = versions[fullname];
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// Remove old hash property if it exists
|
|
333
|
+
delete manifest.hash;
|
|
334
|
+
|
|
335
|
+
writeFileSync(changeConfig, JSON.stringify(manifest, null, 2), "utf8");
|
|
336
|
+
console.log('manifest hashes written successfully');
|
|
189
337
|
} else {
|
|
338
|
+
let result = Object.keys(current).sort().reduce(
|
|
339
|
+
(obj, key) => { obj[key] = current[key]; return obj; },
|
|
340
|
+
{}
|
|
341
|
+
);
|
|
190
342
|
writeFileSync(changeConfig, JSON.stringify(result, null, 2), "utf8");
|
|
191
343
|
console.log('folder hashes written successfully');
|
|
192
344
|
}
|
|
@@ -250,7 +402,8 @@ let options = {
|
|
|
250
402
|
hashFile: null,
|
|
251
403
|
hashExcludeFolders: ['node_modules', 'coverage', 'dist', 'bin', 'obj', '__pycache__', '.vs', '.nx', '.vscode', '.idea', '.git', '.github', '.azuredevops', '.release'],
|
|
252
404
|
hashExcludeFiles: ['.npmrc', 'CHANGELOG.md', 'README.md'],
|
|
253
|
-
dependencies: "dependencies.json"
|
|
405
|
+
dependencies: "dependencies.json",
|
|
406
|
+
dependenciesProvided: false
|
|
254
407
|
};
|
|
255
408
|
|
|
256
409
|
if (process.argv.length === 2) {
|
|
@@ -281,6 +434,7 @@ if (process.argv.length === 2) {
|
|
|
281
434
|
let name = argv[i].substring(2);
|
|
282
435
|
if (options[name] !== undefined) {
|
|
283
436
|
options[name] = argv[i + 1];
|
|
437
|
+
if (name === 'dependencies') options.dependenciesProvided = true;
|
|
284
438
|
i++;
|
|
285
439
|
} else {
|
|
286
440
|
console.error(`Expected a known option, got ${argv[i]}`);
|
|
@@ -290,12 +444,24 @@ if (process.argv.length === 2) {
|
|
|
290
444
|
}
|
|
291
445
|
}
|
|
292
446
|
|
|
293
|
-
const
|
|
447
|
+
const hasGitHubManifest = existsSync(path.join(options.prefixPath, '.github/manifest.json'));
|
|
448
|
+
const hasAdoManifest = existsSync(path.join(options.prefixPath, '.cicd/manifest.json'));
|
|
449
|
+
const hasGitHubHash = existsSync(path.join(options.prefixPath, '.github/hash.json'));
|
|
450
|
+
|
|
451
|
+
const configDir = (hasGitHubManifest || hasGitHubHash) ? '.github' : '.cicd';
|
|
294
452
|
if (!options.hashFile) {
|
|
295
|
-
|
|
453
|
+
if (configDir === '.github' && hasGitHubManifest) {
|
|
454
|
+
options.hashFile = '.github/manifest.json';
|
|
455
|
+
} else if (configDir === '.cicd' && hasAdoManifest) {
|
|
456
|
+
options.hashFile = '.cicd/manifest.json';
|
|
457
|
+
} else {
|
|
458
|
+
options.hashFile = configDir + '/hash.json';
|
|
459
|
+
}
|
|
296
460
|
}
|
|
461
|
+
const usesManifestHashFile = options.hashFile.replace(/\\/g, '/').endsWith('/manifest.json');
|
|
462
|
+
|
|
297
463
|
if (!options.platform) {
|
|
298
|
-
options.platform =
|
|
464
|
+
options.platform = configDir === '.github' ? 'github' : 'ado';
|
|
299
465
|
}
|
|
300
466
|
|
|
301
467
|
async function initPackage(options) {
|