enablement-build-monorepo-version 1.0.27 → 2.0.2
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/LICENSE +1 -1
- package/README.md +225 -5
- package/package.json +17 -6
- package/src/index.mjs +340 -0
- package/src/lib.mjs +406 -0
- package/{version.mjs → src/version.mjs} +13 -2
- package/tests/dependencies.json +2198 -0
- package/tests/lib.test.mjs +528 -0
- package/tests/package.json +25 -0
- package/tests/pyproject.toml +15 -0
- package/tests/version.json +5 -0
- package/index.mjs +0 -318
- /package/{folder-hash.mjs → src/folder-hash.mjs} +0 -0
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,5 +1,225 @@
|
|
|
1
|
-
# enablement-build-monorepo-version
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
# enablement-build-monorepo-version
|
|
2
|
+
|
|
3
|
+
Detects which packages in a monorepo have changed by hashing their source folders, computes the next semantic version for each changed package, and emits Azure DevOps pipeline variables. Dependency propagation is recursive — changing a leaf package automatically marks all transitive dependents as changed too.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx enablement-build-monorepo-version@latest [flags]
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## How it works
|
|
14
|
+
|
|
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 a saved state file (`.cicd/hash.json` by default).
|
|
17
|
+
3. **Propagate** — Any package whose hash changed is marked `CHANGED`. All transitive dependents (read from `dependencies.json`) are also marked `CHANGED` via a breadth-first traversal.
|
|
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
|
+
5. **Output** — Azure DevOps `##vso[task.setvariable]` lines are written to stdout for consumption by downstream pipeline steps.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## CLI flags
|
|
24
|
+
|
|
25
|
+
| Flag | Default | Description |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| `--hash` | off | Compute and **save** current hashes to the hash state file. Must be run before `--changed` / `--version` to establish a baseline. |
|
|
28
|
+
| `--changed` | off | Print the list of changed packages and emit a `changed` pipeline variable. |
|
|
29
|
+
| `--version` | off | Emit a pipeline variable per changed package containing its next version. |
|
|
30
|
+
| `--save` | off | Write the bumped version back into each changed package's version manifest (`package.json`, `version.json`, or `pyproject.toml`). Requires `--version`. |
|
|
31
|
+
| `--tag` | off | Create a git tag (`<short-name>/<version>`) for each changed package, where `short-name` is the npm package name with its scope prefix removed (e.g. `@scope/lib-foo` → `lib-foo/1.2.3`). |
|
|
32
|
+
| `--retag` | off | Convert existing tags that use the old folder-name format to the new short-package-name format. Non-destructive: old tags are left in place. |
|
|
33
|
+
| `--init` | off | Inject `release:*` scripts into the root `package.json` of the target project. Adds scripts that are missing; strips `--children` from any that already exist. Combine with `--try` to preview changes. |
|
|
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
|
+
| `--debug` | off | Verbose logging of hashes, comparisons, and version resolution. |
|
|
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
|
+
| `--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
|
+
| `--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/hash.json` if a `.github/` directory exists at the root, otherwise `.cicd/hash.json`. |
|
|
40
|
+
| `--dependencies <path>` | `dependencies.json` | Path to the NX-style dependency graph used for transitive change propagation. |
|
|
41
|
+
| `--hashExcludeFolders <list>` | see below | Comma-separated folder names to skip when hashing. |
|
|
42
|
+
| `--hashExcludeFiles <list>` | see below | Comma-separated file names to skip when hashing. |
|
|
43
|
+
| `--hashFiles <list>` | — | Comma-separated individual file paths to include in the hash state (outside of `children` folders). |
|
|
44
|
+
|
|
45
|
+
**Default excluded folders:** `node_modules`, `coverage`, `dist`, `bin`, `obj`, `__pycache__`, `.vs`, `.nx`, `.vscode`, `.idea`, `.git`, `.github`, `.azuredevops`, `.release`
|
|
46
|
+
|
|
47
|
+
**Default excluded files:** `.npmrc`, `CHANGELOG.md`, `README.md`
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Supported version manifests
|
|
52
|
+
|
|
53
|
+
Each package directory is scanned for a version manifest in this priority order:
|
|
54
|
+
|
|
55
|
+
| File | Format | Version field |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| `package.json` | JSON | `"version": "1.2.3"` |
|
|
58
|
+
| `version.json` | JSON (same shape as `package.json`) | `"version": "1.2.3"` |
|
|
59
|
+
| `pyproject.toml` | TOML | `version = "1.2.3"` under `[project]` or `[tool.poetry]` |
|
|
60
|
+
|
|
61
|
+
The first file found is used. If none exists the package is treated as version `0.0.0`.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Version bump rules
|
|
66
|
+
|
|
67
|
+
Versions follow semver and are bumped based on the **current version** in the package's version manifest:
|
|
68
|
+
|
|
69
|
+
| Trigger | Bump | Example |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| `fix:` prefix in any recent commit message | Patch (`0.0.x`) | `1.2.3` → `1.2.4` |
|
|
72
|
+
| `feat:` prefix in any recent commit message | Minor (`0.x.0`) | `1.2.3` → `1.3.0` |
|
|
73
|
+
| `BREAKING` anywhere in any recent commit message | Major (`x.0.0`) | `1.2.3` → `2.0.0` |
|
|
74
|
+
|
|
75
|
+
> Currently the version bump always applies a patch increment — conventional-commit scanning via `git log` is stubbed. The bump logic is in `src/version.mjs`.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Pipeline variable output
|
|
80
|
+
|
|
81
|
+
The output format is auto-detected from the presence of a `.github/` directory at the root. It can also be forced with `--platform ado` or `--platform github`.
|
|
82
|
+
|
|
83
|
+
### Azure DevOps (default when no `.github/` directory)
|
|
84
|
+
|
|
85
|
+
Variables are written to stdout using the `##vso` task command:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
##vso[task.setvariable variable=<safeName>;isoutput=true;]<version>
|
|
89
|
+
##vso[task.setvariable variable=changed;isoutput=true]["pkg-a","pkg-b"]
|
|
90
|
+
##vso[task.setvariable variable=components;isoutput=true]true
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### GitHub Actions (when `.github/` directory is present)
|
|
94
|
+
|
|
95
|
+
Variables are written to the `$GITHUB_OUTPUT` file (falls back to stdout if the env var is not set):
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
safeName=<version>
|
|
99
|
+
changed=["pkg-a","pkg-b"]
|
|
100
|
+
components=true
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Consume them in a subsequent step with `${{ steps.<step-id>.outputs.<name> }}`.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
### Variable reference
|
|
108
|
+
|
|
109
|
+
| Variable | Value |
|
|
110
|
+
|---|---|
|
|
111
|
+
| `<safeName>` (per changed package) | Next version for changed packages; previous version for unchanged ones. `safeName` is the folder name with `-`, `_`, and `.` removed. |
|
|
112
|
+
| `changed` | JSON array of changed package folder names. |
|
|
113
|
+
| `<folderName>` | `true` for each top-level folder containing at least one changed package. |
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## `dependencies.json` format
|
|
118
|
+
|
|
119
|
+
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
|
+
|
|
121
|
+
```json
|
|
122
|
+
{
|
|
123
|
+
"graph": {
|
|
124
|
+
"dependencies": {
|
|
125
|
+
"@scope/pkg-a": [
|
|
126
|
+
{ "source": "@scope/pkg-a", "target": "@scope/shared-lib", "type": "static" }
|
|
127
|
+
]
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Generate it from an NX workspace with:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
nx graph --file=dependencies.json
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
If the file is missing or the `--dependencies` path doesn't exist, change propagation is skipped and only directly-changed packages are reported (a warning is printed).
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Typical pipeline usage
|
|
144
|
+
|
|
145
|
+
```yaml
|
|
146
|
+
# Step 1 — establish baseline hashes (run once, commit the result)
|
|
147
|
+
# Workspace directories are auto-detected from pnpm-workspace.yaml / package.json workspaces
|
|
148
|
+
- script: npx enablement-build-monorepo-version --hash
|
|
149
|
+
|
|
150
|
+
# Step 2 — detect changes and emit version variables for downstream steps
|
|
151
|
+
- script: npx enablement-build-monorepo-version --changed --version
|
|
152
|
+
name: versions
|
|
153
|
+
|
|
154
|
+
# Step 3 — consume a variable from step 2
|
|
155
|
+
- script: echo "$(versions.mypackagename)"
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
To bump and persist the new versions back to `package.json`:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
npx enablement-build-monorepo-version --changed --version --save
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
To override the auto-detected directories, pass `--children` explicitly:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
npx enablement-build-monorepo-version --changed --version --children components,saas
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Bootstrapping a new repo
|
|
173
|
+
|
|
174
|
+
Run `--init` once from the root of any monorepo to inject the standard `release:*` scripts into its `package.json`:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
npx enablement-build-monorepo-version --init
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Preview what would change without writing anything:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
npx enablement-build-monorepo-version --init --try
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The following scripts are injected (skipped if already present; `--children` stripped if already there):
|
|
187
|
+
|
|
188
|
+
| Script | Command |
|
|
189
|
+
|---|---|
|
|
190
|
+
| `release:try` | dry-run of the full release flow |
|
|
191
|
+
| `release:changed` | list changed packages |
|
|
192
|
+
| `release:version` | bump and save versions |
|
|
193
|
+
| `release:finalize` | write hashes |
|
|
194
|
+
| `release:retag` | migrate tags to package-name format |
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Development
|
|
199
|
+
|
|
200
|
+
```
|
|
201
|
+
src/
|
|
202
|
+
├── index.mjs CLI entry point and main() orchestration
|
|
203
|
+
├── lib.mjs Core logic (exported for testing)
|
|
204
|
+
├── version.mjs Semver bump logic
|
|
205
|
+
└── folder-hash.mjs Recursive folder hashing
|
|
206
|
+
|
|
207
|
+
tests/
|
|
208
|
+
├── lib.test.mjs Unit tests for lib.mjs
|
|
209
|
+
├── dependencies.json Sample NX dependency graph fixture
|
|
210
|
+
├── package.json Sample package.json fixture
|
|
211
|
+
├── version.json Sample version.json fixture
|
|
212
|
+
└── pyproject.toml Sample pyproject.toml fixture
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Run the test suite:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
pnpm test
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Watch mode:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
pnpm test:watch
|
|
225
|
+
```
|
package/package.json
CHANGED
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "enablement-build-monorepo-version",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"description": "This detects changes in the children packages of a monorepo.",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"module": "./index.mjs",
|
|
7
|
-
"bin": "./index.mjs",
|
|
6
|
+
"module": "./src/index.mjs",
|
|
7
|
+
"bin": "./src/index.mjs",
|
|
8
8
|
"license": "MIT",
|
|
9
|
-
"author": "
|
|
10
|
-
"
|
|
11
|
-
"
|
|
9
|
+
"author": "Contributors",
|
|
10
|
+
"jest": {
|
|
11
|
+
"testEnvironment": "node",
|
|
12
|
+
"testMatch": [
|
|
13
|
+
"**/*.test.mjs"
|
|
14
|
+
]
|
|
12
15
|
},
|
|
13
16
|
"dependencies": {
|
|
14
17
|
"minimatch": "~10.1.1"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"jest": "^29.0.0"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"start": "node src/index.mjs",
|
|
24
|
+
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
|
25
|
+
"test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch"
|
|
15
26
|
}
|
|
16
27
|
}
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync, lstatSync } from "fs";
|
|
4
|
+
import { exec } from "child_process";
|
|
5
|
+
|
|
6
|
+
import { hashElement } from "./folder-hash.mjs";
|
|
7
|
+
import { dependencyMap, compare, getCurrentVersion, updateVersion, retagToPackageNames, detectOldFormatTags, resolveWorkspaceChildren, applyInitScripts, RELEASE_SCRIPTS, emitVariable } from "./lib.mjs";
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async function main(options) {
|
|
11
|
+
return new Promise((mainResolve, mainreject) => {
|
|
12
|
+
const hashOptions = { encoding: 'hex', folders: { exclude: options.hashExcludeFolders }, files: { exclude: options.hashExcludeFiles } };
|
|
13
|
+
const current = {};
|
|
14
|
+
const changeList = [];
|
|
15
|
+
const scanlist = [];
|
|
16
|
+
const changedFolders = [];
|
|
17
|
+
|
|
18
|
+
if (!options.children) {
|
|
19
|
+
const detected = resolveWorkspaceChildren(options.prefixPath);
|
|
20
|
+
if (detected) {
|
|
21
|
+
if (options.debug) console.log(`Auto-detected workspace children: ${detected}`);
|
|
22
|
+
options.children = detected;
|
|
23
|
+
} else {
|
|
24
|
+
options.children = 'packages';
|
|
25
|
+
console.log('\x1b[33m%s\x1b[0m', 'Warning: no workspace config found, defaulting --children to "packages"');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (options.try) {
|
|
30
|
+
console.log('\x1b[36m%s\x1b[0m', '[try] Dry-run mode — no changes will be made\n');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const changeConfig = path.join(options.prefixPath, options.hashFile);
|
|
34
|
+
let previous = {};
|
|
35
|
+
|
|
36
|
+
// make sure hash config file exists
|
|
37
|
+
if (!existsSync(changeConfig)) {
|
|
38
|
+
if (options.try) {
|
|
39
|
+
console.log('\x1b[36m%s\x1b[0m', `[try] Hash file ${changeConfig} not found — treating previous state as empty`);
|
|
40
|
+
} else {
|
|
41
|
+
writeFileSync(changeConfig, "{}", "utf8");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let dependencies = {};
|
|
46
|
+
if (options.dependencies) {
|
|
47
|
+
if (existsSync(options.dependencies)) {
|
|
48
|
+
dependencies = dependencyMap(options.dependencies);
|
|
49
|
+
if (options.debug) console.log('Loaded dependencies\n', JSON.stringify(dependencies));
|
|
50
|
+
} else {
|
|
51
|
+
console.log('\x1b[33m%s\x1b[0m', `Could not load dependency file ${options.dependencies}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// load previous hashes
|
|
56
|
+
const data = existsSync(changeConfig) ? readFileSync(changeConfig, "utf8") : "{}";
|
|
57
|
+
previous = JSON.parse(data);
|
|
58
|
+
if (options.debug) console.log("PREVIOUS", JSON.stringify(previous));
|
|
59
|
+
|
|
60
|
+
options.hashFiles.forEach(file => {
|
|
61
|
+
scanlist.push(new Promise((resolve, reject) => {
|
|
62
|
+
const absPath = path.resolve(file);
|
|
63
|
+
const directoryPath = path.dirname(absPath);
|
|
64
|
+
const filename = path.basename(absPath);
|
|
65
|
+
const foldername = path.basename(directoryPath);
|
|
66
|
+
hashElement(filename, directoryPath, options).then(async hashedFile => {
|
|
67
|
+
if (options.debug) console.log("HASH", JSON.stringify(hashedFile));
|
|
68
|
+
if (options.hash) {
|
|
69
|
+
current[foldername + "." + hashedFile.name] = { hash: hashedFile.hash };
|
|
70
|
+
}
|
|
71
|
+
resolve("OK");
|
|
72
|
+
})
|
|
73
|
+
.catch(error => {
|
|
74
|
+
return console.error('hashing failed:', error);
|
|
75
|
+
});
|
|
76
|
+
}));
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
let children = options.children.replace(/\s/g, ",");
|
|
80
|
+
children.split(',').forEach((packageFolder) => {
|
|
81
|
+
scanlist.push(new Promise((resolve, reject) => {
|
|
82
|
+
hashElement(path.join(options.prefixPath, packageFolder), hashOptions).then(async hash => {
|
|
83
|
+
const children = hash.children;
|
|
84
|
+
if (options.debug) console.log("HASH", JSON.stringify(children));
|
|
85
|
+
|
|
86
|
+
for (let i = 0; i < children.length; i++) {
|
|
87
|
+
let name = children[i].name;
|
|
88
|
+
if (name.substring(0, 1) === "_" || name === "version") continue;
|
|
89
|
+
if (lstatSync(path.join(options.prefixPath, packageFolder, name)).isFile()) continue;
|
|
90
|
+
if (!existsSync(path.join(options.prefixPath, packageFolder, name))) continue;
|
|
91
|
+
|
|
92
|
+
delete children[i].children;
|
|
93
|
+
let v = getCurrentVersion(options.prefixPath, packageFolder, name);
|
|
94
|
+
current[name] = { hash: children[i].hash, ...v };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (options.changed || options.version || options.tag) {
|
|
98
|
+
let results = await compare(packageFolder, previous, current, dependencies, options);
|
|
99
|
+
if (options.debug) console.log("COMPARE", JSON.stringify(results));
|
|
100
|
+
|
|
101
|
+
for (let i = 0; i < results.length; i++) {
|
|
102
|
+
if (results[i].value.changed) {
|
|
103
|
+
changeList.push(results[i].value.name);
|
|
104
|
+
if (changedFolders.indexOf(results[i].value.packageFolder) < 0) changedFolders.push(results[i].value.packageFolder);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
for (let i = 0; i < results.length; i++) {
|
|
109
|
+
if (results[i].value.packageFolder !== packageFolder) continue;
|
|
110
|
+
if (options.version) {
|
|
111
|
+
if (options.debug) console.log(results[i]);
|
|
112
|
+
let safeName = results[i].value.name;
|
|
113
|
+
safeName = safeName.replace(/-/g, '').replace(/_/g, '').replace(/\./g, '');
|
|
114
|
+
if (results[i].value.changed) {
|
|
115
|
+
emitVariable(safeName, results[i].value.version, options.platform);
|
|
116
|
+
if (options.saveVersion) {
|
|
117
|
+
scanlist.push(updateVersion(packageFolder, results[i].value.name, results[i].value.version, options));
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
emitVariable(safeName, results[i].value.previous, options.platform);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (options.tag) {
|
|
124
|
+
scanlist.push(new Promise((resolve, reject) => {
|
|
125
|
+
let v = results[i].value.version;
|
|
126
|
+
if (!v) v = getCurrentVersion(options.prefixPath, packageFolder, results[i].value.name)?.version;
|
|
127
|
+
const shortName = (results[i].value.fullName || results[i].value.name).replace(/^@[^/]+\//, '');
|
|
128
|
+
let rev = shortName + '@' + v;
|
|
129
|
+
let tagPath = shortName + '/' + v;
|
|
130
|
+
if (options.try) {
|
|
131
|
+
console.log('\x1b[36m%s\x1b[0m', `[try] Would create tag ${tagPath}`);
|
|
132
|
+
resolve(rev);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
exec(`git describe --tags "${tagPath}"`, (err) => {
|
|
136
|
+
if (err) {
|
|
137
|
+
exec(`git tag "${tagPath}" -m "${rev}"`, (err2) => {
|
|
138
|
+
if (err2) { reject(err2); return; }
|
|
139
|
+
resolve(rev);
|
|
140
|
+
});
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// save current hashes
|
|
150
|
+
if (options.hash) {
|
|
151
|
+
let names = Object.keys(current);
|
|
152
|
+
for (let i = 0; i < names.length; i++) {
|
|
153
|
+
if (current[names[i]].packageFolder !== packageFolder) continue;
|
|
154
|
+
let v = getCurrentVersion(options.prefixPath, packageFolder, names[i]);
|
|
155
|
+
current[names[i]].version = v.version;
|
|
156
|
+
current[names[i]].fullName = v.fullName;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
resolve("OK");
|
|
160
|
+
})
|
|
161
|
+
.catch(error => {
|
|
162
|
+
return console.error('hashing failed:', error);
|
|
163
|
+
});
|
|
164
|
+
}));
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
Promise.all(scanlist).then(() => {
|
|
168
|
+
if (options.changed) {
|
|
169
|
+
console.log(`CHANGED - ${JSON.stringify(changeList)}`);
|
|
170
|
+
emitVariable('changed', JSON.stringify(changeList), options.platform);
|
|
171
|
+
|
|
172
|
+
if (changedFolders.length > 0) {
|
|
173
|
+
if (options.debug) console.log(`FOLDERS CHANGED - ${JSON.stringify(changedFolders)}`);
|
|
174
|
+
changedFolders.map((packageFolder) => {
|
|
175
|
+
packageFolder = packageFolder.replace(/\//g, '_');
|
|
176
|
+
emitVariable(packageFolder, 'true', options.platform);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// write current hashes
|
|
182
|
+
if (options.hash) {
|
|
183
|
+
let result = Object.keys(current).sort().reduce(
|
|
184
|
+
(obj, key) => { obj[key] = current[key]; return obj; },
|
|
185
|
+
{}
|
|
186
|
+
);
|
|
187
|
+
if (options.try) {
|
|
188
|
+
console.log('\x1b[36m%s\x1b[0m', `[try] Would write updated hashes to ${changeConfig}`);
|
|
189
|
+
} else {
|
|
190
|
+
writeFileSync(changeConfig, JSON.stringify(result, null, 2), "utf8");
|
|
191
|
+
console.log('folder hashes written successfully');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const finish = () => {
|
|
196
|
+
if (options.retag) {
|
|
197
|
+
if (options.try) {
|
|
198
|
+
detectOldFormatTags(options).then((offending) => {
|
|
199
|
+
if (offending.length === 0) {
|
|
200
|
+
console.log('\x1b[36m%s\x1b[0m', '[try] No tags found in old format — nothing to retag');
|
|
201
|
+
} else {
|
|
202
|
+
console.log('\x1b[36m%s\x1b[0m', `[try] Would retag ${offending.length} tag(s):`);
|
|
203
|
+
offending.forEach(({ old, suggested }) =>
|
|
204
|
+
console.log('\x1b[36m%s\x1b[0m', `[try] ${old} → ${suggested}`)
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
mainResolve("DONE");
|
|
208
|
+
}, mainreject);
|
|
209
|
+
} else {
|
|
210
|
+
retagToPackageNames(options).then(() => mainResolve("DONE"), mainreject);
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
mainResolve("DONE");
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
if (options.tag) {
|
|
218
|
+
detectOldFormatTags(options).then((offending) => {
|
|
219
|
+
if (offending.length > 0) {
|
|
220
|
+
console.log('\x1b[33m%s\x1b[0m', `\nWarning: ${offending.length} tag(s) use the old folder-name format and will not match new tags:`);
|
|
221
|
+
offending.forEach(({ old, suggested }) =>
|
|
222
|
+
console.log('\x1b[33m%s\x1b[0m', ` ${old} → ${suggested}`)
|
|
223
|
+
);
|
|
224
|
+
console.log('\x1b[33m%s\x1b[0m', 'Run with --retag to migrate these to the new package-name format.\n');
|
|
225
|
+
}
|
|
226
|
+
finish();
|
|
227
|
+
}, mainreject);
|
|
228
|
+
} else {
|
|
229
|
+
finish();
|
|
230
|
+
}
|
|
231
|
+
}, mainreject);
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
let options = {
|
|
236
|
+
saveVersion: false,
|
|
237
|
+
changed: false,
|
|
238
|
+
version: false,
|
|
239
|
+
hash: false,
|
|
240
|
+
tag: false,
|
|
241
|
+
retag: false,
|
|
242
|
+
init: false,
|
|
243
|
+
try: false,
|
|
244
|
+
commit: false,
|
|
245
|
+
children: null,
|
|
246
|
+
platform: null,
|
|
247
|
+
hashFiles: [],
|
|
248
|
+
prefixPath: './',
|
|
249
|
+
debug: false,
|
|
250
|
+
hashFile: null,
|
|
251
|
+
hashExcludeFolders: ['node_modules', 'coverage', 'dist', 'bin', 'obj', '__pycache__', '.vs', '.nx', '.vscode', '.idea', '.git', '.github', '.azuredevops', '.release'],
|
|
252
|
+
hashExcludeFiles: ['.npmrc', 'CHANGELOG.md', 'README.md'],
|
|
253
|
+
dependencies: "dependencies.json"
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
if (process.argv.length === 2) {
|
|
257
|
+
console.error('Expected at least one argument!');
|
|
258
|
+
process.exit(1);
|
|
259
|
+
} else {
|
|
260
|
+
let argv = process.argv;
|
|
261
|
+
for (let i = 2; i < argv.length; i++) {
|
|
262
|
+
if (argv[i] === "--save") options.saveVersion = true;
|
|
263
|
+
else if (argv[i] === "--debug") options.debug = true;
|
|
264
|
+
else if (argv[i] === "--changed") options.changed = true;
|
|
265
|
+
else if (argv[i] === "--version") options.version = true;
|
|
266
|
+
else if (argv[i] === "--hash") options.hash = true;
|
|
267
|
+
else if (argv[i] === "--tag") options.tag = true;
|
|
268
|
+
else if (argv[i] === "--retag") options.retag = true;
|
|
269
|
+
else if (argv[i] === "--init") options.init = true;
|
|
270
|
+
else if (argv[i] === "--try") options.try = true;
|
|
271
|
+
else if (argv[i] === "--hashExcludeFolders" || argv[i] === "--hashExcludeFiles") {
|
|
272
|
+
let name = argv[i].substring(2);
|
|
273
|
+
options[name] = argv[i + 1].split(',');
|
|
274
|
+
i++;
|
|
275
|
+
}
|
|
276
|
+
else if (argv[i] === "--hashFiles") {
|
|
277
|
+
let name = argv[i].substring(2);
|
|
278
|
+
options[name] = argv[i + 1].split(',');
|
|
279
|
+
i++;
|
|
280
|
+
} else if (argv[i].substring(0, 2) === "--") {
|
|
281
|
+
let name = argv[i].substring(2);
|
|
282
|
+
if (options[name] !== undefined) {
|
|
283
|
+
options[name] = argv[i + 1];
|
|
284
|
+
i++;
|
|
285
|
+
} else {
|
|
286
|
+
console.error(`Expected a known option, got ${argv[i]}`);
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const hasGitHub = existsSync(path.join(options.prefixPath, '.github'));
|
|
294
|
+
if (!options.hashFile) {
|
|
295
|
+
options.hashFile = hasGitHub ? '.github/hash.json' : '.cicd/hash.json';
|
|
296
|
+
}
|
|
297
|
+
if (!options.platform) {
|
|
298
|
+
options.platform = hasGitHub ? 'github' : 'ado';
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function initPackage(options) {
|
|
302
|
+
const pkgFile = path.join(options.prefixPath, 'package.json');
|
|
303
|
+
if (!existsSync(pkgFile)) {
|
|
304
|
+
console.error(`No package.json found at ${pkgFile}`);
|
|
305
|
+
process.exit(1);
|
|
306
|
+
}
|
|
307
|
+
const pkg = JSON.parse(readFileSync(pkgFile, 'utf8'));
|
|
308
|
+
const result = applyInitScripts(pkg);
|
|
309
|
+
|
|
310
|
+
if (result.added.length === 0 && result.updated.length === 0) {
|
|
311
|
+
console.log('[init] No changes needed — all release:* scripts are already present');
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (options.try) {
|
|
316
|
+
result.added.forEach(name =>
|
|
317
|
+
console.log('\x1b[36m%s\x1b[0m', `[try] Would add ${name}: ${RELEASE_SCRIPTS[name]}`));
|
|
318
|
+
result.updated.forEach(name =>
|
|
319
|
+
console.log('\x1b[36m%s\x1b[0m', `[try] Would update ${name}: remove --children flag`));
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
writeFileSync(pkgFile, JSON.stringify(result.pkg, null, 2) + '\n', 'utf8');
|
|
324
|
+
result.added.forEach(name => console.log(`[init] Added ${name}`));
|
|
325
|
+
result.updated.forEach(name => console.log(`[init] Updated ${name}: removed --children flag`));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
(async () => {
|
|
329
|
+
try {
|
|
330
|
+
if (options.init) {
|
|
331
|
+
await initPackage(options);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const text = await main(options);
|
|
335
|
+
console.log(text);
|
|
336
|
+
} catch (e) {
|
|
337
|
+
console.log(e);
|
|
338
|
+
process.exit(1);
|
|
339
|
+
}
|
|
340
|
+
})();
|