versionary 0.16.0 → 0.18.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/dist/cli/index.js CHANGED
@@ -99,6 +99,9 @@ async function main() {
99
99
  }
100
100
  const plan = (0, plan_js_1.createReleasePlan)();
101
101
  if (!plan.nextVersion) {
102
+ if (!flags["dry-run"]) {
103
+ await (0, pr_js_1.closeStaleReviewRequestIfExists)(process.cwd(), { logger });
104
+ }
102
105
  const message = "No releasable commits found. Nothing to do.";
103
106
  if (flags.json) {
104
107
  emitJson({
@@ -203,12 +206,17 @@ async function main() {
203
206
  return 0;
204
207
  }
205
208
  if (command === "pr") {
206
- if (flags["dry-run"]) {
207
- const plan = (0, plan_js_1.createReleasePlan)();
208
- if (!plan.nextVersion) {
209
- console.log("No releasable commits found. Nothing to do.");
210
- return 0;
209
+ const plan = (0, plan_js_1.createReleasePlan)();
210
+ if (!plan.nextVersion) {
211
+ if (!flags["dry-run"]) {
212
+ await (0, pr_js_1.closeStaleReviewRequestIfExists)(process.cwd(), {
213
+ logger: console,
214
+ });
211
215
  }
216
+ console.log("No releasable commits found. Nothing to do.");
217
+ return 0;
218
+ }
219
+ if (flags["dry-run"]) {
212
220
  console.log(`Dry run: would prepare release PR branch ${plan.releaseBranchPrefix} for ${plan.nextVersion}`);
213
221
  return 0;
214
222
  }
@@ -8,6 +8,7 @@ exports.loadConfig = loadConfig;
8
8
  const node_fs_1 = __importDefault(require("node:fs"));
9
9
  const node_path_1 = __importDefault(require("node:path"));
10
10
  const jsonc_parser_1 = require("jsonc-parser");
11
+ const resolve_js_1 = require("../strategy/resolve.js");
11
12
  const schema_js_1 = require("./schema.js");
12
13
  const SUPPORTED_FILES = [
13
14
  { file: "versionary.jsonc", format: "jsonc" },
@@ -22,6 +23,34 @@ function parseConfig(raw, format) {
22
23
  function isRecord(value) {
23
24
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
24
25
  }
26
+ function validateReleaseTypes(config) {
27
+ try {
28
+ (0, resolve_js_1.resolveVersionStrategy)(config);
29
+ }
30
+ catch (error) {
31
+ if (error instanceof Error) {
32
+ throw error;
33
+ }
34
+ const known = (0, resolve_js_1.listKnownReleaseTypes)().join(", ");
35
+ throw new Error(`Unsupported release-type. Supported release types: ${known}.`);
36
+ }
37
+ for (const [packagePath, packageConfig] of Object.entries(config.packages ?? {})) {
38
+ const packageReleaseType = packageConfig["release-type"];
39
+ if (!packageReleaseType) {
40
+ continue;
41
+ }
42
+ try {
43
+ (0, resolve_js_1.resolveVersionStrategy)({ ...config, "release-type": packageReleaseType });
44
+ }
45
+ catch (error) {
46
+ if (error instanceof Error) {
47
+ throw new Error(`${error.message} (in packages["${packagePath}"])`);
48
+ }
49
+ const known = (0, resolve_js_1.listKnownReleaseTypes)().join(", ");
50
+ throw new Error(`Unsupported release-type in packages["${packagePath}"]. Supported release types: ${known}.`);
51
+ }
52
+ }
53
+ }
25
54
  function findConfigFile(cwd) {
26
55
  for (const candidate of SUPPORTED_FILES) {
27
56
  const candidatePath = node_path_1.default.join(cwd, candidate.file);
@@ -45,6 +74,7 @@ function loadConfig(cwd = process.cwd()) {
45
74
  throw new Error('The "plugins" config key is no longer supported. Versionary uses built-in integrations only.');
46
75
  }
47
76
  const validated = schema_js_1.configSchema.parse(parsed);
77
+ validateReleaseTypes(validated);
48
78
  return {
49
79
  path: found.path,
50
80
  format: found.format,
@@ -437,17 +437,26 @@ function applyRevertSuppression(commits) {
437
437
  }
438
438
  const presentShas = new Set(commits.map((commit) => commit.hash.toLowerCase()));
439
439
  const reverted = new Set();
440
+ const suppressedReverts = new Set();
440
441
  for (const commit of commits) {
441
442
  if (!commit.isRevert) {
442
443
  continue;
443
444
  }
445
+ const revertsOnlyInWindow = commit.revertedShas.length > 0 &&
446
+ commit.revertedShas.every((sha) => presentShas.has(sha));
447
+ if (revertsOnlyInWindow) {
448
+ suppressedReverts.add(commit.hash.toLowerCase());
449
+ }
444
450
  for (const sha of commit.revertedShas) {
445
451
  if (presentShas.has(sha)) {
446
452
  reverted.add(sha);
447
453
  }
448
454
  }
449
455
  }
450
- return commits.filter((commit) => !reverted.has(commit.hash.toLowerCase()));
456
+ return commits.filter((commit) => {
457
+ const hash = commit.hash.toLowerCase();
458
+ return !reverted.has(hash) && !suppressedReverts.has(hash);
459
+ });
451
460
  }
452
461
  function analyzeParsedCommits(commits) {
453
462
  let result = null;
@@ -12,8 +12,14 @@ export declare function renderSimpleReleaseNotes(input: {
12
12
  }>;
13
13
  }, options?: {
14
14
  includeFooter?: boolean;
15
+ headerLabel?: string;
16
+ }): string;
17
+ export declare function renderReviewRequestFooter(): string;
18
+ export declare function renderReleasePlanChangelog(plan: ReleasePlan, options?: {
19
+ headerLabel?: string;
20
+ includeFooter?: boolean;
21
+ cwd?: string;
15
22
  }): string;
16
- export declare function renderReleasePlanChangelog(plan: ReleasePlan): string;
17
23
  /** @deprecated Use renderReleasePlanChangelog. */
18
24
  export declare function renderSimpleChangelog(plan: SimplePlan): string;
19
25
  export declare function renderPackageChangelogSection(input: {
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.renderSimpleReleaseNotes = renderSimpleReleaseNotes;
7
+ exports.renderReviewRequestFooter = renderReviewRequestFooter;
7
8
  exports.renderReleasePlanChangelog = renderReleasePlanChangelog;
8
9
  exports.renderSimpleChangelog = renderSimpleChangelog;
9
10
  exports.renderPackageChangelogSection = renderPackageChangelogSection;
@@ -13,6 +14,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
13
14
  const node_path_1 = __importDefault(require("node:path"));
14
15
  const commits_js_1 = require("../git/commits.js");
15
16
  const repo_url_js_1 = require("../git/repo-url.js");
17
+ const REVIEW_REQUEST_FOOTER = "---\n\nThis PR was generated by [Versionary](https://github.com/jolars/versionary).";
16
18
  function formatDate() {
17
19
  return new Date().toISOString().slice(0, 10);
18
20
  }
@@ -75,6 +77,7 @@ function groupCommitLines(commits, repoUrl) {
75
77
  const features = [];
76
78
  const fixes = [];
77
79
  const reverts = [];
80
+ const effectiveCommits = (0, commits_js_1.applyRevertSuppression)(commits);
78
81
  const getRevertedSubject = (commit) => {
79
82
  const normalizedDescription = (commit.description ?? "")
80
83
  .trim()
@@ -97,7 +100,7 @@ function groupCommitLines(commits, repoUrl) {
97
100
  const revertedCommit = (0, commits_js_1.parseConventionalCommitMessage)(revertedSubject);
98
101
  return (0, commits_js_1.inferReleaseTypeFromParsedCommit)(revertedCommit) !== null;
99
102
  };
100
- for (const commit of commits) {
103
+ for (const commit of effectiveCommits) {
101
104
  const type = (0, commits_js_1.inferReleaseTypeFromParsedCommit)(commit);
102
105
  if (!type) {
103
106
  continue;
@@ -136,9 +139,10 @@ function groupCommitLines(commits, repoUrl) {
136
139
  }
137
140
  function renderSimpleReleaseNotes(input, options = {}) {
138
141
  const repoUrl = (0, repo_url_js_1.resolveRepositoryWebBaseUrl)(input.cwd ?? process.cwd());
142
+ const headerLabel = options.headerLabel ?? input.nextVersion;
139
143
  const header = repoUrl
140
- ? `## [${input.nextVersion}](${repoUrl}/compare/v${input.currentVersion}...v${input.nextVersion}) (${formatDate()})`
141
- : `## ${input.nextVersion} (${formatDate()})`;
144
+ ? `## [${headerLabel}](${repoUrl}/compare/v${input.currentVersion}...v${input.nextVersion}) (${formatDate()})`
145
+ : `## ${headerLabel} (${formatDate()})`;
142
146
  const grouped = groupCommitLines(input.commits, repoUrl);
143
147
  const sections = [];
144
148
  if (grouped.breaking.length > 0) {
@@ -156,13 +160,16 @@ function renderSimpleReleaseNotes(input, options = {}) {
156
160
  if (input.dependencies && input.dependencies.length > 0) {
157
161
  sections.push("### Dependencies", ...input.dependencies.map((dependency) => `- updated ${dependency.name} to v${dependency.version}`), "");
158
162
  }
159
- const lines = [header, "", ...sections];
163
+ const body = [header, "", ...sections].join("\n").trimEnd();
160
164
  if (options.includeFooter) {
161
- lines.push("\n---\n\nThis PR was generated by [Versionary](https://github.com/jolars/versionary).");
165
+ return `${body}\n\n${renderReviewRequestFooter()}`;
162
166
  }
163
- return lines.join("\n");
167
+ return body;
168
+ }
169
+ function renderReviewRequestFooter() {
170
+ return REVIEW_REQUEST_FOOTER;
164
171
  }
165
- function renderReleasePlanChangelog(plan) {
172
+ function renderReleasePlanChangelog(plan, options = {}) {
166
173
  if (!plan.nextVersion) {
167
174
  return "";
168
175
  }
@@ -194,8 +201,11 @@ function renderReleasePlanChangelog(plan) {
194
201
  currentVersion: plan.currentVersion,
195
202
  nextVersion: plan.nextVersion,
196
203
  commits: dedupedCommits,
197
- cwd: process.cwd(),
204
+ cwd: options.cwd ?? process.cwd(),
198
205
  dependencies,
206
+ }, {
207
+ includeFooter: options.includeFooter,
208
+ headerLabel: options.headerLabel,
199
209
  });
200
210
  }
201
211
  /** @deprecated Use renderReleasePlanChangelog. */
@@ -20,6 +20,13 @@ export declare function renderSimpleReviewRequestBody(version: string, previousV
20
20
  export declare function openOrUpdateReviewRequest(cwd: string, branch: string, title: string, version: string, previousVersion: string, commits: ParsedCommit[], plan?: SimplePlan | null, options?: {
21
21
  logger?: VersionaryPluginContext["logger"];
22
22
  }): Promise<string>;
23
+ export declare function closeStaleReviewRequestIfExists(cwd?: string, options?: {
24
+ logger?: VersionaryPluginContext["logger"];
25
+ }): Promise<{
26
+ closed: boolean;
27
+ url?: string;
28
+ number?: number;
29
+ }>;
23
30
  /** @deprecated Use prepareReleasePr. */
24
31
  export declare function prepareSimpleReleasePr(cwd?: string, options?: {
25
32
  logger?: VersionaryPluginContext["logger"];
@@ -7,6 +7,7 @@ exports.splitSafeDirtyFiles = splitSafeDirtyFiles;
7
7
  exports.prepareReleasePr = prepareReleasePr;
8
8
  exports.renderSimpleReviewRequestBody = renderSimpleReviewRequestBody;
9
9
  exports.openOrUpdateReviewRequest = openOrUpdateReviewRequest;
10
+ exports.closeStaleReviewRequestIfExists = closeStaleReviewRequestIfExists;
10
11
  exports.prepareSimpleReleasePr = prepareSimpleReleasePr;
11
12
  exports.openOrUpdateSimpleReviewRequest = openOrUpdateSimpleReviewRequest;
12
13
  exports.pushReleaseBranch = pushReleaseBranch;
@@ -348,25 +349,15 @@ function renderSimpleReviewRequestBody(version, previousVersion, commits, plan =
348
349
  .sort((a, b) => a.name.localeCompare(b.name));
349
350
  return directSources;
350
351
  };
351
- const relabelSectionHeader = (notes, packageLabel, nextVersion) => {
352
- const linkedHeader = notes.match(/^##\s+\[([^\]]+)\]\(([^)]+)\)\s+\(([^)]+)\)/u);
353
- if (linkedHeader) {
354
- const [, , compareUrl, date] = linkedHeader;
355
- return notes.replace(/^##\s+\[[^\]]+\]\([^)]+\)\s+\([^)]+\)/u, `## [${packageLabel}: ${nextVersion}](${compareUrl}) (${date})`);
356
- }
357
- const plainHeader = notes.match(/^##\s+([^\s]+)\s+\(([^)]+)\)/u);
358
- if (plainHeader) {
359
- const [, , date] = plainHeader;
360
- return notes.replace(/^##\s+[^\s]+\s+\([^)]+\)/u, `## ${packageLabel}: ${nextVersion} (${date})`);
361
- }
362
- return notes;
363
- };
364
352
  if (plan?.packages && plan.packages.length > 1) {
365
353
  const sections = [];
366
354
  const rootPackage = plan.packages.find((pkg) => pkg.path === "." && pkg.nextVersion);
367
355
  if (rootPackage?.nextVersion) {
368
- const rootNotes = (0, changelog_js_1.renderReleasePlanChangelog)(plan);
369
- sections.push(relabelSectionHeader(rootNotes, formatPackageLabel("."), rootPackage.nextVersion));
356
+ const rootNotes = (0, changelog_js_1.renderReleasePlanChangelog)(plan, {
357
+ headerLabel: `${formatPackageLabel(".")}: ${rootPackage.nextVersion}`,
358
+ cwd,
359
+ });
360
+ sections.push(rootNotes);
370
361
  }
371
362
  const packageSections = plan.packages
372
363
  .filter((pkg) => pkg.path !== "." && pkg.nextVersion)
@@ -379,15 +370,18 @@ function renderSimpleReviewRequestBody(version, previousVersion, commits, plan =
379
370
  commits: pkg.commits,
380
371
  cwd,
381
372
  dependencies: propagatedDependencies,
382
- }, { includeFooter: false });
383
- return relabelSectionHeader(notes, packageLabel, pkg.nextVersion ?? "");
373
+ }, {
374
+ includeFooter: false,
375
+ headerLabel: `${packageLabel}: ${pkg.nextVersion ?? ""}`,
376
+ });
377
+ return notes;
384
378
  });
385
379
  sections.push(...packageSections);
386
380
  const bodySections = sections.join("\n\n");
387
381
  if (bodySections.length === 0) {
388
- return "This PR was generated by Versionary.";
382
+ return (0, changelog_js_1.renderReviewRequestFooter)();
389
383
  }
390
- return `${bodySections}\n\nThis PR was generated by Versionary.`;
384
+ return `${bodySections}\n\n${(0, changelog_js_1.renderReviewRequestFooter)()}`;
391
385
  }
392
386
  return (0, changelog_js_1.renderSimpleReleaseNotes)({
393
387
  currentVersion: previousVersion,
@@ -415,6 +409,32 @@ async function openOrUpdateReviewRequest(cwd, branch, title, version, previousVe
415
409
  });
416
410
  return result.url;
417
411
  }
412
+ function hasScmRuntimeContext() {
413
+ const hasRepository = Boolean(process.env.GITHUB_REPOSITORY);
414
+ const hasToken = Boolean(process.env.VERSIONARY_PR_TOKEN ??
415
+ process.env.GH_TOKEN ??
416
+ process.env.GITHUB_TOKEN);
417
+ return hasRepository && hasToken;
418
+ }
419
+ async function closeStaleReviewRequestIfExists(cwd = process.cwd(), options = {}) {
420
+ if (!hasScmRuntimeContext()) {
421
+ return { closed: false };
422
+ }
423
+ const plan = (0, plan_js_1.createReleasePlan)(cwd);
424
+ const scmClient = (0, client_js_1.getScmClient)();
425
+ const result = await scmClient.closeReviewRequestIfExists({
426
+ baseBranch: process.env.VERSIONARY_BASE_BRANCH ?? "main",
427
+ headBranch: plan.releaseBranchPrefix,
428
+ reason: "Closing stale release PR because no releasable commits remain for the current baseline/tag state.",
429
+ }, {
430
+ cwd,
431
+ logger: options.logger,
432
+ });
433
+ if (result.closed && result.number) {
434
+ options.logger?.info(`Closed stale release review request #${result.number} for ${plan.releaseBranchPrefix}.`);
435
+ }
436
+ return result;
437
+ }
418
438
  /** @deprecated Use prepareReleasePr. */
419
439
  function prepareSimpleReleasePr(cwd = process.cwd(), options = {}) {
420
440
  return prepareReleasePr(cwd, options);
@@ -5,6 +5,7 @@ export interface ReleaseTargetInput {
5
5
  version: string;
6
6
  notes: string;
7
7
  draft?: boolean;
8
+ makeLatest?: "true" | "false" | "legacy";
8
9
  }
9
10
  export interface ReleaseExecutionContext {
10
11
  createReleaseMetadata: (input: ReleaseTargetInput) => Promise<VersionaryScmReleaseMetadataResult>;
@@ -183,6 +183,7 @@ async function runReleaseDetailed(cwd = process.cwd(), options = {}) {
183
183
  version: target.version,
184
184
  notes: releaseNotes,
185
185
  draft: loaded.config["release-draft"] ?? false,
186
+ makeLatest: target.path === "." ? "true" : "false",
186
187
  }, {
187
188
  createReleaseMetadata: (input) => scmClient.createReleaseMetadata(input, {
188
189
  cwd,
@@ -174,6 +174,68 @@ function createGitHubPlugin() {
174
174
  state: toReviewRequestState(created, `pull request #${created.number} in ${repoRef(repo)}`),
175
175
  };
176
176
  },
177
+ async closeReviewRequestIfExists(input, _context) {
178
+ const repo = getRepoFromEnv();
179
+ const octokit = new rest_1.Octokit({ auth: getGitHubToken() });
180
+ const listHead = resolveHeadForList(repo, input.headBranch);
181
+ let existing;
182
+ try {
183
+ const response = await octokit.pulls.list({
184
+ owner: repo.owner,
185
+ repo: repo.repo,
186
+ state: "open",
187
+ head: listHead,
188
+ base: input.baseBranch,
189
+ per_page: 100,
190
+ });
191
+ existing = response.data;
192
+ }
193
+ catch (error) {
194
+ const { message } = parseGitHubError(error);
195
+ throw new Error(`Failed listing open pull requests for branch "${input.headBranch}" into "${input.baseBranch}": [${repoRef(repo)}] ${message}`);
196
+ }
197
+ if (existing.length > 1) {
198
+ const matches = existing.map((item) => `#${item.number}`).join(", ");
199
+ throw new Error(`Ambiguous open pull request matches for "${input.headBranch}" into "${input.baseBranch}": [${repoRef(repo)}] ${matches}`);
200
+ }
201
+ if (existing.length === 0) {
202
+ return { closed: false };
203
+ }
204
+ const pr = existing[0];
205
+ let updated;
206
+ try {
207
+ const response = await octokit.pulls.update({
208
+ owner: repo.owner,
209
+ repo: repo.repo,
210
+ pull_number: pr.number,
211
+ state: "closed",
212
+ });
213
+ updated = response.data;
214
+ }
215
+ catch (error) {
216
+ const { message } = parseGitHubError(error);
217
+ throw new Error(`Failed closing pull request #${pr.number}: [${repoRef(repo)} base=${input.baseBranch} head=${input.headBranch}] ${message}`);
218
+ }
219
+ try {
220
+ await octokit.issues.createComment({
221
+ owner: repo.owner,
222
+ repo: repo.repo,
223
+ issue_number: pr.number,
224
+ body: input.reason,
225
+ });
226
+ }
227
+ catch (error) {
228
+ const { status, message } = parseGitHubError(error);
229
+ if (status !== 404 && status !== 410) {
230
+ throw new Error(`Failed commenting on closed pull request #${pr.number}: [${repoRef(repo)} base=${input.baseBranch} head=${input.headBranch}] ${message}`);
231
+ }
232
+ }
233
+ return {
234
+ closed: true,
235
+ number: updated.number,
236
+ url: updated.html_url,
237
+ };
238
+ },
177
239
  async createReleaseMetadata(input, _context) {
178
240
  const repo = getRepoFromEnv();
179
241
  const octokit = new rest_1.Octokit({ auth: getGitHubToken() });
@@ -200,6 +262,7 @@ function createGitHubPlugin() {
200
262
  name: input.tag,
201
263
  body: input.notes,
202
264
  draft: input.draft ?? false,
265
+ make_latest: input.makeLatest,
203
266
  });
204
267
  data = response.data;
205
268
  }
@@ -20,11 +20,22 @@ export interface ScmReviewRequestResult {
20
20
  url: string;
21
21
  state: "open" | "closed" | "merged";
22
22
  }
23
+ export interface ScmCloseReviewRequestInput {
24
+ baseBranch: string;
25
+ headBranch: string;
26
+ reason: string;
27
+ }
28
+ export interface ScmCloseReviewRequestResult {
29
+ closed: boolean;
30
+ number?: number;
31
+ url?: string;
32
+ }
23
33
  export interface ScmReleaseMetadataInput {
24
34
  tag: string;
25
35
  version: string;
26
36
  notes: string;
27
37
  draft?: boolean;
38
+ makeLatest?: "true" | "false" | "legacy";
28
39
  }
29
40
  export interface ScmReleaseMetadataResult {
30
41
  url: string;
@@ -42,6 +53,7 @@ export interface ScmReleaseReferenceCommentsResult {
42
53
  export interface ScmClient {
43
54
  provider: ScmProvider;
44
55
  createOrUpdateReviewRequest: (input: ScmReviewRequestInput, context: ScmClientContext) => Promise<ScmReviewRequestResult>;
56
+ closeReviewRequestIfExists: (input: ScmCloseReviewRequestInput, context: ScmClientContext) => Promise<ScmCloseReviewRequestResult>;
45
57
  createReleaseMetadata: (input: ScmReleaseMetadataInput, context: ScmClientContext) => Promise<ScmReleaseMetadataResult>;
46
58
  createReleaseReferenceComments?: (input: ScmReleaseReferenceCommentsInput, context: ScmClientContext) => Promise<ScmReleaseReferenceCommentsResult>;
47
59
  }
@@ -19,5 +19,10 @@ function listKnownReleaseTypes() {
19
19
  }
20
20
  function resolveVersionStrategy(config) {
21
21
  const releaseType = config["release-type"] ?? "simple";
22
- return strategyRegistry[releaseType] ?? simple_js_1.simpleVersionStrategy;
22
+ const strategy = strategyRegistry[releaseType];
23
+ if (!strategy) {
24
+ const known = listKnownReleaseTypes().join(", ");
25
+ throw new Error(`Unsupported release-type "${releaseType}". Supported release types: ${known}.`);
26
+ }
27
+ return strategy;
23
28
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "versionary",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
4
  "description": "Automatic release framework based on conventional commits and semantic versioning",
5
5
  "keywords": [
6
6
  "releasing",
@@ -34,9 +34,9 @@
34
34
  "zod": "^4.1.12"
35
35
  },
36
36
  "devDependencies": {
37
- "@types/node": "^24.7.2",
37
+ "@types/node": "^25.6.0",
38
38
  "tsx": "^4.20.6",
39
- "typescript": "^5.9.3",
39
+ "typescript": "^6.0.3",
40
40
  "vitest": "^4.1.4"
41
41
  },
42
42
  "scripts": {