enablement-build-monorepo-version 1.0.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Chris Doty
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # enablement-build-monorepo-version
2
+
3
+ This detects changes in the children packages of a monorepo.
4
+
5
+
@@ -0,0 +1,425 @@
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
+ };
package/index.mjs ADDED
@@ -0,0 +1,303 @@
1
+ #!/usr/bin/env node
2
+ import path from 'path';
3
+ import { readFile,readFileSync, writeFile, writeFileSync, existsSync, lstatSync } from "fs";
4
+ import { exec } from "child_process";
5
+
6
+ import { hashElement } from "./folder-hash.mjs";
7
+ import { Version } from "./version.mjs";
8
+
9
+
10
+ function dependencyMap(outputFile) {
11
+ let depends = readFileSync(outputFile,"utf8");
12
+ depends = JSON.parse(depends).graph.dependencies;
13
+ let keys = Object.keys(depends);
14
+ let inverted = {};
15
+ for(let i=0;i<keys.length;i++) {
16
+ let name = keys[i];
17
+ let p = depends[name];
18
+ for(let j=0;j<p.length;j++) {
19
+ var d = p[j];
20
+ inverted[d.target] = inverted[d.target]||[];
21
+ inverted[d.target].push(name);
22
+ }
23
+ }
24
+ return inverted;
25
+ }
26
+
27
+
28
+ function buildResult(packageFolder, name,last_version,status,options) {
29
+ if(options.debug) console.log(packageFolder, name,`- ${status}`);
30
+ if(options.version) {
31
+ let version_file = path.join(options.prefixPath, packageFolder,name,"package.json");
32
+ return Version(name,last_version,version_file, options,{"name":name,changed:(status!="UNCHANGED"),status,packageFolder});
33
+ } else {
34
+ return new Promise((resolve, reject) => {
35
+ resolve({"name":name,changed:(status!="UNCHANGED"),status,packageFolder});
36
+ });
37
+ }
38
+ }
39
+
40
+ function getCurrentVersion(pathPrefix,packageFolder, name) {
41
+ const projectFile = path.join(pathPrefix, packageFolder,name,"package.json");
42
+
43
+ if(!existsSync(projectFile)) return {"version":"0.0.0"};
44
+ let data = readFileSync(projectFile, "utf8")
45
+ let pkg = JSON.parse(data);
46
+ return {version:pkg.version,fullName:pkg.name,packageFolder};
47
+ }
48
+
49
+ async function updateVersion(packageFolder,name,version) {
50
+ var value = new Promise((resolve, reject) => {
51
+ const projectFile = path.join(options.prefixPath, packageFolder,name,"package.json");
52
+ if(!existsSync(projectFile)) return resolve("NOT FOUND");
53
+
54
+ readFile(projectFile, "utf8", (error, data) => {
55
+ if (error) {
56
+ console.log(error);
57
+ reject(error);
58
+ return;
59
+ }
60
+ let versionFile = JSON.parse(data);
61
+ if(versionFile["version"]!==version) {
62
+ versionFile["version"] = version;
63
+ writeFile(projectFile, JSON.stringify(versionFile, null, 2), "utf8",
64
+ (error) => {
65
+ if (error) {
66
+ console.log(error);
67
+ reject(error);
68
+ return;
69
+ }
70
+ resolve("OK");
71
+ });
72
+ } else resolve("NOT NEEDED");
73
+ });
74
+
75
+ });
76
+
77
+ return value;
78
+ }
79
+
80
+
81
+ async function compare(packageFolder, previous,current,dependencies,options) {
82
+ let packages = Object.keys(current);
83
+ let plist = [];
84
+ let nameMap = {};
85
+
86
+ packages.forEach((name) => {
87
+ let fullName = current[name].fullName;
88
+ nameMap[fullName] = name;
89
+ });
90
+
91
+ packages.forEach((name) => {
92
+ let fullName = current[name].fullName;
93
+ if(current[name].packageFolder!==packageFolder) return;
94
+ if(previous[name]===undefined) {
95
+ plist.push(buildResult(packageFolder,name,current[name].version,"NEW",options));
96
+ } else {
97
+ if(current[name].hash!==previous[name].hash) {
98
+ plist.push(buildResult(packageFolder,name,current[name].version,"CHANGED",options));
99
+ if(dependencies[fullName] && dependencies[fullName].length>0) {
100
+ let d = dependencies[fullName];
101
+ for(let i=0;i<d.length;i++) {
102
+ let depName = nameMap[d[i]];
103
+ plist.push(buildResult(packageFolder,depName,current[depName].version,"CHANGED",options));
104
+ }
105
+ }
106
+ } else {
107
+ plist.push(buildResult(packageFolder,name,current[name].version,"UNCHANGED",options));
108
+ }
109
+ }
110
+ });
111
+ return Promise.allSettled(plist);
112
+ }
113
+
114
+ async function main(options) {
115
+ return new Promise((mainResolve, mainreject) => {
116
+ const hashOptions = { encoding: 'hex', folders: { exclude: options.hashExcludeFolders }, files: { exclude: options.hashExcludeFiles } }
117
+ const current = {};
118
+ const changeList = [];
119
+ const scanlist = [];
120
+
121
+ options.children.split(',').forEach((packageFolder)=>{
122
+ scanlist.push(new Promise((resolve, reject) => {
123
+ hashElement(path.join(options.prefixPath,packageFolder), hashOptions).then(async hash => {
124
+ // calculate latest folder hashes
125
+ const children = hash.children;
126
+ for(let i=0;i<children.length;i++)
127
+ {
128
+ let name = children[i].name;
129
+ if(name.substring(0,1)==="_" || name==="version" ) continue;
130
+ if(lstatSync(path.join( options.prefixPath, packageFolder,name)).isFile()) continue;
131
+ if(!existsSync(path.join( options.prefixPath, packageFolder,name))) continue;
132
+
133
+ delete children[i].children;
134
+ let v = getCurrentVersion(options.prefixPath,packageFolder,name);
135
+ current[name]={hash:children[i].hash,...v};
136
+ }
137
+
138
+ const changeConfig = path.join(options.prefixPath,options.hashFile);
139
+ let previous = {};
140
+
141
+ // make sure hash config file exits
142
+ if(!existsSync(changeConfig)) {
143
+ writeFileSync(changeConfig, "{}", "utf8");
144
+ }
145
+
146
+ // load previous hashes
147
+ readFile(changeConfig, "utf8", async (error, data) => {
148
+ if (error) {
149
+ console.log(error);
150
+ reject(error);
151
+ return;
152
+ }
153
+ previous=JSON.parse(data);
154
+
155
+ if(options.changed || options.version) {
156
+
157
+ let dependencies={};
158
+ if(options.dependencies) {
159
+ if(existsSync(options.dependencies)) {
160
+ dependencies = dependencyMap(options.dependencies);
161
+ if(options.debug) console.log('Loaded dependencies\n',JSON.stringify(dependencies));
162
+ } else {
163
+ console.log('\x1b[33m%s\x1b[0m', `Could not load dependency file ${options.dependencies}`);
164
+ }
165
+ }
166
+
167
+ let results = await compare(packageFolder, previous,current,dependencies,options);
168
+ if(options.debug) console.log(JSON.stringify(results));
169
+
170
+ for(let i=0;i<results.length;i++) {
171
+ if(results[i].value.changed)
172
+ changeList.push(results[i].value.name)
173
+ }
174
+
175
+ let plist=[];
176
+ for(let i=0;i<results.length;i++) {
177
+ if(results[i].value.packageFolder!==packageFolder) continue;
178
+ if(options.version) {
179
+ if(options.debug) console.log(results[i]);
180
+ let safeName = results[i].value.name;
181
+ safeName=safeName.replace(/-/g,'_');
182
+ if(results[i].value.changed) {
183
+ console.log(`##vso[task.setvariable variable=${safeName};isoutput=true;]${results[i].value.version}`);
184
+ if(options.saveVersion) {
185
+ plist.push(updateVersion(packageFolder,results[i].value.name,results[i].value.version));
186
+ }
187
+ } else {
188
+ console.log(`##vso[task.setvariable variable=${safeName};isoutput=true;]${results[i].value.previous}`);
189
+ }
190
+ }
191
+ if(options.tag) {
192
+ plist.push(new Promise((resolve,reject)=>{
193
+ let rev=results[i].value.name+'@'+results[i].value.version;
194
+ exec(`git describe --tags ${rev}`, (err, tag, stderr) => {
195
+ if (err) {
196
+ exec(`git tag ${rev} -m "${rev}"`, (err, tag, stderr) => {
197
+ if (err) {
198
+ reject(err);
199
+ return;
200
+ }
201
+ resolve(rev);
202
+ });
203
+ return;
204
+ }
205
+ });
206
+ }));
207
+ }
208
+ }
209
+ if(plist.length>0 && options.debug) {
210
+ console.log(await Promise.allSettled(plist));
211
+ }
212
+ }
213
+
214
+ // write current hashes
215
+ if(options.hash) {
216
+ let names = Object.keys(current);
217
+ for(let i=0;i<names.length;i++) {
218
+ if(current[names[i]].packageFolder!==packageFolder) continue;
219
+ let v = getCurrentVersion(options.prefixPath,packageFolder,names[i]);
220
+ current[names[i]].version = v.version;
221
+ current[names[i]].fullName = v.fullName;
222
+ }
223
+
224
+ writeFileSync(changeConfig, JSON.stringify(current, null, 2), "utf8");
225
+ console.log('folder hashes written successfully');
226
+ }
227
+
228
+ resolve("OK");
229
+ });
230
+ })
231
+ .catch(error => {
232
+ return console.error('hashing failed:', error);
233
+ });
234
+ }));
235
+ });
236
+
237
+ Promise.all(scanlist).then(()=>{
238
+ if(options.changed) {
239
+ console.log(`CHANGED - ${JSON.stringify(changeList)}`);
240
+ console.log(`##vso[task.setvariable variable=changed;isoutput=true]${JSON.stringify(changeList)}`);
241
+ }
242
+
243
+ mainResolve("DONE");
244
+ },mainreject);
245
+ });
246
+ }
247
+
248
+ let options = {
249
+ saveVersion:false,
250
+ changed:false,
251
+ version:false,
252
+ hash:false,
253
+ tag:false,
254
+ commit:false,
255
+ // push:false,
256
+ children:"packages",
257
+ prefixPath:'./',
258
+ debug:false,
259
+ hashFile:".cicd/hash.json",
260
+ hashExcludeFolders:['node_modules', 'coverage', 'dist'],
261
+ hashExcludeFiles:['.npmrc','CHANGELOG.md','README.md'],
262
+ dependencies:"dependencies.json"
263
+ };
264
+
265
+ if (process.argv.length === 2) {
266
+ console.error('Expected at least one argument!');
267
+ process.exit(1);
268
+ } else {
269
+ let argv = process.argv;
270
+ for(let i=2;i<argv.length;i++) {
271
+ if(argv[i]==="--save") options.saveVersion=true;
272
+ else if(argv[i]==="--debug") options.debug=true;
273
+ else if(argv[i]==="--changed") options.changed=true;
274
+ else if(argv[i]==="--version") options.version=true;
275
+ else if(argv[i]==="--hash") options.hash=true;
276
+ else if(argv[i]==="--tag") options.tag=true;
277
+ else if(argv[i]==="--hashExcludeFolders" || argv[i]==="--hashExcludeFiles") {
278
+ let name = argv[i].substring(2);
279
+ options[name] = argv[i+1].split(',');
280
+ i++;
281
+ } else
282
+ if(argv[i].substring(0,2)==="--") {
283
+ let name = argv[i].substring(2);
284
+ if(options[name]!==undefined) {
285
+ options[name] = argv[i+1];
286
+ i++;
287
+ } else {
288
+ console.error(`Expected a known option, got ${argv[i]}`);
289
+ process.exit(1);
290
+ }
291
+ }
292
+ }
293
+ }
294
+
295
+ (async () => {
296
+ try {
297
+ const text = await main(options);
298
+ console.log(text);
299
+ } catch (e) {
300
+ console.log(e);
301
+ process.exit(1);
302
+ }
303
+ })();
package/package.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "enablement-build-monorepo-version",
3
+ "version": "1.0.1",
4
+ "description": "This detects changes in the children packages of a monorepo.",
5
+ "type": "module",
6
+ "module": "./index.mjs",
7
+ "bin": "./index.mjs",
8
+ "license": "MIT",
9
+ "author": "Chris Doty",
10
+ "scripts": {
11
+ "start": "node index.mjs"
12
+ },
13
+ "dependencies": {
14
+ "minimatch": "~5.1.2"
15
+ }
16
+ }
package/version.mjs ADDED
@@ -0,0 +1,89 @@
1
+ import * as url from 'url';
2
+ import fs from "fs";
3
+ import { exec } from "child_process";
4
+
5
+ const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
6
+
7
+ const processMessage = function(message,tag,imageName, options, args) {
8
+ let previous = tag;
9
+ let parts = tag.split('@');
10
+ if(parts.length>1) tag=parts[1];
11
+ let suffix = tag.trim().split('-');
12
+ if(suffix.length>0) {
13
+ tag=suffix[0];
14
+ suffix=suffix[1];
15
+ } else suffix=false;
16
+
17
+ parts = tag.trim().split('.');
18
+ parts.length = 3;
19
+ let build = false;
20
+
21
+ console.log("PROCESS -",imageName,tag,message)
22
+
23
+ if (message.indexOf("fix:") >= 0 || message==='') {
24
+ parts[2] = parseInt(parts[2]);
25
+ parts[2]++;
26
+ build = true;
27
+ }
28
+ if (message.indexOf("feat:") >= 0) {
29
+ parts[1] = parseInt(parts[1]);
30
+ parts[1]++;
31
+ parts[2] = 0;
32
+ build = true;
33
+ }
34
+ if (message.indexOf("BREAKING") >= 0) {
35
+ parts[0] = parseInt(parts[0]);
36
+ parts[0]++;
37
+ parts[1] = '0';
38
+ parts[2] = '0';
39
+ build = true;
40
+ }
41
+
42
+ tag = parts.join('.');
43
+ if(suffix) tag=tag+'-'+suffix;
44
+
45
+ if(options.debug) console.log('\x1b[32m%s\x1b[0m', `Next version: ${imageName}@${tag}`);
46
+ return {"name":imageName, "tag":tag,"version":tag, build, previous, ...args};
47
+ };
48
+
49
+ async function Version(imageName, last_version, version_file, options, args)
50
+ {
51
+
52
+ const readConfig = function(resolve, reject) {
53
+ try {
54
+ if(options.debug) console.log('\x1b[32m%s\x1b[0m', `reading config: ${version_file}`);
55
+
56
+ let data = fs.readFileSync(version_file, 'utf8');
57
+ let tag = JSON.parse(data).version;
58
+ tag = tag.trim();
59
+ if(options.debug) console.log('\x1b[32m%s\x1b[0m', `Found version: ${imageName}@${tag}`);
60
+ resolve(processMessage("fix: no version tag found", tag, imageName, options, args));
61
+ } catch {
62
+ if(options.debug) console.log('\x1b[33m%s\x1b[0m', 'Could not find last or next version: ');
63
+ resolve({"name":imageName,"tag":`${imageName}@0.0.0`,"previous":"0.0.0","version":"0.0.1", ...args});
64
+ }
65
+ };
66
+
67
+ // if(args.changed)
68
+ // return new Promise((resolve, reject) => {
69
+ // let cmd= `git log ${imageName}/${last_version}..HEAD --oneline -- packages/spinner --no-merges`;
70
+ // exec(cmd, (err, message, stderr) => {
71
+ // if (err) {
72
+ // console.log('\x1b[33m%s\x1b[0m', 'Could not find any revisions because: ');
73
+ // console.log('\x1b[31m%s\x1b[0m', stderr);
74
+ // console.log('\x1b[31m%s\x1b[0m', cmd);
75
+
76
+ // readConfig(resolve, reject);
77
+
78
+ // return;
79
+ // }
80
+ // resolve(processMessage(message, last_version, imageName, options, args));
81
+ // });
82
+ // });
83
+
84
+ return new Promise((resolve, reject) => {
85
+ readConfig(resolve, reject)
86
+ });
87
+ }
88
+
89
+ export { Version };