@modulify/conventional-release 0.1.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 +380 -0
- package/bin/cli.cjs +308 -0
- package/bin/cli.mjs +291 -0
- package/bin/index.cjs +7 -0
- package/bin/index.js +9 -0
- package/dist/config.d.ts +12 -0
- package/dist/constants.d.ts +6 -0
- package/dist/execute.d.ts +9 -0
- package/dist/index.cjs +712 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.mjs +712 -0
- package/dist/plan.d.ts +24 -0
- package/dist/reporter.d.ts +11 -0
- package/dist/runtime.d.ts +36 -0
- package/package.json +98 -0
- package/types/domain.d.ts +124 -0
- package/types/index.d.ts +23 -0
- package/types/release.d.ts +93 -0
package/README.md
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
# @modulify/conventional-release
|
|
2
|
+
|
|
3
|
+
Release orchestration package for conventional workflows.
|
|
4
|
+
|
|
5
|
+
This workspace combines:
|
|
6
|
+
- semantic version recommendation from `@modulify/conventional-bump`,
|
|
7
|
+
- changelog rendering and writing from `@modulify/conventional-changelog`,
|
|
8
|
+
- package manifest updates,
|
|
9
|
+
- release finalization with commit and tag creation.
|
|
10
|
+
|
|
11
|
+
The package is library-first. It exposes:
|
|
12
|
+
- `createScope()` to inspect what would be released,
|
|
13
|
+
- `run()` to apply the release flow,
|
|
14
|
+
- `conventional-release` as a config-driven CLI binary.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
yarn add -D @modulify/conventional-release
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Other package managers:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install -D @modulify/conventional-release
|
|
26
|
+
pnpm add -D @modulify/conventional-release
|
|
27
|
+
bun add -d @modulify/conventional-release
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Mental model
|
|
31
|
+
|
|
32
|
+
The package works in two stages:
|
|
33
|
+
|
|
34
|
+
1. `createScope(options)` discovers the release scope for the repository.
|
|
35
|
+
It resolves packages, filters workspaces, detects affected packages, and produces ordered release slices.
|
|
36
|
+
2. `run(options)` resolves the same scope and applies side effects.
|
|
37
|
+
It updates manifests, writes the changelog, creates a commit, and creates tags.
|
|
38
|
+
|
|
39
|
+
`Scope` is the declarative view of a release.
|
|
40
|
+
`Slice` is one execution unit inside that scope.
|
|
41
|
+
|
|
42
|
+
In `sync` mode there is usually one slice for the whole repository.
|
|
43
|
+
In `async` mode each affected package gets its own slice.
|
|
44
|
+
In `hybrid` mode packages can be split into named `partitions`.
|
|
45
|
+
|
|
46
|
+
## Quick start
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { run } from '@modulify/conventional-release'
|
|
50
|
+
|
|
51
|
+
const result = await run()
|
|
52
|
+
|
|
53
|
+
if (!result.changed) {
|
|
54
|
+
console.log('No changes since last release')
|
|
55
|
+
} else {
|
|
56
|
+
for (const slice of result.slices) {
|
|
57
|
+
if (!slice.changed) continue
|
|
58
|
+
|
|
59
|
+
console.log(slice.id, slice.nextVersion, slice.tag)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## CLI
|
|
65
|
+
|
|
66
|
+
The package ships a `conventional-release` binary.
|
|
67
|
+
|
|
68
|
+
Typical usage:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
conventional-release
|
|
72
|
+
conventional-release --dry
|
|
73
|
+
conventional-release --dry --verbose --tags
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
From a project script:
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"scripts": {
|
|
81
|
+
"release": "conventional-release",
|
|
82
|
+
"release:dry": "conventional-release --dry"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Without adding a local script:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
npx @modulify/conventional-release --dry
|
|
91
|
+
npm exec conventional-release -- --dry
|
|
92
|
+
yarn dlx @modulify/conventional-release --dry
|
|
93
|
+
pnpm dlx @modulify/conventional-release --dry
|
|
94
|
+
bunx @modulify/conventional-release --dry
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Useful flags:
|
|
98
|
+
|
|
99
|
+
- `--dry`: compute versions, files, and tags without write-side effects
|
|
100
|
+
- `--verbose`: show detailed per-slice progress output
|
|
101
|
+
- `--tags`: print generated tags in the final summary
|
|
102
|
+
- `--release-as <type>`: force `major`, `minor`, or `patch`
|
|
103
|
+
- `--prerelease <channel>`: use `alpha`, `beta`, or `rc`
|
|
104
|
+
|
|
105
|
+
The CLI reads the same repository configuration as the library API and wires a lifecycle reporter into `run()`.
|
|
106
|
+
|
|
107
|
+
## Inspect before running
|
|
108
|
+
|
|
109
|
+
Use `createScope()` when you want a dry, deterministic view of the release shape:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import { createScope } from '@modulify/conventional-release'
|
|
113
|
+
|
|
114
|
+
const scope = await createScope({
|
|
115
|
+
mode: 'hybrid',
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
console.log(scope.mode)
|
|
119
|
+
console.log(scope.packages.map((pkg) => pkg.path))
|
|
120
|
+
console.log(scope.slices.map((slice) => slice.id))
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
This is useful for:
|
|
124
|
+
- the built-in package CLI,
|
|
125
|
+
- custom CLIs,
|
|
126
|
+
- dashboards,
|
|
127
|
+
- approval flows,
|
|
128
|
+
- tests around release planning.
|
|
129
|
+
|
|
130
|
+
## Running a release
|
|
131
|
+
|
|
132
|
+
`run()` applies the release flow and returns per-slice results:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { run } from '@modulify/conventional-release'
|
|
136
|
+
|
|
137
|
+
const result = await run({
|
|
138
|
+
mode: 'sync',
|
|
139
|
+
dry: true,
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
console.log(result.changed)
|
|
143
|
+
console.log(result.files)
|
|
144
|
+
console.log(result.slices)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
When `dry: true` is used, the package still resolves versions, tags, and touched files, but skips write-side effects.
|
|
148
|
+
|
|
149
|
+
## Configuration sources
|
|
150
|
+
|
|
151
|
+
Configuration is resolved in this order:
|
|
152
|
+
|
|
153
|
+
1. `package.json` field `release`
|
|
154
|
+
2. `release.config.ts`, `release.config.mjs`, or `release.config.js`
|
|
155
|
+
3. inline options passed to `run()` or `createScope()`
|
|
156
|
+
|
|
157
|
+
Inline options always win.
|
|
158
|
+
|
|
159
|
+
Example `package.json`:
|
|
160
|
+
|
|
161
|
+
```json
|
|
162
|
+
{
|
|
163
|
+
"name": "example-repo",
|
|
164
|
+
"version": "1.0.0",
|
|
165
|
+
"release": {
|
|
166
|
+
"mode": "sync",
|
|
167
|
+
"tagPrefix": "v"
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Example `release.config.ts`:
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
import type { Options } from '@modulify/conventional-release'
|
|
176
|
+
|
|
177
|
+
const config: Options = {
|
|
178
|
+
mode: 'hybrid',
|
|
179
|
+
partitions: {
|
|
180
|
+
core: {
|
|
181
|
+
mode: 'sync',
|
|
182
|
+
workspaces: ['@scope/core-*'],
|
|
183
|
+
},
|
|
184
|
+
plugins: {
|
|
185
|
+
mode: 'async',
|
|
186
|
+
workspaces: ['packages/plugins/*'],
|
|
187
|
+
tagPrefix: 'plugin-',
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export default config
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Common options
|
|
196
|
+
|
|
197
|
+
The most important public options are:
|
|
198
|
+
|
|
199
|
+
- `mode`: release strategy, one of `sync`, `async`, or `hybrid`
|
|
200
|
+
- `releaseAs`: explicit semver bump override such as `major`, `minor`, or `patch`
|
|
201
|
+
- `prerelease`: prerelease channel, one of `alpha`, `beta`, or `rc`
|
|
202
|
+
- `fromTag`: explicit lower bound tag for advisory commit analysis
|
|
203
|
+
- `tagPrefix`: tag matcher used during advisory commit analysis
|
|
204
|
+
- `workspaces`: include and exclude filters for workspace discovery
|
|
205
|
+
- `partitions`: named hybrid slices for mixed release strategies
|
|
206
|
+
- `dependencyPolicy`: how internal dependency ranges are updated, one of `preserve`, `caret`, or `exact`
|
|
207
|
+
- `install`: whether install should run after manifest updates
|
|
208
|
+
- `tagName`, `tagMessage`, `commitMessage`: custom formatters for release output
|
|
209
|
+
- `changelogFile`: changelog output path relative to the repository root
|
|
210
|
+
|
|
211
|
+
Important:
|
|
212
|
+
- `tagPrefix` affects release discovery and commit analysis boundaries.
|
|
213
|
+
- `tagPrefix` does not format the new tag name by itself.
|
|
214
|
+
- To change produced tag names, use `tagName`.
|
|
215
|
+
|
|
216
|
+
## Single-package repository
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
import { run } from '@modulify/conventional-release'
|
|
220
|
+
|
|
221
|
+
await run({
|
|
222
|
+
mode: 'sync',
|
|
223
|
+
fromTag: 'v1.0.0',
|
|
224
|
+
})
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
This is the simplest setup and usually produces one slice:
|
|
228
|
+
- one next version,
|
|
229
|
+
- one commit,
|
|
230
|
+
- one tag.
|
|
231
|
+
|
|
232
|
+
By default, a changed `sync` slice produces a tag like `v1.2.3`.
|
|
233
|
+
|
|
234
|
+
## Monorepo with independent packages
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
import { run } from '@modulify/conventional-release'
|
|
238
|
+
|
|
239
|
+
await run({
|
|
240
|
+
mode: 'async',
|
|
241
|
+
workspaces: {
|
|
242
|
+
include: ['packages/*'],
|
|
243
|
+
},
|
|
244
|
+
})
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
This creates one slice per affected package.
|
|
248
|
+
|
|
249
|
+
By default, each changed async slice produces a tag like `package-name@1.2.3`.
|
|
250
|
+
|
|
251
|
+
## Monorepo with grouped release behavior
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
import { run } from '@modulify/conventional-release'
|
|
255
|
+
|
|
256
|
+
await run({
|
|
257
|
+
mode: 'hybrid',
|
|
258
|
+
partitions: {
|
|
259
|
+
app: {
|
|
260
|
+
mode: 'sync',
|
|
261
|
+
workspaces: ['@scope/app', '@scope/web'],
|
|
262
|
+
},
|
|
263
|
+
plugins: {
|
|
264
|
+
mode: 'async',
|
|
265
|
+
workspaces: ['packages/plugins/*'],
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
})
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
This is useful when some packages must move in lockstep, while others can release independently.
|
|
272
|
+
|
|
273
|
+
By default, partition slices use tags like `partition-name@1.2.3`.
|
|
274
|
+
|
|
275
|
+
## Result shape
|
|
276
|
+
|
|
277
|
+
`run()` returns:
|
|
278
|
+
|
|
279
|
+
- `changed`: whether at least one slice changed version
|
|
280
|
+
- `files`: all files touched by changed slices
|
|
281
|
+
- `packages`: all packages in resolved scope
|
|
282
|
+
- `affected`: packages affected by the current working tree
|
|
283
|
+
- `slices`: ordered slice results with:
|
|
284
|
+
- `id`
|
|
285
|
+
- `kind`
|
|
286
|
+
- `mode`
|
|
287
|
+
- `packages`
|
|
288
|
+
- `currentVersion`
|
|
289
|
+
- `nextVersion`
|
|
290
|
+
- `releaseType`
|
|
291
|
+
- `tag`
|
|
292
|
+
- `commitMessage`
|
|
293
|
+
- `tagMessage`
|
|
294
|
+
|
|
295
|
+
Example:
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
const result = await run({ dry: true })
|
|
299
|
+
|
|
300
|
+
for (const slice of result.slices) {
|
|
301
|
+
console.log({
|
|
302
|
+
id: slice.id,
|
|
303
|
+
changed: slice.changed,
|
|
304
|
+
nextVersion: slice.nextVersion,
|
|
305
|
+
tag: slice.tag,
|
|
306
|
+
})
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## Install behavior
|
|
311
|
+
|
|
312
|
+
After manifest updates the package can run the repository package manager install command.
|
|
313
|
+
|
|
314
|
+
`install` supports three forms:
|
|
315
|
+
|
|
316
|
+
- `false`: skip install entirely
|
|
317
|
+
- `true` or omitted: run install with default extra arguments for the detected package manager
|
|
318
|
+
- `string[]`: run install and append these extra arguments after the `install` subcommand
|
|
319
|
+
|
|
320
|
+
Example:
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
await run({
|
|
324
|
+
install: ['--mode=skip-build'],
|
|
325
|
+
})
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
That becomes conceptually:
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
<package-manager> install --mode=skip-build
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
## Package manager detection
|
|
335
|
+
|
|
336
|
+
The package detects the package manager in this order:
|
|
337
|
+
|
|
338
|
+
1. `package.json#packageManager`
|
|
339
|
+
2. lockfiles in the repository root
|
|
340
|
+
3. fallback to `npm`
|
|
341
|
+
|
|
342
|
+
Recognized lockfiles:
|
|
343
|
+
|
|
344
|
+
- `yarn.lock`
|
|
345
|
+
- `pnpm-lock.yaml`
|
|
346
|
+
- `package-lock.json`
|
|
347
|
+
- `bun.lock`
|
|
348
|
+
- `bun.lockb`
|
|
349
|
+
|
|
350
|
+
Default install extras:
|
|
351
|
+
|
|
352
|
+
- `yarn`: `--no-immutable`
|
|
353
|
+
- `npm`: no extra args
|
|
354
|
+
- `pnpm`: no extra args
|
|
355
|
+
- `bun`: no extra args
|
|
356
|
+
|
|
357
|
+
## Tag and message customization
|
|
358
|
+
|
|
359
|
+
Custom formatters receive a `TagContext` object:
|
|
360
|
+
|
|
361
|
+
```ts
|
|
362
|
+
import { run } from '@modulify/conventional-release'
|
|
363
|
+
|
|
364
|
+
await run({
|
|
365
|
+
tagName: ({ version, partition, packages }) => {
|
|
366
|
+
const name = partition ?? packages[0]?.name ?? 'release'
|
|
367
|
+
|
|
368
|
+
return `${name}@${version}`
|
|
369
|
+
},
|
|
370
|
+
commitMessage: ({ tag }) => `chore(release): ${tag}`,
|
|
371
|
+
tagMessage: ({ tag }) => `chore(release): ${tag}`,
|
|
372
|
+
})
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
## Notes
|
|
376
|
+
|
|
377
|
+
- The package detects the package manager from `package.json#packageManager` or lockfiles.
|
|
378
|
+
- The default fallback package manager is `npm`.
|
|
379
|
+
- The package does not perform `git push`.
|
|
380
|
+
- CLI-style push hints belong in the CLI layer, not in the library result.
|
package/bin/cli.cjs
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const gitToolkit = require("@modulify/git-toolkit");
|
|
4
|
+
const shell = require("@modulify/git-toolkit/shell");
|
|
5
|
+
const index = require("../dist/index.cjs");
|
|
6
|
+
const yargs = require("yargs");
|
|
7
|
+
const helpers = require("yargs/helpers");
|
|
8
|
+
const chalk = require("chalk");
|
|
9
|
+
const figures = require("figures");
|
|
10
|
+
const util = require("node:util");
|
|
11
|
+
function _interopNamespaceDefault(e) {
|
|
12
|
+
const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
|
|
13
|
+
if (e) {
|
|
14
|
+
for (const k in e) {
|
|
15
|
+
if (k !== "default") {
|
|
16
|
+
const d = Object.getOwnPropertyDescriptor(e, k);
|
|
17
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
18
|
+
enumerable: true,
|
|
19
|
+
get: () => e[k]
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
n.default = e;
|
|
25
|
+
return Object.freeze(n);
|
|
26
|
+
}
|
|
27
|
+
const util__namespace = /* @__PURE__ */ _interopNamespaceDefault(util);
|
|
28
|
+
const DEFAULTS = {
|
|
29
|
+
dry: false,
|
|
30
|
+
verbose: false,
|
|
31
|
+
tags: false
|
|
32
|
+
};
|
|
33
|
+
class CliParseError extends Error {
|
|
34
|
+
help;
|
|
35
|
+
}
|
|
36
|
+
async function parseArgv(argv = process.argv) {
|
|
37
|
+
const parser = yargs(helpers.hideBin(argv)).locale("en").scriptName("conventional-release").usage("Usage: $0 [options]").option("release-as", {
|
|
38
|
+
alias: "r",
|
|
39
|
+
describe: "Specify the release type (major|minor|patch)",
|
|
40
|
+
requiresArg: true,
|
|
41
|
+
string: true
|
|
42
|
+
}).option("prerelease", {
|
|
43
|
+
alias: "p",
|
|
44
|
+
describe: "Specify the prerelease type (alpha|beta|rc)",
|
|
45
|
+
requiresArg: true,
|
|
46
|
+
string: true
|
|
47
|
+
}).option("dry", {
|
|
48
|
+
type: "boolean",
|
|
49
|
+
default: DEFAULTS.dry,
|
|
50
|
+
describe: "See the commands that running release would run"
|
|
51
|
+
}).option("verbose", {
|
|
52
|
+
type: "boolean",
|
|
53
|
+
default: DEFAULTS.verbose,
|
|
54
|
+
describe: "Show detailed per-slice progress output"
|
|
55
|
+
}).option("tags", {
|
|
56
|
+
type: "boolean",
|
|
57
|
+
default: DEFAULTS.tags,
|
|
58
|
+
describe: "Show generated tags in the final output"
|
|
59
|
+
}).exitProcess(false).check((options) => {
|
|
60
|
+
if (!["alpha", "beta", "rc", void 0].includes(options.prerelease)) {
|
|
61
|
+
throw new Error("prerelease should be one of alpha, beta, rc or undefined");
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
}).showHelpOnFail(false).fail((message) => {
|
|
65
|
+
throw new Error(message);
|
|
66
|
+
}).alias("version", "v").alias("help", "h").example("$0", "Update changelog and tag release").example("$0 --dry --verbose", "Show a detailed dry-run release preview").pkgConf("release").wrap(97);
|
|
67
|
+
let parsed;
|
|
68
|
+
try {
|
|
69
|
+
parsed = await parser.parseAsync();
|
|
70
|
+
} catch (error) {
|
|
71
|
+
const failure = new CliParseError(error.message);
|
|
72
|
+
const help = await parser.getHelp();
|
|
73
|
+
failure.help = [help].flat().join("\n");
|
|
74
|
+
throw failure;
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
releaseAs: parsed.releaseAs,
|
|
78
|
+
prerelease: parsed.prerelease,
|
|
79
|
+
dry: parsed.dry,
|
|
80
|
+
verbose: parsed.verbose,
|
|
81
|
+
tags: parsed.tags
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function createReporter({
|
|
85
|
+
output,
|
|
86
|
+
git,
|
|
87
|
+
showTags = false,
|
|
88
|
+
verbosity = "summary"
|
|
89
|
+
}) {
|
|
90
|
+
if (verbosity === "detailed") {
|
|
91
|
+
return new DetailedReporter({
|
|
92
|
+
output,
|
|
93
|
+
git,
|
|
94
|
+
showTags
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return new SummaryReporter({
|
|
98
|
+
output,
|
|
99
|
+
git,
|
|
100
|
+
showTags
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
class SummaryReporter {
|
|
104
|
+
output;
|
|
105
|
+
git;
|
|
106
|
+
showTags;
|
|
107
|
+
scope = null;
|
|
108
|
+
position = /* @__PURE__ */ new Map();
|
|
109
|
+
constructor({
|
|
110
|
+
output,
|
|
111
|
+
git,
|
|
112
|
+
showTags
|
|
113
|
+
}) {
|
|
114
|
+
this.output = output;
|
|
115
|
+
this.git = git;
|
|
116
|
+
this.showTags = showTags;
|
|
117
|
+
}
|
|
118
|
+
async onStart(context) {
|
|
119
|
+
this.output.info(
|
|
120
|
+
context.dry ? "Starting dry release" : "Starting release"
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
async onScope(scope, context) {
|
|
124
|
+
this.scope = scope;
|
|
125
|
+
this.position = new Map(
|
|
126
|
+
scope.slices.map((slice, index2) => [slice.id, index2 + 1])
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
async onSliceStart(slice) {
|
|
130
|
+
this.output.info("Running slice %s", [this.describeProgress(slice)]);
|
|
131
|
+
}
|
|
132
|
+
async onSuccess(result) {
|
|
133
|
+
if (!result.changed) {
|
|
134
|
+
this.output.success("No changes since last release");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const changed = result.slices.filter((slice) => slice.changed);
|
|
138
|
+
const primary = changed[0];
|
|
139
|
+
const packages = collectPackages(changed);
|
|
140
|
+
this.output.success("Release slices: %s", [String(changed.length)]);
|
|
141
|
+
this.output.success("Updated packages: %s", [String(packages.length)]);
|
|
142
|
+
if (primary) {
|
|
143
|
+
this.output.success("Next version: %s", [primary.nextVersion]);
|
|
144
|
+
}
|
|
145
|
+
if (result.dry) {
|
|
146
|
+
this.output.info("No committing or tagging since this was a dry run");
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
this.output.success("Committed %s staged files", [String(result.files.length)]);
|
|
150
|
+
if (this.showTags) {
|
|
151
|
+
const tags = collectTags(changed);
|
|
152
|
+
if (tags.length) {
|
|
153
|
+
this.output.success("Tags: %s", [tags.join(", ")]);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
this.output.info("Run `%s` to publish", [
|
|
157
|
+
`git push --follow-tags origin ${await this.resolveBranch()}`
|
|
158
|
+
]);
|
|
159
|
+
}
|
|
160
|
+
async onError(error) {
|
|
161
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
162
|
+
this.output.error(message);
|
|
163
|
+
}
|
|
164
|
+
describeProgress(slice) {
|
|
165
|
+
const position = this.position.get(slice.id);
|
|
166
|
+
const total = this.scope?.slices.length;
|
|
167
|
+
const label = describeSlice(slice);
|
|
168
|
+
return position && total ? `${position}/${total}: ${label}` : label;
|
|
169
|
+
}
|
|
170
|
+
async resolveBranch() {
|
|
171
|
+
try {
|
|
172
|
+
return await this.git.revParse("HEAD", { abbrevRef: true });
|
|
173
|
+
} catch {
|
|
174
|
+
return "%branch%";
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
class DetailedReporter extends SummaryReporter {
|
|
179
|
+
async onScope(scope, context) {
|
|
180
|
+
await super.onScope(scope, context);
|
|
181
|
+
this.output.info("%s scope: %s packages, %s affected, %s slices", [
|
|
182
|
+
scope.mode,
|
|
183
|
+
String(scope.packages.length),
|
|
184
|
+
String(scope.affected.length),
|
|
185
|
+
String(scope.slices.length)
|
|
186
|
+
]);
|
|
187
|
+
}
|
|
188
|
+
async onSliceSuccess(slice) {
|
|
189
|
+
if (!slice.changed) {
|
|
190
|
+
this.output.warn("Completed slice %s without version changes (%s)", [
|
|
191
|
+
describeSlice(slice),
|
|
192
|
+
slice.currentVersion
|
|
193
|
+
]);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
this.output.success("Completed slice %s: %s -> %s (%s)", [
|
|
197
|
+
describeSlice(slice),
|
|
198
|
+
slice.currentVersion,
|
|
199
|
+
slice.nextVersion,
|
|
200
|
+
slice.releaseType
|
|
201
|
+
]);
|
|
202
|
+
if (this.showTags && slice.tag) {
|
|
203
|
+
this.output.info("Tag: %s", [slice.tag]);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
async onSuccess(result) {
|
|
207
|
+
await super.onSuccess(result);
|
|
208
|
+
if (!result.changed) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const packages = collectPackages(result.slices.filter((slice) => slice.changed));
|
|
212
|
+
this.output.info("Updated packages: %s", [describePackages(packages)]);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function describeSlice(slice) {
|
|
216
|
+
if (slice.partition) {
|
|
217
|
+
return `${slice.partition} [${describePackages(slice.packages)}]`;
|
|
218
|
+
}
|
|
219
|
+
return describePackages(slice.packages);
|
|
220
|
+
}
|
|
221
|
+
function describePackages(packages) {
|
|
222
|
+
return packages.map((pkg) => pkg.name ?? pkg.path).join(", ");
|
|
223
|
+
}
|
|
224
|
+
function collectTags(slices) {
|
|
225
|
+
return slices.map((slice) => slice.tag).filter((tag) => !!tag);
|
|
226
|
+
}
|
|
227
|
+
function collectPackages(slices) {
|
|
228
|
+
const packages = [];
|
|
229
|
+
const seen = /* @__PURE__ */ new Set();
|
|
230
|
+
for (const slice of slices) {
|
|
231
|
+
for (const pkg of slice.packages) {
|
|
232
|
+
const identity = pkg.name ?? pkg.path;
|
|
233
|
+
if (seen.has(identity)) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
seen.add(identity);
|
|
237
|
+
packages.push(pkg);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return packages;
|
|
241
|
+
}
|
|
242
|
+
class ConsoleOutput {
|
|
243
|
+
write(message) {
|
|
244
|
+
console.info(message);
|
|
245
|
+
}
|
|
246
|
+
writeError(message) {
|
|
247
|
+
console.error(message);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
class Output {
|
|
251
|
+
output;
|
|
252
|
+
theme;
|
|
253
|
+
constructor({
|
|
254
|
+
dry,
|
|
255
|
+
output = new ConsoleOutput(),
|
|
256
|
+
theme = createDefaultTheme(dry)
|
|
257
|
+
}) {
|
|
258
|
+
this.output = output;
|
|
259
|
+
this.theme = theme;
|
|
260
|
+
}
|
|
261
|
+
info(template, context = [], figure = this.theme.info) {
|
|
262
|
+
this.output.write(format(template, context, figure));
|
|
263
|
+
}
|
|
264
|
+
success(template, context = [], figure = this.theme.success) {
|
|
265
|
+
this.output.write(format(template, context, figure));
|
|
266
|
+
}
|
|
267
|
+
warn(template, context = [], figure = this.theme.warning) {
|
|
268
|
+
this.output.write(format(template, context, figure));
|
|
269
|
+
}
|
|
270
|
+
error(template, context = [], figure = this.theme.error) {
|
|
271
|
+
this.output.writeError(format(template, context, figure));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function createDefaultTheme(dry) {
|
|
275
|
+
return {
|
|
276
|
+
success: dry ? chalk.yellow(figures.tick) : chalk.green(figures.tick),
|
|
277
|
+
warning: chalk.yellow(figures.warning),
|
|
278
|
+
error: chalk.red(figures.cross),
|
|
279
|
+
info: chalk.blue(figures.info)
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
function format(template, context, figure) {
|
|
283
|
+
const bold = (arg) => chalk.bold(arg);
|
|
284
|
+
const message = util__namespace.format(template, ...context.map(bold));
|
|
285
|
+
return `${figure} ${message}`;
|
|
286
|
+
}
|
|
287
|
+
async function main(argv = process.argv) {
|
|
288
|
+
const cwd = process.cwd();
|
|
289
|
+
const options = await parseArgv(argv);
|
|
290
|
+
const output = new Output({
|
|
291
|
+
dry: options.dry,
|
|
292
|
+
output: new ConsoleOutput()
|
|
293
|
+
});
|
|
294
|
+
const git = new gitToolkit.GitCommander({ sh: new shell.Runner(cwd) });
|
|
295
|
+
await index.run({
|
|
296
|
+
cwd,
|
|
297
|
+
dry: options.dry,
|
|
298
|
+
releaseAs: options.releaseAs,
|
|
299
|
+
prerelease: options.prerelease,
|
|
300
|
+
reporter: createReporter({
|
|
301
|
+
output,
|
|
302
|
+
git,
|
|
303
|
+
showTags: options.tags,
|
|
304
|
+
verbosity: options.verbose ? "detailed" : "summary"
|
|
305
|
+
})
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
exports.main = main;
|