versionary 0.32.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/action/index.js +2 -2
- package/dist/cli/index.js +18 -9
- package/dist/config/schema.d.ts +4 -7
- package/dist/config/schema.js +8 -18
- package/dist/release/artifact-rules.d.ts +2 -2
- package/dist/release/artifact-rules.js +4 -4
- package/dist/release/changelog.d.ts +1 -3
- package/dist/release/changelog.js +37 -14
- package/dist/release/plan.d.ts +25 -5
- package/dist/release/plan.js +252 -64
- package/dist/release/pr.d.ts +7 -26
- package/dist/release/pr.js +11 -65
- package/dist/release/release-as.d.ts +29 -0
- package/dist/release/release-as.js +70 -0
- package/dist/release/release.d.ts +0 -8
- package/dist/release/release.js +0 -8
- package/dist/release/state.js +3 -12
- package/dist/strategy/composite.js +11 -0
- package/dist/strategy/node.js +15 -0
- package/dist/strategy/rust.js +50 -3
- package/dist/strategy/types.d.ts +11 -0
- package/dist/types/config.d.ts +1 -3
- package/package.json +2 -2
package/dist/release/plan.js
CHANGED
|
@@ -4,6 +4,7 @@ import { loadConfig } from "../config/load-config.js";
|
|
|
4
4
|
import { analyzeParsedCommits, applyRevertSuppression, getParsedCommitsForPath, getParsedCommitsSinceLastTag, } from "../git/commits.js";
|
|
5
5
|
import { resolvePackageStrategyContext } from "../strategy/package-context.js";
|
|
6
6
|
import { resolveVersionStrategy } from "../strategy/resolve.js";
|
|
7
|
+
import { releaseTypeBetween, resolveReleaseAsOverride } from "./release-as.js";
|
|
7
8
|
import { bumpVersion, maxReleaseType } from "./semver.js";
|
|
8
9
|
import { readBaselineSha, readReleaseTargets } from "./state.js";
|
|
9
10
|
function getMode(configMode) {
|
|
@@ -78,19 +79,34 @@ export function createReleasePlan(cwd = process.cwd()) {
|
|
|
78
79
|
}
|
|
79
80
|
const effectiveCommits = applyRevertSuppression(parsedCommits);
|
|
80
81
|
const commits = effectiveCommits;
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
82
|
+
const analyzedType = analyzeParsedCommits(parsedCommits);
|
|
83
|
+
const override = resolveReleaseAsOverride(commits, packageCurrentVersion);
|
|
84
|
+
let releaseType;
|
|
85
|
+
let nextVersion;
|
|
86
|
+
let bumpReason;
|
|
87
|
+
if (override) {
|
|
88
|
+
// An explicit `Release-As:` footer forces a release with the requested
|
|
89
|
+
// version, even when the conventional-commit analysis produces no bump.
|
|
90
|
+
nextVersion = override.version;
|
|
91
|
+
releaseType = releaseTypeBetween(packageCurrentVersion, override.version);
|
|
92
|
+
bumpReason = "release-as";
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
releaseType = analyzedType;
|
|
96
|
+
nextVersion = releaseType
|
|
97
|
+
? bumpVersion(packageCurrentVersion, releaseType, {
|
|
98
|
+
allowStableMajor: allowStableMajorForPath(pkg.path),
|
|
99
|
+
})
|
|
100
|
+
: null;
|
|
101
|
+
bumpReason = nextVersion ? "direct" : undefined;
|
|
102
|
+
}
|
|
87
103
|
return {
|
|
88
104
|
path: pkg.path,
|
|
89
105
|
implicitRoot: pkg.implicitRoot,
|
|
90
106
|
releaseType,
|
|
91
107
|
currentVersion: packageCurrentVersion,
|
|
92
108
|
nextVersion,
|
|
93
|
-
bumpReason
|
|
109
|
+
bumpReason,
|
|
94
110
|
commits,
|
|
95
111
|
parsedCommits,
|
|
96
112
|
resolvedVersionFile: packageContext.versionFile,
|
|
@@ -119,39 +135,179 @@ export function createReleasePlan(cwd = process.cwd()) {
|
|
|
119
135
|
...(implicitRootPlan ? [implicitRootPlan] : []),
|
|
120
136
|
].sort((a, b) => a.path.localeCompare(b.path));
|
|
121
137
|
const packageCurrentVersionByPath = {};
|
|
122
|
-
const packageNextVersionByPath = {};
|
|
123
138
|
const strategyPackagesByName = new Map();
|
|
139
|
+
// Strategy contexts are shared by reference with their group so that forcing
|
|
140
|
+
// a bump during the fixpoint below is immediately visible to the next
|
|
141
|
+
// `propagateDependentPatchImpacts` query.
|
|
142
|
+
const strategyContextByPath = new Map();
|
|
143
|
+
const strategyByPath = new Map();
|
|
124
144
|
for (const packagePlan of packagePlans) {
|
|
125
145
|
const packageConfig = loaded.config.packages?.[packagePlan.path] ?? {};
|
|
126
146
|
const packageContext = resolvePackageStrategyContext(loaded.config, packagePlan.path, packageConfig);
|
|
147
|
+
const strategyContext = {
|
|
148
|
+
packagePath: packagePlan.path,
|
|
149
|
+
versionFile: packagePlan.resolvedVersionFile,
|
|
150
|
+
currentVersion: packagePlan.currentVersion,
|
|
151
|
+
nextVersion: packagePlan.nextVersion,
|
|
152
|
+
};
|
|
153
|
+
strategyContextByPath.set(packagePlan.path, strategyContext);
|
|
154
|
+
strategyByPath.set(packagePlan.path, packageContext.strategy);
|
|
127
155
|
const existingGroup = strategyPackagesByName.get(packageContext.strategy.name);
|
|
128
156
|
if (existingGroup) {
|
|
129
|
-
existingGroup.packages.push(
|
|
130
|
-
packagePath: packagePlan.path,
|
|
131
|
-
versionFile: packagePlan.resolvedVersionFile,
|
|
132
|
-
currentVersion: packagePlan.currentVersion,
|
|
133
|
-
nextVersion: packagePlan.nextVersion,
|
|
134
|
-
});
|
|
157
|
+
existingGroup.packages.push(strategyContext);
|
|
135
158
|
}
|
|
136
159
|
else {
|
|
137
160
|
strategyPackagesByName.set(packageContext.strategy.name, {
|
|
138
161
|
strategy: packageContext.strategy,
|
|
139
|
-
packages: [
|
|
140
|
-
{
|
|
141
|
-
packagePath: packagePlan.path,
|
|
142
|
-
versionFile: packagePlan.resolvedVersionFile,
|
|
143
|
-
currentVersion: packagePlan.currentVersion,
|
|
144
|
-
nextVersion: packagePlan.nextVersion,
|
|
145
|
-
},
|
|
146
|
-
],
|
|
162
|
+
packages: [strategyContext],
|
|
147
163
|
});
|
|
148
164
|
}
|
|
149
165
|
packageCurrentVersionByPath[packagePlan.path] = packagePlan.currentVersion;
|
|
150
|
-
|
|
151
|
-
|
|
166
|
+
}
|
|
167
|
+
const workingPlans = packagePlans.map((pkgPlan) => ({ ...pkgPlan }));
|
|
168
|
+
const workingPlanByPath = new Map(workingPlans.map((pkgPlan) => [pkgPlan.path, pkgPlan]));
|
|
169
|
+
const forcePatchBump = (target, reason) => {
|
|
170
|
+
const current = packageCurrentVersionByPath[target.path] ?? target.currentVersion;
|
|
171
|
+
target.releaseType = "patch";
|
|
172
|
+
target.nextVersion = bumpVersion(current, "patch", {
|
|
173
|
+
allowStableMajor: allowStableMajorForPath(target.path),
|
|
174
|
+
});
|
|
175
|
+
target.bumpReason = reason;
|
|
176
|
+
const strategyContext = strategyContextByPath.get(target.path);
|
|
177
|
+
if (strategyContext) {
|
|
178
|
+
strategyContext.nextVersion = target.nextVersion;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
/**
|
|
182
|
+
* Packages that record a version requirement on `sourcePath`, found by
|
|
183
|
+
* asking the strategy who would need a requirement rewrite if `sourcePath`
|
|
184
|
+
* alone released. Works whether or not `sourcePath` is currently bumping,
|
|
185
|
+
* so it doubles as a reverse-edge lookup for a package that has not entered
|
|
186
|
+
* the plan yet.
|
|
187
|
+
*/
|
|
188
|
+
const findDependents = (sourcePath) => {
|
|
189
|
+
const strategyContext = strategyContextByPath.get(sourcePath);
|
|
190
|
+
// The implicit root can name a version file that does not exist. Nothing
|
|
191
|
+
// can record a requirement on a package that has no manifest, and asking
|
|
192
|
+
// the strategy would make it read one.
|
|
193
|
+
if (!strategyContext ||
|
|
194
|
+
!fs.existsSync(path.join(cwd, strategyContext.versionFile))) {
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
const strategy = strategyByPath.get(sourcePath);
|
|
198
|
+
const strategyGroup = strategy
|
|
199
|
+
? strategyPackagesByName.get(strategy.name)
|
|
200
|
+
: undefined;
|
|
201
|
+
if (!strategyGroup?.strategy.propagateDependentPatchImpacts) {
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
const hypotheticalVersion = strategyContext.nextVersion ??
|
|
205
|
+
bumpVersion(packageCurrentVersionByPath[sourcePath] ??
|
|
206
|
+
strategyContext.currentVersion, "patch", { allowStableMajor: allowStableMajorForPath(sourcePath) });
|
|
207
|
+
return strategyGroup.strategy.propagateDependentPatchImpacts(cwd, strategyGroup.packages.map((pkg) => ({
|
|
208
|
+
...pkg,
|
|
209
|
+
nextVersion: pkg.packagePath === sourcePath ? hypotheticalVersion : null,
|
|
210
|
+
})));
|
|
211
|
+
};
|
|
212
|
+
const isPublishable = (packagePath) => {
|
|
213
|
+
const strategy = strategyByPath.get(packagePath);
|
|
214
|
+
const strategyContext = strategyContextByPath.get(packagePath);
|
|
215
|
+
if (!strategy?.isPublishable || !strategyContext) {
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
// Abstaining counts as publishable: the enforcement below should only be
|
|
219
|
+
// skipped on a positive signal that nothing reaches a registry.
|
|
220
|
+
return strategy.isPublishable(cwd, strategyContext) !== false;
|
|
221
|
+
};
|
|
222
|
+
// Forward dependency edges, inverted from the reverse-edge lookup above.
|
|
223
|
+
// Whether one package records a requirement on another is a property of the
|
|
224
|
+
// manifests, not of the versions in flight, so this is computed once.
|
|
225
|
+
const dependenciesByPath = new Map();
|
|
226
|
+
for (const pkgPlan of workingPlans) {
|
|
227
|
+
for (const dependentPath of findDependents(pkgPlan.path)) {
|
|
228
|
+
const existing = dependenciesByPath.get(dependentPath);
|
|
229
|
+
if (existing) {
|
|
230
|
+
existing.add(pkgPlan.path);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
dependenciesByPath.set(dependentPath, new Set([pkgPlan.path]));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Every package reachable by following dependency edges down from a
|
|
238
|
+
* publishable package that is currently releasing. Staleness matters
|
|
239
|
+
* transitively: a release that pulls in a stale grandparent resolves against
|
|
240
|
+
* the stale published copy just as readily as a direct dependency does.
|
|
241
|
+
*/
|
|
242
|
+
const collectReleasingDependencyClosure = () => {
|
|
243
|
+
const reachable = new Set();
|
|
244
|
+
const queue = workingPlans
|
|
245
|
+
.filter((pkgPlan) => pkgPlan.nextVersion && isPublishable(pkgPlan.path))
|
|
246
|
+
.map((pkgPlan) => pkgPlan.path);
|
|
247
|
+
while (queue.length > 0) {
|
|
248
|
+
const currentPath = queue.shift();
|
|
249
|
+
if (currentPath === undefined) {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
for (const dependencyPath of dependenciesByPath.get(currentPath) ?? []) {
|
|
253
|
+
if (reachable.has(dependencyPath)) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
reachable.add(dependencyPath);
|
|
257
|
+
queue.push(dependencyPath);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return reachable;
|
|
261
|
+
};
|
|
262
|
+
// Both rules below can enable each other: forcing a bump creates a dependent
|
|
263
|
+
// whose requirement must be rewritten, and rewriting a dependent can in turn
|
|
264
|
+
// expose a stale dependency a further level up. Iterating to a fixpoint
|
|
265
|
+
// avoids having to order them, and terminates because a package can only
|
|
266
|
+
// ever move from not-releasing to releasing.
|
|
267
|
+
for (let iteration = 0; iteration <= workingPlans.length; iteration += 1) {
|
|
268
|
+
let changed = false;
|
|
269
|
+
// Rule 1: a package whose recorded requirement on a releasing sibling
|
|
270
|
+
// would change must release too, or the rewritten requirement ships in the
|
|
271
|
+
// release commit without a version to publish it under.
|
|
272
|
+
for (const strategyGroup of strategyPackagesByName.values()) {
|
|
273
|
+
const impacted = strategyGroup.strategy.propagateDependentPatchImpacts?.(cwd, strategyGroup.packages) ?? [];
|
|
274
|
+
for (const impactedPath of impacted) {
|
|
275
|
+
const target = workingPlanByPath.get(impactedPath);
|
|
276
|
+
if (!target || target.nextVersion) {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
forcePatchBump(target, "dependency-propagation");
|
|
280
|
+
changed = true;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// Rule 2: a package that changed since its own last release but produced
|
|
284
|
+
// no bump would leave a releasing dependent pointing at a stale published
|
|
285
|
+
// copy. Commit type is deliberately not consulted — the exposure comes
|
|
286
|
+
// from the change existing, not from how it was labeled.
|
|
287
|
+
const reachableDependencies = collectReleasingDependencyClosure();
|
|
288
|
+
for (const candidate of workingPlans) {
|
|
289
|
+
if (candidate.nextVersion || candidate.commits.length === 0) {
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (!reachableDependencies.has(candidate.path)) {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (!isPublishable(candidate.path)) {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
forcePatchBump(candidate, "stale-dependency");
|
|
299
|
+
changed = true;
|
|
300
|
+
}
|
|
301
|
+
if (!changed) {
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const packageNextVersionByPath = {};
|
|
306
|
+
for (const pkgPlan of workingPlans) {
|
|
307
|
+
if (pkgPlan.nextVersion) {
|
|
308
|
+
packageNextVersionByPath[pkgPlan.path] = pkgPlan.nextVersion;
|
|
152
309
|
}
|
|
153
310
|
}
|
|
154
|
-
const impactedPaths = new Set();
|
|
155
311
|
const dependencySourcePathsByPackage = new Map();
|
|
156
312
|
const addDependencySourcePath = (targetPath, sourcePath) => {
|
|
157
313
|
if (targetPath === sourcePath) {
|
|
@@ -164,48 +320,30 @@ export function createReleasePlan(cwd = process.cwd()) {
|
|
|
164
320
|
}
|
|
165
321
|
dependencySourcePathsByPackage.set(targetPath, new Set([sourcePath]));
|
|
166
322
|
};
|
|
323
|
+
// Attribute each dependent's requirement rewrite to the specific sources
|
|
324
|
+
// driving it, using the settled version set so chained bumps are credited.
|
|
167
325
|
for (const strategyGroup of strategyPackagesByName.values()) {
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
for (const sourcePackage of sourcePackages) {
|
|
174
|
-
const scopedImpacts = strategyGroup.strategy.propagateDependentPatchImpacts?.(cwd, strategyGroup.packages.map((pkg) => ({
|
|
175
|
-
...pkg,
|
|
176
|
-
nextVersion: pkg.packagePath === sourcePackage.packagePath
|
|
177
|
-
? sourcePackage.nextVersion
|
|
178
|
-
: null,
|
|
179
|
-
}))) ?? [];
|
|
180
|
-
for (const impactedPath of scopedImpacts) {
|
|
326
|
+
for (const sourcePackage of strategyGroup.packages) {
|
|
327
|
+
if (!sourcePackage.nextVersion) {
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
for (const impactedPath of findDependents(sourcePackage.packagePath)) {
|
|
181
331
|
addDependencySourcePath(impactedPath, sourcePackage.packagePath);
|
|
182
332
|
}
|
|
183
333
|
}
|
|
184
334
|
}
|
|
185
|
-
const propagatedPackages =
|
|
335
|
+
const propagatedPackages = workingPlans.map((pkgPlan) => {
|
|
186
336
|
const dependencySourcePaths = [
|
|
187
337
|
...(dependencySourcePathsByPackage.get(pkgPlan.path) ??
|
|
188
338
|
new Set()),
|
|
189
339
|
]
|
|
190
340
|
.filter((sourcePath) => Boolean(packageNextVersionByPath[sourcePath]))
|
|
191
341
|
.sort((a, b) => a.localeCompare(b));
|
|
192
|
-
if (
|
|
193
|
-
|
|
194
|
-
return pkgPlan;
|
|
195
|
-
}
|
|
196
|
-
return {
|
|
197
|
-
...pkgPlan,
|
|
198
|
-
dependencySourcePaths,
|
|
199
|
-
};
|
|
342
|
+
if (dependencySourcePaths.length === 0) {
|
|
343
|
+
return pkgPlan;
|
|
200
344
|
}
|
|
201
|
-
const current = packageCurrentVersionByPath[pkgPlan.path] ?? pkgPlan.currentVersion;
|
|
202
345
|
return {
|
|
203
346
|
...pkgPlan,
|
|
204
|
-
releaseType: "patch",
|
|
205
|
-
nextVersion: bumpVersion(current, "patch", {
|
|
206
|
-
allowStableMajor: allowStableMajorForPath(pkgPlan.path),
|
|
207
|
-
}),
|
|
208
|
-
bumpReason: "dependency-propagation",
|
|
209
347
|
dependencySourcePaths,
|
|
210
348
|
};
|
|
211
349
|
});
|
|
@@ -217,6 +355,11 @@ export function createReleasePlan(cwd = process.cwd()) {
|
|
|
217
355
|
}
|
|
218
356
|
}
|
|
219
357
|
const adjustedPackages = propagatedPackages.map((pkgPlan) => {
|
|
358
|
+
if (pkgPlan.bumpReason === "release-as") {
|
|
359
|
+
// An explicit override pins this package's version; do not let a
|
|
360
|
+
// followed source recompute it out from under the requested version.
|
|
361
|
+
return pkgPlan;
|
|
362
|
+
}
|
|
220
363
|
const followsSources = followsByPath.get(pkgPlan.path) ?? [];
|
|
221
364
|
const bumpingSources = followsSources
|
|
222
365
|
.map((sourcePath) => propagatedPackages.find((pkg) => pkg.path === sourcePath))
|
|
@@ -237,6 +380,7 @@ export function createReleasePlan(cwd = process.cwd()) {
|
|
|
237
380
|
].sort((a, b) => a.localeCompare(b));
|
|
238
381
|
const sourceDrove = combinedReleaseType !== ownReleaseType ||
|
|
239
382
|
pkgPlan.bumpReason === "dependency-propagation" ||
|
|
383
|
+
pkgPlan.bumpReason === "stale-dependency" ||
|
|
240
384
|
pkgPlan.bumpReason === undefined;
|
|
241
385
|
const baseVersion = packageCurrentVersionByPath[pkgPlan.path] ?? pkgPlan.currentVersion;
|
|
242
386
|
const nextVersion = combinedReleaseType
|
|
@@ -272,12 +416,19 @@ export function createReleasePlan(cwd = process.cwd()) {
|
|
|
272
416
|
commits: rootPackagePlan.commits,
|
|
273
417
|
};
|
|
274
418
|
}
|
|
419
|
+
const rootOverridden = rootPackagePlan.bumpReason === "release-as";
|
|
275
420
|
if (monorepoMode === "fixed") {
|
|
276
|
-
const
|
|
421
|
+
const analyzedFixedType = analyzeParsedCommits(adjustedPackages.flatMap((pkgPlan) => pkgPlan.parsedCommits));
|
|
277
422
|
const fixedBaseVersion = rootPackagePlan.currentVersion;
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
423
|
+
// A `Release-As:` footer on the shared (root) version pins every package.
|
|
424
|
+
const fixedType = rootOverridden
|
|
425
|
+
? rootPackagePlan.releaseType
|
|
426
|
+
: analyzedFixedType;
|
|
427
|
+
const fixedNextVersion = rootOverridden
|
|
428
|
+
? rootPackagePlan.nextVersion
|
|
429
|
+
: analyzedFixedType
|
|
430
|
+
? bumpVersion(fixedBaseVersion, analyzedFixedType, { allowStableMajor })
|
|
431
|
+
: null;
|
|
281
432
|
const adjusted = adjustedPackages.map((pkgPlan) => ({
|
|
282
433
|
...pkgPlan,
|
|
283
434
|
releaseType: fixedType,
|
|
@@ -300,11 +451,20 @@ export function createReleasePlan(cwd = process.cwd()) {
|
|
|
300
451
|
.map(({ implicitRoot: _implicitRoot, ...pkgPlan }) => pkgPlan),
|
|
301
452
|
};
|
|
302
453
|
}
|
|
303
|
-
const
|
|
454
|
+
const analyzedOverallType = analyzeParsedCommits(adjustedPackages.flatMap((pkgPlan) => pkgPlan.parsedCommits));
|
|
304
455
|
const overallBaseVersion = rootPackagePlan.currentVersion;
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
456
|
+
// A root-level `Release-As:` footer drives the aggregate version too, keeping
|
|
457
|
+
// the top-level plan consistent with the pinned root package.
|
|
458
|
+
const overallType = rootOverridden
|
|
459
|
+
? rootPackagePlan.releaseType
|
|
460
|
+
: analyzedOverallType;
|
|
461
|
+
const overallNextVersion = rootOverridden
|
|
462
|
+
? rootPackagePlan.nextVersion
|
|
463
|
+
: analyzedOverallType
|
|
464
|
+
? bumpVersion(overallBaseVersion, analyzedOverallType, {
|
|
465
|
+
allowStableMajor,
|
|
466
|
+
})
|
|
467
|
+
: null;
|
|
308
468
|
return {
|
|
309
469
|
mode: "simple",
|
|
310
470
|
releaseType: overallType,
|
|
@@ -320,9 +480,37 @@ export function createReleasePlan(cwd = process.cwd()) {
|
|
|
320
480
|
packages: visiblePackages.map(({ implicitRoot: _implicitRoot, ...pkgPlan }) => pkgPlan),
|
|
321
481
|
};
|
|
322
482
|
}
|
|
323
|
-
/**
|
|
324
|
-
|
|
325
|
-
|
|
483
|
+
/**
|
|
484
|
+
* The root package's own release, as distinct from the plan-level aggregate.
|
|
485
|
+
*
|
|
486
|
+
* `plan.releaseType`/`plan.nextVersion`/`plan.commits` describe the repository
|
|
487
|
+
* as a whole: the aggregate release type folds in every package's commits and
|
|
488
|
+
* is then applied to root's version number. That answers "is anything
|
|
489
|
+
* releasing, and how large is the biggest change anywhere" — not "what is root
|
|
490
|
+
* releasing". A sibling's `feat` therefore lifts the aggregate to a minor even
|
|
491
|
+
* when root's own `exclude-paths` drop that commit, so anything that names or
|
|
492
|
+
* describes root's release must go through here instead. Using the aggregate
|
|
493
|
+
* would report a version no version file carries and list commits root
|
|
494
|
+
* deliberately excluded.
|
|
495
|
+
*
|
|
496
|
+
* Falls back to the aggregate when the plan has no explicit root package: the
|
|
497
|
+
* top-level changelog is then a repository-wide summary with no package of its
|
|
498
|
+
* own to describe.
|
|
499
|
+
*/
|
|
500
|
+
export function resolveRootReleaseView(plan) {
|
|
501
|
+
const rootPackage = plan.packages?.find((pkg) => pkg.path === ".");
|
|
502
|
+
if (!rootPackage) {
|
|
503
|
+
return {
|
|
504
|
+
currentVersion: plan.currentVersion,
|
|
505
|
+
nextVersion: plan.nextVersion,
|
|
506
|
+
commits: plan.commits,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
return {
|
|
510
|
+
currentVersion: rootPackage.currentVersion,
|
|
511
|
+
nextVersion: rootPackage.nextVersion,
|
|
512
|
+
commits: rootPackage.commits,
|
|
513
|
+
};
|
|
326
514
|
}
|
|
327
515
|
export function resolvePackageDependencies(plan, packagePath) {
|
|
328
516
|
const target = plan.packages?.find((pkg) => pkg.path === packagePath);
|
package/dist/release/pr.d.ts
CHANGED
|
@@ -1,15 +1,7 @@
|
|
|
1
1
|
import type { ParsedCommit } from "../git/commits.js";
|
|
2
2
|
import type { VersionaryChangelogFormat, VersionaryConfig } from "../types/config.js";
|
|
3
3
|
import type { VersionaryPluginContext } from "../types/plugins.js";
|
|
4
|
-
import { type ReleasePlan
|
|
5
|
-
export declare function getNextReleaseFile(config: VersionaryConfig): string;
|
|
6
|
-
export declare function readNextReleaseHighlights(cwd: string, config: VersionaryConfig): {
|
|
7
|
-
content: string;
|
|
8
|
-
filePath: string;
|
|
9
|
-
} | null;
|
|
10
|
-
export declare function consumeNextReleaseFile(cwd: string, filePath: string): {
|
|
11
|
-
tracked: boolean;
|
|
12
|
-
};
|
|
4
|
+
import { type ReleasePlan } from "./plan.js";
|
|
13
5
|
/**
|
|
14
6
|
* Read the manual-notes ("Unreleased") prose from the top of a changelog file.
|
|
15
7
|
* Returns an empty string when the file is absent or has no notes block.
|
|
@@ -17,15 +9,13 @@ export declare function consumeNextReleaseFile(cwd: string, filePath: string): {
|
|
|
17
9
|
export declare function readChangelogHighlights(cwd: string, changelogFile: string, format: VersionaryChangelogFormat): string;
|
|
18
10
|
export interface ResolvedReleaseHighlights {
|
|
19
11
|
highlights: string;
|
|
20
|
-
source: "changelog" | "
|
|
21
|
-
filePath?: string;
|
|
12
|
+
source: "changelog" | "none";
|
|
22
13
|
}
|
|
23
14
|
/**
|
|
24
|
-
* Resolve release highlights
|
|
25
|
-
*
|
|
26
|
-
* (emitting a warning) so existing setups keep working.
|
|
15
|
+
* Resolve release highlights from the editable "Unreleased" section at the top
|
|
16
|
+
* of the changelog.
|
|
27
17
|
*/
|
|
28
|
-
export declare function resolveReleaseHighlights(cwd: string,
|
|
18
|
+
export declare function resolveReleaseHighlights(cwd: string, changelogFile: string, format: VersionaryChangelogFormat): ResolvedReleaseHighlights;
|
|
29
19
|
export declare function splitSafeDirtyFiles(files: string[]): {
|
|
30
20
|
ignored: string[];
|
|
31
21
|
blocking: string[];
|
|
@@ -42,8 +32,8 @@ export declare function prepareReleasePr(cwd?: string, options?: {
|
|
|
42
32
|
updated: boolean;
|
|
43
33
|
highlights: string;
|
|
44
34
|
};
|
|
45
|
-
export declare function renderSimpleReviewRequestBody(version: string, previousVersion: string, commits: ParsedCommit[], plan?:
|
|
46
|
-
export declare function openOrUpdateReviewRequest(cwd: string, branch: string, title: string, version: string, previousVersion: string, commits: ParsedCommit[], plan?:
|
|
35
|
+
export declare function renderSimpleReviewRequestBody(version: string, previousVersion: string, commits: ParsedCommit[], plan?: ReleasePlan | null, cwd?: string, highlights?: string, loadedConfig?: VersionaryConfig): string;
|
|
36
|
+
export declare function openOrUpdateReviewRequest(cwd: string, branch: string, title: string, version: string, previousVersion: string, commits: ParsedCommit[], plan?: ReleasePlan | null, options?: {
|
|
47
37
|
logger?: VersionaryPluginContext["logger"];
|
|
48
38
|
highlights?: string;
|
|
49
39
|
}): Promise<string>;
|
|
@@ -54,14 +44,5 @@ export declare function closeStaleReviewRequestIfExists(cwd?: string, options?:
|
|
|
54
44
|
url?: string;
|
|
55
45
|
number?: number;
|
|
56
46
|
}>;
|
|
57
|
-
/** @deprecated Use prepareReleasePr. */
|
|
58
|
-
export declare function prepareSimpleReleasePr(cwd?: string, options?: {
|
|
59
|
-
logger?: VersionaryPluginContext["logger"];
|
|
60
|
-
}): ReturnType<typeof prepareReleasePr>;
|
|
61
|
-
/** @deprecated Use openOrUpdateReviewRequest. */
|
|
62
|
-
export declare function openOrUpdateSimpleReviewRequest(cwd: string, branch: string, title: string, version: string, previousVersion: string, commits: ParsedCommit[], plan?: SimplePlan | null, options?: {
|
|
63
|
-
logger?: VersionaryPluginContext["logger"];
|
|
64
|
-
highlights?: string;
|
|
65
|
-
}): Promise<string>;
|
|
66
47
|
export declare function pushReleaseBranch(cwd: string, branch: string): void;
|
|
67
48
|
export declare function isReleaseCommitMessage(commitMessage: string): boolean;
|
package/dist/release/pr.js
CHANGED
|
@@ -17,38 +17,6 @@ const SAFE_DIRTY_FILES = new Set([
|
|
|
17
17
|
"npm-shrinkwrap.json",
|
|
18
18
|
]);
|
|
19
19
|
const VERSIONARY_RELEASE_TRAILER = "Versionary-Release: true";
|
|
20
|
-
export function getNextReleaseFile(config) {
|
|
21
|
-
return config["next-release-file"] ?? "NEXT_RELEASE.md";
|
|
22
|
-
}
|
|
23
|
-
export function readNextReleaseHighlights(cwd, config) {
|
|
24
|
-
const filePath = getNextReleaseFile(config);
|
|
25
|
-
const fullPath = path.join(cwd, filePath);
|
|
26
|
-
if (!fs.existsSync(fullPath)) {
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
const content = fs.readFileSync(fullPath, "utf8").trim();
|
|
30
|
-
return { content, filePath };
|
|
31
|
-
}
|
|
32
|
-
function isTrackedFile(cwd, filePath) {
|
|
33
|
-
try {
|
|
34
|
-
execFileSync("git", ["ls-files", "--error-unmatch", "--", filePath], {
|
|
35
|
-
cwd,
|
|
36
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
37
|
-
});
|
|
38
|
-
return true;
|
|
39
|
-
}
|
|
40
|
-
catch {
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
export function consumeNextReleaseFile(cwd, filePath) {
|
|
45
|
-
const tracked = isTrackedFile(cwd, filePath);
|
|
46
|
-
const fullPath = path.join(cwd, filePath);
|
|
47
|
-
if (fs.existsSync(fullPath)) {
|
|
48
|
-
fs.rmSync(fullPath);
|
|
49
|
-
}
|
|
50
|
-
return { tracked };
|
|
51
|
-
}
|
|
52
20
|
/**
|
|
53
21
|
* Read the manual-notes ("Unreleased") prose from the top of a changelog file.
|
|
54
22
|
* Returns an empty string when the file is absent or has no notes block.
|
|
@@ -62,24 +30,14 @@ export function readChangelogHighlights(cwd, changelogFile, format) {
|
|
|
62
30
|
return extractUnreleasedNotes(existing, format).highlights;
|
|
63
31
|
}
|
|
64
32
|
/**
|
|
65
|
-
* Resolve release highlights
|
|
66
|
-
*
|
|
67
|
-
* (emitting a warning) so existing setups keep working.
|
|
33
|
+
* Resolve release highlights from the editable "Unreleased" section at the top
|
|
34
|
+
* of the changelog.
|
|
68
35
|
*/
|
|
69
|
-
export function resolveReleaseHighlights(cwd,
|
|
36
|
+
export function resolveReleaseHighlights(cwd, changelogFile, format) {
|
|
70
37
|
const fromChangelog = readChangelogHighlights(cwd, changelogFile, format);
|
|
71
38
|
if (fromChangelog.length > 0) {
|
|
72
39
|
return { highlights: fromChangelog, source: "changelog" };
|
|
73
40
|
}
|
|
74
|
-
const fromFile = readNextReleaseHighlights(cwd, config);
|
|
75
|
-
if (fromFile) {
|
|
76
|
-
logger?.warn(`${fromFile.filePath} is deprecated; add release notes under an "Unreleased" heading at the top of ${changelogFile} instead.`);
|
|
77
|
-
return {
|
|
78
|
-
highlights: fromFile.content,
|
|
79
|
-
source: "file",
|
|
80
|
-
filePath: fromFile.filePath,
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
41
|
return { highlights: "", source: "none" };
|
|
84
42
|
}
|
|
85
43
|
function listTrackedDirtyFiles(cwd) {
|
|
@@ -297,19 +255,16 @@ export function prepareReleasePr(cwd = process.cwd(), options = {}) {
|
|
|
297
255
|
}
|
|
298
256
|
const updatedArtifactFiles = applyConfiguredArtifactRules(cwd, loaded.config, plan);
|
|
299
257
|
const packageReleaseMetadata = buildPackageReleaseMetadata(cwd, plan, loaded.config);
|
|
300
|
-
const highlightsResult = resolveReleaseHighlights(cwd,
|
|
258
|
+
const highlightsResult = resolveReleaseHighlights(cwd, plan.changelogFile, plan.changelogFormat);
|
|
301
259
|
const highlights = highlightsResult.highlights;
|
|
302
260
|
const section = renderReleasePlanChangelog(plan, { highlights, cwd });
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
//
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
if (tracked) {
|
|
311
|
-
consumedHighlightsPath = highlightsResult.filePath;
|
|
312
|
-
}
|
|
261
|
+
const updatedChangelogFiles = [];
|
|
262
|
+
// An empty section means root itself is not releasing, only siblings are.
|
|
263
|
+
// Writing then would leave a bare heading behind and, worse, strip the
|
|
264
|
+
// manual-notes block that nothing has folded in yet.
|
|
265
|
+
if (section.length > 0) {
|
|
266
|
+
prependChangelog(cwd, plan.changelogFile, section, plan.changelogFormat);
|
|
267
|
+
updatedChangelogFiles.push(plan.changelogFile);
|
|
313
268
|
}
|
|
314
269
|
for (const packagePlan of plan.packages ?? []) {
|
|
315
270
|
if (!packagePlan.nextVersion || packagePlan.path === ".") {
|
|
@@ -357,7 +312,6 @@ export function prepareReleasePr(cwd = process.cwd(), options = {}) {
|
|
|
357
312
|
...updatedVersionFiles,
|
|
358
313
|
...updatedArtifactFiles,
|
|
359
314
|
...updatedChangelogFiles,
|
|
360
|
-
...(consumedHighlightsPath ? [consumedHighlightsPath] : []),
|
|
361
315
|
]),
|
|
362
316
|
];
|
|
363
317
|
execFileSync("git", ["add", ...filesToAdd], {
|
|
@@ -497,14 +451,6 @@ export async function closeStaleReviewRequestIfExists(cwd = process.cwd(), optio
|
|
|
497
451
|
}
|
|
498
452
|
return result;
|
|
499
453
|
}
|
|
500
|
-
/** @deprecated Use prepareReleasePr. */
|
|
501
|
-
export function prepareSimpleReleasePr(cwd = process.cwd(), options = {}) {
|
|
502
|
-
return prepareReleasePr(cwd, options);
|
|
503
|
-
}
|
|
504
|
-
/** @deprecated Use openOrUpdateReviewRequest. */
|
|
505
|
-
export async function openOrUpdateSimpleReviewRequest(cwd, branch, title, version, previousVersion, commits, plan = null, options = {}) {
|
|
506
|
-
return openOrUpdateReviewRequest(cwd, branch, title, version, previousVersion, commits, plan, options);
|
|
507
|
-
}
|
|
508
454
|
export function pushReleaseBranch(cwd, branch) {
|
|
509
455
|
execFileSync("git", ["push", "--force-with-lease", "origin", branch], {
|
|
510
456
|
cwd,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ParsedCommit } from "../git/commits.js";
|
|
2
|
+
import { type ReleaseType } from "./semver.js";
|
|
3
|
+
/**
|
|
4
|
+
* Footer token (case-insensitive) that requests an explicit next version, e.g.
|
|
5
|
+
*
|
|
6
|
+
* chore: graduate to stable release
|
|
7
|
+
*
|
|
8
|
+
* Release-As: 1.0.0
|
|
9
|
+
*
|
|
10
|
+
* The override forces a release with the requested version even when the
|
|
11
|
+
* conventional-commit analysis alone would produce no bump.
|
|
12
|
+
*/
|
|
13
|
+
export declare const RELEASE_AS_FOOTER_TOKEN = "release-as";
|
|
14
|
+
export interface ReleaseAsOverride {
|
|
15
|
+
/** The requested target version (leading `v` stripped, validated SemVer). */
|
|
16
|
+
version: string;
|
|
17
|
+
/** Hash of the commit carrying the winning footer. */
|
|
18
|
+
sourceHash: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Scan a release window for a `Release-As:` footer override.
|
|
22
|
+
*
|
|
23
|
+
* Returns the explicit target version, or `null` when no footer is present.
|
|
24
|
+
* Throws on an invalid version, a downgrade or no-op relative to
|
|
25
|
+
* `currentVersion`, or conflicting override versions within the window.
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveReleaseAsOverride(commits: ParsedCommit[], currentVersion: string): ReleaseAsOverride | null;
|
|
28
|
+
/** Derive the semantic release level implied by moving `from` -> `to`. */
|
|
29
|
+
export declare function releaseTypeBetween(from: string, to: string): ReleaseType;
|