@anolilab/multi-semantic-release 1.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/CHANGELOG.md +276 -0
- package/LICENSE.md +12 -0
- package/README.md +410 -0
- package/bin/cli.js +109 -0
- package/lib/create-inline-plugin-creator.js +270 -0
- package/lib/get-commits-filtered.js +86 -0
- package/lib/get-config-multi-semrel.js +78 -0
- package/lib/get-config-semantic.js +36 -0
- package/lib/get-config.js +33 -0
- package/lib/get-manifest.js +92 -0
- package/lib/git.js +41 -0
- package/lib/logger.js +77 -0
- package/lib/multi-semantic-release.js +274 -0
- package/lib/rescoped-stream.js +33 -0
- package/lib/update-deps.js +388 -0
- package/lib/utils/blork.js +23 -0
- package/lib/utils/clean-path.js +24 -0
- package/lib/utils/get-version.js +37 -0
- package/lib/utils/merge-config.js +21 -0
- package/lib/utils/recognize-format.js +25 -0
- package/lib/utils/stream-to-array.js +60 -0
- package/package.json +142 -0
package/lib/logger.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import dbg from "debug";
|
|
2
|
+
import singnale from "signale";
|
|
3
|
+
|
|
4
|
+
const { Signale } = singnale;
|
|
5
|
+
const severityOrder = ["error", "warn", "info", "debug", "trace"];
|
|
6
|
+
const assertLevel = (level, limit) => severityOrder.indexOf(level) <= severityOrder.indexOf(limit);
|
|
7
|
+
const aliases = {
|
|
8
|
+
complete: "info",
|
|
9
|
+
failure: "error",
|
|
10
|
+
log: "info",
|
|
11
|
+
success: "info",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const logger = {
|
|
15
|
+
config: {
|
|
16
|
+
_level: "info",
|
|
17
|
+
_signale: {},
|
|
18
|
+
_stderr: process.stderr,
|
|
19
|
+
_stdout: process.stdout,
|
|
20
|
+
set level(l) {
|
|
21
|
+
if (!l) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (assertLevel(l, "debug")) {
|
|
25
|
+
dbg.enable("msr:");
|
|
26
|
+
}
|
|
27
|
+
if (assertLevel(l, "trace")) {
|
|
28
|
+
dbg.enable("semantic-release:");
|
|
29
|
+
}
|
|
30
|
+
this._level = l;
|
|
31
|
+
},
|
|
32
|
+
get level() {
|
|
33
|
+
return this._level;
|
|
34
|
+
},
|
|
35
|
+
set stdio([stderr, stdout]) {
|
|
36
|
+
this._stdout = stdout;
|
|
37
|
+
this._stderr = stderr;
|
|
38
|
+
this._signale = new Signale({
|
|
39
|
+
config: { displayLabel: false, displayTimestamp: true },
|
|
40
|
+
// scope: "multirelease",
|
|
41
|
+
stream: stdout,
|
|
42
|
+
types: {
|
|
43
|
+
complete: { badge: "🎉", color: "green", label: "", stream: [stdout] },
|
|
44
|
+
error: { color: "red", label: "", stream: [stderr] },
|
|
45
|
+
log: { badge: "•", color: "magenta", label: "", stream: [stdout] },
|
|
46
|
+
success: { color: "green", label: "", stream: [stdout] },
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
get stdio() {
|
|
51
|
+
return [this._stderr, this._stdout];
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
prefix: "msr:",
|
|
55
|
+
withScope(prefix) {
|
|
56
|
+
return {
|
|
57
|
+
...this,
|
|
58
|
+
debug: dbg(prefix || this.prefix),
|
|
59
|
+
prefix,
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
// eslint-disable-next-line unicorn/no-array-reduce
|
|
63
|
+
...[...severityOrder, ...Object.keys(aliases)].reduce((m, l) => {
|
|
64
|
+
// eslint-disable-next-line no-param-reassign,security/detect-object-injection,func-names
|
|
65
|
+
m[l] = function (...arguments_) {
|
|
66
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
67
|
+
if (assertLevel(aliases[l] || l, this.config.level)) {
|
|
68
|
+
// eslint-disable-next-line no-console,security/detect-object-injection
|
|
69
|
+
(this.config._signale[l] || console[l] || (() => {}))(this.prefix, ...arguments_);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
return m;
|
|
73
|
+
}, {}),
|
|
74
|
+
debug: dbg("msr:"),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export default logger;
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { topo } from "@semrel-extra/topo";
|
|
5
|
+
// eslint-disable-next-line you-dont-need-lodash-underscore/cast-array
|
|
6
|
+
import { castArray, sortBy, template } from "lodash-es";
|
|
7
|
+
import semanticRelease from "semantic-release";
|
|
8
|
+
|
|
9
|
+
import createInlinePluginCreator from "./create-inline-plugin-creator.js";
|
|
10
|
+
import getConfig from "./get-config.js";
|
|
11
|
+
import getConfigMultiSemrel from "./get-config-multi-semrel.js";
|
|
12
|
+
import getConfigSemantic from "./get-config-semantic.js";
|
|
13
|
+
import getManifest from "./get-manifest.js";
|
|
14
|
+
import logger from "./logger.js";
|
|
15
|
+
import RescopedStream from "./rescoped-stream.js";
|
|
16
|
+
import { check } from "./utils/blork.js";
|
|
17
|
+
import cleanPath from "./utils/clean-path.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Load details about a package.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} path The path to load details about.
|
|
23
|
+
* @param {Object} allOptions Options that apply to all packages.
|
|
24
|
+
* @param {MultiContext} multiContext Context object for the multirelease.
|
|
25
|
+
* @returns {Promise<Package|void>} A package object, or void if the package was skipped.
|
|
26
|
+
*
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
29
|
+
async function getPackage(path, { cwd, env, globalOptions, inputOptions, stderr, stdout }) {
|
|
30
|
+
// Make path absolute.
|
|
31
|
+
// eslint-disable-next-line no-param-reassign
|
|
32
|
+
path = cleanPath(path, cwd);
|
|
33
|
+
|
|
34
|
+
const directory = dirname(path);
|
|
35
|
+
|
|
36
|
+
// Get package.json file contents.
|
|
37
|
+
const manifest = getManifest(path);
|
|
38
|
+
const { name } = manifest;
|
|
39
|
+
|
|
40
|
+
// Combine list of all dependency names.
|
|
41
|
+
const deps = Object.keys({
|
|
42
|
+
...manifest.dependencies,
|
|
43
|
+
...manifest.devDependencies,
|
|
44
|
+
...manifest.peerDependencies,
|
|
45
|
+
...manifest.optionalDependencies,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Load the package-specific options.
|
|
49
|
+
const packageOptions = await getConfig(directory);
|
|
50
|
+
|
|
51
|
+
// The 'final options' are the global options merged with package-specific options.
|
|
52
|
+
// We merge this ourselves because package-specific options can override global options.
|
|
53
|
+
const finalOptions = { ...globalOptions, ...packageOptions, ...inputOptions };
|
|
54
|
+
|
|
55
|
+
// Make a fake logger so semantic-release's get-config doesn't fail.
|
|
56
|
+
const fakeLogger = { error() {}, log() {} };
|
|
57
|
+
|
|
58
|
+
// Use semantic-release's internal config with the final options (now we have the right `options.plugins` setting) to get the plugins object and the options including defaults.
|
|
59
|
+
// We need this so we can call e.g. plugins.analyzeCommit() to be able to affect the input and output of the whole set of plugins.
|
|
60
|
+
const { options, plugins } = await getConfigSemantic({ cwd: directory, env, stderr, stdout }, finalOptions);
|
|
61
|
+
|
|
62
|
+
// Return package object.
|
|
63
|
+
return { deps, dir: directory, fakeLogger, manifest, name, options, path, plugins };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Release an individual package.
|
|
68
|
+
*
|
|
69
|
+
* @param {Package} pkg The specific package.
|
|
70
|
+
* @param {Function} createInlinePlugin A function that creates an inline plugin.
|
|
71
|
+
* @param {MultiContext} multiContext Context object for the multirelease.
|
|
72
|
+
* @param {Object} flags Argv flags.
|
|
73
|
+
* @returns {Promise<void>} Promise that resolves when done.
|
|
74
|
+
*
|
|
75
|
+
* @internal
|
|
76
|
+
*/
|
|
77
|
+
async function releasePackage(package_, createInlinePlugin, multiContext, flags) {
|
|
78
|
+
// Vars.
|
|
79
|
+
const { dir, name, options: packageOptions } = package_;
|
|
80
|
+
const { env, stderr, stdout } = multiContext;
|
|
81
|
+
|
|
82
|
+
// Make an 'inline plugin' for this package.
|
|
83
|
+
// The inline plugin is the only plugin we call semanticRelease() with.
|
|
84
|
+
// The inline plugin functions then call e.g. plugins.analyzeCommits() manually and sometimes manipulate the responses.
|
|
85
|
+
const inlinePlugin = createInlinePlugin(package_);
|
|
86
|
+
|
|
87
|
+
// Set the options that we call semanticRelease() with.
|
|
88
|
+
// This consists of:
|
|
89
|
+
// - The global options (e.g. from the top level package.json)
|
|
90
|
+
// - The package options (e.g. from the specific package's package.json)
|
|
91
|
+
const options = { ...packageOptions, ...inlinePlugin };
|
|
92
|
+
|
|
93
|
+
// Add the package name into tagFormat.
|
|
94
|
+
// Thought about doing a single release for the tag (merging several packages), but it's impossible to prevent Github releasing while allowing NPM to continue.
|
|
95
|
+
// It'd also be difficult to merge all the assets into one release without full editing/overriding the plugins.
|
|
96
|
+
const tagFormatContext = {
|
|
97
|
+
name,
|
|
98
|
+
// eslint-disable-next-line no-template-curly-in-string
|
|
99
|
+
version: "${version}",
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// eslint-disable-next-line no-template-curly-in-string
|
|
103
|
+
const tagFormatDefault = "${name}@${version}";
|
|
104
|
+
|
|
105
|
+
options.tagFormat = template(flags.tagFormat || tagFormatDefault)(tagFormatContext);
|
|
106
|
+
|
|
107
|
+
// These are the only two options that MSR shares with semrel
|
|
108
|
+
// Set them manually for now, defaulting to the msr versions
|
|
109
|
+
// This is approach can be reviewed if there's ever more crossover.
|
|
110
|
+
// - debug is only supported in semrel as a CLI arg, always default to MSR
|
|
111
|
+
options.debug = flags.debug;
|
|
112
|
+
// - dryRun should use the msr version if specified, otherwise fallback to semrel
|
|
113
|
+
options.dryRun = flags.dryRun === undefined ? options.dryRun : flags.dryRun;
|
|
114
|
+
options.ci = flags.ci === undefined ? options.ci : flags.ci;
|
|
115
|
+
options.branches = flags.branches ? castArray(flags.branches) : options.branches;
|
|
116
|
+
|
|
117
|
+
// This options are needed for plugins that do not rely on `pluginOptions` and extract them independently.
|
|
118
|
+
options._pkgOptions = packageOptions;
|
|
119
|
+
|
|
120
|
+
// Call semanticRelease() on the directory and save result to pkg.
|
|
121
|
+
// Don't need to log out errors as semantic-release already does that.
|
|
122
|
+
// eslint-disable-next-line no-param-reassign
|
|
123
|
+
package_.result = await semanticRelease(options, {
|
|
124
|
+
cwd: dir,
|
|
125
|
+
env,
|
|
126
|
+
stderr: new RescopedStream(stderr, name),
|
|
127
|
+
stdout: new RescopedStream(stdout, name),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
return package_;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The multi-release context.
|
|
135
|
+
* @typedef MultiContext
|
|
136
|
+
* @param {Package[]} packages Array of all packages in this multirelease.
|
|
137
|
+
* @param {Package[]} releasing Array of packages that will release.
|
|
138
|
+
* @param {string} cwd The current working directory.
|
|
139
|
+
* @param {Object} env The environment variables.
|
|
140
|
+
* @param {Logger} logger The logger for the multirelease.
|
|
141
|
+
* @param {Stream} stdout The output stream for this multirelease.
|
|
142
|
+
* @param {Stream} stderr The error stream for this multirelease.
|
|
143
|
+
*/
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Details about an individual package in a multirelease
|
|
147
|
+
* @typedef Package
|
|
148
|
+
* @param {string} path String path to `package.json` for the package.
|
|
149
|
+
* @param {string} dir The working directory for the package.
|
|
150
|
+
* @param {string} name The name of the package, e.g. `my-amazing-package`
|
|
151
|
+
* @param {string[]} deps Array of all dependency package names for the package (merging dependencies, devDependencies, peerDependencies).
|
|
152
|
+
* @param {Package[]} localDeps Array of local dependencies this package relies on.
|
|
153
|
+
* @param {context|void} context The semantic-release context for this package's release (filled in once semantic-release runs).
|
|
154
|
+
* @param {undefined|Result|false} result The result of semantic-release (object with lastRelease, nextRelease, commits, releases), false if this package was skipped (no changes or similar), or undefined if the package's release hasn't completed yet.
|
|
155
|
+
* @param {Object} _lastRelease The last release object for the package before its current release (set during anaylze-commit)
|
|
156
|
+
* @param {Object} _nextRelease The next release object (the release the package is releasing for this cycle) (set during generateNotes)
|
|
157
|
+
*/
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Perform a multirelease.
|
|
161
|
+
*
|
|
162
|
+
* @param {string[]} paths An array of paths to package.json files.
|
|
163
|
+
* @param {Object} inputOptions An object containing semantic-release options.
|
|
164
|
+
* @param {Object} settings An object containing: cwd, env, stdout, stderr (mainly for configuring tests).
|
|
165
|
+
* @param {Object} _flags Argv flags.
|
|
166
|
+
* @returns {Promise<Package[]>} Promise that resolves to a list of package objects with `result` property describing whether it released or not.
|
|
167
|
+
*/
|
|
168
|
+
// eslint-disable-next-line sonarjs/cognitive-complexity
|
|
169
|
+
async function multiSemanticRelease(
|
|
170
|
+
paths,
|
|
171
|
+
inputOptions = {},
|
|
172
|
+
{ cwd = process.cwd(), env: environment = process.env, stderr = process.stderr, stdout = process.stdout } = {},
|
|
173
|
+
_flags = {},
|
|
174
|
+
) {
|
|
175
|
+
if (paths) {
|
|
176
|
+
check(paths, "paths: string[]");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
check(cwd, "cwd: directory");
|
|
180
|
+
check(environment, "env: objectlike");
|
|
181
|
+
check(stdout, "stdout: stream");
|
|
182
|
+
check(stderr, "stderr: stream");
|
|
183
|
+
|
|
184
|
+
// eslint-disable-next-line no-param-reassign
|
|
185
|
+
cwd = cleanPath(cwd);
|
|
186
|
+
|
|
187
|
+
const flags = {
|
|
188
|
+
deps: {},
|
|
189
|
+
...(await getConfigMultiSemrel(cwd, _flags)),
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const require = createRequire(import.meta.url);
|
|
193
|
+
const multisemrelPackageJson = require("../package.json");
|
|
194
|
+
const semrelPkgJson = require("semantic-release/package.json");
|
|
195
|
+
|
|
196
|
+
// Setup logger.
|
|
197
|
+
logger.config.stdio = [stderr, stdout];
|
|
198
|
+
logger.config.level = flags.logLevel;
|
|
199
|
+
|
|
200
|
+
if (flags.silent) {
|
|
201
|
+
logger.config.level = "silent";
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (flags.debug) {
|
|
205
|
+
logger.config.level = "debug";
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
logger.info(`multi-semantic-release version: ${multisemrelPackageJson.version}`);
|
|
209
|
+
logger.info(`semantic-release version: ${semrelPkgJson.version}`);
|
|
210
|
+
|
|
211
|
+
if (flags.debug) {
|
|
212
|
+
logger.info(`flags: ${JSON.stringify(flags, null, 2)}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Vars.
|
|
216
|
+
const globalOptions = await getConfig(cwd);
|
|
217
|
+
const multiContext = { cwd, env: environment, globalOptions, inputOptions, stderr, stdout };
|
|
218
|
+
const { packages: _packages, queue } = await topo({
|
|
219
|
+
cwd,
|
|
220
|
+
filter: ({ manifest, manifestAbsPath, manifestRelPath }) =>
|
|
221
|
+
(!flags.ignorePrivate || !manifest.private) && (paths ? paths.includes(manifestAbsPath) || paths.includes(manifestRelPath) : true),
|
|
222
|
+
workspacesExtra: Array.isArray(flags.ignorePackages) ? flags.ignorePackages.map((p) => `!${p}`) : [],
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Get list of package.json paths according to workspaces.
|
|
226
|
+
// eslint-disable-next-line no-param-reassign
|
|
227
|
+
paths = paths || Object.values(_packages).map((package_) => package_.manifestPath);
|
|
228
|
+
|
|
229
|
+
// Start.
|
|
230
|
+
logger.complete(`Started multirelease! Loading ${paths.length} packages...`);
|
|
231
|
+
|
|
232
|
+
// Load packages from paths.
|
|
233
|
+
// eslint-disable-next-line compat/compat
|
|
234
|
+
const packages = await Promise.all(paths.map((path) => getPackage(path, multiContext)));
|
|
235
|
+
|
|
236
|
+
packages.forEach((package_) => {
|
|
237
|
+
// Once we load all the packages we can find their cross refs
|
|
238
|
+
// Make a list of local dependencies.
|
|
239
|
+
// Map dependency names (e.g. my-awesome-dep) to their actual package objects in the packages array.
|
|
240
|
+
// eslint-disable-next-line no-param-reassign
|
|
241
|
+
package_.localDeps = [...new Set(package_.deps.map((d) => packages.find((p) => d === p.name)).filter(Boolean))];
|
|
242
|
+
|
|
243
|
+
logger.success(`Loaded package ${package_.name}`);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
logger.complete(`Queued ${queue.length} packages! Starting release...`);
|
|
247
|
+
|
|
248
|
+
// Release all packages.
|
|
249
|
+
const createInlinePlugin = createInlinePluginCreator(packages, multiContext, flags);
|
|
250
|
+
// eslint-disable-next-line unicorn/no-array-reduce
|
|
251
|
+
const released = await queue.reduce(async (_m, _name) => {
|
|
252
|
+
const m = await _m;
|
|
253
|
+
const package_ = packages.find(({ name }) => name === _name);
|
|
254
|
+
|
|
255
|
+
if (package_) {
|
|
256
|
+
const { result } = await releasePackage(package_, createInlinePlugin, multiContext, flags);
|
|
257
|
+
|
|
258
|
+
if (result) {
|
|
259
|
+
return m + 1;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return m;
|
|
264
|
+
// eslint-disable-next-line compat/compat
|
|
265
|
+
}, Promise.resolve(0));
|
|
266
|
+
|
|
267
|
+
// Return packages list.
|
|
268
|
+
logger.complete(`Released ${released} of ${queue.length} packages, semantically!`);
|
|
269
|
+
|
|
270
|
+
return sortBy(packages, ({ name }) => queue.indexOf(name));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Exports.
|
|
274
|
+
export default multiSemanticRelease;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Writable } from "node:stream";
|
|
2
|
+
|
|
3
|
+
import { check } from "./utils/blork.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Create a stream that passes messages through while rewriting scope.
|
|
7
|
+
* Replaces `[semantic-release]` with a custom scope (e.g. `[my-awesome-package]`) so output makes more sense.
|
|
8
|
+
*
|
|
9
|
+
* @param {stream.Writable} stream The actual stream to write messages to.
|
|
10
|
+
* @param {string} scope The string scope for the stream (instances of the text `[semantic-release]` are replaced in the stream).
|
|
11
|
+
* @returns {stream.Writable} Object that's compatible with stream.Writable (implements a `write()` property).
|
|
12
|
+
*
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
class RescopedStream extends Writable {
|
|
16
|
+
// Constructor.
|
|
17
|
+
constructor(stream, scope) {
|
|
18
|
+
super();
|
|
19
|
+
check(scope, "scope: string");
|
|
20
|
+
check(stream, "stream: stream");
|
|
21
|
+
this._stream = stream;
|
|
22
|
+
this._scope = scope;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Custom write method.
|
|
26
|
+
write(message) {
|
|
27
|
+
check(message, "msg: string");
|
|
28
|
+
this._stream.write(message.replace("[semantic-release]", `[${this._scope}]`));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Exports.
|
|
33
|
+
export default RescopedStream;
|