rman 1.0.12 → 1.2.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/README.md +90 -70
- package/cli.js +226 -14
- package/commands/build.command.js +1 -0
- package/commands/changed.command.js +2 -2
- package/commands/changelog.command.js +13 -15
- package/commands/config.command.js +61 -0
- package/commands/diff.command.js +9 -4
- package/commands/exec.command.js +2 -8
- package/commands/github-release.command.js +1 -0
- package/commands/info.command.d.ts +9 -0
- package/commands/info.command.js +12 -2
- package/commands/run.command.js +5 -8
- package/commands/test.command.js +1 -0
- package/commands/version.command.js +53 -14
- package/constants.js +1 -1
- package/core/config.d.ts +265 -17
- package/core/config.js +651 -76
- package/core/custom-command.d.ts +133 -0
- package/core/custom-command.js +99 -0
- package/core/extends-config.d.ts +27 -0
- package/core/extends-config.js +89 -0
- package/core/manifest.d.ts +222 -0
- package/core/manifest.js +150 -0
- package/core/merge-config.d.ts +70 -0
- package/core/merge-config.js +193 -0
- package/core/package.d.ts +73 -7
- package/core/package.js +86 -24
- package/core/plugin.d.ts +112 -0
- package/core/plugin.js +189 -0
- package/core/repository.d.ts +91 -1
- package/core/repository.js +277 -132
- package/core/resolve-target.d.ts +12 -0
- package/core/resolve-target.js +33 -0
- package/core/run-step.d.ts +75 -0
- package/core/run-step.js +1 -0
- package/core/version-scheme.d.ts +134 -0
- package/core/version-scheme.js +148 -0
- package/core/workspace.d.ts +68 -0
- package/core/workspace.js +83 -0
- package/index.d.ts +55 -8
- package/index.js +42 -7
- package/interfaces/rman-config.interface.d.ts +222 -46
- package/package.json +16 -7
- package/services/change-hash.service.d.ts +88 -0
- package/services/change-hash.service.js +112 -0
- package/services/changelog.service.d.ts +8 -13
- package/services/changelog.service.js +12 -11
- package/services/conventional-commits.service.d.ts +73 -0
- package/services/conventional-commits.service.js +116 -0
- package/services/docker-publish.service.js +1 -1
- package/services/exec.service.js +1 -1
- package/services/github-release.service.d.ts +2 -2
- package/services/github-release.service.js +10 -5
- package/services/list.service.js +5 -2
- package/services/run.service.d.ts +112 -6
- package/services/run.service.js +265 -89
- package/services/system-info.d.ts +22 -7
- package/services/system-info.js +8 -23
- package/services/version-plan.service.d.ts +244 -0
- package/services/version-plan.service.js +414 -0
- package/services/version.service.d.ts +102 -82
- package/services/version.service.js +226 -434
- package/services.d.ts +5 -3
- package/services.js +5 -3
- package/utils/bin-path.d.ts +59 -0
- package/utils/bin-path.js +82 -0
- package/utils/child-tracker.d.ts +16 -0
- package/utils/child-tracker.js +30 -0
- package/utils/exec.d.ts +13 -2
- package/utils/exec.js +17 -17
- package/utils/git.d.ts +9 -3
- package/utils/git.js +10 -2
- package/utils/package-filter.d.ts +33 -2
- package/utils/package-filter.js +47 -7
- package/utils/printable-config.d.ts +15 -0
- package/utils/printable-config.js +42 -0
- package/utils/release-version.js +3 -3
- package/utils/run-bin.d.ts +46 -0
- package/utils/run-bin.js +63 -0
- package/utils/version-stamp.d.ts +14 -6
- package/utils/version-stamp.js +25 -13
- package/commands/ci.command.js +0 -30
- package/commands/clean.command.d.ts +0 -3
- package/commands/clean.command.js +0 -36
- package/commands/publish.command.d.ts +0 -3
- package/commands/publish.command.js +0 -225
- package/rmanrc.schema.json +0 -392
- package/services/ci.service.d.ts +0 -40
- package/services/ci.service.js +0 -204
- package/services/clean.service.d.ts +0 -42
- package/services/clean.service.js +0 -226
- package/services/publish.service.d.ts +0 -79
- package/services/publish.service.js +0 -273
- package/utils/change-hash.d.ts +0 -68
- package/utils/change-hash.js +0 -98
- package/utils/conventional-commits.d.ts +0 -52
- package/utils/conventional-commits.js +0 -90
- package/utils/npm-run-path.d.ts +0 -67
- package/utils/npm-run-path.js +0 -63
- package/utils/workspace-range.d.ts +0 -17
- package/utils/workspace-range.js +0 -28
- /package/commands/{ci.command.d.ts → config.command.d.ts} +0 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import type { Package } from '../core/package.js';
|
|
2
|
+
import type { Repository } from '../core/repository.js';
|
|
3
|
+
import { VersionScheme } from '../core/version-scheme.js';
|
|
4
|
+
import { type CommitInfo, GitHelper } from '../utils/git.js';
|
|
5
|
+
import { type PackageFilterOptions } from '../utils/package-filter.js';
|
|
6
|
+
/**
|
|
7
|
+
* Question A, computed and nothing else: **which packages have changed since their last release,
|
|
8
|
+
* and what version would each get.** Reads git, the packages' current versions and `.rmanrc`;
|
|
9
|
+
* writes nothing, touches no manifest, makes no commit. `VersionService.applyPlan` does the writes.
|
|
10
|
+
*
|
|
11
|
+
* **Abstract, so the core cannot produce a plan on its own** - a repository gets one from the
|
|
12
|
+
* planner its `plugins` contribute (`rman-node`'s `NodeVersionPlanService` for a Node repository).
|
|
13
|
+
* That is not ceremony: what a release *is* differs by ecosystem, and two of the decisions below
|
|
14
|
+
* have no answer that is true of repositories in general.
|
|
15
|
+
*
|
|
16
|
+
* What stays here is what is true of any repository rman manages, and would only be copied by every
|
|
17
|
+
* plugin if it were moved out: groups, the reading of a commit as a `ChangeKind`, the in-group and
|
|
18
|
+
* cross-group cascade *mechanics*, and the monorepo root's own release identity. The bump *names*
|
|
19
|
+
* are not here either - they are the `VersionScheme`'s, so nothing in this file spells
|
|
20
|
+
* `patch`/`minor`/`major`. `getPlan` is a template holding
|
|
21
|
+
* those together - it is a plain method, not `final`, so a technology this shape genuinely does not
|
|
22
|
+
* fit overrides it outright and keeps the pieces (they are all `protected`) it still wants.
|
|
23
|
+
*
|
|
24
|
+
* The two abstract members are the ones a plugin must answer:
|
|
25
|
+
*
|
|
26
|
+
* - **`detectBoundary`** - since when is a package unreleased. Git tags are the usual answer and
|
|
27
|
+
* `detectChangeHash` is exported for it, but *which* registry to fall back to when a package has
|
|
28
|
+
* no tag yet is the ecosystem's business (npm's `npm view`, another's something else).
|
|
29
|
+
* - **`cascade`** - which of a group's other members a bump of a given size has to reach, named in
|
|
30
|
+
* the scheme's own `bumpNames` (so a planner and the scheme it ships with share one vocabulary
|
|
31
|
+
* and the core needs none of its own). The familiar
|
|
32
|
+
* patch/minor/major mapping is a statement about **npm's dependency ranges**, not about releases:
|
|
33
|
+
* it holds because `^1.2.0` already tolerates a patch, so a patch needs no downstream republish.
|
|
34
|
+
* An ecosystem pinning exact versions has to release every dependent for a patch too, and a core
|
|
35
|
+
* that assumed the caret would quietly be wrong for it.
|
|
36
|
+
*/
|
|
37
|
+
export declare abstract class VersionPlanService {
|
|
38
|
+
/**
|
|
39
|
+
* Computes what a version bump *would* do, across every package `.rmanrc group` puts together -
|
|
40
|
+
* never writes anything (no manifest edits, no git commits/tags) and safe to call any time,
|
|
41
|
+
* including as the "preview" a bare `rman version` (no bump given) stops at.
|
|
42
|
+
*
|
|
43
|
+
* Packages are partitioned into groups by their resolved `group` value (cascaded): `true`
|
|
44
|
+
* (the default) puts every such package into one implicit repo-wide group; a string joins
|
|
45
|
+
* exactly the other packages sharing that same string, regardless of the repo's default; `false`
|
|
46
|
+
* makes a package its own solo group. Each group's "current version" is always the highest
|
|
47
|
+
* version currently found among its own members (never persisted anywhere) - see
|
|
48
|
+
* `resolveGroupKey`.
|
|
49
|
+
*
|
|
50
|
+
* Within a group, a member with real commits since its own last release (or an explicit `bump`)
|
|
51
|
+
* contributes a bump; the group takes the largest of them (`VersionScheme.highestBump`) and its
|
|
52
|
+
* new version is the current one advanced by that. Which members actually receive it is
|
|
53
|
+
* `cascade`'s answer, applied by `computeGroupPlan`.
|
|
54
|
+
*
|
|
55
|
+
* Across groups: a package depending on another group's bumped package always gets its scheme's
|
|
56
|
+
* **smallest** bump of its own (never inheriting the source's) - the dependency reference itself
|
|
57
|
+
* is the only thing that changed for it. Whether that re-triggers its own group's cascade is
|
|
58
|
+
* `cascade`'s answer for that smallest bump (`'changed'` under npm, so it does not), but it can
|
|
59
|
+
* itself ripple into a third group, and so on, until nothing new is affected - see
|
|
60
|
+
* `rippleCrossGroup`.
|
|
61
|
+
*
|
|
62
|
+
* A monorepo's root package is never a real member of any group (it's never published on its
|
|
63
|
+
* own) - it gets one trailing entry instead, carrying the repository's own release identity: the
|
|
64
|
+
* single group's version when there is one, a calendar version once there are several - see
|
|
65
|
+
* `buildRootEntry`.
|
|
66
|
+
*
|
|
67
|
+
* "Since its own last release" is `detectBoundary`'s answer, which for every plugin so far is the
|
|
68
|
+
* shared `detectChangeHash` - the same boundary `changelog` measures from, so the two never
|
|
69
|
+
* disagree about which commits are unreleased.
|
|
70
|
+
*/
|
|
71
|
+
getPlan(repository: Repository, options?: VersionPlanService.Options): Promise<VersionPlanService.Entry[]>;
|
|
72
|
+
/**
|
|
73
|
+
* The commit a package's changes are measured **since** - its last release boundary, or
|
|
74
|
+
* `undefined` for a package that has never been released (the caller then reads the whole
|
|
75
|
+
* history, since nothing in it has shipped).
|
|
76
|
+
*
|
|
77
|
+
* Abstract because the fallback is: git tags answer this for any repository and
|
|
78
|
+
* `detectChangeHash` is exported for exactly that, but a package with no tag yet (rman adopted
|
|
79
|
+
* onto a repository with real release history) can only be placed by asking wherever its releases
|
|
80
|
+
* actually went - which is a registry only the ecosystem knows about.
|
|
81
|
+
*
|
|
82
|
+
* `options` is the object `getPlan` was called with, for a planner whose boundary depends on what
|
|
83
|
+
* the run asked for. A planner's *own* configuration does not come through here - it is an object
|
|
84
|
+
* now, so an injectable registry lookup belongs in its constructor (the `Deps` pattern
|
|
85
|
+
* `ChangelogService`/`PublishService` already use), where a test can supply one without every
|
|
86
|
+
* caller's options type having to know about it.
|
|
87
|
+
*/
|
|
88
|
+
protected abstract detectBoundary(git: GitHelper, pkg: Package, options: VersionPlanService.Options): Promise<string | undefined>;
|
|
89
|
+
/**
|
|
90
|
+
* How far into its own group a bump of this size has to reach - see
|
|
91
|
+
* `VersionPlanService.Cascade`.
|
|
92
|
+
*
|
|
93
|
+
* Abstract because the answer is a statement about how this ecosystem's packages *refer* to each
|
|
94
|
+
* other, not about versions. With npm's caret ranges a dependent already accepts its dependency's
|
|
95
|
+
* patch, so nothing downstream needs republishing; pin exact versions instead and every dependent
|
|
96
|
+
* needs a release of its own for the same patch. There is no mapping that is true of both, and a
|
|
97
|
+
* wrong one here is invisible - it produces a plan that simply releases too little.
|
|
98
|
+
*/
|
|
99
|
+
protected abstract cascade(bump: string): VersionPlanService.Cascade;
|
|
100
|
+
/**
|
|
101
|
+
* The largest bump `commits` ask for, in `scheme`'s own names.
|
|
102
|
+
*
|
|
103
|
+
* Two steps, and keeping them apart is the point. **What happened** comes from the commit message
|
|
104
|
+
* - a `!` marker or `BREAKING CHANGE:` footer is `'breaking'`, a `feat:` is `'feature'`, anything
|
|
105
|
+
* else (a `fix:`, an unrecognized type, a non-conventional subject) is `'fix'`, since something
|
|
106
|
+
* changed and at least the smallest release is warranted. **How the number moves** is then
|
|
107
|
+
* `scheme.bumpFor`'s answer, because "patch" is a sentence about a semver number and not about a
|
|
108
|
+
* commit.
|
|
109
|
+
*
|
|
110
|
+
* A `Release-As:` footer (see `parseReleaseAs`) replaces what that one commit's own
|
|
111
|
+
* subject/footers would otherwise imply - the escape hatch for a `feat:` that has to ship as a
|
|
112
|
+
* patch right now, without waiting for the rest of a minor's worth of work. It names a bump
|
|
113
|
+
* directly rather than a kind, so it is checked against `scheme.bumpNames` and a word the scheme
|
|
114
|
+
* does not declare falls through to what the commit itself said - which is what keeps a typo, and
|
|
115
|
+
* another tool's `Release-As: 1.2.3` in history rman was adopted onto, from deciding a release.
|
|
116
|
+
*
|
|
117
|
+
* Not abstract: conventional commits are a convention about *commit messages*, which no ecosystem
|
|
118
|
+
* owns. A planner for a repository writing them differently overrides this.
|
|
119
|
+
*/
|
|
120
|
+
protected detectBump(commits: CommitInfo[], scheme: VersionScheme): string;
|
|
121
|
+
/** `.rmanrc group` (cascaded): `true` (the default - see `resolveConfig`'s cascade, this is what a
|
|
122
|
+
* package inherits when nobody sets it at all) puts a package in the one implicit repo-wide
|
|
123
|
+
* group; a non-empty string joins exactly the other packages sharing that string, regardless of
|
|
124
|
+
* the repo's own default; `false` makes it a solo group of one. */
|
|
125
|
+
protected resolveGroupKey(pkg: Package): string;
|
|
126
|
+
/** The human-readable name behind a `groupKey` - what a plan's `group` column shows. */
|
|
127
|
+
protected groupLabel(key: string): string;
|
|
128
|
+
/**
|
|
129
|
+
* Decides one group's new version and which of its members actually receive it, writing an
|
|
130
|
+
* `Entry` per member into `entries`. `changeByPackage` holds each eligible package's own detected
|
|
131
|
+
* bump (or `undefined` for one with no real commits since its last boundary) - `undefined`
|
|
132
|
+
* here always means "unchanged", never "explicit version" (that path is handled separately).
|
|
133
|
+
*
|
|
134
|
+
* How wide the bump goes is `cascade`'s answer, and only its answer: an explicit
|
|
135
|
+
* `rman version <v>` has no bump left to consult, so it reaches the changed members alone.
|
|
136
|
+
*/
|
|
137
|
+
protected computeGroupPlan(key: string, members: Package[], changeByPackage: Map<string, VersionPlanService.Change>, explicitVersion: string | undefined, preid: string | undefined, entries: Map<string, VersionPlanService.Entry>): void;
|
|
138
|
+
/**
|
|
139
|
+
* A package depending on another group's bumped package always receives its scheme's **smallest**
|
|
140
|
+
* bump, computed from its *own* group's current ceiling (the highest `to`/version among its group
|
|
141
|
+
* right now) - never the source's version, and never the source's bump. Runs as a worklist until
|
|
142
|
+
* nothing new is affected, since bumping one package can itself cross into a third group, and so
|
|
143
|
+
* on; never touches a same-group dependent that `cascade` deliberately left alone.
|
|
144
|
+
*
|
|
145
|
+
* The smallest regardless of ecosystem, and not by oversight: nothing about the dependent changed
|
|
146
|
+
* except the reference it carries, so there is nothing for a larger bump to describe. Which bump
|
|
147
|
+
* *is* the smallest is the scheme's to say (`smallestBump`) - `patch` under semver, `revision` for
|
|
148
|
+
* a four-part scheme. A planner for which a changed dependency is not a release at all overrides
|
|
149
|
+
* this to do nothing.
|
|
150
|
+
*/
|
|
151
|
+
protected rippleCrossGroup(packages: Package[], entries: Map<string, VersionPlanService.Entry>, preid: string | undefined): void;
|
|
152
|
+
/**
|
|
153
|
+
* A monorepo root is never published on its own, but its version is still the repository's release
|
|
154
|
+
* identity - what a GitHub Release is named after. How it's computed depends on how many version
|
|
155
|
+
* lines the repo has, derived rather than configured (see `usesCalendarVersion`):
|
|
156
|
+
*
|
|
157
|
+
* - **One group**: the root simply follows it, so the repo and its packages share one number.
|
|
158
|
+
* - **Several groups** (or a repo already on calendar): a calendar version (`2026.9.15-1430`).
|
|
159
|
+
* There is no meaningful shared number to report - the old "highest version among the groups"
|
|
160
|
+
* rule would leave the root standing still whenever a *lower* line released, so a release could
|
|
161
|
+
* happen with no identity of its own, and a semver-looking identity would anyway claim something
|
|
162
|
+
* untrue about packages sitting on entirely different lines.
|
|
163
|
+
*
|
|
164
|
+
* Reports `'no-change'` (not `'bump'`) when nothing in the repository changed at all.
|
|
165
|
+
*/
|
|
166
|
+
protected buildRootEntry(repository: Repository, memberEntries: VersionPlanService.Entry[], context: {
|
|
167
|
+
groupCount: number;
|
|
168
|
+
lastReleaseVersion?: string;
|
|
169
|
+
now: () => Date;
|
|
170
|
+
}): VersionPlanService.Entry;
|
|
171
|
+
}
|
|
172
|
+
export declare namespace VersionPlanService {
|
|
173
|
+
/**
|
|
174
|
+
* How far into its own group a bump reaches - `VersionPlanService.cascade`'s answer, per bump.
|
|
175
|
+
*
|
|
176
|
+
* - `'changed'` - only the members with commits of their own.
|
|
177
|
+
* - `'dependents'` - those, plus every in-group package that (transitively) depends on one.
|
|
178
|
+
* - `'group'` - every member, changed or not.
|
|
179
|
+
*/
|
|
180
|
+
type Cascade = 'changed' | 'dependents' | 'group';
|
|
181
|
+
/** What was found for one package before groups are resolved: the bump its own commits ask for
|
|
182
|
+
* (in its scheme's names, or `undefined` when an explicit version makes the question moot) and
|
|
183
|
+
* why, which becomes the plan entry's `reason`. */
|
|
184
|
+
interface Change {
|
|
185
|
+
bump?: string;
|
|
186
|
+
reason: string;
|
|
187
|
+
}
|
|
188
|
+
interface Options extends PackageFilterOptions {
|
|
189
|
+
/** One of the root scheme's own `bumpNames` (applied to every group that has real changes) or a
|
|
190
|
+
* concrete version it recognizes (applied as the literal new version wherever something
|
|
191
|
+
* changed) - either way, this replaces auto-detection entirely. Omit to auto-detect the bump
|
|
192
|
+
* per group from conventional-commit subjects since each package's/group's last release tag. */
|
|
193
|
+
bump?: string;
|
|
194
|
+
/** A package with uncommitted local changes is excluded from bumping (status `'skip'`)
|
|
195
|
+
* instead of aborting the whole plan (status `'error'`). Default false. */
|
|
196
|
+
ignoreDirty?: boolean;
|
|
197
|
+
/** Makes every computed bump a prerelease (`1.2.3` -> `1.3.0-beta.0` for a `minor`, say)
|
|
198
|
+
* tagged with this identifier, instead of a normal release - same idea as `npm version
|
|
199
|
+
* <type> --preid <name>`. A group already sitting on a matching prerelease (same identifier)
|
|
200
|
+
* just has its prerelease counter incremented instead of jumping to a new base version - see
|
|
201
|
+
* `VersionScheme.next`. Has no effect when `bump` is an explicit semver version rather than a keyword
|
|
202
|
+
* (there's no bump left to "pre-fix" at that point). */
|
|
203
|
+
preid?: string;
|
|
204
|
+
/** Clock behind a monorepo root's calendar release version - injectable so tests are
|
|
205
|
+
* deterministic. Default `() => new Date()`. */
|
|
206
|
+
now?: () => Date;
|
|
207
|
+
}
|
|
208
|
+
/** One package's outcome in a version plan - see `getPlan`. */
|
|
209
|
+
interface Entry {
|
|
210
|
+
package: Package;
|
|
211
|
+
/** Internal group identity packages are batched by (not for display) - see `resolveGroupKey`. */
|
|
212
|
+
groupKey: string;
|
|
213
|
+
/** Human-readable group name: `'default'` for the implicit repo-wide group, the configured
|
|
214
|
+
* name for a named `group`, or the package's own name when it isn't grouped with anyone. */
|
|
215
|
+
group: string;
|
|
216
|
+
status: 'bump' | 'skip' | 'error' | 'no-change';
|
|
217
|
+
from: string;
|
|
218
|
+
/** Only set when `status === 'bump'`. */
|
|
219
|
+
to?: string;
|
|
220
|
+
/** Human-readable explanation - e.g. why a package was skipped, or why it's being bumped
|
|
221
|
+
* despite having no commits of its own (a dependency of it changed elsewhere). */
|
|
222
|
+
reason?: string;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Registers the planner every `version`/`changed` run will use, replacing any previous one.
|
|
226
|
+
*
|
|
227
|
+
* **One slot, last registration wins** - unlike `Manifest`/`Workspace`, which keep a list and take
|
|
228
|
+
* the first provider that *recognizes* a repository. A planner has nothing to recognize: asked for
|
|
229
|
+
* a plan it always has one, so "first that answers" would just mean "first registered" and a
|
|
230
|
+
* repository layering its own policy plugin after `rman-node` could never take effect - which is
|
|
231
|
+
* the only reason to name two in the first place.
|
|
232
|
+
*/
|
|
233
|
+
function setPlanner(planner: VersionPlanService): void;
|
|
234
|
+
/** For tests, which would otherwise leak a planner into every later case in the process. */
|
|
235
|
+
function clearPlanner(): void;
|
|
236
|
+
/**
|
|
237
|
+
* The registered planner. **Throws** when there is none, rather than falling back to some
|
|
238
|
+
* built-in default: `Manifest` and `Workspace` can degrade honestly (a package named after its
|
|
239
|
+
* directory, a repository that is its own single package), but there is no version plan that is
|
|
240
|
+
* merely a diminished one - a wrong cascade or a wrong boundary reports a release that is
|
|
241
|
+
* plausible and untrue.
|
|
242
|
+
*/
|
|
243
|
+
function getPlanner(): VersionPlanService;
|
|
244
|
+
}
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { assertOneScheme, semverScheme, VersionScheme } from '../core/version-scheme.js';
|
|
3
|
+
import { GitHelper } from '../utils/git.js';
|
|
4
|
+
import { filterPackages } from '../utils/package-filter.js';
|
|
5
|
+
import { findLastReleaseVersion, formatCalendarVersion, usesCalendarVersion } from '../utils/release-version.js';
|
|
6
|
+
import { ConventionalCommitsService } from './conventional-commits.service.js';
|
|
7
|
+
/**
|
|
8
|
+
* Question A, computed and nothing else: **which packages have changed since their last release,
|
|
9
|
+
* and what version would each get.** Reads git, the packages' current versions and `.rmanrc`;
|
|
10
|
+
* writes nothing, touches no manifest, makes no commit. `VersionService.applyPlan` does the writes.
|
|
11
|
+
*
|
|
12
|
+
* **Abstract, so the core cannot produce a plan on its own** - a repository gets one from the
|
|
13
|
+
* planner its `plugins` contribute (`rman-node`'s `NodeVersionPlanService` for a Node repository).
|
|
14
|
+
* That is not ceremony: what a release *is* differs by ecosystem, and two of the decisions below
|
|
15
|
+
* have no answer that is true of repositories in general.
|
|
16
|
+
*
|
|
17
|
+
* What stays here is what is true of any repository rman manages, and would only be copied by every
|
|
18
|
+
* plugin if it were moved out: groups, the reading of a commit as a `ChangeKind`, the in-group and
|
|
19
|
+
* cross-group cascade *mechanics*, and the monorepo root's own release identity. The bump *names*
|
|
20
|
+
* are not here either - they are the `VersionScheme`'s, so nothing in this file spells
|
|
21
|
+
* `patch`/`minor`/`major`. `getPlan` is a template holding
|
|
22
|
+
* those together - it is a plain method, not `final`, so a technology this shape genuinely does not
|
|
23
|
+
* fit overrides it outright and keeps the pieces (they are all `protected`) it still wants.
|
|
24
|
+
*
|
|
25
|
+
* The two abstract members are the ones a plugin must answer:
|
|
26
|
+
*
|
|
27
|
+
* - **`detectBoundary`** - since when is a package unreleased. Git tags are the usual answer and
|
|
28
|
+
* `detectChangeHash` is exported for it, but *which* registry to fall back to when a package has
|
|
29
|
+
* no tag yet is the ecosystem's business (npm's `npm view`, another's something else).
|
|
30
|
+
* - **`cascade`** - which of a group's other members a bump of a given size has to reach, named in
|
|
31
|
+
* the scheme's own `bumpNames` (so a planner and the scheme it ships with share one vocabulary
|
|
32
|
+
* and the core needs none of its own). The familiar
|
|
33
|
+
* patch/minor/major mapping is a statement about **npm's dependency ranges**, not about releases:
|
|
34
|
+
* it holds because `^1.2.0` already tolerates a patch, so a patch needs no downstream republish.
|
|
35
|
+
* An ecosystem pinning exact versions has to release every dependent for a patch too, and a core
|
|
36
|
+
* that assumed the caret would quietly be wrong for it.
|
|
37
|
+
*/
|
|
38
|
+
export class VersionPlanService {
|
|
39
|
+
/**
|
|
40
|
+
* Computes what a version bump *would* do, across every package `.rmanrc group` puts together -
|
|
41
|
+
* never writes anything (no manifest edits, no git commits/tags) and safe to call any time,
|
|
42
|
+
* including as the "preview" a bare `rman version` (no bump given) stops at.
|
|
43
|
+
*
|
|
44
|
+
* Packages are partitioned into groups by their resolved `group` value (cascaded): `true`
|
|
45
|
+
* (the default) puts every such package into one implicit repo-wide group; a string joins
|
|
46
|
+
* exactly the other packages sharing that same string, regardless of the repo's default; `false`
|
|
47
|
+
* makes a package its own solo group. Each group's "current version" is always the highest
|
|
48
|
+
* version currently found among its own members (never persisted anywhere) - see
|
|
49
|
+
* `resolveGroupKey`.
|
|
50
|
+
*
|
|
51
|
+
* Within a group, a member with real commits since its own last release (or an explicit `bump`)
|
|
52
|
+
* contributes a bump; the group takes the largest of them (`VersionScheme.highestBump`) and its
|
|
53
|
+
* new version is the current one advanced by that. Which members actually receive it is
|
|
54
|
+
* `cascade`'s answer, applied by `computeGroupPlan`.
|
|
55
|
+
*
|
|
56
|
+
* Across groups: a package depending on another group's bumped package always gets its scheme's
|
|
57
|
+
* **smallest** bump of its own (never inheriting the source's) - the dependency reference itself
|
|
58
|
+
* is the only thing that changed for it. Whether that re-triggers its own group's cascade is
|
|
59
|
+
* `cascade`'s answer for that smallest bump (`'changed'` under npm, so it does not), but it can
|
|
60
|
+
* itself ripple into a third group, and so on, until nothing new is affected - see
|
|
61
|
+
* `rippleCrossGroup`.
|
|
62
|
+
*
|
|
63
|
+
* A monorepo's root package is never a real member of any group (it's never published on its
|
|
64
|
+
* own) - it gets one trailing entry instead, carrying the repository's own release identity: the
|
|
65
|
+
* single group's version when there is one, a calendar version once there are several - see
|
|
66
|
+
* `buildRootEntry`.
|
|
67
|
+
*
|
|
68
|
+
* "Since its own last release" is `detectBoundary`'s answer, which for every plugin so far is the
|
|
69
|
+
* shared `detectChangeHash` - the same boundary `changelog` measures from, so the two never
|
|
70
|
+
* disagree about which commits are unreleased.
|
|
71
|
+
*/
|
|
72
|
+
async getPlan(repository, options = {}) {
|
|
73
|
+
const bump = options.bump?.trim();
|
|
74
|
+
/**
|
|
75
|
+
* Both halves are the *root* scheme's to judge: an explicit `rman version <bump|version>` is one
|
|
76
|
+
* value for the whole run, so there is no per-package scheme to ask. (A group numbering
|
|
77
|
+
* differently from the root would have to be given its own run - `assertOneScheme` only promises
|
|
78
|
+
* that a group agrees with itself.)
|
|
79
|
+
*/
|
|
80
|
+
const scheme = repository.rootPackage.versionScheme;
|
|
81
|
+
const explicitBump = bump && scheme.bumpNames.includes(bump) ? bump : undefined;
|
|
82
|
+
const explicitVersion = bump && !explicitBump && scheme.isValid(bump) ? bump : undefined;
|
|
83
|
+
if (bump && !explicitBump && !explicitVersion) {
|
|
84
|
+
/**
|
|
85
|
+
* Every word here comes from the scheme that rejected it - the bump names as well as the
|
|
86
|
+
* format's name. Neither is rman's to state: `patch`/`minor`/`major` are semver's words for
|
|
87
|
+
* how a number moves, and a `major.minor.build.revision` scheme has four sizes and no `patch`
|
|
88
|
+
* at all. Listing the scheme's own names also beats a vague "a valid version", which leaves
|
|
89
|
+
* the reader to guess which spelling they missed.
|
|
90
|
+
*/
|
|
91
|
+
throw new Error(`Invalid "bump": "${bump}" (expected ${scheme.bumpNames.map(n => `"${n}"`).join(', ')}, ` +
|
|
92
|
+
`or a valid ${scheme.name} version)`);
|
|
93
|
+
}
|
|
94
|
+
const git = new GitHelper({ cwd: repository.dirname });
|
|
95
|
+
const packages = filterPackages(repository.getPackages(), options);
|
|
96
|
+
const dirtyFiles = await git.listDirtyFiles({ absolute: true });
|
|
97
|
+
const isDirty = (pkg) => dirtyFiles.some(f => !path.relative(pkg.dirname, f).startsWith('..'));
|
|
98
|
+
const entries = new Map();
|
|
99
|
+
const eligible = [];
|
|
100
|
+
for (const pkg of packages) {
|
|
101
|
+
if (isDirty(pkg)) {
|
|
102
|
+
entries.set(pkg.name, {
|
|
103
|
+
package: pkg,
|
|
104
|
+
groupKey: this.resolveGroupKey(pkg),
|
|
105
|
+
group: this.groupLabel(this.resolveGroupKey(pkg)),
|
|
106
|
+
status: options.ignoreDirty ? 'skip' : 'error',
|
|
107
|
+
from: pkg.version,
|
|
108
|
+
reason: 'uncommitted local changes',
|
|
109
|
+
});
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
eligible.push(pkg);
|
|
113
|
+
}
|
|
114
|
+
const commitMessage = repository.rootPackage.config?.version?.commitMessage;
|
|
115
|
+
const changeByPackage = new Map();
|
|
116
|
+
await Promise.all(eligible.map(async (pkg) => {
|
|
117
|
+
if (explicitVersion) {
|
|
118
|
+
changeByPackage.set(pkg.name, { bump: undefined, reason: `explicit version ${explicitVersion}` });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const since = await this.detectBoundary(git, pkg, options);
|
|
122
|
+
const commits = since ? await git.listCommits({ hash: since }) : await git.listAllCommits();
|
|
123
|
+
const belongsToPkg = (c) => c.files.some(f => !path.relative(pkg.dirname, f).startsWith('..'));
|
|
124
|
+
const real = commits.filter(c => belongsToPkg(c) && !ConventionalCommitsService.isReleaseCommit(c.subject, commitMessage));
|
|
125
|
+
if (!real.length)
|
|
126
|
+
return;
|
|
127
|
+
changeByPackage.set(pkg.name, {
|
|
128
|
+
bump: explicitBump ?? this.detectBump(real, pkg.versionScheme),
|
|
129
|
+
reason: since ? `changed since ${since}` : 'unreleased commits',
|
|
130
|
+
});
|
|
131
|
+
}));
|
|
132
|
+
const groups = new Map();
|
|
133
|
+
for (const pkg of eligible) {
|
|
134
|
+
const key = this.resolveGroupKey(pkg);
|
|
135
|
+
const list = groups.get(key);
|
|
136
|
+
if (list)
|
|
137
|
+
list.push(pkg);
|
|
138
|
+
else
|
|
139
|
+
groups.set(key, [pkg]);
|
|
140
|
+
}
|
|
141
|
+
for (const [key, members] of groups) {
|
|
142
|
+
this.computeGroupPlan(key, members, changeByPackage, explicitVersion, options.preid, entries);
|
|
143
|
+
}
|
|
144
|
+
this.rippleCrossGroup(packages, entries, options.preid);
|
|
145
|
+
const result = packages.map(pkg => entries.get(pkg.name));
|
|
146
|
+
if (repository.monorepo) {
|
|
147
|
+
result.push(this.buildRootEntry(repository, result, {
|
|
148
|
+
groupCount: groups.size,
|
|
149
|
+
lastReleaseVersion: await findLastReleaseVersion(git, repository.rootPackage),
|
|
150
|
+
now: options.now ?? (() => new Date()),
|
|
151
|
+
}));
|
|
152
|
+
}
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* The largest bump `commits` ask for, in `scheme`'s own names.
|
|
157
|
+
*
|
|
158
|
+
* Two steps, and keeping them apart is the point. **What happened** comes from the commit message
|
|
159
|
+
* - a `!` marker or `BREAKING CHANGE:` footer is `'breaking'`, a `feat:` is `'feature'`, anything
|
|
160
|
+
* else (a `fix:`, an unrecognized type, a non-conventional subject) is `'fix'`, since something
|
|
161
|
+
* changed and at least the smallest release is warranted. **How the number moves** is then
|
|
162
|
+
* `scheme.bumpFor`'s answer, because "patch" is a sentence about a semver number and not about a
|
|
163
|
+
* commit.
|
|
164
|
+
*
|
|
165
|
+
* A `Release-As:` footer (see `parseReleaseAs`) replaces what that one commit's own
|
|
166
|
+
* subject/footers would otherwise imply - the escape hatch for a `feat:` that has to ship as a
|
|
167
|
+
* patch right now, without waiting for the rest of a minor's worth of work. It names a bump
|
|
168
|
+
* directly rather than a kind, so it is checked against `scheme.bumpNames` and a word the scheme
|
|
169
|
+
* does not declare falls through to what the commit itself said - which is what keeps a typo, and
|
|
170
|
+
* another tool's `Release-As: 1.2.3` in history rman was adopted onto, from deciding a release.
|
|
171
|
+
*
|
|
172
|
+
* Not abstract: conventional commits are a convention about *commit messages*, which no ecosystem
|
|
173
|
+
* owns. A planner for a repository writing them differently overrides this.
|
|
174
|
+
*/
|
|
175
|
+
detectBump(commits, scheme) {
|
|
176
|
+
const asked = [scheme.bumpFor('fix')];
|
|
177
|
+
for (const c of commits) {
|
|
178
|
+
const override = ConventionalCommitsService.parseReleaseAs(c.body);
|
|
179
|
+
/**
|
|
180
|
+
* Honoured only when `scheme` actually declares it. Measured, because the obvious `if
|
|
181
|
+
* (override)` is wrong in a way that looks fine: a footer the scheme does not know would
|
|
182
|
+
* *replace* what the commit itself said, so a `feat:` carrying release-please's own
|
|
183
|
+
* `Release-As: 1.2.3` came out a patch instead of a minor. An unrecognized word means "no
|
|
184
|
+
* override", exactly as it did when only three were ever recognized.
|
|
185
|
+
*/
|
|
186
|
+
if (override && scheme.bumpNames.includes(override)) {
|
|
187
|
+
asked.push(override);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
asked.push(scheme.bumpFor(kindOf(c)));
|
|
191
|
+
}
|
|
192
|
+
return scheme.highestBump(asked);
|
|
193
|
+
}
|
|
194
|
+
/** `.rmanrc group` (cascaded): `true` (the default - see `resolveConfig`'s cascade, this is what a
|
|
195
|
+
* package inherits when nobody sets it at all) puts a package in the one implicit repo-wide
|
|
196
|
+
* group; a non-empty string joins exactly the other packages sharing that string, regardless of
|
|
197
|
+
* the repo's own default; `false` makes it a solo group of one. */
|
|
198
|
+
resolveGroupKey(pkg) {
|
|
199
|
+
const g = pkg.config?.group;
|
|
200
|
+
if (g === false)
|
|
201
|
+
return `solo:${pkg.name}`;
|
|
202
|
+
if (typeof g === 'string' && g)
|
|
203
|
+
return `named:${g}`;
|
|
204
|
+
return 'default';
|
|
205
|
+
}
|
|
206
|
+
/** The human-readable name behind a `groupKey` - what a plan's `group` column shows. */
|
|
207
|
+
groupLabel(key) {
|
|
208
|
+
if (key.startsWith('named:'))
|
|
209
|
+
return key.slice('named:'.length);
|
|
210
|
+
if (key.startsWith('solo:'))
|
|
211
|
+
return key.slice('solo:'.length);
|
|
212
|
+
return key;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Decides one group's new version and which of its members actually receive it, writing an
|
|
216
|
+
* `Entry` per member into `entries`. `changeByPackage` holds each eligible package's own detected
|
|
217
|
+
* bump (or `undefined` for one with no real commits since its last boundary) - `undefined`
|
|
218
|
+
* here always means "unchanged", never "explicit version" (that path is handled separately).
|
|
219
|
+
*
|
|
220
|
+
* How wide the bump goes is `cascade`'s answer, and only its answer: an explicit
|
|
221
|
+
* `rman version <v>` has no bump left to consult, so it reaches the changed members alone.
|
|
222
|
+
*/
|
|
223
|
+
computeGroupPlan(key, members, changeByPackage, explicitVersion, preid, entries) {
|
|
224
|
+
const label = this.groupLabel(key);
|
|
225
|
+
/** One version line, so one scheme - the members are about to be compared against each other and
|
|
226
|
+
* bumped together, which two schemes would make meaningless. */
|
|
227
|
+
assertOneScheme(members.map(m => ({ packageName: m.name, scheme: m.versionScheme })), label);
|
|
228
|
+
const scheme = members[0]?.versionScheme ?? semverScheme;
|
|
229
|
+
const changed = members.filter(m => changeByPackage.has(m.name));
|
|
230
|
+
if (!changed.length) {
|
|
231
|
+
for (const m of members) {
|
|
232
|
+
entries.set(m.name, { package: m, groupKey: key, group: label, status: 'no-change', from: m.version });
|
|
233
|
+
}
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const current = scheme.highestVersion(members.map(m => m.version)) ?? members[0].version;
|
|
237
|
+
let to;
|
|
238
|
+
let bump;
|
|
239
|
+
if (explicitVersion) {
|
|
240
|
+
to = explicitVersion;
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
/** The largest any changed member asked for - `changed` is non-empty here, so this always
|
|
244
|
+
* resolves; the `!` is for the type. */
|
|
245
|
+
bump = scheme.highestBump(changed.map(m => changeByPackage.get(m.name).bump));
|
|
246
|
+
to = scheme.next(current, bump, { preid });
|
|
247
|
+
}
|
|
248
|
+
const bumping = new Set(changed);
|
|
249
|
+
const cascade = bump ? this.cascade(bump) : 'changed';
|
|
250
|
+
if (cascade === 'group') {
|
|
251
|
+
for (const m of members)
|
|
252
|
+
bumping.add(m);
|
|
253
|
+
}
|
|
254
|
+
else if (cascade === 'dependents') {
|
|
255
|
+
const worklist = [...changed];
|
|
256
|
+
while (worklist.length) {
|
|
257
|
+
const cur = worklist.pop();
|
|
258
|
+
for (const m of members) {
|
|
259
|
+
if (bumping.has(m))
|
|
260
|
+
continue;
|
|
261
|
+
if (m.dependencies.includes(cur)) {
|
|
262
|
+
bumping.add(m);
|
|
263
|
+
worklist.push(m);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/** Why a member with no commits of its own is being bumped. `'group'` reaches members that
|
|
269
|
+
* depend on nothing at all, so calling those a "dependent" was simply untrue. */
|
|
270
|
+
const inherited = cascade === 'group' ? `in-group member of a ${bump} change` : `in-group dependent of a ${bump} change`;
|
|
271
|
+
for (const m of members) {
|
|
272
|
+
if (bumping.has(m)) {
|
|
273
|
+
const own = changeByPackage.get(m.name);
|
|
274
|
+
entries.set(m.name, {
|
|
275
|
+
package: m,
|
|
276
|
+
groupKey: key,
|
|
277
|
+
group: label,
|
|
278
|
+
status: 'bump',
|
|
279
|
+
from: m.version,
|
|
280
|
+
to,
|
|
281
|
+
reason: own?.reason ?? inherited,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
entries.set(m.name, { package: m, groupKey: key, group: label, status: 'no-change', from: m.version });
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* A package depending on another group's bumped package always receives its scheme's **smallest**
|
|
291
|
+
* bump, computed from its *own* group's current ceiling (the highest `to`/version among its group
|
|
292
|
+
* right now) - never the source's version, and never the source's bump. Runs as a worklist until
|
|
293
|
+
* nothing new is affected, since bumping one package can itself cross into a third group, and so
|
|
294
|
+
* on; never touches a same-group dependent that `cascade` deliberately left alone.
|
|
295
|
+
*
|
|
296
|
+
* The smallest regardless of ecosystem, and not by oversight: nothing about the dependent changed
|
|
297
|
+
* except the reference it carries, so there is nothing for a larger bump to describe. Which bump
|
|
298
|
+
* *is* the smallest is the scheme's to say (`smallestBump`) - `patch` under semver, `revision` for
|
|
299
|
+
* a four-part scheme. A planner for which a changed dependency is not a release at all overrides
|
|
300
|
+
* this to do nothing.
|
|
301
|
+
*/
|
|
302
|
+
rippleCrossGroup(packages, entries, preid) {
|
|
303
|
+
const worklist = [...entries.values()].filter(e => e.status === 'bump');
|
|
304
|
+
while (worklist.length) {
|
|
305
|
+
const source = worklist.shift();
|
|
306
|
+
for (const pkg of packages) {
|
|
307
|
+
const entry = entries.get(pkg.name);
|
|
308
|
+
if (entry.status === 'bump' || entry.groupKey === source.groupKey)
|
|
309
|
+
continue;
|
|
310
|
+
if (!pkg.dependencies.includes(source.package))
|
|
311
|
+
continue;
|
|
312
|
+
/** The group always contains `pkg` itself, so there is always at least one version here -
|
|
313
|
+
* the fallback is for the type, not for a case that can happen. */
|
|
314
|
+
const groupCeiling = pkg.versionScheme.highestVersion(packages
|
|
315
|
+
.filter(p => entries.get(p.name).groupKey === entry.groupKey)
|
|
316
|
+
.map(p => entries.get(p.name).to ?? p.version)) ?? pkg.version;
|
|
317
|
+
const next = {
|
|
318
|
+
...entry,
|
|
319
|
+
status: 'bump',
|
|
320
|
+
to: pkg.versionScheme.next(groupCeiling, pkg.versionScheme.smallestBump(), { preid }),
|
|
321
|
+
reason: `depends on ${source.package.name}@${source.to}`,
|
|
322
|
+
};
|
|
323
|
+
entries.set(pkg.name, next);
|
|
324
|
+
worklist.push(next);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* A monorepo root is never published on its own, but its version is still the repository's release
|
|
330
|
+
* identity - what a GitHub Release is named after. How it's computed depends on how many version
|
|
331
|
+
* lines the repo has, derived rather than configured (see `usesCalendarVersion`):
|
|
332
|
+
*
|
|
333
|
+
* - **One group**: the root simply follows it, so the repo and its packages share one number.
|
|
334
|
+
* - **Several groups** (or a repo already on calendar): a calendar version (`2026.9.15-1430`).
|
|
335
|
+
* There is no meaningful shared number to report - the old "highest version among the groups"
|
|
336
|
+
* rule would leave the root standing still whenever a *lower* line released, so a release could
|
|
337
|
+
* happen with no identity of its own, and a semver-looking identity would anyway claim something
|
|
338
|
+
* untrue about packages sitting on entirely different lines.
|
|
339
|
+
*
|
|
340
|
+
* Reports `'no-change'` (not `'bump'`) when nothing in the repository changed at all.
|
|
341
|
+
*/
|
|
342
|
+
buildRootEntry(repository, memberEntries, context) {
|
|
343
|
+
const root = repository.rootPackage;
|
|
344
|
+
const anyBumped = memberEntries.some(e => e.status === 'bump');
|
|
345
|
+
if (!anyBumped) {
|
|
346
|
+
return { package: root, groupKey: '__root__', group: 'root', status: 'no-change', from: root.version };
|
|
347
|
+
}
|
|
348
|
+
const calendar = usesCalendarVersion({
|
|
349
|
+
groupCount: context.groupCount,
|
|
350
|
+
rootVersion: root.version,
|
|
351
|
+
lastReleaseVersion: context.lastReleaseVersion,
|
|
352
|
+
});
|
|
353
|
+
const finalVersions = memberEntries.map(e => e.to ?? e.from);
|
|
354
|
+
const to = calendar
|
|
355
|
+
? formatCalendarVersion(context.now())
|
|
356
|
+
: (root.versionScheme.highestVersion(finalVersions) ?? root.version);
|
|
357
|
+
return {
|
|
358
|
+
package: root,
|
|
359
|
+
groupKey: '__root__',
|
|
360
|
+
group: 'root',
|
|
361
|
+
status: 'bump',
|
|
362
|
+
from: root.version,
|
|
363
|
+
to,
|
|
364
|
+
reason: calendar
|
|
365
|
+
? 'repository release identity - several version lines, so no shared number to report'
|
|
366
|
+
: 'informational - monorepo root is never published on its own',
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
(function (VersionPlanService) {
|
|
371
|
+
/**
|
|
372
|
+
* Registers the planner every `version`/`changed` run will use, replacing any previous one.
|
|
373
|
+
*
|
|
374
|
+
* **One slot, last registration wins** - unlike `Manifest`/`Workspace`, which keep a list and take
|
|
375
|
+
* the first provider that *recognizes* a repository. A planner has nothing to recognize: asked for
|
|
376
|
+
* a plan it always has one, so "first that answers" would just mean "first registered" and a
|
|
377
|
+
* repository layering its own policy plugin after `rman-node` could never take effect - which is
|
|
378
|
+
* the only reason to name two in the first place.
|
|
379
|
+
*/
|
|
380
|
+
function setPlanner(planner) {
|
|
381
|
+
current = planner;
|
|
382
|
+
}
|
|
383
|
+
VersionPlanService.setPlanner = setPlanner;
|
|
384
|
+
/** For tests, which would otherwise leak a planner into every later case in the process. */
|
|
385
|
+
function clearPlanner() {
|
|
386
|
+
current = undefined;
|
|
387
|
+
}
|
|
388
|
+
VersionPlanService.clearPlanner = clearPlanner;
|
|
389
|
+
/**
|
|
390
|
+
* The registered planner. **Throws** when there is none, rather than falling back to some
|
|
391
|
+
* built-in default: `Manifest` and `Workspace` can degrade honestly (a package named after its
|
|
392
|
+
* directory, a repository that is its own single package), but there is no version plan that is
|
|
393
|
+
* merely a diminished one - a wrong cascade or a wrong boundary reports a release that is
|
|
394
|
+
* plausible and untrue.
|
|
395
|
+
*/
|
|
396
|
+
function getPlanner() {
|
|
397
|
+
if (!current) {
|
|
398
|
+
throw new Error('No version planner is registered, so no version plan can be computed. Name a plugin that ' +
|
|
399
|
+
'contributes one in .rmanrc "plugins" - "rman-node" for a Node repository.');
|
|
400
|
+
}
|
|
401
|
+
return current;
|
|
402
|
+
}
|
|
403
|
+
VersionPlanService.getPlanner = getPlanner;
|
|
404
|
+
let current;
|
|
405
|
+
})(VersionPlanService || (VersionPlanService = {}));
|
|
406
|
+
/** What one commit says happened, with no reference to any version format - `VersionScheme.bumpFor`
|
|
407
|
+
* turns it into a number's movement. Everything unrecognized is a `'fix'`: something changed, so
|
|
408
|
+
* the smallest release is still warranted. */
|
|
409
|
+
function kindOf(commit) {
|
|
410
|
+
const parsed = ConventionalCommitsService.parseSubject(commit.subject);
|
|
411
|
+
if (parsed?.breaking || ConventionalCommitsService.hasBreakingChangeFooter(commit.body))
|
|
412
|
+
return 'breaking';
|
|
413
|
+
return parsed?.type === 'feat' ? 'feature' : 'fix';
|
|
414
|
+
}
|