versionary 1.0.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/cli/index.js CHANGED
@@ -5,6 +5,20 @@ import { createReleasePlan } from "../release/plan.js";
5
5
  import { closeStaleReviewRequestIfExists, isReleaseCommitMessage, openOrUpdateReviewRequest, prepareReleasePr, pushReleaseBranch, resolveReleaseHighlights, } from "../release/pr.js";
6
6
  import { runRelease, runReleaseDetailed } from "../release/release.js";
7
7
  import { verifyProject } from "../release/verify-project.js";
8
+ /**
9
+ * What a dry run would actually release. `plan.nextVersion` is the repository
10
+ * aggregate, which in a monorepo names no real release, so prefer the
11
+ * per-package versions whenever the plan has them.
12
+ */
13
+ function formatDryRunReleaseSubject(plan) {
14
+ const releasing = (plan.packages ?? []).filter((pkg) => pkg.nextVersion);
15
+ if (releasing.length === 0) {
16
+ return plan.nextVersion ?? "";
17
+ }
18
+ return releasing
19
+ .map((pkg) => `${pkg.path === "." ? plan.packageName : pkg.path} ${pkg.nextVersion}`)
20
+ .join(", ");
21
+ }
8
22
  function printVerifyResult() {
9
23
  const result = verifyProject();
10
24
  const categories = [
@@ -114,7 +128,7 @@ async function main() {
114
128
  return 0;
115
129
  }
116
130
  if (flags["dry-run"]) {
117
- const dryRunMessage = `Dry run: would prepare release PR branch ${plan.releaseBranchPrefix} for ${plan.nextVersion}`;
131
+ const dryRunMessage = `Dry run: would prepare release PR branch ${plan.releaseBranchPrefix} for ${formatDryRunReleaseSubject(plan)}`;
118
132
  if (flags.json) {
119
133
  emitJson({
120
134
  action: "pr-dry-run",
@@ -218,7 +232,7 @@ async function main() {
218
232
  return 0;
219
233
  }
220
234
  if (flags["dry-run"]) {
221
- console.log(`Dry run: would prepare release PR branch ${plan.releaseBranchPrefix} for ${plan.nextVersion}`);
235
+ console.log(`Dry run: would prepare release PR branch ${plan.releaseBranchPrefix} for ${formatDryRunReleaseSubject(plan)}`);
222
236
  return 0;
223
237
  }
224
238
  const pr = prepareReleasePr(process.cwd(), { logger: console });
@@ -2,7 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { applyRevertSuppression, inferReleaseTypeFromParsedCommit, parseConventionalCommitMessage, } from "../git/commits.js";
4
4
  import { resolveRepositoryWebBaseUrl } from "../git/repo-url.js";
5
- import { resolvePackageDependencies } from "./plan.js";
5
+ import { resolvePackageDependencies, resolveRootReleaseView, } from "./plan.js";
6
6
  import { isVersionHeading } from "./semver.js";
7
7
  const REVIEW_REQUEST_FOOTER = "---\n\nThis PR was generated by [Versionary](https://github.com/jolars/versionary).";
8
8
  function formatDate() {
@@ -192,24 +192,27 @@ export function renderReviewRequestFooter() {
192
192
  return REVIEW_REQUEST_FOOTER;
193
193
  }
194
194
  export function renderReleasePlanChangelog(plan, options = {}) {
195
- if (!plan.nextVersion) {
195
+ // Root's changelog describes root's own release, so it must be driven by the
196
+ // root package's version and commit set rather than the plan-level aggregate.
197
+ const root = resolveRootReleaseView(plan);
198
+ if (!root.nextVersion) {
196
199
  return "";
197
200
  }
198
201
  const dedupedCommits = [
199
- ...new Map(plan.commits.map((commit) => [commit.hash, commit])).values(),
202
+ ...new Map(root.commits.map((commit) => [commit.hash, commit])).values(),
200
203
  ];
201
204
  if (plan.changelogFormat === "r-news") {
202
205
  return renderRNewsReleaseNotes({
203
206
  packageName: plan.packageName,
204
- nextVersion: plan.nextVersion,
207
+ nextVersion: root.nextVersion,
205
208
  commits: dedupedCommits,
206
209
  cwd: process.cwd(),
207
210
  highlights: options.highlights,
208
211
  });
209
212
  }
210
213
  return renderReleaseNotesSection({
211
- currentVersion: plan.currentVersion,
212
- nextVersion: plan.nextVersion,
214
+ currentVersion: root.currentVersion,
215
+ nextVersion: root.nextVersion,
213
216
  commits: dedupedCommits,
214
217
  cwd: options.cwd ?? process.cwd(),
215
218
  dependencies: resolvePackageDependencies(plan, "."),
@@ -34,6 +34,28 @@ export declare function getChangelogDefaults(config: {
34
34
  changelogFormat: VersionaryChangelogFormat;
35
35
  };
36
36
  export declare function createReleasePlan(cwd?: string): ReleasePlan;
37
+ /**
38
+ * The root package's own release, as distinct from the plan-level aggregate.
39
+ *
40
+ * `plan.releaseType`/`plan.nextVersion`/`plan.commits` describe the repository
41
+ * as a whole: the aggregate release type folds in every package's commits and
42
+ * is then applied to root's version number. That answers "is anything
43
+ * releasing, and how large is the biggest change anywhere" — not "what is root
44
+ * releasing". A sibling's `feat` therefore lifts the aggregate to a minor even
45
+ * when root's own `exclude-paths` drop that commit, so anything that names or
46
+ * describes root's release must go through here instead. Using the aggregate
47
+ * would report a version no version file carries and list commits root
48
+ * deliberately excluded.
49
+ *
50
+ * Falls back to the aggregate when the plan has no explicit root package: the
51
+ * top-level changelog is then a repository-wide summary with no package of its
52
+ * own to describe.
53
+ */
54
+ export declare function resolveRootReleaseView(plan: ReleasePlan): {
55
+ currentVersion: string;
56
+ nextVersion: string | null;
57
+ commits: ParsedCommit[];
58
+ };
37
59
  export declare function resolvePackageDependencies(plan: ReleasePlan, packagePath: string): Array<{
38
60
  name: string;
39
61
  version: string;
@@ -480,6 +480,38 @@ export function createReleasePlan(cwd = process.cwd()) {
480
480
  packages: visiblePackages.map(({ implicitRoot: _implicitRoot, ...pkgPlan }) => pkgPlan),
481
481
  };
482
482
  }
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
+ };
514
+ }
483
515
  export function resolvePackageDependencies(plan, packagePath) {
484
516
  const target = plan.packages?.find((pkg) => pkg.path === packagePath);
485
517
  if (!target) {
@@ -258,8 +258,14 @@ export function prepareReleasePr(cwd = process.cwd(), options = {}) {
258
258
  const highlightsResult = resolveReleaseHighlights(cwd, plan.changelogFile, plan.changelogFormat);
259
259
  const highlights = highlightsResult.highlights;
260
260
  const section = renderReleasePlanChangelog(plan, { highlights, cwd });
261
- prependChangelog(cwd, plan.changelogFile, section, plan.changelogFormat);
262
- const updatedChangelogFiles = [plan.changelogFile];
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);
268
+ }
263
269
  for (const packagePlan of plan.packages ?? []) {
264
270
  if (!packagePlan.nextVersion || packagePlan.path === ".") {
265
271
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "versionary",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Automatic release framework based on conventional commits and semantic versioning",
5
5
  "keywords": [
6
6
  "releasing",