enablement-build-monorepo-version 2.0.8 → 6.0.0
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 +11 -9
- package/package.json +2 -2
- package/src/index.mjs +23 -142
- package/src/lib.mjs +19 -72
- package/src/version.mjs +162 -79
- package/tests/index.test.mjs +206 -0
- package/tests/version.test.mjs +34 -0
- package/src/folder-hash.mjs +0 -425
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ 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
|
|
16
|
+
2. **Compare** — Hashes are diffed against state in `--hashFile`, which defaults to `{configDir}/hash.json`.
|
|
17
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.
|
|
@@ -34,9 +34,10 @@ npx enablement-build-monorepo-version@latest [flags]
|
|
|
34
34
|
| `--try` | off | Dry-run mode. Prints what each operation **would** do without writing any files, creating tags, or modifying any manifests. Combine with any other flags to preview their effect. |
|
|
35
35
|
| `--debug` | off | Verbose logging of hashes, comparisons, and version resolution. |
|
|
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
|
+
| `--exclude <dirs>` | — | Comma- or space-separated list of top-level directories to drop from whichever list of children is in effect — auto-detected or from `--children` (e.g. auto-detect everything but skip `tools`). |
|
|
38
|
+
| `--platform <name>` | `ado` | Select the CI output format: `ado` (Azure DevOps) or `github` (GitHub Actions). |
|
|
38
39
|
| `--prefixPath <path>` | `./` | Root path prepended to all file lookups. Useful when running from a different working directory. |
|
|
39
|
-
| `--hashFile <path>` |
|
|
40
|
+
| `--hashFile <path>` | `{configDir}/hash.json` | Path (relative to `prefixPath`) where hash state is stored between runs. Hashes are always read from and written to a `hash.json` file; if this points at a `manifest.json`, it is redirected to a sibling `hash.json` in that same directory, and only the `hash` property of each package is updated there — no other property is written. |
|
|
40
41
|
| `--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
42
|
| `--hashExcludeFolders <list>` | see below | Comma-separated folder names to skip when hashing. |
|
|
42
43
|
| `--hashExcludeFiles <list>` | see below | Comma-separated file names to skip when hashing. |
|
|
@@ -78,9 +79,9 @@ Versions follow semver and are bumped based on the **current version** in the pa
|
|
|
78
79
|
|
|
79
80
|
## Pipeline variable output
|
|
80
81
|
|
|
81
|
-
The output format is
|
|
82
|
+
The output format is selected with `--platform`; it defaults to Azure DevOps. Pass `--platform github` for GitHub Actions output.
|
|
82
83
|
|
|
83
|
-
### Azure DevOps (default
|
|
84
|
+
### Azure DevOps (default)
|
|
84
85
|
|
|
85
86
|
Variables are written to stdout using the `##vso` task command:
|
|
86
87
|
|
|
@@ -90,7 +91,7 @@ Variables are written to stdout using the `##vso` task command:
|
|
|
90
91
|
##vso[task.setvariable variable=components;isoutput=true]true
|
|
91
92
|
```
|
|
92
93
|
|
|
93
|
-
### GitHub Actions (
|
|
94
|
+
### GitHub Actions (`--platform github`)
|
|
94
95
|
|
|
95
96
|
Variables are written to the `$GITHUB_OUTPUT` file (falls back to stdout if the env var is not set):
|
|
96
97
|
|
|
@@ -208,9 +209,10 @@ The following scripts are injected (skipped if already present; `--children` str
|
|
|
208
209
|
```
|
|
209
210
|
src/
|
|
210
211
|
├── index.mjs CLI entry point and main() orchestration
|
|
211
|
-
├── lib.mjs Core logic (exported for testing)
|
|
212
|
-
|
|
213
|
-
|
|
212
|
+
├── lib.mjs Core logic (exported for testing); also re-exports
|
|
213
|
+
│ workspace detection, folder hashing, and hash.json
|
|
214
|
+
│ handling from the enablement-build-manifest package
|
|
215
|
+
└── version.mjs Semver bump logic
|
|
214
216
|
|
|
215
217
|
tests/
|
|
216
218
|
├── lib.test.mjs Unit tests for lib.mjs
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "enablement-build-monorepo-version",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
4
4
|
"description": "This detects changes in the children packages of a monorepo.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./src/index.mjs",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
]
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"
|
|
17
|
+
"enablement-build-manifest": "1.4.0"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"jest": "^30.4.2"
|
package/src/index.mjs
CHANGED
|
@@ -3,66 +3,9 @@ import path from 'path';
|
|
|
3
3
|
import { readFileSync, writeFileSync, existsSync, lstatSync } from "fs";
|
|
4
4
|
import { exec } from "child_process";
|
|
5
5
|
|
|
6
|
-
import { hashElement } from "./
|
|
7
|
-
import { dependencyMap, compare, getCurrentVersion, updateVersion, retagToPackageNames, detectOldFormatTags, resolveWorkspaceChildren, applyInitScripts, RELEASE_SCRIPTS, emitVariable } from "./lib.mjs";
|
|
6
|
+
import { dependencyMap, compare, getCurrentVersion, updateVersion, retagToPackageNames, detectOldFormatTags, resolveWorkspaceChildren, excludeChildren, applyInitScripts, RELEASE_SCRIPTS, emitVariable, hashElement, resolveHashFilePath, ensureHashFileExists, loadHashFile, writeHashFile } from "./lib.mjs";
|
|
8
7
|
|
|
9
8
|
|
|
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
|
-
name: entry.fullName,
|
|
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
9
|
function bumpPatchVersion(version) {
|
|
67
10
|
const value = (version || '0.0.0').trim();
|
|
68
11
|
const parts = value.split('-');
|
|
@@ -120,21 +63,24 @@ async function main(options) {
|
|
|
120
63
|
}
|
|
121
64
|
}
|
|
122
65
|
|
|
66
|
+
// --exclude drops specific root directories from whichever children list
|
|
67
|
+
// is in effect, auto-detected or explicit.
|
|
68
|
+
if (options.exclude.length > 0) {
|
|
69
|
+
options.children = excludeChildren(options.children, options.exclude) || options.children;
|
|
70
|
+
}
|
|
71
|
+
|
|
123
72
|
if (options.try) {
|
|
124
73
|
console.log('\x1b[36m%s\x1b[0m', '[try] Dry-run mode — no changes will be made\n');
|
|
125
74
|
}
|
|
126
75
|
|
|
127
|
-
|
|
76
|
+
// Hashes are always stored in a hash.json file. When --hashFile points at a
|
|
77
|
+
// manifest.json (legacy "manifest mode"), redirect to the sibling hash.json
|
|
78
|
+
// instead of writing hash data into the manifest.
|
|
79
|
+
const { changeConfig, usesManifestHashFile } = resolveHashFilePath(options.prefixPath, options.hashFile);
|
|
128
80
|
let previous = {};
|
|
129
81
|
|
|
130
|
-
// make sure hash config file exists
|
|
131
|
-
|
|
132
|
-
if (options.try) {
|
|
133
|
-
console.log('\x1b[36m%s\x1b[0m', `[try] Hash file ${changeConfig} not found — treating previous state as empty`);
|
|
134
|
-
} else {
|
|
135
|
-
writeFileSync(changeConfig, "{}", "utf8");
|
|
136
|
-
}
|
|
137
|
-
}
|
|
82
|
+
// make sure hash config file exists
|
|
83
|
+
ensureHashFileExists(changeConfig, options);
|
|
138
84
|
|
|
139
85
|
let dependencies = {};
|
|
140
86
|
|
|
@@ -155,16 +101,8 @@ async function main(options) {
|
|
|
155
101
|
}
|
|
156
102
|
}
|
|
157
103
|
|
|
158
|
-
// Load
|
|
159
|
-
|
|
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 = {};
|
|
104
|
+
// Load previous hashes, keyed by short package folder name.
|
|
105
|
+
previous = loadHashFile(changeConfig);
|
|
168
106
|
if (options.debug) console.log("PREVIOUS", JSON.stringify(previous));
|
|
169
107
|
|
|
170
108
|
options.hashFiles.forEach(file => {
|
|
@@ -205,21 +143,7 @@ async function main(options) {
|
|
|
205
143
|
}
|
|
206
144
|
|
|
207
145
|
if (options.changed || options.version || options.tag) {
|
|
208
|
-
|
|
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);
|
|
146
|
+
let results = await compare(packageFolder, previous, current, dependencies, options);
|
|
223
147
|
if (options.debug) console.log("COMPARE", JSON.stringify(results));
|
|
224
148
|
|
|
225
149
|
for (let i = 0; i < results.length; i++) {
|
|
@@ -308,39 +232,8 @@ async function main(options) {
|
|
|
308
232
|
if (options.hash) {
|
|
309
233
|
if (options.try) {
|
|
310
234
|
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].name = hashData[fullname].name;
|
|
323
|
-
delete manifest.cicd[fullname].folderName;
|
|
324
|
-
manifest.cicd[fullname].packageFolder = hashData[fullname].packageFolder;
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
// Update versions section
|
|
328
|
-
if (!manifest.versions) manifest.versions = {};
|
|
329
|
-
Object.keys(versions).forEach(fullname => {
|
|
330
|
-
manifest.versions[fullname] = versions[fullname];
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
// Remove old hash property if it exists
|
|
334
|
-
delete manifest.hash;
|
|
335
|
-
|
|
336
|
-
writeFileSync(changeConfig, JSON.stringify(manifest, null, 2), "utf8");
|
|
337
|
-
console.log('manifest hashes written successfully');
|
|
338
235
|
} else {
|
|
339
|
-
|
|
340
|
-
(obj, key) => { obj[key] = current[key]; return obj; },
|
|
341
|
-
{}
|
|
342
|
-
);
|
|
343
|
-
writeFileSync(changeConfig, JSON.stringify(result, null, 2), "utf8");
|
|
236
|
+
writeHashFile(changeConfig, current, { usesManifestHashFile });
|
|
344
237
|
console.log('folder hashes written successfully');
|
|
345
238
|
}
|
|
346
239
|
}
|
|
@@ -396,7 +289,8 @@ let options = {
|
|
|
396
289
|
try: false,
|
|
397
290
|
commit: false,
|
|
398
291
|
children: null,
|
|
399
|
-
|
|
292
|
+
exclude: [],
|
|
293
|
+
platform: 'ado',
|
|
400
294
|
hashFiles: [],
|
|
401
295
|
prefixPath: './',
|
|
402
296
|
debug: false,
|
|
@@ -422,9 +316,9 @@ if (process.argv.length === 2) {
|
|
|
422
316
|
else if (argv[i] === "--retag") options.retag = true;
|
|
423
317
|
else if (argv[i] === "--init") options.init = true;
|
|
424
318
|
else if (argv[i] === "--try") options.try = true;
|
|
425
|
-
else if (argv[i] === "--hashExcludeFolders" || argv[i] === "--hashExcludeFiles") {
|
|
319
|
+
else if (argv[i] === "--exclude" || argv[i] === "--hashExcludeFolders" || argv[i] === "--hashExcludeFiles") {
|
|
426
320
|
let name = argv[i].substring(2);
|
|
427
|
-
options[name] = argv[i + 1].split(',');
|
|
321
|
+
options[name] = argv[i + 1].replace(/\s/g, ',').split(',');
|
|
428
322
|
i++;
|
|
429
323
|
}
|
|
430
324
|
else if (argv[i] === "--hashFiles") {
|
|
@@ -445,25 +339,12 @@ if (process.argv.length === 2) {
|
|
|
445
339
|
}
|
|
446
340
|
}
|
|
447
341
|
|
|
448
|
-
const hasGitHubManifest = existsSync(path.join(options.prefixPath, '.github/manifest.json'));
|
|
449
|
-
const hasAdoManifest = existsSync(path.join(options.prefixPath, '.cicd/manifest.json'));
|
|
450
|
-
const hasGitHubHash = existsSync(path.join(options.prefixPath, '.github/hash.json'));
|
|
451
342
|
|
|
452
|
-
const configDir =
|
|
343
|
+
const configDir = options.platform === 'github' ? '.github' : '.cicd';
|
|
453
344
|
if (!options.hashFile) {
|
|
454
|
-
|
|
455
|
-
options.hashFile = '.github/manifest.json';
|
|
456
|
-
} else if (configDir === '.cicd' && hasAdoManifest) {
|
|
457
|
-
options.hashFile = '.cicd/manifest.json';
|
|
458
|
-
} else {
|
|
459
|
-
options.hashFile = configDir + '/hash.json';
|
|
460
|
-
}
|
|
345
|
+
options.hashFile = configDir + "/hash.json";
|
|
461
346
|
}
|
|
462
|
-
const usesManifestHashFile = options.hashFile.replace(/\\/g, '/').endsWith('/manifest.json');
|
|
463
347
|
|
|
464
|
-
if (!options.platform) {
|
|
465
|
-
options.platform = configDir === '.github' ? 'github' : 'ado';
|
|
466
|
-
}
|
|
467
348
|
|
|
468
349
|
async function initPackage(options) {
|
|
469
350
|
const pkgFile = path.join(options.prefixPath, 'package.json');
|
package/src/lib.mjs
CHANGED
|
@@ -9,78 +9,25 @@ import {
|
|
|
9
9
|
import { exec } from "child_process";
|
|
10
10
|
import { Version } from "./version.mjs";
|
|
11
11
|
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
//
|
|
15
|
-
export
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
} else if (trimmed && !/^\s/.test(line)) {
|
|
32
|
-
inPackages = false;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return patterns;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Extract unique top-level directory names from workspace glob patterns.
|
|
40
|
-
// e.g. ['packages/*', 'apps/**'] → ['packages', 'apps']
|
|
41
|
-
export function extractDirsFromPatterns(patterns) {
|
|
42
|
-
const dirs = new Set();
|
|
43
|
-
for (const pattern of patterns) {
|
|
44
|
-
const dir = pattern
|
|
45
|
-
.split("/")[0]
|
|
46
|
-
.replace(/[*?[\]{}]/g, "")
|
|
47
|
-
.trim();
|
|
48
|
-
if (dir) dirs.add(dir);
|
|
49
|
-
}
|
|
50
|
-
return [...dirs];
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Read pnpm-workspace.yaml or package.json workspaces and return a
|
|
54
|
-
// comma-separated string of top-level directories, or null if not found.
|
|
55
|
-
export function resolveWorkspaceChildren(prefixPath) {
|
|
56
|
-
const pnpmFile = path.join(prefixPath, "pnpm-workspace.yaml");
|
|
57
|
-
if (existsSync(pnpmFile)) {
|
|
58
|
-
try {
|
|
59
|
-
const patterns = parseWorkspacePatterns(readFileSync(pnpmFile, "utf8"));
|
|
60
|
-
const dirs = extractDirsFromPatterns(patterns);
|
|
61
|
-
if (dirs.length > 0) return dirs.join(",");
|
|
62
|
-
} catch {}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const pkgFile = path.join(prefixPath, "package.json");
|
|
66
|
-
if (existsSync(pkgFile)) {
|
|
67
|
-
try {
|
|
68
|
-
const pkg = JSON.parse(readFileSync(pkgFile, "utf8"));
|
|
69
|
-
if (pkg.workspaces) {
|
|
70
|
-
const ws = pkg.workspaces;
|
|
71
|
-
const patterns = Array.isArray(ws)
|
|
72
|
-
? ws
|
|
73
|
-
: Array.isArray(ws?.packages)
|
|
74
|
-
? ws.packages
|
|
75
|
-
: [];
|
|
76
|
-
const dirs = extractDirsFromPatterns(patterns);
|
|
77
|
-
if (dirs.length > 0) return dirs.join(",");
|
|
78
|
-
}
|
|
79
|
-
} catch {}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
12
|
+
// Workspace child-directory resolution lives in the manifest package (it also
|
|
13
|
+
// needs to know package roots) and is re-exported here so callers of this
|
|
14
|
+
// module don't need to know that.
|
|
15
|
+
export {
|
|
16
|
+
parseWorkspacePatterns,
|
|
17
|
+
extractDirsFromPatterns,
|
|
18
|
+
resolveWorkspaceChildren,
|
|
19
|
+
excludeChildren,
|
|
20
|
+
} from "enablement-build-manifest/src/workspace.mjs";
|
|
21
|
+
|
|
22
|
+
// Folder hashing (the recursive hash engine) and hash.json file handling both
|
|
23
|
+
// live in the manifest package too, re-exported here for the same reason.
|
|
24
|
+
export {
|
|
25
|
+
hashElement,
|
|
26
|
+
resolveHashFilePath,
|
|
27
|
+
ensureHashFileExists,
|
|
28
|
+
loadHashFile,
|
|
29
|
+
writeHashFile,
|
|
30
|
+
} from "enablement-build-manifest/src/hashFile.mjs";
|
|
84
31
|
|
|
85
32
|
// ─── pyproject.toml version read/write ───────────────────────────────────────
|
|
86
33
|
|
package/src/version.mjs
CHANGED
|
@@ -1,100 +1,183 @@
|
|
|
1
1
|
import * as url from 'url';
|
|
2
2
|
import fs from "fs";
|
|
3
|
-
import {
|
|
3
|
+
import { execSync } from "child_process";
|
|
4
4
|
|
|
5
5
|
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
7
|
+
function getBumpType(message) {
|
|
8
|
+
const text = (message || "").trim();
|
|
9
|
+
if (!text) return "patch";
|
|
10
|
+
|
|
11
|
+
// Breaking change markers take precedence over all other commit types.
|
|
12
|
+
if (/BREAKING\s+CHANGES?:/im.test(text)) return "major";
|
|
13
|
+
if (/^\s*[a-z]+(?:\([^\n\r)]*\))?!:/im.test(text)) return "major";
|
|
14
|
+
|
|
15
|
+
const headers = text
|
|
16
|
+
.split(/\r?\n/)
|
|
17
|
+
.filter(Boolean)
|
|
18
|
+
.map((line) => line.trim())
|
|
19
|
+
.filter((line) => /^[a-z]+(?:\([^\n\r)]*\))?!?:/i.test(line));
|
|
20
|
+
|
|
21
|
+
const hasType = (types) => headers.some((line) => {
|
|
22
|
+
const match = line.match(/^([a-z]+)(?:\([^\n\r)]*\))?!?:/i);
|
|
23
|
+
return match ? types.includes(match[1].toLowerCase()) : false;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
if (hasType(["feat", "refactor"])) return "minor";
|
|
27
|
+
if (hasType(["fix", "perf", "revert", "chore", "docs", "style", "test", "build", "ci"])) return "patch";
|
|
28
|
+
|
|
29
|
+
// Keep current behavior conservative when type is unknown.
|
|
30
|
+
return "patch";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function processMessage(message, tag, imageName, options, args) {
|
|
34
|
+
const previous = tag;
|
|
35
|
+
let normalizedTag = tag;
|
|
36
|
+
|
|
37
|
+
const atParts = normalizedTag.split('@');
|
|
38
|
+
if (atParts.length > 1) normalizedTag = atParts[1];
|
|
39
|
+
|
|
40
|
+
const suffixParts = normalizedTag.trim().split('-');
|
|
41
|
+
let suffix = false;
|
|
42
|
+
if (suffixParts.length > 1) {
|
|
43
|
+
normalizedTag = suffixParts[0];
|
|
44
|
+
suffix = suffixParts.slice(1).join('-');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const parts = normalizedTag.trim().split('.');
|
|
18
48
|
parts.length = 3;
|
|
19
|
-
let build = false;
|
|
20
49
|
|
|
21
|
-
|
|
50
|
+
let major = Number.parseInt(parts[0] || '0', 10) || 0;
|
|
51
|
+
let minor = Number.parseInt(parts[1] || '0', 10) || 0;
|
|
52
|
+
let patch = Number.parseInt(parts[2] || '0', 10) || 0;
|
|
22
53
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
54
|
+
const bump = getBumpType(message);
|
|
55
|
+
let build = false;
|
|
56
|
+
|
|
57
|
+
if (bump === "major") {
|
|
58
|
+
major += 1;
|
|
59
|
+
minor = 0;
|
|
60
|
+
patch = 0;
|
|
26
61
|
build = true;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
parts[1]++;
|
|
31
|
-
parts[2] = 0;
|
|
62
|
+
} else if (bump === "minor") {
|
|
63
|
+
minor += 1;
|
|
64
|
+
patch = 0;
|
|
32
65
|
build = true;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
parts[0] = parseInt(parts[0]);
|
|
36
|
-
parts[0]++;
|
|
37
|
-
parts[1] = '0';
|
|
38
|
-
parts[2] = '0';
|
|
66
|
+
} else {
|
|
67
|
+
patch += 1;
|
|
39
68
|
build = true;
|
|
40
69
|
}
|
|
41
70
|
|
|
42
|
-
|
|
43
|
-
if(suffix)
|
|
71
|
+
normalizedTag = `${major}.${minor}.${patch}`;
|
|
72
|
+
if (suffix) normalizedTag = `${normalizedTag}-${suffix}`;
|
|
44
73
|
|
|
45
|
-
if(options.debug)
|
|
46
|
-
|
|
47
|
-
};
|
|
74
|
+
if (options.debug) {
|
|
75
|
+
console.log('\x1b[32m%s\x1b[0m', `Bump type: ${bump}`);
|
|
76
|
+
console.log('\x1b[32m%s\x1b[0m', `Next version: ${imageName}@${normalizedTag}`);
|
|
77
|
+
}
|
|
48
78
|
|
|
49
|
-
|
|
50
|
-
|
|
79
|
+
return { name: imageName, tag: normalizedTag, version: normalizedTag, build, previous, ...args };
|
|
80
|
+
}
|
|
51
81
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
|
|
82
|
+
function readVersionTag(versionFile) {
|
|
83
|
+
const data = fs.readFileSync(versionFile, 'utf8');
|
|
84
|
+
|
|
85
|
+
if (versionFile.endsWith('.toml')) {
|
|
86
|
+
let inSection = false;
|
|
87
|
+
for (const line of data.split('\n')) {
|
|
88
|
+
const t = line.trim();
|
|
89
|
+
if (/^\[project\]$/.test(t) || /^\[tool\.poetry\]$/.test(t)) {
|
|
90
|
+
inSection = true;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (/^\[/.test(t)) {
|
|
94
|
+
inSection = false;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (inSection) {
|
|
98
|
+
const m = t.match(/^version\s*=\s*["']([^"']+)["']/);
|
|
99
|
+
if (m) return (m[1] || '0.0.0').trim();
|
|
68
100
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
101
|
+
}
|
|
102
|
+
return '0.0.0';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return (JSON.parse(data).version || '0.0.0').trim();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function findBaseTag(imageName, lastVersion) {
|
|
109
|
+
const shortName = String(imageName || '').split('/').pop();
|
|
110
|
+
const candidates = [
|
|
111
|
+
`${shortName}/${lastVersion}`,
|
|
112
|
+
`${imageName}/${lastVersion}`,
|
|
113
|
+
`${shortName}@${lastVersion}`,
|
|
114
|
+
`${imageName}@${lastVersion}`,
|
|
115
|
+
`v${lastVersion}`,
|
|
116
|
+
`${lastVersion}`
|
|
117
|
+
].filter(Boolean);
|
|
118
|
+
|
|
119
|
+
for (const candidate of candidates) {
|
|
120
|
+
try {
|
|
121
|
+
execSync(`git rev-parse -q --verify "refs/tags/${candidate}"`, { stdio: 'ignore' });
|
|
122
|
+
return candidate;
|
|
72
123
|
} catch {
|
|
73
|
-
|
|
74
|
-
resolve({"name":imageName,"tag":`${imageName}@0.0.0`,"previous":"0.0.0","version":"0.0.1", ...args});
|
|
124
|
+
// Try next tag format.
|
|
75
125
|
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function readCommitMessagesSinceTag(imageName, lastVersion, args, options) {
|
|
132
|
+
const scopePath = args?.packageFolder && args?.name ? `${args.packageFolder}/${args.name}` : null;
|
|
133
|
+
const hasScope = scopePath && fs.existsSync(scopePath);
|
|
134
|
+
|
|
135
|
+
const baseTag = findBaseTag(imageName, lastVersion);
|
|
136
|
+
if (!baseTag) {
|
|
137
|
+
if (options.debug) {
|
|
138
|
+
console.log('\x1b[33m%s\x1b[0m', `No matching base tag found for ${imageName}@${lastVersion}; falling back to recent history.`);
|
|
139
|
+
}
|
|
140
|
+
const fallbackCmd = hasScope
|
|
141
|
+
? `git log HEAD --pretty=%B --no-merges -- "${scopePath}"`
|
|
142
|
+
: `git log HEAD --pretty=%B --no-merges`;
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
return execSync(fallbackCmd, { encoding: 'utf8' }).trim();
|
|
146
|
+
} catch {
|
|
147
|
+
return '';
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const cmd = hasScope
|
|
152
|
+
? `git log "${baseTag}..HEAD" --pretty=%B --no-merges -- "${scopePath}"`
|
|
153
|
+
: `git log "${baseTag}..HEAD" --pretty=%B --no-merges`;
|
|
154
|
+
|
|
155
|
+
if (options.debug) {
|
|
156
|
+
console.log('\x1b[32m%s\x1b[0m', `git log range: ${baseTag}..HEAD${hasScope ? ` (${scopePath})` : ''}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
return execSync(cmd, { encoding: 'utf8' }).trim();
|
|
161
|
+
} catch {
|
|
162
|
+
return '';
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function Version(imageName, lastVersion, versionFile, options, args) {
|
|
167
|
+
try {
|
|
168
|
+
if (options.debug) console.log('\x1b[32m%s\x1b[0m', `reading config: ${versionFile}`);
|
|
169
|
+
const tag = readVersionTag(versionFile);
|
|
170
|
+
if (options.debug) console.log('\x1b[32m%s\x1b[0m', `Found version: ${imageName}@${tag}`);
|
|
171
|
+
|
|
172
|
+
const message = args?.changed
|
|
173
|
+
? readCommitMessagesSinceTag(imageName, lastVersion, args, options)
|
|
174
|
+
: '';
|
|
175
|
+
|
|
176
|
+
return processMessage(message, tag, imageName, options, args);
|
|
177
|
+
} catch {
|
|
178
|
+
if (options.debug) console.log('\x1b[33m%s\x1b[0m', 'Could not find last or next version: ');
|
|
179
|
+
return { name: imageName, tag: `${imageName}@0.0.0`, previous: '0.0.0', version: '0.0.1', ...args };
|
|
180
|
+
}
|
|
98
181
|
}
|
|
99
182
|
|
|
100
|
-
export { Version };
|
|
183
|
+
export { Version, getBumpType, processMessage };
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { describe, it, expect } from "@jest/globals";
|
|
2
|
+
import { execFileSync } from "child_process";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
import {
|
|
7
|
+
mkdtempSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
readFileSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from "fs";
|
|
13
|
+
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = path.dirname(__filename);
|
|
16
|
+
const repoRoot = path.resolve(__dirname, "..", "..");
|
|
17
|
+
const cliPath = path.resolve(repoRoot, "monorepo-version", "src", "index.mjs");
|
|
18
|
+
|
|
19
|
+
function setupWorkspace() {
|
|
20
|
+
const root = mkdtempSync(path.join(os.tmpdir(), "monorepo-version-"));
|
|
21
|
+
|
|
22
|
+
mkdirSync(path.join(root, "packages", "example"), { recursive: true });
|
|
23
|
+
mkdirSync(path.join(root, ".cicd"), { recursive: true });
|
|
24
|
+
|
|
25
|
+
writeFileSync(
|
|
26
|
+
path.join(root, "package.json"),
|
|
27
|
+
JSON.stringify(
|
|
28
|
+
{
|
|
29
|
+
name: "tmp-monorepo",
|
|
30
|
+
private: true,
|
|
31
|
+
workspaces: ["packages/*"],
|
|
32
|
+
},
|
|
33
|
+
null,
|
|
34
|
+
2,
|
|
35
|
+
) + "\n",
|
|
36
|
+
"utf8",
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
writeFileSync(
|
|
40
|
+
path.join(root, "packages", "example", "package.json"),
|
|
41
|
+
JSON.stringify(
|
|
42
|
+
{
|
|
43
|
+
name: "@scope/example",
|
|
44
|
+
version: "1.0.0",
|
|
45
|
+
},
|
|
46
|
+
null,
|
|
47
|
+
2,
|
|
48
|
+
) + "\n",
|
|
49
|
+
"utf8",
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
writeFileSync(path.join(root, "packages", "example", "index.js"), "export default 1;\n", "utf8");
|
|
53
|
+
|
|
54
|
+
return root;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function runRelease(root, hashFile) {
|
|
58
|
+
const stdout = execFileSync(
|
|
59
|
+
"node",
|
|
60
|
+
[
|
|
61
|
+
cliPath,
|
|
62
|
+
"--hash",
|
|
63
|
+
"--changed",
|
|
64
|
+
"--children",
|
|
65
|
+
"packages",
|
|
66
|
+
"--prefixPath",
|
|
67
|
+
root,
|
|
68
|
+
"--hashFile",
|
|
69
|
+
hashFile,
|
|
70
|
+
],
|
|
71
|
+
{
|
|
72
|
+
cwd: repoRoot,
|
|
73
|
+
encoding: "utf8",
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const changedMatch = stdout.match(/CHANGED - (\[[^\n]*\])/);
|
|
78
|
+
if (!changedMatch) {
|
|
79
|
+
throw new Error(`Could not find changed list in output:\n${stdout}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return JSON.parse(changedMatch[1]);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
describe("index CLI hash loading", () => {
|
|
86
|
+
it("supports non-manifest hash mode across repeated runs", () => {
|
|
87
|
+
const root = setupWorkspace();
|
|
88
|
+
try {
|
|
89
|
+
const first = runRelease(root, ".cicd/hash.json");
|
|
90
|
+
const second = runRelease(root, ".cicd/hash.json");
|
|
91
|
+
|
|
92
|
+
expect(first).toContain("example");
|
|
93
|
+
expect(second).toEqual([]);
|
|
94
|
+
|
|
95
|
+
const hashJson = JSON.parse(readFileSync(path.join(root, ".cicd", "hash.json"), "utf8"));
|
|
96
|
+
expect(hashJson.version).toBe(2);
|
|
97
|
+
expect(typeof hashJson.hash.example).toBe("string");
|
|
98
|
+
expect(hashJson.hash.example).toBeTruthy();
|
|
99
|
+
} finally {
|
|
100
|
+
rmSync(root, { recursive: true, force: true });
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("redirects manifest hash mode to a sibling hash.json", () => {
|
|
105
|
+
const root = setupWorkspace();
|
|
106
|
+
try {
|
|
107
|
+
const first = runRelease(root, ".cicd/manifest.json");
|
|
108
|
+
const second = runRelease(root, ".cicd/manifest.json");
|
|
109
|
+
|
|
110
|
+
expect(first).toContain("example");
|
|
111
|
+
expect(second).toEqual([]);
|
|
112
|
+
|
|
113
|
+
const hashJson = JSON.parse(readFileSync(path.join(root, ".cicd", "hash.json"), "utf8"));
|
|
114
|
+
expect(hashJson.version).toBe(2);
|
|
115
|
+
expect(typeof hashJson.hash.example).toBe("string");
|
|
116
|
+
expect(hashJson.hash.example).toBeTruthy();
|
|
117
|
+
} finally {
|
|
118
|
+
rmSync(root, { recursive: true, force: true });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("index CLI --exclude", () => {
|
|
124
|
+
function setupWorkspaceWithTwoRoots() {
|
|
125
|
+
const root = mkdtempSync(path.join(os.tmpdir(), "monorepo-version-"));
|
|
126
|
+
|
|
127
|
+
mkdirSync(path.join(root, "packages", "example"), { recursive: true });
|
|
128
|
+
mkdirSync(path.join(root, "other-pkgs", "skip-me"), { recursive: true });
|
|
129
|
+
mkdirSync(path.join(root, ".cicd"), { recursive: true });
|
|
130
|
+
|
|
131
|
+
writeFileSync(
|
|
132
|
+
path.join(root, "package.json"),
|
|
133
|
+
JSON.stringify({ name: "tmp-monorepo", private: true, workspaces: ["packages/*", "other-pkgs/*"] }, null, 2) + "\n",
|
|
134
|
+
"utf8",
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
writeFileSync(
|
|
138
|
+
path.join(root, "packages", "example", "package.json"),
|
|
139
|
+
JSON.stringify({ name: "@scope/example", version: "1.0.0" }, null, 2) + "\n",
|
|
140
|
+
"utf8",
|
|
141
|
+
);
|
|
142
|
+
writeFileSync(
|
|
143
|
+
path.join(root, "other-pkgs", "skip-me", "package.json"),
|
|
144
|
+
JSON.stringify({ name: "@scope/skip-me", version: "1.0.0" }, null, 2) + "\n",
|
|
145
|
+
"utf8",
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
return root;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
it("drops an auto-detected root directory from scanning", () => {
|
|
152
|
+
const root = setupWorkspaceWithTwoRoots();
|
|
153
|
+
try {
|
|
154
|
+
const stdout = execFileSync(
|
|
155
|
+
"node",
|
|
156
|
+
[cliPath, "--hash", "--changed", "--exclude", "other-pkgs", "--prefixPath", root],
|
|
157
|
+
{ cwd: repoRoot, encoding: "utf8" },
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const changedMatch = stdout.match(/CHANGED - (\[[^\n]*\])/);
|
|
161
|
+
const changed = JSON.parse(changedMatch[1]);
|
|
162
|
+
|
|
163
|
+
expect(changed).toContain("example");
|
|
164
|
+
expect(changed).not.toContain("skip-me");
|
|
165
|
+
|
|
166
|
+
const hashJson = JSON.parse(readFileSync(path.join(root, ".cicd", "hash.json"), "utf8"));
|
|
167
|
+
expect(hashJson.hash.example).toBeTruthy();
|
|
168
|
+
expect(hashJson.hash["skip-me"]).toBeUndefined();
|
|
169
|
+
} finally {
|
|
170
|
+
rmSync(root, { recursive: true, force: true });
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe("index CLI --children defaulting", () => {
|
|
176
|
+
it('falls back to scanning "packages" and warns when no workspace config exists and --children is omitted', () => {
|
|
177
|
+
const root = mkdtempSync(path.join(os.tmpdir(), "monorepo-version-"));
|
|
178
|
+
try {
|
|
179
|
+
mkdirSync(path.join(root, "packages", "example"), { recursive: true });
|
|
180
|
+
mkdirSync(path.join(root, ".cicd"), { recursive: true });
|
|
181
|
+
// Deliberately no pnpm-workspace.yaml and no root package.json at all.
|
|
182
|
+
|
|
183
|
+
writeFileSync(
|
|
184
|
+
path.join(root, "packages", "example", "package.json"),
|
|
185
|
+
JSON.stringify({ name: "@scope/example", version: "1.0.0" }, null, 2) + "\n",
|
|
186
|
+
"utf8",
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const stdout = execFileSync(
|
|
190
|
+
"node",
|
|
191
|
+
[cliPath, "--hash", "--changed", "--prefixPath", root],
|
|
192
|
+
{ cwd: repoRoot, encoding: "utf8" },
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
expect(stdout).toContain('defaulting --children to "packages"');
|
|
196
|
+
|
|
197
|
+
const changedMatch = stdout.match(/CHANGED - (\[[^\n]*\])/);
|
|
198
|
+
expect(JSON.parse(changedMatch[1])).toContain("example");
|
|
199
|
+
|
|
200
|
+
const hashJson = JSON.parse(readFileSync(path.join(root, ".cicd", "hash.json"), "utf8"));
|
|
201
|
+
expect(hashJson.hash.example).toBeTruthy();
|
|
202
|
+
} finally {
|
|
203
|
+
rmSync(root, { recursive: true, force: true });
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it, expect } from '@jest/globals';
|
|
2
|
+
import { getBumpType, processMessage } from '../src/version.mjs';
|
|
3
|
+
|
|
4
|
+
describe('version bump parsing', () => {
|
|
5
|
+
const options = { debug: false };
|
|
6
|
+
|
|
7
|
+
it('treats feat as minor', () => {
|
|
8
|
+
const result = processMessage('feat: add endpoint', '1.2.3', 'pkg', options, {});
|
|
9
|
+
expect(result.version).toBe('1.3.0');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('treats refactor as minor', () => {
|
|
13
|
+
const result = processMessage('refactor: simplify auth flow', '1.2.3', 'pkg', options, {});
|
|
14
|
+
expect(result.version).toBe('1.3.0');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('treats BREAKING CHANGES footer as major with precedence', () => {
|
|
18
|
+
const message = 'feat: redesign auth\n\nBREAKING CHANGES: removes legacy token format';
|
|
19
|
+
const result = processMessage(message, '1.2.3', 'pkg', options, {});
|
|
20
|
+
expect(result.version).toBe('2.0.0');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('treats ! conventional commit marker as major', () => {
|
|
24
|
+
const result = processMessage('feat!: remove old API', '1.2.3', 'pkg', options, {});
|
|
25
|
+
expect(result.version).toBe('2.0.0');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('classifies bump types directly', () => {
|
|
29
|
+
expect(getBumpType('feat(parser): support x')).toBe('minor');
|
|
30
|
+
expect(getBumpType('refactor(core): split module')).toBe('minor');
|
|
31
|
+
expect(getBumpType('fix: adjust typo')).toBe('patch');
|
|
32
|
+
expect(getBumpType('feat!: breaking')).toBe('major');
|
|
33
|
+
});
|
|
34
|
+
});
|
package/src/folder-hash.mjs
DELETED
|
@@ -1,425 +0,0 @@
|
|
|
1
|
-
import crypto from 'crypto';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import fs from 'fs';
|
|
4
|
-
import { minimatch } from 'minimatch'
|
|
5
|
-
|
|
6
|
-
const defaultOptions = {
|
|
7
|
-
algo: 'sha1', // see crypto.getHashes() for options
|
|
8
|
-
encoding: 'base64', // 'base64', 'base64url', 'hex' or 'binary'
|
|
9
|
-
files: {
|
|
10
|
-
exclude: [],
|
|
11
|
-
include: [],
|
|
12
|
-
matchBasename: true,
|
|
13
|
-
matchPath: false,
|
|
14
|
-
ignoreBasename: false,
|
|
15
|
-
ignoreRootName: false,
|
|
16
|
-
},
|
|
17
|
-
folders: {
|
|
18
|
-
exclude: [],
|
|
19
|
-
include: [],
|
|
20
|
-
matchBasename: true,
|
|
21
|
-
matchPath: false,
|
|
22
|
-
ignoreBasename: false,
|
|
23
|
-
ignoreRootName: false,
|
|
24
|
-
},
|
|
25
|
-
symbolicLinks: {
|
|
26
|
-
include: true,
|
|
27
|
-
ignoreBasename: false,
|
|
28
|
-
ignoreTargetPath: true,
|
|
29
|
-
ignoreTargetContent: false,
|
|
30
|
-
ignoreTargetContentAfterError: false,
|
|
31
|
-
},
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
const debug = function(txt) {
|
|
35
|
-
return params => {
|
|
36
|
-
console.log(txt, params);
|
|
37
|
-
return params;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Use the environment variable DEBUG to log output, e.g. `set DEBUG=fhash:*`
|
|
42
|
-
const log = {
|
|
43
|
-
match: function() {},
|
|
44
|
-
params: function(params) {return params},
|
|
45
|
-
err: debug('fhash:err'),
|
|
46
|
-
symlink: debug('fhash:symlink'),
|
|
47
|
-
queue: function() {}
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
function prep(fs) {
|
|
51
|
-
let queue = [];
|
|
52
|
-
let queueTimer = undefined;
|
|
53
|
-
|
|
54
|
-
function hashElement(name, dir, options, callback) {
|
|
55
|
-
callback = arguments[arguments.length - 1];
|
|
56
|
-
|
|
57
|
-
return parseParameters(arguments)
|
|
58
|
-
.then(({ basename, dir, options }) => {
|
|
59
|
-
// this is only used for the root level
|
|
60
|
-
options.skipMatching = true;
|
|
61
|
-
return fs.promises
|
|
62
|
-
.lstat(path.join(dir, basename))
|
|
63
|
-
.then(stats => {
|
|
64
|
-
stats.name = basename;
|
|
65
|
-
return stats;
|
|
66
|
-
})
|
|
67
|
-
.then(stats => hashElementPromise(stats, dir, options, true));
|
|
68
|
-
})
|
|
69
|
-
.then(result => {
|
|
70
|
-
if (isFunction(callback)) {
|
|
71
|
-
return callback(undefined, result);
|
|
72
|
-
} else {
|
|
73
|
-
return result;
|
|
74
|
-
}
|
|
75
|
-
})
|
|
76
|
-
.catch(reason => {
|
|
77
|
-
log.err('Fatal error:', reason);
|
|
78
|
-
if (isFunction(callback)) {
|
|
79
|
-
return callback(reason);
|
|
80
|
-
} else {
|
|
81
|
-
throw reason;
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* @param {fs.Stats} stats folder element, can also be of type fs.Dirent
|
|
88
|
-
* @param {string} dirname
|
|
89
|
-
* @param {Options} options
|
|
90
|
-
* @param {boolean} isRootElement
|
|
91
|
-
*/
|
|
92
|
-
function hashElementPromise(stats, dirname, options, isRootElement = false) {
|
|
93
|
-
const name = stats.name;
|
|
94
|
-
let promise = undefined;
|
|
95
|
-
if (stats.isDirectory()) {
|
|
96
|
-
promise = hashFolderPromise(name, dirname, options, isRootElement);
|
|
97
|
-
} else if (stats.isFile()) {
|
|
98
|
-
promise = hashFilePromise(name, dirname, options, isRootElement);
|
|
99
|
-
} else if (stats.isSymbolicLink()) {
|
|
100
|
-
promise = hashSymLinkPromise(name, dirname, options, isRootElement);
|
|
101
|
-
} else {
|
|
102
|
-
log.err('hashElementPromise cannot handle ', stats);
|
|
103
|
-
return Promise.resolve({ name, hash: 'Error: unknown element type' });
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return promise.catch(err => {
|
|
107
|
-
if (err.code && (err.code === 'EMFILE' || err.code === 'ENFILE')) {
|
|
108
|
-
log.queue(`queued ${dirname}/${name} because of ${err.code}`);
|
|
109
|
-
|
|
110
|
-
const promise = new Promise((resolve, reject) => {
|
|
111
|
-
queue.push(() => {
|
|
112
|
-
log.queue(`Will processs queued ${dirname}/${name}`);
|
|
113
|
-
return hashElementPromise(stats, dirname, options, isRootElement)
|
|
114
|
-
.then(ok => resolve(ok))
|
|
115
|
-
.catch(err => reject(err));
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
if (queueTimer === undefined) {
|
|
120
|
-
queueTimer = setTimeout(processQueue, 0);
|
|
121
|
-
}
|
|
122
|
-
return promise;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
throw err;
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function processQueue() {
|
|
130
|
-
queueTimer = undefined;
|
|
131
|
-
const runnables = queue;
|
|
132
|
-
queue = [];
|
|
133
|
-
runnables.forEach(run => run());
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
async function hashFolderPromise(name, dir, options, isRootElement = false) {
|
|
137
|
-
const folderPath = path.join(dir, name);
|
|
138
|
-
let ignoreBasenameOnce = options.ignoreBasenameOnce;
|
|
139
|
-
delete options.ignoreBasenameOnce;
|
|
140
|
-
|
|
141
|
-
if (options.skipMatching) {
|
|
142
|
-
// this is currently only used for the root folder
|
|
143
|
-
log.match(`skipped '${folderPath}'`);
|
|
144
|
-
delete options.skipMatching;
|
|
145
|
-
} else if (ignore(name, folderPath, options.folders)) {
|
|
146
|
-
return undefined;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const files = await fs.promises.readdir(folderPath, { withFileTypes: true });
|
|
150
|
-
const children = await Promise.all(
|
|
151
|
-
files
|
|
152
|
-
.sort((a, b) => a.name.localeCompare(b.name))
|
|
153
|
-
.map(child => hashElementPromise(child, folderPath, options)),
|
|
154
|
-
);
|
|
155
|
-
|
|
156
|
-
if (ignoreBasenameOnce) options.ignoreBasenameOnce = true;
|
|
157
|
-
const hash = new HashedFolder(name, children.filter(notUndefined), options, isRootElement);
|
|
158
|
-
return hash;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function hashFilePromise(name, dir, options, isRootElement = false) {
|
|
162
|
-
const filePath = path.join(dir, name);
|
|
163
|
-
|
|
164
|
-
if (options.skipMatching) {
|
|
165
|
-
// this is currently only used for the root folder
|
|
166
|
-
log.match(`skipped '${filePath}'`);
|
|
167
|
-
delete options.skipMatching;
|
|
168
|
-
} else if (ignore(name, filePath, options.files)) {
|
|
169
|
-
return Promise.resolve(undefined);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return new Promise((resolve, reject) => {
|
|
173
|
-
try {
|
|
174
|
-
const hash = crypto.createHash(options.algo);
|
|
175
|
-
if (
|
|
176
|
-
options.files.ignoreBasename ||
|
|
177
|
-
options.ignoreBasenameOnce ||
|
|
178
|
-
(isRootElement && options.files.ignoreRootName)
|
|
179
|
-
) {
|
|
180
|
-
delete options.ignoreBasenameOnce;
|
|
181
|
-
log.match(`omitted name of ${filePath} from hash`);
|
|
182
|
-
} else {
|
|
183
|
-
hash.update(name);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const f = fs.createReadStream(filePath);
|
|
187
|
-
f.on('error', err => {
|
|
188
|
-
reject(err);
|
|
189
|
-
});
|
|
190
|
-
f.pipe(hash, { end: false });
|
|
191
|
-
|
|
192
|
-
f.on('end', () => {
|
|
193
|
-
const hashedFile = new HashedFile(name, hash, options.encoding);
|
|
194
|
-
return resolve(hashedFile);
|
|
195
|
-
});
|
|
196
|
-
} catch (ex) {
|
|
197
|
-
return reject(ex);
|
|
198
|
-
}
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
async function hashSymLinkPromise(name, dir, options, isRootElement = false) {
|
|
203
|
-
const target = await fs.promises.readlink(path.join(dir, name));
|
|
204
|
-
log.symlink(`handling symbolic link ${name} -> ${target}`);
|
|
205
|
-
if (options.symbolicLinks.include) {
|
|
206
|
-
if (options.symbolicLinks.ignoreTargetContent) {
|
|
207
|
-
return symLinkIgnoreTargetContent(name, target, options, isRootElement);
|
|
208
|
-
} else {
|
|
209
|
-
return symLinkResolve(name, dir, target, options, isRootElement);
|
|
210
|
-
}
|
|
211
|
-
} else {
|
|
212
|
-
log.symlink('skipping symbolic link');
|
|
213
|
-
return Promise.resolve(undefined);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function symLinkIgnoreTargetContent(name, target, options, isRootElement) {
|
|
218
|
-
delete options.skipMatching; // only used for the root level
|
|
219
|
-
log.symlink('ignoring symbolic link target content');
|
|
220
|
-
const hash = crypto.createHash(options.algo);
|
|
221
|
-
if (!options.symbolicLinks.ignoreBasename && !(isRootElement && options.files.ignoreRootName)) {
|
|
222
|
-
log.symlink('hash basename');
|
|
223
|
-
hash.update(name);
|
|
224
|
-
}
|
|
225
|
-
if (!options.symbolicLinks.ignoreTargetPath) {
|
|
226
|
-
log.symlink('hash targetpath');
|
|
227
|
-
hash.update(target);
|
|
228
|
-
}
|
|
229
|
-
return Promise.resolve(new HashedFile(name, hash, options.encoding));
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
async function symLinkResolve(name, dir, target, options, isRootElement) {
|
|
233
|
-
delete options.skipMatching; // only used for the root level
|
|
234
|
-
if (options.symbolicLinks.ignoreBasename) {
|
|
235
|
-
options.ignoreBasenameOnce = true;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
try {
|
|
239
|
-
const stats = await fs.promises.stat(path.join(dir, name));
|
|
240
|
-
stats.name = name;
|
|
241
|
-
const temp = await hashElementPromise(stats, dir, options, isRootElement);
|
|
242
|
-
|
|
243
|
-
if (!options.symbolicLinks.ignoreTargetPath) {
|
|
244
|
-
const hash = crypto.createHash(options.algo);
|
|
245
|
-
hash.update(temp.hash);
|
|
246
|
-
log.symlink('hash targetpath');
|
|
247
|
-
hash.update(target);
|
|
248
|
-
temp.hash = hash.digest(options.encoding);
|
|
249
|
-
}
|
|
250
|
-
return temp;
|
|
251
|
-
} catch (err) {
|
|
252
|
-
if (options.symbolicLinks.ignoreTargetContentAfterError) {
|
|
253
|
-
log.symlink(`Ignoring error "${err.code}" when hashing symbolic link ${name}`, err);
|
|
254
|
-
const hash = crypto.createHash(options.algo);
|
|
255
|
-
if (
|
|
256
|
-
!options.symbolicLinks.ignoreBasename &&
|
|
257
|
-
!(isRootElement && options.files.ignoreRootName)
|
|
258
|
-
) {
|
|
259
|
-
hash.update(name);
|
|
260
|
-
}
|
|
261
|
-
if (!options.symbolicLinks.ignoreTargetPath) {
|
|
262
|
-
hash.update(target);
|
|
263
|
-
}
|
|
264
|
-
return new HashedFile(name, hash, options.encoding);
|
|
265
|
-
} else {
|
|
266
|
-
log.symlink(`Error "${err.code}": When hashing symbolic link ${name}`, err);
|
|
267
|
-
throw err;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function ignore(name, path, rules) {
|
|
273
|
-
if (rules.exclude) {
|
|
274
|
-
if (rules.matchBasename && rules.exclude(name)) {
|
|
275
|
-
log.match(`exclude basename '${name}'`);
|
|
276
|
-
return true;
|
|
277
|
-
} else if (rules.matchPath && rules.exclude(path)) {
|
|
278
|
-
log.match(`exclude path '${path}'`);
|
|
279
|
-
return true;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
if (rules.include) {
|
|
283
|
-
if (rules.matchBasename && rules.include(name)) {
|
|
284
|
-
log.match(`include basename '${name}'`);
|
|
285
|
-
return false;
|
|
286
|
-
} else if (rules.matchPath && rules.include(path)) {
|
|
287
|
-
log.match(`include path '${path}'`);
|
|
288
|
-
return false;
|
|
289
|
-
} else {
|
|
290
|
-
log.match(`include rule failed for path '${path}'`);
|
|
291
|
-
return true;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
log.match(`Will not ignore unmatched '${path}'`);
|
|
296
|
-
return false;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
return hashElement;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function parseParameters(args) {
|
|
303
|
-
let basename = args[0],
|
|
304
|
-
dir = args[1],
|
|
305
|
-
options_ = args[2];
|
|
306
|
-
|
|
307
|
-
if (!isString(basename)) {
|
|
308
|
-
return Promise.reject(new TypeError('First argument must be a string'));
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
if (!isString(dir)) {
|
|
312
|
-
dir = path.dirname(basename);
|
|
313
|
-
basename = path.basename(basename);
|
|
314
|
-
options_ = args[1];
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// parse options (fallback default options)
|
|
318
|
-
if (!isObject(options_)) options_ = {};
|
|
319
|
-
const options = {
|
|
320
|
-
algo: options_.algo || defaultOptions.algo,
|
|
321
|
-
encoding: options_.encoding || defaultOptions.encoding,
|
|
322
|
-
files: Object.assign({}, defaultOptions.files, options_.files),
|
|
323
|
-
folders: Object.assign({}, defaultOptions.folders, options_.folders),
|
|
324
|
-
match: Object.assign({}, defaultOptions.match, options_.match),
|
|
325
|
-
symbolicLinks: Object.assign({}, defaultOptions.symbolicLinks, options_.symbolicLinks),
|
|
326
|
-
};
|
|
327
|
-
|
|
328
|
-
// transform match globs to Regex
|
|
329
|
-
options.files.exclude = reduceGlobPatterns(options.files.exclude);
|
|
330
|
-
options.files.include = reduceGlobPatterns(options.files.include);
|
|
331
|
-
options.folders.exclude = reduceGlobPatterns(options.folders.exclude);
|
|
332
|
-
options.folders.include = reduceGlobPatterns(options.folders.include);
|
|
333
|
-
|
|
334
|
-
return Promise.resolve(log.params({ basename, dir, options }));
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
const HashedFolder = function HashedFolder(name, children, options, isRootElement = false) {
|
|
338
|
-
this.name = name;
|
|
339
|
-
this.children = children;
|
|
340
|
-
|
|
341
|
-
const hash = crypto.createHash(options.algo);
|
|
342
|
-
if (
|
|
343
|
-
options.folders.ignoreBasename ||
|
|
344
|
-
options.ignoreBasenameOnce ||
|
|
345
|
-
(isRootElement && options.folders.ignoreRootName)
|
|
346
|
-
) {
|
|
347
|
-
delete options.ignoreBasenameOnce;
|
|
348
|
-
log.match(`omitted name of folder ${name} from hash`);
|
|
349
|
-
} else {
|
|
350
|
-
hash.update(name);
|
|
351
|
-
}
|
|
352
|
-
children.forEach(child => {
|
|
353
|
-
if (child.hash) {
|
|
354
|
-
hash.update(child.hash);
|
|
355
|
-
}
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
this.hash = hash.digest(options.encoding);
|
|
359
|
-
};
|
|
360
|
-
|
|
361
|
-
HashedFolder.prototype.toString = function (padding = '') {
|
|
362
|
-
const first = `${padding}{ name: '${this.name}', hash: '${this.hash}',\n`;
|
|
363
|
-
padding += ' ';
|
|
364
|
-
|
|
365
|
-
return `${first}${padding}children: ${this.childrenToString(padding)}}`;
|
|
366
|
-
};
|
|
367
|
-
|
|
368
|
-
HashedFolder.prototype.childrenToString = function (padding = '') {
|
|
369
|
-
if (this.children.length === 0) {
|
|
370
|
-
return '[]';
|
|
371
|
-
} else {
|
|
372
|
-
const nextPadding = padding + ' ';
|
|
373
|
-
const children = this.children.map(child => child.toString(nextPadding)).join('\n');
|
|
374
|
-
return `[\n${children}\n${padding}]`;
|
|
375
|
-
}
|
|
376
|
-
};
|
|
377
|
-
|
|
378
|
-
const HashedFile = function HashedFile(name, hash, encoding) {
|
|
379
|
-
this.name = name;
|
|
380
|
-
this.hash = hash.digest(encoding);
|
|
381
|
-
};
|
|
382
|
-
|
|
383
|
-
HashedFile.prototype.toString = function (padding = '') {
|
|
384
|
-
return padding + "{ name: '" + this.name + "', hash: '" + this.hash + "' }";
|
|
385
|
-
};
|
|
386
|
-
|
|
387
|
-
function isFunction(any) {
|
|
388
|
-
return typeof any === 'function';
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
function isString(str) {
|
|
392
|
-
return typeof str === 'string' || str instanceof String;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
function isObject(obj) {
|
|
396
|
-
return obj !== null && typeof obj === 'object';
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
function notUndefined(obj) {
|
|
400
|
-
return typeof obj !== 'undefined';
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
function reduceGlobPatterns(globs) {
|
|
404
|
-
if (isFunction(globs)) {
|
|
405
|
-
return globs;
|
|
406
|
-
} else if (!globs || !Array.isArray(globs) || globs.length === 0) {
|
|
407
|
-
return undefined;
|
|
408
|
-
} else {
|
|
409
|
-
// combine globs into one single RegEx
|
|
410
|
-
const regex = new RegExp(
|
|
411
|
-
globs
|
|
412
|
-
.reduce((acc, exclude) => {
|
|
413
|
-
return acc + '|' + minimatch.makeRe(exclude).source;
|
|
414
|
-
}, '')
|
|
415
|
-
.substr(1),
|
|
416
|
-
);
|
|
417
|
-
return param => regex.test(param);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
const hashElement = prep(fs);
|
|
422
|
-
export {
|
|
423
|
-
defaultOptions as defaults,
|
|
424
|
-
hashElement
|
|
425
|
-
};
|